当前位置:   article > 正文

排序篇(叁)终篇

排序篇(叁)终篇

-----------远途的路固然值得探索,但当下的路也应踏实


上篇讲了大部分的排序思想,那么这篇博客呢,也是承接上篇博客的排序。围绕比较复杂的快排、归并排序来讲。

(1)快速排序:

快速排序的思想很简单:

任取一个值作为key值(比较值)。要的结果是——比key大的在右边,比key小的在左边。分成<key 的和>key的区间。并重复一样的操作。直到让每个元素在相应位置为止。

①PartSort1(hoare版)

hoare 也称 左右指针法。是最早的快排思想。

  1. int left = 0;
  2. int right = n - 1;
  3. //找到key
  4. int keyi = left;
  5. //一趟key
  6. while (left < right)
  7. {
  8. //先让right走?
  9. while (left< right && a[right] >= a[keyi])
  10. right--;
  11. while (left < right && a[left] <= a[keyi])
  12. left++;
  13. Swap(&a[left], &a[right]);
  14. }
  15. //最后left 再与key值 交换 把key排到该排的位置
  16. Swap(&a[left], &a[keyi]);

上图说明,进行一次快排,可以让key 排到自己有序序列的位置。

下面的问题是常常会犯的错误:

为什么要让right(右指针先走)?

 为什么比较要+等号?前面不是已经有限制条件了吗,为什么还要加?


实现多趟快排1(递归版):

 因为要递归,仅仅需要上面hoare找到返回排好的k 的分离点,此时分离点不是在keyi的下标而是在left的下标。 把这个函数抽象出来

  1. //hoare
  2. int PartSort1(int* a,int left,int right)
  3. {
  4. //找到key
  5. int keyi = left;
  6. //一趟key
  7. while (left < right)
  8. {
  9. //先让right走?
  10. while (left < right && a[right] >= a[keyi])
  11. right--;
  12. while (left < right && a[left] <= a[keyi])
  13. left++;
  14. Swap(&a[left], &a[right]);
  15. }
  16. Swap(&a[left], &a[keyi]);
  17. return left; //left的值和keyi发生交换 但keyi在的位置是left!!
  18. }

因为抽象出来的函数,需要left 和 right 下标 ,所以对原先QuickSort函数就要改一改参数。

  1. void QuickSort(int* a,int left,int right)
  2. {
  3. int keyIndext = PartSort1(a, left, right);
  4. //此时得到keyIndex 把原数组分为两个区间
  5. //[left,keyIndex-1] [keyIndex+1,right]
  6. QuickSort(a, left, keyIndex - 1);
  7. QuickSort(a, keyIndex + 1, right);
  8. }

既然是递归,那一定得有递归的终止条件>

  1. void QuickSort(int* a,int left,int right)
  2. {
  3. if (left >= right) //终止条件
  4. return;
  5. int keyIndex = PartSort1(a, left, right);
  6. //此时得到keyIndex 把原数组分为两个区间
  7. //[left,keyIndex-1] [keyIndex+1,right]
  8. QuickSort(a, left, keyIndex - 1);
  9. QuickSort(a, keyIndex + 1, right);
  10. }

最后也就排序完成啦~

 此刻有个小插曲,找key值的另外两种方法~:

①挖坑法:

什么是挖坑法呢。其实就是把key值的位置,当成坑。先从右边找小的值,填入。此时右边小的值又成为一个新的坑……

  1. int PartSort2(int a[], int left, int right)
  2. {
  3. int keyIndex = GetMid(a, left, right);
  4. std::swap(a[keyIndex], a[left]);
  5. //比较值
  6. int key = a[left];
  7. //从左边填
  8. int hole = left;
  9. while (left < right)
  10. {
  11. //左边出现坑 在右边找比pivot小的值
  12. while (left < right && a[right] >= key)
  13. {
  14. --right;
  15. }
  16. //找到了 进行左填坑 自己变为坑
  17. a[hole] = a[right];
  18. hole = right;
  19. //找到了 进行右填坑 自己变为坑
  20. while (left < right && a[left] <= key)
  21. {
  22. ++left;
  23. }
  24. a[hole] = a[left];
  25. hole = left;
  26. }
  27. a[hole] = key;
  28. return hole;
  29. }

 

②前后指针法:

  1. int cur = left+1;
  2. int prev = left;
  3. int keyi = left;
  4. while (cur <=right)
  5. {
  6. if (a[cur] < a[keyi] && prev++ != cur)
  7. {
  8. //这里解释为什么加个条件判断..因为如果cur prev都指向同一个数,也就没必要交换。当然不加也要的
  9. Swap(&a[cur], &a[prev]);
  10. }
  11. cur++;
  12. }
  13. Swap(&a[prev], &a[keyi]);
  14. return prev;

 

关于找key值也就告一段落。

可能有人会问,如果是升序,每次找的key都是最小的。每次排序都会从第一个到最后一个排,最坏的情况下达到的时间复杂度O(N^2),还不如直接插入排序来得直接、简洁。

