排序算法(三)
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