算法-分治法(Divide-and-Conquer)

算法-分治法(Divide-and-Conquer)

分治法简介

分治法是把一个复杂的问题分成两个或多个相同或相似的子问题,再把子问题分成更小的子问题直到最后子问题可以简单地直接求解,原问题的解即子问题的解的合并,这个思想是很多高效算法的基础。

分治法的基本思想将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

分治策略

分治策略:对于一个规模为n的问题,

  • 若该问题可以容易的解决(比如规模n较小)则直接解决,
  • 否则将其分解为k个规模较小的子问题
    这些子问题互相独立且与原问题形式相同,递归地解决这些子问题,然后将各个子问题的解合并得到原问题的解。

如果原问题可以分割成k个子问题,1<k<=n,且这些子问题均可解,并且利用这些子问题的解求出原问题的解,那么分治方法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归经常同时应用在算法设计之中。

分治法使用场景

  1. 该问题的规模缩小到一定的程度就可以很容易得到解决。

  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

  3. 利用该问题分解出的子问题的解可以合并为该问题的解。

  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

  5. 第一条特征是绝大多数问题可以满足的,问题的复杂性一般是随着问题规模的增加而增加

  6. 第二条特征是应用分治法的前提。它是大多数问题可以满足的,此特征反映了递归思想的应用

  7. 第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条,而不具备第三条特征,则可以考虑使用贪心法或者动态规划法

  8. 第四条关系到分治法的效率,如果各个子问题是不独立的则分治法要重复的解决公共的子问题,此时虽然可用分治法,但一般使用动态规划法较好。

分治法的基本步骤

  1. 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  2. 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  3. 合并:将各个子问题的解合并为原问题的解

分治法的复杂度分析

分治法的时间复杂度

分治法的空间复杂度

可使用分治法求解的一些经典问题

  • 二分搜索
  • 大整数乘法
  • Strassen矩阵乘法
  • 棋盘覆盖
  • 归并排序
  • 快速排序
  • 线性时间选择
  • 最接近点对问题
  • 循环赛日程表
  • 汉诺塔

归并排序(merge sort)

归并排序简介

归并排序是将两个或两个以上的有序表组合成一个新的有序表(也称为二路归并 )。其基本思想是:先将N个数据看成N个长度为1的表(分治法中的“”),将相邻两个表合并,得到长度为2的N/2个有序表,进一步将相邻的表合并,得到长度为4的N/4个有序表,以此类推,直到所有数据合并成一个长度为N的有序表位置。(分治法中的“治”)每一次归并称为一趟。

  • 要解决归并问题,首先要解决两两归并问题(两个有序表合并成一个有序表)

public static void mergeTwo(int[] data,int first,int mid,int last,int[] tmp){
        
        //把data[first]-data[mid]当做第一个有序序列 ,这里设为A
        //把data[mid+1]-data[last]当做第二个有序序列,这里设为B
        //将两个有序序列合并,形成的新序列为data[first]-data[last]
        int i = first, j = mid + 1;  
        int m = mid,   n = last;  
        int k = 0; 
        while(i<=m&&j<=n){
            //A序列和B序列依次从起始值开始比较
            //如果A序列值小,就将其移值tmp中
            //并且A下标i+1;tmp下标k+1
            if(data[i]<data[j]){
                tmp[k++] =data[i++];
            }else{
                //如果B序列值小,就将其移值tmp中
                //并且B下标i+1;tmp下标k+1
                tmp[k++] = data[j++];
            }
        }
        
        //如果A序列或者B序列已经全部移到tmp中
        //则剩余的另一个序列依次移到tmp中
        while(i<=m){
            tmp[k++] =data[i++];
        }
        while(j<=n){
            tmp[k++] = data[j++];
        }
        
        //遍历tmp,将tmp中元素移会data,此时data[first]-data[last]为有序序列
        for (i = 0; i < k; i++)  {
            data[first + i] = tmp[i]; 
        }
    }
  • 将两个有序表合成一个有序表之后,要开始实现“分”的部分,这里分为两种方法
  1. 自底向上
  2. 自顶向下

自底向上的基本思想是:第一趟归并排序时,将待排序的文件R[1…n]看做是n个长度为1的有序文件,将这些子文件两两归并

  • 若n为偶数,则得到n/2个长度为2的有序文件;
  • 若n为奇数,则最后一个子文件轮空(不参与归并,直接进入下一趟归并),估本趟归并完成后,前n/2-1个有序子文件长度为2,单最后一个子文件长度仍为1;

第二趟归并则是将第一趟归并所得到的n/2个有序文件再进行两两归并,以此类推,直到得到最后长度为n的有序文件。

package sortDemo;

public class MargeSort {
    
    public static void main(String[] args) {
        int[] sort ={3,2,1,4,6,5,8,9,10,7} ;
        System.out.println("排序前:");
        print(sort);
        int[] tmp = new int[sort.length];
        mergeSort(sort,0,sort.length-1,tmp);
        System.out.println("\n排序后:");
        print(sort);
    }
    
    public static void mergeSort(int[] data,int first,int last,int[] tmp){
        if(first<last){
            int mid = (last-first)/2+first;
            //使左侧有序
            mergeSort(data,first,mid,tmp);
            //使右侧有序
            mergeSort(data,mid+1,last,tmp);
            //合并两个有序的子序列
            mergeTwo(data, first, mid, last, tmp);
        }
    }
    
    public static void mergeTwo(int[] data,int first,int mid,int last,int[] tmp){
        
        //把data[first]-data[mid]当做第一个有序序列 ,这里设为A
        //把data[mid+1]-data[last]当做第二个有序序列,这里设为B
        //将两个有序序列合并,形成的新序列为data[first]-data[last]
        int i = first, j = mid + 1;  
        int m = mid,   n = last;  
        int k = 0; 
        while(i<=m&&j<=n){
            //A序列和B序列依次从起始值开始比较
            //如果A序列值小,就将其移值tmp中
            //并且A下标i+1;tmp下标k+1
            if(data[i]<data[j]){
                tmp[k++] =data[i++];
            }else{
                //如果B序列值小,就将其移值tmp中
                //并且B下标i+1;tmp下标k+1
                tmp[k++] = data[j++];
            }
        }
        
        //如果A序列或者B序列已经全部移到tmp中
        //则剩余的另一个序列依次移到tmp中
        while(i<=m){
            tmp[k++] =data[i++];
        }
        while(j<=n){
            tmp[k++] = data[j++];
        }
        
        //遍历tmp,将tmp中元素移会data,此时data[first]-data[last]为有序序列
        for (i = 0; i < k; i++)  {
            data[first + i] = tmp[i]; 
        }
    }
    
    public static void print(int[] a){
        for (int i = 0; i < a.length; i++) {
            System.out.print(a[i]+" ");
        }
        System.out.println();
    }
}
算法分析:

  1. 稳定性
    归并排序不会改变元素的相对位置,所以是稳定的
  2. 是否为原地算法
    归并排序可用顺序存储结构,也易于在链表上实现,因此占用了额外的存储空间,是非原地算法
  3. 时间复杂度
    对长度为n的文件,需要进行[log2n]趟二路归并,每一趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是最坏情况下均是O(nlgn).
发布了9 篇原创文章 · 获赞 0 · 访问量 177

猜你喜欢

转载自blog.csdn.net/qq_36629741/article/details/104659460