天天看点

算法入门---java语言实现的归并排序小结

  1. /**
  2. * 归并排序 基本思想:将一个无序的集合,一半一半的递归的分成好几个有序的子集合(最终分成只有一个元素,那么也就相当于是有序了),
  3. * 然后在两两的依次归并到一块。 重要的核心在于这个归并的过程。 归并的简单思想:
  4. * 首先这两个子序列是有序的,那么给定两个变量分别指向两个子序列的第一个元素。依次的比较谁小放在前面,此处需要借助一下一个
  5. * 临时的数组, 最后排完的时候谁没排完直接全部放在最后即可。
  6. * 归并排序一个关键点就是你应该定义好索引,按照你的索引来写好代码。比如
  7. * 临时数组{ ,C, , , , , , } 索引指向 将要添加进去的位置。如当前C前面都要数,那么就指向C
  8. * 需要归并的子数组{A, , , , , } 、 B, , , , , } 索引指向当前需要比较的数值所在。
  9. * A 、 B需要比较然后往临时数组存入,那么就指向A B
  10. *
  11. *
  12. * @author zhaoyuan
  13. *
  14. */
  15. public class MergeSort {
  16. /**
  17. * 注意参数代表
  18. * @param src 源数组
  19. * @param left 数组的要排序的左边最小的索引
  20. * @param right 数组要排序的右边最后一个索引 (所以你传值的时候就不能传数组长度)
  21. */
  22. public static void sort(int[] src,int left,int right){
  23. if(left >= right){//大于等于都不需要再拆分了。
  24. return;
  25. }
  26. //这边可能溢出,这个mid是左子序列的最后一个元素的索引
  27. //比如: 1 2 3 4 mid = (0+3)/2 = 1 也就是元素2.
  28. //比如:1 2 3 4 5 mid = (0+4)/2 = 2 也就是 元素3,此时按照 (1 2 3) (4 5)划分.
  29. int mid = (left + right)/2;
  30. //此处是先拆分在合并的顺序
  31. //拆分左子序列。
  32. sort(src,left,mid);
  33. sort(src,mid+1,right);
  34. //拆分完毕后就行合并。
  35. merge(src,left,mid,right);
  36. }
  37. /**
  38. * 合并:src[left... mid] src[mid+1.....right],注意这两个已是有序的集合
  39. * 一定要注意参数的代表,此时的设计是按照闭区间来的。
  40. * @param src 数据源
  41. * @param left 要排序的序列的左边的最小索引
  42. * @param mid 要排序的序列的左边的最大索引
  43. * @param right 要排序的序列的右边的最大索引(右边的最小索引就是 mid+1嘛)
  44. */
  45. public static void merge(int[] src,int left,int mid,int right){
  46. //归并排序单纯的依靠在当前数组移动、赋值等来实现最终排序比较难。
  47. //我们此处是利用一个额外的数组的方式来实现的。核心思想还是比较然后放入数组合适的位置。
  48. int[] temp = new int[right-left+1];//因为我们传入的都是索引,所以长度应该最后+1.
  49. //此处有个小思想,此处你需要控制三个索引,那么一般来说你都要把这些索引先存一下。
  50. int i = left;//左边需要比较的元素的索引
  51. int j = mid+1;//右边需要比较的元素的索引
  52. int k = 0;//将要放入的临时数组的索引。
  53. //两个处在有交集的时候。此时我们传入的都是索引,所以i<= mid都可以(mid是最后一个索引)。
  54. //right也同理,它是最后一个索引
  55. while((i<=mid)&&(j<=right)){//也可以看出你把参数中的索引先保存一份的必要性。
  56. //然后比较找到比较小的放在前面,并且相应的变量++,等于可以保证稳定.
  57. if(src[i] <= src[j]){
  58. //证明应该把src[i]放入到临时数组temp当中
  59. //temp[k] = src[i];
  60. //赋值完毕后应该,k应该向后移动一位等待下一个输入.
  61. //k++;
  62. //i也该向后移动一位,去找下一个需要比较的元素。
  63. //i++;
  64. //优化代码结构:
  65. temp[k++] = src[i++];
  66. }else{
  67. //道理同上
  68. temp[k++] = src[j++];
  69. }
  70. //以上都不会超出索引?
  71. //分析: i、j是有可能超出mid或者right的。但是超出以后我们并没有对相关的数组元素
  72. // 进行赋值或者什么的,并不会处问题。
  73. // k :是不会超出它的范围的,因为i和j的最大值加起来再加1才是它的最大值。
  74. }
  75. //接下来再分析某一个数组比另一个长的时候,此时应该把长的那个多余的元素全放入temp中,在前面的while玄幻外面
  76. //当左序列还未排完
  77. while(i <= mid){//此时必然是右边排完了就是j>right了,但是i还<=mid,注意把等于考虑进去。
  78. temp[k++] = src[i++];
  79. }
  80. //当右边序列还未排完
  81. while(j <= right){//同理
  82. temp[k++] = src[j++];
  83. }
  84. //排完以后把临时数组赋值到对应的原数组的位置。
  85. for(int t = 0;t <temp.length;t++){
  86. src[left++] = temp[t];
  87. }
  88. }
  89. }
