当前位置:   article > 正文

八大排序(下)冒泡、快排、归并、计数

八大排序(下)冒泡、快排、归并、计数

这里是八大排序的下篇

其他的四个排序在这篇博客:

一、冒泡排序

前言:冒泡排序我们可谓是在熟悉不过了,第一个学会的排序就是冒泡,还记得当初刚学C语言的时候,完全不会冒泡排序,都是借鉴书上的或者别人的代码写的,知道是怎么个事,但是无从下手,好不容易写出来,不是这里错就是那里错,还挺怀念那个时候的,当然你无法同时拥有青春和对青春的感悟!话不多说,我们进入正题。

冒泡排序的思想是从左到右逐个比较,如果左边的值必右边的值要大,那么我们两个就交换位置,再继续往后面比较,如果左边的值必右边的值要小,那么我的就变成了你,再往后面比较,直到到达了末尾。

这个步骤说白了就是遍历数组,将大的值往后面放

冒泡排序代码实现 

  1. void BubbleSort(int* a, int n)
  2. {
  3. for (int i = 0; i < n; i++)
  4. {
  5. bool flag = false;
  6. for (int j = 0; j < n - i - 1; j++)
  7. {
  8. if (a[j] > a[j + 1])
  9. {
  10. swap(a[j], a[j + 1]);
  11. flag = true;
  12. }
  13. }
  14. if (flag == false)
  15. break;
  16. }
  17. }

这里需要注意的地方就是对于j的控制,因为我们一趟可以将最大的值放在最后面,因此我们后续的排序就不要管放在最后面的值, 每一趟排序都会让我们多少一次比较,因此写出   j < n - i - 1  这样的代码。

同时为了处理有序数组无需再次排序的问题,我们引入了flag,如果一趟排序完成后flag没有变化,就证明了目前的数据是有序的,因此直接break就好。但如果不是这种有序情况冒泡排序的时间复杂度是O(n^2)。

二、快速排序

不知道大家冒泡排序这块甜点吃得咋样了,现在要上一点点强度啦!!!

快速排序是一个自负的小伙子,他竟然敢直接以快速命名,有点嚣张的小伙子,让我们看看他是怎么个事。

1.霍尔法

快速排序的主要思想是找到某一个key(key我们可以随便选)该放在的位置,并使得key的左边都比他小,key的右边都比他大即可。

我们直接上图 

就如上图所言,我们拍一次序后变成了下面这样

保证了 key(6)这个位置的合理性,到这里我们就可以通过递归的方式,又分别处理它的左边和右边 ,直到保证数组有序。

那么现在我们该如果使得他变成上面这个样子呢?

我们可以发现,当左为key的时候,我们让右边找比key小的,找到后停止,继续让左边找比Key大的,如果找到后,左右两个值还没相遇,我们就交换这样就保证了左边存的是比key小的,右边存的是比key大的,直到最后相遇时,再将key和相遇点交换即可。

那么问题又来了,为什么左边为key要从右边先出发呢? 我们举个例子

找到后交换 

再次寻找 

再次交换 

到这里问题就出现了,你说我现在是左边先走还是右边先走呢?

右边先走找比key小,因此找到了4,然后左边走,走到4后left==right因此循环结束,将5和4交换,完美,没问题!

左边先走找比key大,因此找到了6,同时此时left==right 循环也结束了,完了,5和6交换位置不对了 ,寄了!

为了避免出现这种情况,我们选择key在一侧,就另一侧先走

下面我们上代码(注:代码选择的是右边为key)

  1. int PartSort1(int* a, int left, int right)
  2. {
  3. int key = right;
  4. while (left < right)
  5. {
  6. while (left < right && a[left] <= a[key])
  7. {
  8. left++;
  9. }
  10. while (left < right && a[right] >= a[key])
  11. {
  12. right--;
  13. }
  14. swap(a[left], a[right]);
  15. }
  16. swap(a[left], a[key]);
  17. return left;
  18. }

 当然,这只是一趟的过程,我们还要递归他,去找他的左和右。