针对这个问题,就有必要对快排进行一定的优化。


快速排序的优化:

①key值的该如何找?

我们不难看出,如果key值取得足够得二分,那么快排分的区间越接近二分,效率也越高。

所以就想到一个三数取中的办法,确定key值。

  1. int GetMid(int* a, int left, int right)
  2. {
  3. int MidIndex = (left + right) >> 1;
  4. if (a[left] < a[MidIndex])
  5. {
  6. if (a[MidIndex] < a[right])
  7. {
  8. return MidIndex;
  9. }//a[MidIndex] > a[right]
  10. else if (a[right] < a[left])
  11. {
  12. return left;
  13. }
  14. else
  15. {
  16. return right;
  17. }
  18. }
  19. else //a[minIndex] < a[left]
  20. {
  21. if (a[MidIndex] > a[right])
  22. {
  23. return MidIndex;
  24. }
  25. else if (a[right] > a[left])
  26. {
  27. return left;
  28. }
  29. else
  30. {
  31. return right;
  32. }
  33. }
  34. }

三数取中的代码较为绕,但是细细去分还是能够很好的理解的。

截图一部分:

 ②小区间的优化:

在现如今的编译器和cpu,递归造成的性能损耗,并不那么严重。只是可能,当递归的层次足够深时,会导致栈溢出。而快排的递归排序,把它看出接近二分法的递归,建立栈帧,有很多次。

越递归到下面,建立的栈帧数量越多。但是其本身的数字群很小。

所以可以借助其他排序,来砍掉这部分冗杂的、数字量小,数量大的群体。

  1. void InsertSort(int* a, int n)
  2. {
  3. for (int i = 0;i < n-1;i++)
  4. {
  5. int end = i;
  6. int tmp = a[end + 1];
  7. while (end > 0)
  8. {
  9. if (a[end] > tmp)
  10. {
  11. a[end + 1] = a[end];
  12. end--;
  13. }
  14. else //end 减 为-1 或tmp大的时候
  15. {
  16. break;
  17. }
  18. }
  19. a[end + 1] = tmp;
  20. }
  21. }
  22. if (begin - end > 10)
  23. {
  24. QuickSort(a, begin, keyIndex - 1);
  25. QuickSort(a, keyIndex + 1, end);
  26. }
  27. else
  28. {
  29. InsertSort(a+begin,end- begin +1);
  30. }

InsertSort 的参数设计需要多加考量。建议就是多画图。


实现快排多趟2(非递归):

  1. void QuickSortNorR(int* a, int begin, int end)
  2. {
  3. st Stack;
  4. InitStack(&Stack);
  5. //先压的右边
  6. StackPush(&Stack, begin);
  7. StackPush(&Stack, end);
  8. //不为空就不出来
  9. while (!EmptyStack(&Stack))
  10. {
  11. //先压 的end 就先取出begin
  12. int right = StackTop(&Stack);
  13. StackPop(&Stack);
  14. int left = StackTop(&Stack);
  15. StackPop(&Stack);
  16. //0 ,9
  17. //找三数
  18. int keyi = PartSort1(a, left, right);
  19. //压入左右区间
  20. if (left < keyi - 1)
  21. {
  22. StackPush(&Stack, left);
  23. StackPush(&Stack, keyi - 1);
  24. }
  25. if (keyi + 1 < end)
  26. {
  27. StackPush(&Stack, keyi + 1);
  28. StackPush(&Stack, end);
  29. }
  30. }
  31. DelStack(&Stack);
  32. }

快速排序的性能:

快排是很强悍的排序方法,在做到上面所述的三数取中、小区间优化(较大的数有明显变化)

时间复杂度可以达到:O(N*logN);

在使用栈的情况,空间复杂度为O(N);

但不具有稳定性。

 (2)归并排序

基本思想:

该算法是采用分治法。将区间切割为不能再分割的区间,并有序化。再将已有序的子序列合并,得到完全有序的序列。
---------归并算这些排序里面较难理解的。所以有句话说——只可意会不可言传~

归并排序的实现1(递归):

 分割后的合并,需要借助新的数组来保存 合并后的值,以便于拷贝回到原区间。

  1. void MergeSort(int* a, int begin,int end)
  2. {
  3. //开辟同样大小的数组
  4. int* tmp = (int*)malloc(sizeof(int) * n);
  5. //因为牵涉到递归,不用每次都开辟一个同样大小的空间数组
  6. _MergeSort(a, begin,end,tmp);
  7. free(tmp);
  8. }
  9. void TestMergeSort()
  10. {
  11. int arr[] = { 10,6,7,1,3,9,4,2 };
  12. printf("MergeSortBefore:");
  13. PrintSort(arr, sizeof(arr) / sizeof(int));
  14. MergeSort(arr,0,sizeof(arr) / sizeof(int)-1); //参数这里选择传三个
  15. printf("MergeSortRAfter:");
  16. PrintSort(arr, sizeof(arr) / sizeof(int));
  17. }