算法入门---java语言实现的归并排序小结

简单的优化:

  1. public static void sort(int[] src,int left,int right){
  2. //if(left >= right){
  3. // return;
  4. //}
  5. //优化1:在元素比较少时用插入排序. 亲测这个优化效率提高还是很明显的!
  6. // 原因:首先是数组比较小的时候近乎有序的概率很高,而且从2个相合并开始每次合并的时候最序列都是有序的
  7. // 其次无论是n^2、nlogn 算法复杂度,其实前面都有系数的。而在n比较小的时候系数就很关键了。插入
  8. // 的系数比归并小。
  9. if(right- left<= 15 ){
  10. InsertSort.sort(src, left, right);
  11. return;
  12. }
  13. int mid = (left + right)/2;
  14. sort(src,left,mid);
  15. sort(src,mid+1,right);
  16. //优化2:因为左右子序列本来就是有序的,所以当左边的序列小于右边的序列的时候本身
  17. // 就是有序的,不需要再合并。感觉上没有花多少,毕竟在较为有序的情况下里面也没执行多少。
  18. // 当然有些时候还会产生影响(毕竟这个也需要占时间.)
  19. if(src[mid] > src[mid+1]){
  20. //拆分完毕后就行合并。
  21. merge(src,left,mid,right);
  22. }
  23. }

插入排序:

  1. public static void sort(int[] src, int left, int right) {
  2. //一定要注意这时候传入的是区间,所以i = left+1, i <=right;
  3. //不要从i = 0开始到,len这样!
  4. for (int i = left+1; i <= right; i++) {
  5. int temp = src[i];
  6. int j;
  7. for (j = i; j > 0 && temp < src[j - 1]; j--) {
  8. src[j] = src[j - 1];
  9. }
  10. src[j] = temp;
  11. }
  12. }

自底向上的归并排序(网图):