下面是递归代码

  1. void QuickSort(int* a, int left, int right)
  2. {
  3. if (right - left < 1)
  4. {
  5. return;
  6. }
  7. int key = PartSort2(a, left, right);
  8. QuickSort(a, left, key - 1);
  9. QuickSort(a, key + 1, right);
  10. }

这是一种实现,也是快排之父霍尔的思路,后续基于快排的思想,又产生了  挖坑法、前后指针法

让我们继续学习吧

2.挖坑法

挖坑法思想:先定一个坑,并保存坑里的值,最左边或者最右边都可以,依然是右边找比他小的,找到后就将这个值放到上一个坑中,又将这个值设置为坑,再让左边去找比他大的,找到后依然放到坑中,这个位置又变为坑。重复循环,直到左右见面即可。

挖坑法无需左为坑右先走,因为他每找到一个就会将自己的值赋给坑,不会发生错过的现象。 

 

代码如下 ,并没有发生交换操作,只有赋值操作。

  1. //挖坑法,默认第一个为坑,前面往后走找到比他大的,
  2. //后面往前走找到比他小的 找到就把值给之前的坑
  3. //现在这个位置又形成新的坑,直到结束循环,再把最开始的坑给到最后的坑
  4. int PartSort2(int* a, int left, int right)
  5. {
  6. int key = a[left];
  7. int hole = left;
  8. while (left < right)
  9. {
  10. while (left < right && a[right] >= key)
  11. {
  12. right--;
  13. }
  14. a[hole] = a[right];
  15. hole = right;
  16. while (left < right && a[left] <= key)
  17. {
  18. left++;
  19. }
  20. a[hole] = a[left];
  21. hole = left;
  22. }
  23. a[hole] = key;
  24. return hole;
  25. }

3.前后指针法

 前后指针法思想:他是一个翻跟斗的思想,目的是将大的往后面翻,也是先找最左为key,初始化时prev在当前key的位置,cur在下一个位置,如果cur遇到比key大的,cur就++,这样便会和prev拉开差距如果cur比prev只大一个,证明他们差距是1,因此就是初始化的差距,中间没有比key大的值,无需交换,prev和cur都++,如果差距不为1,就将cur与prev的下一个进行交换,就让中间这些大个子的往后面走。

具体代码如下 

  1. int PartSort3(int* a, int left, int right)
  2. {
  3. int cur = left + 1;
  4. int prev = left;
  5. int key = a[left];
  6. while (cur <= right)
  7. {
  8. if (a[cur] < key && ++prev != cur)
  9. {
  10. swap(a[prev], a[cur]);
  11. }
  12. ++cur;
  13. }
  14. swap(a[left], a[prev]);
  15. return prev;
  16. }

这里用得很巧如果   ++prev != cur  我才需要交换(因为距离大于1了)同时还把prev给自加了

然后cur是每一次都需要自加的,这样就开始动起来了。

4.快速排序非递归

因为递归可能会导致栈溢出的问题,因此如果差距不大,能不适用递归最好就不要用递归(虽然他真得很简单,我真的喜欢)。

非递归的思想可以用栈和队列来处理,用栈就是深度优先,用队列就是广度优先,这里我们选择使用栈来处理。先将left和right入栈,再取出来left和right,然后使用前面的三个方法去处理,返回key元素该放的位置,再将left和key-1入栈,和right和key+1入栈,模拟递归的方法,直到left>=right就证明这段区间有序不需要在入栈了。

下面是代码

  1. void QuickSortNonR(int* a, int left, int right)
  2. {
  3. stack<int> st;
  4. st.push(left);
  5. st.push(right);
  6. while (!st.empty())
  7. {
  8. int end = st.top();
  9. st.pop();
  10. int begin = st.top();
  11. st.pop();
  12. if (begin >= end)
  13. {
  14. continue;
  15. }
  16. int key = PartSort1(a, begin, end);
  17. st.push(key + 1);
  18. st.push(end);
  19. st.push(begin);
  20. st.push(key - 1);
  21. }
  22. }

但是,如果数组是有序的情况下,快速排序的时间复杂度会成为O(n^2),因为排序一次你的left或right会移动到最末尾,递归起来有一边全是数据,有一边一个数据都没有,这就类似于单叉树或者链表的形状,会变得很慢很慢。

