选择排序与归并排序

来源:互联网 发布:java得到项目路径 编辑:程序博客网 时间:2024/05/17 07:00

选择排序


直接选择排序

【基本思想】

每一趟在待排序列中选出最小(或最大)的元素,依次放在已排好序的元素序列后面(或前面),直至全部的元素排完为止。

直接选择排序也称为简单选择排序。首先在待排序列中选出最小的元素,将它与第一个位置上的元素交换。然后选出次小的元素,将它与第二个位置上的元素交换。以此类推,直至所有元素排成递增序列为止。

选择排序是对整体的选择。只有在确定了最小数(或最大数)的前提下才进行交换, 大大减少了交换的次数。

【空间复杂度】O(1)

【时间复杂度】

平均情况:O(n^2)

最好情况:O(n^2),此时不发生交换,但仍需进行比较

最坏情况:O(n^2)

【稳定性】不稳定

【优点】

交换数据的次数已知(n-1)次

【缺点】

不稳定,比较的次数多

【算法实现】
[java] view plain copy
  1.     /** 
  2.      * 直接选择排序 
  3.      * @param arr 
  4.      */  
  5.     public static void selectSort(int[] arr) {  
  6.         int minIndex;  
  7.         for (int i = 0; i < arr.length - 1; i++) {  
  8.             //初始化默认i为最小值索引  
  9.             minIndex = i;  
  10.             for (int j = i + 1; j < arr.length; j++) {  
  11.                 if (arr[j] < arr[minIndex]) {  
  12.                     //记录最小值的索引  
  13.                     minIndex = j;  
  14.                 }  
  15.             }  
  16.             //若minIndex的值改变了,说明有其它最小值,则交换  
  17.             if (minIndex != i) {  
  18.                 swap(arr, minIndex, i);  
  19.             }  
  20.         }  
  21.     }  
  22.     public static void swap(int[] arr, int i, int j) {  
  23.         int temp = arr[i];  
  24.         arr[i] = arr[j];  
  25.         arr[j] = temp;  
  26.     }  

【本算法解读】

初始时无序区为整个待排序列。算法默认待排序列的第一个元素是最小值,然后从该元素的下一位开始(对应代码:for(int j = i +1, j < arr.length; j ++)),直到最后的元素,都和索引minIndex位置上的元素进行比较。如果发现更小的值,就将该值的索引存放在minIndex中。当所有比较进行完毕后,索引minIndex位置上的元素就是最小值,将其和首元素进行交换。这样就完成了一趟选择排序,此时序列的首元素即处于有序区中,剩下的元素处于无序区中。继续将无序区中的首元素作为最小值,重复上面的操作,并将找到的最小值和无序区首元素交换即可。直至完成所有排序。

【举个栗子】

对于待排序列3,1,4,2
首先认为3是最小值,将其索引0保存在minIndex中,从元素1开始与minIndex位置上的元素(此时是3)进行比较,1<3,则minIndex保存元素1的索引。继续将minIndex位置上的元素(此时是1)与元素4比较,4>1,继续与2比较,1<2,不需要改变。没有再比较的元素了,此时将minIndex位置上的元素和首元素进行交换。则完成一趟选择排序,序列为1,3,4,2。有序区为1,无序区为3,4,2。继续下一趟排序,默认无序区首元素3为最小值,重复上述操作,将找到的无序区最小值,和无序区首元素进行交换。一趟选择排序结束后,序列为1,2,4,3。有序区为1,2,无序区为4,3。重复上述操作直到完成排序。



堆排序

堆排序是借助堆来实现的选择排序。

什么是堆呢?堆是满足下列性质的数列{r1, r2, r3, r4, ..., rn}:

ri<=r2i且ri<=r(2i+1)或者是ri>=r2i且ri>=r(2i+1),前者称为小顶堆,后者称为大顶堆。

例如大顶堆:{10,34,24,85,47,33,53,90},位置i(i从1开始)上的元素小于2i位置上的元素,且小于2i+1位置上的元素

【基本思想】

对一组待排序列的元素,首先将它们按照堆的定义排成一个序列,常称为建堆,从而输出堆顶的最大或最小关键字。然后对剩余的元素再建堆,常称为重新调整成堆,即可得到次大(次小)元素,如此反复进行,直到全部元素排成有序序列为止。

