代码改变世界

排序算法(三)

2012-12-08 13:49  钱吉  阅读(255)  评论(0)    收藏  举报

面介绍的冒泡排序,插入排序,shell排序都是基于两两元素比较,然后移动的排序算法,有着O(N2)的复杂度,今天讲三种比较牛的排序算法,可以将复杂度降低为O(n*lgn)。分别是:1) 堆排序。2)归并排序。3)快速排序

1、堆排序

算法:利用二叉堆(binary heap)的数据结构形式,及其性质对数据进行排序。首先看下什么是二叉堆,wiki解释:

二叉堆是一种特殊的,二叉堆是完全二叉树或者是近似完全二叉树。二叉堆满足堆特性:父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值,且每个结点的左子树右子树都是一个二叉堆(都是最大堆最小堆)。

父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆

二叉堆一般用数组来表示,节点i的左儿子的序号是2i,右儿子节点的下标是2i+1.如下面的两个二叉堆(左边的为最小堆,右边的为最大堆)表示的数组分别为:{1,2,3,4,5,6,7,8,9,10,11}和{11,9,10,5,6,7,8,1,2,3,4}。

        1               11
      /   \           /   \
     2     3         9     10
    / \   / \       / \    / \
   4   5  6  7     5   6  7   8
  / \ / \         / \ / \
 8  9 10 11      1  2 3  4 

二叉堆一般有集中基本操作,首先看一下,给一个数组,怎么建立一个二叉堆:

数组为array[] = {2, 3, 1, 29, 70, 16, 34, 11, 10, 33, 79};先根据数组下标,它构成的完全二叉树是这样的:

明显这无法满足二叉堆的性质,所以我们要对其进行调整。由于我们要将数据从小到大排列,所以构建一个最大堆,即堆顶元素最大。将要进行的操作叫做下滤(percolate down)。下面通过一步步的说明看下怎么实现的。首先我们从上图中红色节点开始(为什么要从红色节点开始?因为它是第一个拥有儿子节点的节点,没有儿子节点的节点不需要移动)。比较其儿子节点,并找到最大的儿子节点,如果本父节点的值小于最大儿子节点,那么交换父节点和最大儿子节点。交换后树结构如下:

然后,当前节点左移一位(这时移动到29,如下图),继续判断是否小于其子节点中的最大值,这里29>11,所以不需要交换。继续左移一位,到达值为1的节点,判断然后交换后如下:

继续左移一位,到达值为1的节点,判断然后交换后如下:

接着移动到值为3的节点,判断后应该是交换3和79。注意3的节点也有子节点,且此时破坏了原来的堆序性质,因为3<33&&3<70,所以此时应该继续对3这个节点进行下滤,直到找到正确位置,类似于冒泡排序,小的节点直接沉到最低端。最后结果为:

然后继续移动当前节点到2,下滤完成以后,最后一个完好的二叉堆就新鲜出炉了。如下:

建好以后,下面就是排序了。我们知道最大堆的堆顶元素是最大的,那么我取出堆顶元素,放到一个新数组的最后,然后恢复最大堆的顺序,再取出堆顶,放入新数组的倒数第二个,依次这样进行,最后得到的新数组就是一个递增排序好了的序列了。不过这样做,浪费空间,可以不用去申请一个新数组吗?答案是:yes。如何实现?很简单,排序时,我们交换堆顶元素和堆底元素(其实就是数组的最后一个元素),然后恢复堆的有序性,堆的大小减一,即舍弃最后的那个最大的元素。接着再交换堆顶和堆底,继续恢复有序性,堆的大小减一,如此反复。。。。最后堆就是一个完全排好序的数组了。是不是很简单?好吧,我承认还是有点复杂的。也许你会问:交换堆顶和堆底后,恢复堆的有序性不会很复杂吗?答案当然是:no!!,因为要恢复堆的有序性,同样只是而且仅仅只是将堆顶元素执行下滤操作而已,这个其实是非常快的,复杂度是O(lgh),h是这个堆的高度。ok,讲完了,上代码:

 1 void HeapSort(int arr[], int nsize)
 2 {
 3     int i;
 4     for(i=nsize/2-1; i>=0; i--)
 5     {
 6         percolatedown(arr, nsize, i);//创建堆
 7     }
 8 
 9     for(i=nsize; i>0; i--)
10     {
11         swap(&arr[0],&arr[i-1]);//交换头尾元素
12         percolatedown(arr, i-1, 0);//重新恢复堆序
13     }
14     
15 }
16 void percolatedown(int arr[], int nsize, int index)
17 {
18     int ntemp;
19     int nchild;
20     for (ntemp=arr[index]; 2*index+1<=nsize-1; index=nchild)
21     {
22         nchild = 2*index+1;//左儿子节点序号
23         if (nchild != nsize-1 && arr[nchild] < arr[nchild+1]) //如果存在右儿子,且左儿子小于右儿子
24         {
25             nchild++;//指向两个儿子中比较大的节点
26         }
27         if (ntemp < arr[nchild])//如果要下滤的值小于儿子节点
28         {
29             arr[index] = arr[nchild];//儿子节点上滤
30         }
31         else
32             break;
33     }
34     arr[index] = ntemp;//将要下滤的节点放在最终正确的位置上
35 }