从上图可以看出来,快排去排序有序数组,甚至会比选择排序还慢,属于跟狗一桌了。因此我们需要优化一下它。

这里我们选择使用三数取中的方法,

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

并在快排中添加如下代码

  1. int midi = GetMidIndex(a, left, right);
  2. swap(a[right], a[midi]);

将key的位置和midi的位置交换一下,防止最坏结果,便可加快有序数组的排序速度

处理之后,快排速度直线上升!!! 

到此我们快速排序就算了结了。

三、归并排序

这又是一个硬菜,我们慢慢来!

1.递归版归并排序

归并排序是采用的分治的思想。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

先两个两个有序,在四个四个有序,一直到全局有序。

局部有序是将两个局部看成两个数组,两个有序数组合并为一个新的有序数组,再将这个有序数组和另外一个有序数组再次合并,直到合并完所有元素。

这需要借助一个size跟原数组一样大的数组来完成拷贝过程,并可以使用memcpy来进行拷贝。 归并排序也可以通过递归来实现

代码如下

  1. void _MergeSort(int* a, int left, int right, int* tmp)
  2. {
  3. if (left >= right)
  4. return;
  5. int mid = (left + right) / 2;
  6. _MergeSort(a, left, mid, tmp);
  7. _MergeSort(a, mid + 1, right, tmp);
  8. int begin1 = left, end1 = mid;
  9. int begin2 = mid + 1, end2 = right;
  10. int i = left;
  11. while (begin1 <= end1 && begin2 <= end2)
  12. {
  13. if (a[begin1] < a[begin2])
  14. {
  15. tmp[i++] = a[begin1++];
  16. }
  17. else
  18. {
  19. tmp[i++] = a[begin2++];
  20. }
  21. }
  22. while (begin1 <= end1)
  23. {
  24. tmp[i++] = a[begin1++];
  25. }
  26. while (begin2 <= end2)
  27. {
  28. tmp[i++] = a[begin2++];
  29. }
  30. memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
  31. }
  32. void MergeSort(int* a, int n)
  33. {
  34. int* tmp = new int[n];
  35. _MergeSort(a, 0, n - 1, tmp);
  36. delete[] tmp;
  37. }

归并排序的思路跟这道力扣题一模一样88. 合并两个有序数组

 2.非递归版归并排序

因为递归对于栈的压力很大,因此奶奶常说能不用递归最好就不要使用递归,我们尝试着不用递归处理它,首先分析栈和队列合不合适,可以发现栈和队列的是适合一直做减法,由一个较大的数值往小了走,但是归并排序需要合并起来,因此栈和队列不合适,我们只能选择尝试while能否处理

首先定义gap,gap为左右两组每一组的个数,如果先将gap定为1,便可以实现将两个sz为1的数组合并起来,形成一个sz=2的有序数组  再通过   gap*=2   通过while循坏控制这个gap,便可达到两两合并,四四合并,直到合并完整个数组。

代码如下

  1. // 归并排序非递归实现
  2. void MergeSortNonR(int* a, int n)
  3. {
  4. int gap = 1;
  5. int* tmp = new int[n];
  6. int j = 0;
  7. while (gap<n)
  8. {
  9. for (int i = 0; i < n; i += gap * 2)
  10. {
  11. int begin1 = i, end1 = i + gap - 1;
  12. int begin2 = i + gap, end2 = i + 2 * gap - 1;
  13. while (begin1 <= end1 && begin2 <= end2)
  14. {
  15. if (a[begin1] < a[begin2])
  16. {
  17. tmp[j++] = a[begin1++];
  18. }
  19. else
  20. {
  21. tmp[j++] = a[begin2++];
  22. }
  23. }
  24. while (begin1 <= end1)
  25. {
  26. tmp[j++] = a[begin1++];
  27. }
  28. while (begin2 <= end2)
  29. {
  30. tmp[j++] = a[begin2++];
  31. }
  32. }
  33. j = 0;
  34. memcpy(a, tmp, sizeof(int)* n);
  35. gap *= 2;
  36. }
  37. }

好像我们的代码看起来没啥问题,我们运行一下

OK ,完美,完美地将数组排好序了。

但是,哥哥们,有大坑啊 