对子函数_MergeSort的编写,主要就分为两个大的方向。

1.分割  2.归并 并写回:

  1. void _MergeSort(int* a, int left, int right, int* tmp)
  2. {
  3. //分割递归终止条件
  4. if (left >= right)
  5. return;
  6. //用中数 分割
  7. int mid = (left + right) >> 1;
  8. //[left,mid] [mid+1,right];
  9. _MergeSort(a, left, mid, tmp);
  10. _MergeSort(a, mid + 1, right, tmp);
  11. //合并
  12. //合并就类似于合并两个有序数组
  13. //两区间 两数组
  14. int begin1 = left, end1 = mid;
  15. int begin2 = mid + 1, end2 = right;
  16. //小的值拿下来
  17. int i = left; //为什么要由left控制?因为每个区间开头就是left
  18. while (begin1 <= end1 && begin2 <= end2) //其中一个数组终止 就结束
  19. {
  20. if (a[begin1] < a[begin2])
  21. tmp[i++] = a[begin1++];
  22. else
  23. tmp[i++] = a[begin2++];
  24. }
  25. while (begin1 <= end1)
  26. tmp[i++] = a[begin1++];
  27. while (begin2<=end2)
  28. tmp[i++] = a[begin2++];
  29. //拷回去
  30. //j从什么地方开始 从什么地方结束?
  31. for (int j = left;j <= right;j++)
  32. {
  33. a[j] = tmp[j];
  34. }
  35. }

理解分割、归并的过程,通过调试是很有效的方法。希望能帮助你。 


归并排序的实现2(非递归) :

什么!还有非递归?一个递归就弄得晕头转向了还来个非递归?!

是的。

因为非递归和递归版的归并的代码部分相同,所以先抽象出这个部分。

非递归版,和递归版,无非只差别在,分割区间上。需要手动分割

  1. void _MergeSortNorR(int* a,int begin1,int end1,int begin2,int end2,int* tmp)
  2. {
  3. int i = left; //为什么要由left控制?因为每个区间开头就是left
  4. while (begin1 <= end1 && begin2 <= end2) //其中一个数组终止 就结束
  5. {
  6. if (a[begin1] < a[begin2])
  7. tmp[i++] = a[begin1++];
  8. else
  9. tmp[i++] = a[begin2++];
  10. }
  11. while (begin1 <= end1)
  12. tmp[i++] = a[begin1++];
  13. while (begin2 <= end2)
  14. tmp[i++] = a[begin2++];
  15. //拷回去
  16. //j从什么地方开始 从什么地方结束?
  17. for (int j = left;j <= right;j++)
  18. {
  19. a[j] = tmp[j];
  20. }
  21. }

所以在设计,非递归版归并的子函数时,参数较多。

 所以我们引入了gap的概念,作为归并的序列有几个。

根据gap的大小,即这次归并要归多少个数~算出gap和下标的关系。

 当然这不是最终的代码,此代码仍然有会出现bug的情况。

  1. void MergeSortNorR(int* a, int n)
  2. {
  3. int* tmp =(int*)malloc(sizeof(int)*n);
  4. int gap = 1;
  5. while (gap < n)
  6. {
  7. //注每次循环i 仅能归并一个
  8. for (int i = 0;i < n;i += 2*gap) //i+=2*gap; 下一个区间
  9. {
  10. //取出每个gap的区间
  11. int begin1 = i, end1 = i + gap - 1, begin2 = i + gap, end2 = i + 2 * gap - 1;
  12. _MergeSortNorR(a, begin1, end1, begin2, end2, tmp);
  13. }
  14. gap *= 2; // 控制每次归并的个数
  15. }
  16. }

 特殊情况:

  1. void MergeSortNorR(int* a, int n)
  2. {
  3. int* tmp =(int*)malloc(sizeof(int)*n);
  4. int gap = 1;
  5. while (gap < n)
  6. {
  7. //注每次循环i 仅能归并一个
  8. for (int i = 0;i < n;i += 2*gap) //i+=2*gap;
  9. {
  10. //取出每个gap的区间
  11. int begin1 = i, end1 = i + gap - 1, begin2 = i + gap, end2 = i + 2 * gap - 1;
  12. //如果第二个区间不存在
  13. if (begin2 >=n)
  14. {
  15. break; //也就不归并了
  16. }
  17. //如果是第二区间缺少 造成越界
  18. if (end2 >=n)
  19. {
  20. //修正
  21. end2 = n - 1;
  22. }
  23. _MergeSortNorR(a, begin1, end1, begin2, end2, tmp);
  24. }
  25. gap *= 2;
  26. }
  27. }

最后也完成了~ 


感谢你的阅读,祝你好运~

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/572763
推荐阅读
相关标签
  

闽ICP备14008679号