测试:

 1 void HeapSort(int arr[], int nsize);
 2 void percolatedown(int arr[], int nsize, int index);
 3 void PrintArray(int arr[], int nlen);
 4 
 5 int main(int argc, char *argv[])
 6 {
 7     int data[16] = {2, 3, 1, 29, 70, 16, 34, 11, 10, 33, 79, 6, 46, 100, 25, 82};
 8     PrintArray(data, 16);
 9     HeapSort(data, 16);
10     PrintArray(data, 16);
11     return 0;
12 }
13 
14 void PrintArray(int arr[], int nlen)
15 {
16     int i;
17     for(i=0; i<nlen; i++)
18     {
19         cout<<arr[i]<<" ";
20     }
21     cout<<endl;
22 }

结果:

原来的数组:2 3 1 29 70 16 34 11 10 33 79 6 46 100 25 82
排序后的数组:1 2 3 6 10 11 16 25 29 33 34 46 70 79 82 100

 2、归并排序

算法先看看对于两个已经排好序的数组,如何合并它们?举《数据结构与算法分析-c++描述》中的例子,例如:有两个数组,{1,13,24,26}和{2,15,27,38}。合并过程如下:设置两个指针,分别指向两数组的头一个元素,第一次开始比较,将两者中的小元素1放入c数组中,然后移动A的指针,比较13和2,将2放入c,移动B的指针,比较13和15,放13到c中,如此循环进行,知道两个指针有一个指向了数组末端。然后将另一个数组的剩余元素放入c即可。

 

 

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

用到与此同样的思想,我们将一个数组的所有元素归并排序,采用分治的思想,将数组从中间一分为二,分别处理左右两个子序列,如果两个子序列都排好序后,我们可以合并它们得到最后的排好序的数组 。而这其中,处理子序列的时候,又可以将子序列分成两个子子序列,如此反复,直到只有一个元素的子序列被处理,这个就是最容易处理的基本情况了。我们采用递归处理实现。

代码:

 1  void MergeSort(int array[], int arrtemp[], int leftpos, int rightpos)
 2   {
 3       int nmiddle;
 4       if(leftpos < rightpos)
 5       {
 6           nmiddle = (leftpos+rightpos)/2;//中间元素
 7           MergeSort(array, arrtemp, leftpos, nmiddle);//递归处理左子序列
 8           MergeSort(array, arrtemp, nmiddle+1, rightpos);//递归处理右子序列
 9           Merge(array, arrtemp, leftpos, nmiddle+1, rightpos);//合并处理两个排过序的子序列
10       }
11   }
12  void Merge(int array[], int arrtemp[], int leftpos, int rightpos, int rightend)
13  {
14      int i;
15      int leftend = rightpos-1;//左边子序列的最后一个元素
16      int temppos = leftpos;//指向新数组的临时指针
17      int ncount = rightend-leftpos+1;//总共有多少个元素
18 
19      /***合并数组***/
20      while(leftpos<=leftend && rightpos<=rightend)
21      {
22          if(array[leftpos]<=array[rightpos])
23              arrtemp[temppos++] = array[leftpos++];
24          else
25              arrtemp[temppos++] = array[rightpos++];
26      }
27      while(leftpos<=leftend)
28         arrtemp[temppos++] = array[leftpos++];
29      while(rightpos<=rightend)
30         arrtemp[temppos++] = array[rightpos++];
31      /***将新数组中排好序的元素拷贝回原数组***/
32      for(i=0; i<ncount; i++)
33      {
34          /**从最后一个元素开始赋值,赋值个数为ncount***/
35          array[rightend] = arrtemp[rightend];//这里不能想当然的写成array[i] = arrtemp[i].
36          rightend--;
37      }
38  }

测试结果与堆排序一样。程序中,为了减少空间复杂度,一次性的申请一个新数组,并在每次合并两个子序列时,利用新数组的部分空间,而不是按照一般思路那样,每次合并都申请一个新的数组空间供merge使用。

3、 快速排序

 快速排序可以参考以前写过的这篇博文:http://www.cnblogs.com/wb-DarkHorse/archive/2012/03/12/wb_DarkHorse.html