当我们arr的长度不再为   n^2  时,便出现了数据越阶的问题我们画个图来看看

数据为9时 第一次归并就出现了问题,我们很简单就可以想到 这个代码只适合 长度为 n^2  这一种情况,因此我们的再做修改。

我们尝试观察哪个地方会越界

int begin1 = i, end1 = i + gap - 1;     // i<n  因此begin1不会越阶
int begin2 = i + gap, end2 = i + 2 * gap - 1;

除开begin1外,其他都有可能会越界 

end1和begin2不越界时,我们就正常排序,当越界时,我们选择直接break跳出循环,后面的无需处理,等着后面再将这个部分与前面所有的排好序数组再次归并即可。

但是我们代码中还是有问题     memcpy(a, tmp, sizeof(int)* n);  每一次都讲整个数据全部拷贝,是不够合理的,因为break后,tmp数组会缺失数据,我们可以选择归并一部分,就拷贝一部分,防止之前break后出现数据丢失的问题。因此可以将  memcpy   放到for循环内部。

因为end2不会改变,因此可以将sz修改成

memcpy(a + i, tmp + i, sizeof(int) * (end2-i+1));

但是当end2越界时,上面这句代码也会出现问题,于是我们在循环中判断一下  

  1. if (end2 >= n)
  2. {
  3. end2 = n - 1;
  4. }

  我们修正end2为n-1即可,终于将坑填完了

写出如下代码

  1. // 归并排序非递归实现
  2. void MergeSortNonR(int* a, int n)
  3. {
  4. int gap = 1;
  5. int* tmp = new int[n];
  6. int j = 0;
  7. while (gap<n)
  8. {
  9. for (int i = 0; i < n; i += gap * 2)
  10. {
  11. int begin1 = i, end1 = i + gap - 1;
  12. int begin2 = i + gap, end2 = i + 2 * gap - 1;
  13. if (end1 >= n || begin2 >= n)
  14. {
  15. break;
  16. }
  17. if (end2 >= n)
  18. {
  19. end2 = n - 1;
  20. }
  21. while (begin1 <= end1 && begin2 <= end2)
  22. {
  23. if (a[begin1] < a[begin2])
  24. {
  25. tmp[j++] = a[begin1++];
  26. }
  27. else
  28. {
  29. tmp[j++] = a[begin2++];
  30. }
  31. }
  32. while (begin1 <= end1)
  33. {
  34. tmp[j++] = a[begin1++];
  35. }
  36. while (begin2 <= end2)
  37. {
  38. tmp[j++] = a[begin2++];
  39. }
  40. memcpy(a + i, tmp + i, sizeof(int) * (end2-i+1));
  41. }
  42. j = 0;
  43. gap *= 2;
  44. }
  45. }

终于将sz不等于n^2处理好了!!!

艰难的归并排序非递归版就完成了!

归并排序的时间复杂度是O(n*logn),跟有无序无关。

 四、计数排序

计数排序通常使用的很少,但是在某些特定的条件下很好用,比如数组是一段非常密集的重复数组,也就是数据很多重复且数组的  max-min   范围很小。

他的思想是先遍历一遍数组,找出最大值和最小值,并相减,然后开辟一个 max-min 大小的空间,用这块空间去统计原数组的值出现的次数,最后再拷贝到原数组中。

代码如下:

  1. void CountSort(int* a, int n)
  2. {
  3. int min = a[0], max = a[0];
  4. for (int i = 0; i < n; i++)
  5. {
  6. if (min > a[i])
  7. {
  8. min = a[i];
  9. }
  10. if (max < a[i])
  11. {
  12. max = a[i];
  13. }
  14. }
  15. int range = max - min + 1;
  16. int* count = new int[range];
  17. memset(count, 0, range * sizeof(int));
  18. for (int i = 0; i < n; i++)
  19. {
  20. count[a[i] - min]++;
  21. }
  22. int k = 0;
  23. for (int i = 0; i < range; i++)
  24. {
  25. while (count[i]--)
  26. {
  27. a[k++] = i + min;
  28. }
  29. }
  30. }

用得很少,思路不也难,非主流排序。

总结

希望大家能够点点赞,求求。

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

闽ICP备14008679号