实现堆排序有两个关键步骤,建堆和调整堆
 如何建堆:首先将待排序列画成一颗完全二叉树,然后把得到的完全二叉树再转换成堆。从最后一个分支节点开始(n/2取下限的节点),依次将所有以分支节点为根的二叉树调整成堆,当这个过程持续到根节点时,整个二叉树就调整成了堆,即建堆完成。

如何调整堆:假设被调整的分支节点为A,它的左孩子为B,右孩子为C。那么当A开始进行堆调整时,以B和以C为根的二叉树都已经为堆。如果节点A的值大于B和C的值(以大顶堆为例),那么以A为根的二叉树已经是堆。如果A节点的值小于B节点或C节点的值,那么节点A与值最大的那个孩子变换位置。此时需要将A继续与和它交换的那个孩子的原来的两个孩子进行比较,依次类推,直到节点A向下渗透到适当的位置为止。

如果要从小到大排序,则使用大顶堆,如果要从大到小排序,则使用小顶堆。原因是堆顶元素需要交换到序列尾部

【空间复杂度】O(1)

【时间复杂度】

平均情况:O(nlog2n)

最好情况:O(nlog2n)

最坏情况:O(nlog2n)它的最坏性能接近于平均性能

【稳定性】不稳定

【优点】

在最坏情况下性能优于快速排序。由于在直接选择排序的基础上利用了比较结果形成。效率提高很大。

【缺点】

不稳定,初始建堆所需比较次数较多,因此记录数较少时不宜采用

【算法实现】
[java] view plain copy
  1. /** 
  2.  * 堆排序 
  3.  * @param arr 
  4.  */  
  5. public static void heapSort(int[] arr) {  
  6.     //建堆,通过调整堆达到建堆的目的  
  7.     //从二叉树最后一个分支节点开始(n/2取下限的节点)  
  8.     for (int i = arr.length / 2; i >= 0; i--) {  
  9.         heapAdjust(arr, i, arr.length - 1);  
  10.     }  
  11.     // 由大顶堆得到有序序列  
  12.     for (int i = arr.length - 1; i >= 1; i--) {  
  13.         swap(arr, 0, i);  
  14.         heapAdjust(arr, 0, i - 1);  
  15.     }  
  16. }  
  17.    //调整堆  
  18. private static void heapAdjust(int[] arr, int start, int end) {  
  19.     int target = arr[start];  
  20.     for (int i = 2 * start + 1; i <= end; i = 2 * i + 1) {  
  21.         //将i指向左孩子和右孩子中的较大值  
  22.         if (i < end && arr[i] < arr[i + 1]) {  
  23.             i++;  
  24.         }  
  25.         //已经是大顶堆,不再需要调整  
  26.         if (target >= arr[i]) {  
  27.             break;  
  28.         }  
  29.         arr[start] = arr[i];  
  30.         start = i;  
  31.     }  
  32.     arr[start] = target;  
  33. }  
  34. public static void swap(int[] arr, int i, int j) {  
  35.     int temp = arr[i];  
  36.     arr[i] = arr[j];  
  37.     arr[j] = temp;  
  38. }  

【本算法解读】

算法也是按照先建堆再调整堆的步骤执行的。第一个for循环,从n/2节点开始依次通过调用headAdjust()来调整堆,最终完成建堆。第二个for循环,利用之前已经建好的大顶堆(首元素为最大值),将首元素交换到序列末尾。然后去掉该元素,再调整堆,再次获得大顶堆(首元素为次大值),将其首元素交换到倒数第二个位置,以此类推。

算法的关键点在于堆调整headAdjust()方法。该方法调整的分支节点为start位置的元素(称其为目标元素)。该元素的左右孩子分别是2*start+1,2*start+2。若目标元素大于等于它的的两个孩子,则已经是大顶堆,不需要调整了。否则,目标元素和两个孩子中的较大值交换(对应代码arr[start] = arr[i],即向下渗透),并将start设置为目标元素交换后所在的位置,重复上述操作,直到目标元素渗透到适当的位置。

【举个栗子】

对于待排序列1,4,3,2
首先为了便于理解,我们可以将其画成二叉树:


转换方法是将待排序列的元素,从上到下,从左到右,依次填入到二叉树的节点中。

开始建堆。本例中实际上只需要调整节点1,所以以调整节点1为例:过程如下图


节点1作为目标元素,先找到其左右孩子(4和3)的较大值4,即比较目标元素1和4,1<4,则交换位置。目标元素1渗透到元素2原来的位置。在此位置上继续寻找,其左右孩子,此时只有一个左孩子,元素2,与目标元素做比较,1<2,则交换位置,此时目标元素1已经向下渗透到最终位置。建堆成功,序列为4,2,3,1。然后通过大顶堆,得到首元素最大值4,并将其移动到序列尾部。取掉元素4后,再次建堆,重复上述操作,完成排序。


