分治思想:合并排序和快速排序

 

分治思想:合并排序和快速排序

  分治思想(Divide-and-conquer):

  作为程序设计的一种方法,有时为了解决一个给定的问题,算法要一次或多次地递归调用自身来解决相关的子问题。这些算法通常采用分子的策略:将一个问题划分成n个规模更小并且结构和原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。

  一般采用分治方法解决问题,按如下三个步骤不断循环:

  • 分解(  Divide  ): 将原问题分解成许多个规模更小并且和原问题结构相似的小问题
  • 控制(Conquer): 从小问题的层次上着手解决问题。(在递归层面上:1. 小问题如何解决 2. 做下一层递归的条件如何设置)
  • 合并(Combine): 将子问题得到的解如何归并成原问题的解。

  快速排序(Quick sort)

  基本思想:

  选择一个数组中一个元素作为主元(pivot), 以该主元作为标杆将数组的元素分为大于(或大于等于)主元和小于(或小于等于)主元的两部分(区分的同时也进行位置调换操作)。这时形成的两个子数组在结构形式上和原数组相同,再寻找它们的主元,以此再分。如此不断循环,原数组被分割成许多小数组,当小数组的元素小于等于三个时,按照分割的标准这时已经形成了有序的排列。当这些小数组有序时,合并形成的大数组就是有序的。

  用分治的思想描述(从小到大排序):

  分解: 选择一个主元A[p], 将数组A[p,..r]划分成两个子数组A[p,..q-1]和A[q+1,..r](通过原地操作实现位置调换), 使得A[p,..q-1]全部小于等于A[p]和A[q+1,..r]全部大于等于A[p],下标p也在这个划分过程中计算。

  控制:通过递归调用,对两个子数组排序

  合并:因为两个子数组是就地排序,对他们进行合并不需要操作,整个数组已实现排序。

  复杂度:

  最佳情况:每次划分形成子数组的大小都相同或者相差一个,这时时间复杂度为O(nlgn)

  最坏情况:每次划分形成的子数组大小为1和n-1,这时时间复杂度为Θ(x2)。

  快速排序的C代码如下:

  

 1 void swap(int *a,int *b)
 2 {
 3     int temp;
 4     temp=*a;
 5     *a=*b;
 6     *b=temp;
 7 }
 8 
 9 int partiotion(int A[],int p,int r)
10 {
11     int pivot;
12     int i,j;
13 
14     /**   划分数组的数随机产生版本
15     int randnum;
16     //使用时间作为随机种子
17     srand(time(NULL));
18 
19     //产生p-r之间的随机数
20     randnum=rand()%(r-p)+p;
21 
22     swap(&A[randnum],&A[r]);
23     **/
24     pivot=A[r];
25 
26     i=p-1;
27 
28     for(j=p;j<r;j++)
29         if(A[j]<=pivot)
30         {
31             i=i+1;
32             swap(&A[i],&A[j]);
33         }
34 
35     swap(&A[i+1],&A[r]);
36         
37     return i;
38 }
39 
40 void quickSort(int A[],int p, int r)
41 {
42     int temp;
43 
44     if(p<r)
45     {
46         temp=partiotion(A,p,r);
47         //划分成2个子数组,再使用递归调用
48         quickSort(A,p,temp);
49         quickSort(A,temp+1,r);
50     }
51 
52 }

 

  合并排序(Merge Sort):

  基本思想:

  对两个已经排序的数组,合并成一个有序的数组时间复杂度为θ(n)和 n 个额外的存储空间。对一个数组排序,首先将数组划分成单个元素,两两实行合并,再对合并形成的小数组合并,如此不断循环。如图1 实例所示:

图 1. 合并排序的一个实例(图片来源《算法导论》)

  分治思想(从小到大排序):

  分解:将原数组化分成两个子数组

  控制:将子数组实现合并到一个数组形成有序序列

  合并:采用递归调用时,将上次合并的结果作为这次的输入。由于递归最底层的数组大小为1, 可以确保两个数的合并是有序的。这样每次递归调用返回的结果也是有序的。最终形成的数组自然是有序的。

  复杂度:输入的结果对时间复杂度影响不大,只和排序的规模有关。时间复杂度为Θ(nlgn)。另外需要O(n)个额外的存储空间。

  合并排序的代码如下(从小到大):  

 1 void combine(int A[],int p, int q, int r)
 2 {
 3     
 4     int *temp1;
 5     int *temp2;
 6     int i,j,k;
 7 
 8     temp1=(int *)malloc(sizeof(int)*(q-p+2));
 9     temp2=(int *)malloc(sizeof(int)*(r-q+2));
10 
11     //注意子数组的长度
12     for(i=0;i<q-p;i++)
13     {
14         temp1[i]=A[p+i];
15     }
16 
17     //哨兵元素:很巧妙,本来应该设置一个无限大的数,这里只取一个实数
18     temp1[i]=357834;
19     
20     for(j=0;j<r-q+1;j++)
21     {
22         temp2[j]=A[q+j];
23     }
24 
25     //哨兵元素
26     temp2[j]=357834;
27 
28     i=0;
29     j=0;
30 
31     for(k=p;k<=r;k++)
32         if(temp1[i]<temp2[j])
33         {
34             A[k]=temp1[i];
35             i++;
36         }
37         else
38         {
39             A[k]=temp2[j];
40             j++;
41         }
42 
43 }
44 
45 
46 
47 void mergeSort(int A[],int p,int q)
48 {
49     if(p<q)
50     {
51         int pi=(int)((p+q)/2);
52         mergeSort(A,p,pi);
53         mergeSort(A,pi+1,q);
54 
55         combine(A,p,pi+1,q);
56     }
57 
58 }

 

  合并排序和快速排序递归差异:

  合并排序递归是通过将更深一层次递归的结果作为这一次的输入(上浮),而快速排序递归是将这次操作为下次递归做准备,作为下次操作的输入(下沉)。

 

  

posted @ 2014-03-10 11:12  游-游  阅读(838)  评论(0编辑  收藏  举报