算法入门---java语言实现的归并排序小结
  1. /**
  2. * 自底向上的的归并。
  3. * 就是以当前数组为考虑的基点,不再进行拆分,而是直接进行归并,
  4. * 先是把一个个元素归并成两两有序的,再把两两有序的归并成四四有序的依次类推,直到归并
  5. * 后大于数组的长度那么就排序完成了。
  6. *
  7. */
  8. public static void sort(int[] src){
  9. int len = src.length;
  10. //此处循环遍历的是归并的每个子数组的元素的个数,一开始子数组是1,然后子数组变成2,
  11. //然后变成4,然后变成8这样一次类推,直到它变成的元素小于数组的长度.
  12. // 1 2 3 4 5
  13. //第二次 ....(1 2)、 (3 4)、 5; 1*2
  14. //第三次....(1 2 3 4)、5; 1*2*2
  15. //第四次....(1 2 3 4 5); 1*2*2
  16. for(int i = 1;i < len;i=2*i){
  17. //内层循环是用来实现两个子元素的归并的,注意每次是跳2*i个索引
  18. //因为归并完比后,他们应该移动2倍的i的距离。
  19. //for(int j=0;j<len;j=j+2*i){
  20. //合并src[j...j+i-1]、src[j+i...j+i+i-1]
  21. //你就一个一个序列的想,就像第一个元素肯定是从j开始然后他有i个元素,但是我们此时要传入索引
  22. //所以就是j-----j+i-1,对于要合并右边的子序列此时就是j+i------j+i+i+i-1.(不是最终长度哦)
  23. // merge(src,j,j+i-1,j+i+i-1);
  24. //}
  25. //但是如上写的时候可能会出问题,因为我们没有考虑边界的问题。
  26. //如:当分完后右边的那个子序列不够数的时候我们再src[j+i+i-1]必然就越界了。
  27. // 还有当右边压根没有序列的时候也不需要排列啊,你也会出错。
  28. //进行优化
  29. //改成符合j+i小于len时才合并,否则就不合并。因为j+i>=i证明就没有序列或者只有一个序列,此时它就是有序的合并什么?
  30. for(int j=0;j+i < len;j = j+2*i){
  31. //防止最后超出数组范围。取两个的最小值。(每次都这么算感觉增加了开销啊)
  32. merge(src,j,j+i-1,Math.min(j+i+i-1,len-1));
  33. }
  34. }
  35. }

         注意此时没有对它进行优化。          自底向上的归并排序的好处:没有使用额外的数组空间。但是听说没有自顶向下的块,基本差不多。

下面贴出优化好的自顶向下的归并排序的测试时间,这是在公司电脑上的和其它测试的环境不一样仅用于此处。

  1. 100000
  2. name: 归并排序1 花费了 = 13ms
  3. name: 归并排序2 花费了 = 6ms
  4. name: 归并排序3 花费了 = 5ms
  5. name: 归并排序4 花费了 = 7ms
  6. name: 归并排序5 花费了 = 4ms
  7. 平均:7
  8. 1000000
  9. name: 归并排序1 花费了 = 58ms
  10. name: 归并排序2 花费了 = 45ms
  11. name: 归并排序3 花费了 = 42ms
  12. name: 归并排序4 花费了 = 41ms
  13. name: 归并排序5 花费了 = 51ms
  14. 平均:47
  15. 10000000
  16. name: 归并排序1 花费了 = 599ms
  17. name: 归并排序2 花费了 = 589ms
  18. name: 归并排序3 花费了 = 581ms
  19. name: 归并排序4 花费了 = 569ms
  20. name: 归并排序5 花费了 = 596ms
  21. 平均: 586
  22. 50000000
  23. name: 归并排序1 花费了 = 3008ms
  24. name: 归并排序2 花费了 = 2939ms
  25. name: 归并排序3 花费了 = 2910ms
  26. name: 归并排序4 花费了 = 2902ms
  27. name: 归并排序5 花费了 = 2902ms
  28. 平均:2932

        从测试的结果分析,感觉时间复杂度不是nlogn,这个可能和数据量有关吧,再扩大数据量电脑直接报异常了,堆内存不够了。但是很明显,它能处理的数据比n^2时间复杂度的大多了,也快看很多。

时间复杂度分析:          O(nlogn) 这是该算法中最好、最坏和平均的时间性能。

         这样分析理解:归并排序需要的层级是每次对2相除,分成两个子数组。那么当有n个数的时候。最终就会分logn(底数是2)个层                                   级。 而每个层级都需要进行归并操作,此时是一个  O(n)的算法复杂度。所以理解上归并的时间复杂度nlogn.注                                   意是以2为 底(2分的时候),其实无论是不是以2为底。只是前面系数不同,而算法复杂度并不看系数,所以一                                   样。 空间复杂度为 O(n):需要额外的空间. 稳定性: 稳定             归并排序可以设计成稳定的,也可以设计成不稳定的。既然可以设计成稳定的,那么我们一般都是设计成稳定的。拆分完毕 后,对左右两个子序列排序,你保证先排左边的,左边大于和等于右边的时候把左边的放到临时数组,这样下来就是有序的。