归并排序

【基本思想】

所谓归并是指,把两个或两个以上的待排序列合并起来,形成一个新的有序序列。2-路归并是指,将两个有序序列合并成为一个有序序列。

2-路归并排序的基本思想是,对于长度为n的无序序列来说,归并排序把它看成是由n个只包括一个元素的有序序列组成,然后进行两两归并,最后形成包含n个元素的有序序列

【空间复杂度】O(n)

由于在实现过程中用到了一个临时序列来暂存归并过程中的中间结果,所以算法的空间复杂度为O(n)

【时间复杂度】

平均情况:O(nlog2n)

最好情况:O(nlog2n),此时不发生交换,但仍需进行比较

最坏情况:O(nlog2n)

对于长度为n的序列,需要进行logn趟2-路归并,而每趟归并的时间开销是O(n),故在任何情况下,2-路归并的时间复杂度都为O(nlogn)

【稳定性】稳定

【优点】

稳定排序。若采用单链表作为存储结构,可实现就地排序

【缺点】

需要O(n)的额外空间

【算法实现】
[java] view plain copy
  1. /** 
  2.  * 归并排序 
  3.  * @param arr 
  4.  * @param left 
  5.  * @param right 
  6.  */  
  7. public static void mergeSort(int[] arr, int left, int right) {  
  8.     if (left >= right) {  
  9.         return;  
  10.     }  
  11.     int mid = (right + left) / 2;  
  12.     //递归划分子序列  
  13.     mergeSort(arr, left, mid);  
  14.     mergeSort(arr, mid + 1, right);  
  15.     //合并子序列  
  16.     merge(arr, left, mid, right);  
  17. }  
  18.   
  19. private static void merge(int[] arr, int left, int mid, int right) {  
  20.     int[] temp = new int[right - left + 1];  
  21.     int i = left;  
  22.     int j = mid + 1;  
  23.     int k = 0;  
  24.     //比较两个子序列元素的大小  
  25.     while (i <= mid && j <= right) {  
  26.         if (arr[i] <= arr[j]) {  
  27.             temp[k++] = arr[i++];  
  28.         } else {  
  29.             temp[k++] = arr[j++];  
  30.         }  
  31.     }  
  32.     //处理左边子序列剩余元素  
  33.     while (i <= mid) {  
  34.         temp[k++] = arr[i++];  
  35.     }  
  36.     //处理右边子序列剩余元素  
  37.     while (j <= right) {  
  38.         temp[k++] = arr[j++];  
  39.     }  
  40.     //将合并后的临时序列替换到原始序列  
  41.     for (int p = 0; p < temp.length; p++) {  
  42.         arr[left + p] = temp[p];  
  43.     }  
  44. }  

【本算法解读】

算法首先通过递归,不断将待排序列划分成两个子序列,子序列再划分成两个子序列,直到每个子序列只含有一个元素(对应代码:if(left >= rihgt){return;}),然后对每对子序列进行合并。合并子序列是通过merge()方法实现,首先定义了一个临时的辅助空间,长度是两个子序列之和。然后逐个比较两个子序列中的元素,元素较小的先放入辅助空间中。若两个子序列长度不同,则必定有一个子序列有元素未放入辅助空间。算法后面分别对左边子序列和右边子序列做了处理。最后,两个子序列的合并结果都存在于辅助空间中,将辅助空间中的有序序列替换到原始序列的对应位置上。

【举个栗子】

对于待排序列1,4,3,2
第一次递归,mid = 1,将待排序列分成(1,4),(3,2)。继续对每个子序列划分子序列。对于序列(1,4,),(3,2),mid 都是0,即分别被划分成(1)(4),(3)(2)直到每个子部分只含有一个元素。然后开始合并,合并(1)(4)得到序列(1,4) ,合并(3)(2)得到序列(2,3)。再次合并(1,4)(2,3),得到最终有序序列(1,2,3,4)


可以发现我们本篇所汇总的算法,时间复杂度最低也就是O(nlog2n),包括上一篇汇总讲到的交换排序和插入排序也是同样的结果。其实,基于比较的排序算法,时间复杂度的下界就是O(nlog2n),而下一篇将要进行汇总的排序算法可以突破下界O(nlog2n),达到O(n)。

0 0