• 交换排序—冒泡排序和快速排序


    目录​​​​​​​

    一、冒泡排序

           冒泡排序特性总结: 

    二、快速排序

    hoare法 

    挖坑法 

    前后指针法 

    快速排序特性总结 

           快排优化 

    三数取中

    小区间优化 

    快排非递归


    一、冒泡排序

    🤔基本思想:

    冒泡排序(Bubble Sort)是一种较简单的排序算法,冒泡排序的思想就是重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。

    🎥动态演示:

    ✍🏻代码展示:

    1. void Swap(int* pa, int* pb)
    2. {
    3. int tmp = *pa;
    4. *pa = *pb;
    5. *pb = tmp;
    6. }
    7. //冒泡排序
    8. void BubbleSort(int* a, int n)
    9. {
    10. for (int i = 0; i < n; i++)
    11. {
    12. //单趟
    13. for (int j = 1; j < n - i; j++)
    14. {
    15. if (a[j - 1] > a[j])
    16. {
    17. Swap(&a[j], &a[j - 1]);
    18. }
    19. }
    20. }
    21. }

    ⏩优化冒泡:

    当我们的数组本来就是有序或者是接近有序的,那我们继续采用上述算法仍然每一趟都进行比较相邻的两个数字大小,这样是不是有点多余,甚至说是提升了其时间复杂度。现在我们可以定义一个常量exchange初始化为0,如果发生比较,那么就将exchange赋值为1,这样在排序的过程中当我们数组为有序的时候直接就能得到结果。

    ✍🏻代码展示:

    1. void Swap(int* pa, int* pb)
    2. {
    3. int tmp = *pa;
    4. *pa = *pb;
    5. *pb = tmp;
    6. }
    7. //冒泡排序
    8. void BubbleSort(int* a, int n)
    9. {
    10. for (int i = 0; i < n; i++)
    11. {
    12. int exchange = 0;
    13. //单趟
    14. for (int j = 1; j < n - i; j++)
    15. {
    16. if (a[j - 1] > a[j])
    17. {
    18. exchange = 1;
    19. Swap(&a[j], &a[j - 1]);
    20. }
    21. }
    22. if (exchange == 0)
    23. {
    24. break;
    25. }
    26. }
    27. }

    冒泡排序特性总结: 

    📝:冒泡排序是最简单排序之一,简单意味着容易理解同时也意味着效率低。

    🍒:时间复杂度:O(N^2)

    🍒:空间复杂度:O(1)

    🍒:稳定性:稳定

    二、快速排序

     🤔基本思想:

    快速排序(Quick Sort)是从冒泡排序算法演变而来的,实际上是在冒泡排序基础上的递归分治法。快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。

    快速排序可以用三种方法实现:

    hoare法

    挖坑法

    前后指针法

    hoare法 

    🤔基本思想:

    hoare法实现快排分为以下步骤:

    1、选出key,key可以是第一个数也可以是最后一个数。

    2、定义下标L和R,L往左走,R往右走。如果选出的key为第一个数则R先走,否则L先走。

    3、R(L)先向前走,找到比key小的位置停下,然后L(R)向后走,找到比key大的值停下。

    4、交换L下标和R下标所对应的值。

    5、继续遍历,同上面原则。

    6、当L和R相遇,把相遇位置的值与key位置的值进行交换。

    排完一趟后可以得到:key左边的值都比key小,右边的值都比key大。

    ⭐:当我们选第一个数为key时,R先走已经决定了相遇位置的值比key小。选最后一个数为key时,L先走也已决定了相遇位置的值比key大。

    🎨画图演示:

    ✍🏻代码展示:

    接下来我们用hoare法进行单趟排序:

    1. int PartSort(int* a, int left, int right)
    2. {
    3. int keyi = left; //选最左边为key
    4. while (left < right)
    5. {
    6. //右边先走,找到比key小的停下
    7. while (lefta[keyi]) //防止数组越界,用left
    8. {
    9. right--;
    10. }
    11. //左边走,找到比key大的停下
    12. while (left < right && a[left] < a[keyi])
    13. {
    14. left++;
    15. }
    16. //交换数据
    17. Swap(&a[left], &a[right]);
    18. }
    19. //left和right相遇,交换值
    20. Swap(&a[left], &a[keyi]);
    21. return left;
    22. }

     单趟排完序后我们发现key左边的值都小于key,右边的值都大于key,key已经到达了其正确位置,此时我们只需要使key左边数据和右边数据有序即可,这里我们采用递归+分治思想。

    ✍🏻代码展示:

    1. //hoare法
    2. int PartSort(int* a, int left, int right)
    3. {
    4. int keyi = left; //选最左边为key
    5. while (left < right)
    6. {
    7. //右边先走,找到比key小的停下
    8. while (lefta[keyi]) //防止数组越界,用left
    9. {
    10. right--;
    11. }
    12. //左边走,找到比key大的停下
    13. while (left < right && a[left] < a[keyi])
    14. {
    15. left++;
    16. }
    17. //交换数据
    18. Swap(&a[left], &a[right]);
    19. }
    20. //left和right相遇,交换值
    21. Swap(&a[left], &a[keyi]);
    22. return left;
    23. }
    24. //快速排序
    25. void QuickSort(int* a, int begin, int end)
    26. {
    27. //子区间只有一个值或者不存在就是最小子问题
    28. if (begin >= end)
    29. {
    30. return;
    31. }
    32. int keyi = PartSort(a, begin, end);
    33. //分成[begin,keyi-1]和[keyi+1,end]两个左右区间进行递归
    34. QuickSort(a, begin, keyi - 1);
    35. QuickSort(a, keyi + 1, end);
    36. }

    挖坑法 

    🤔基本思想:

    挖坑法相对于hoare法本质上并没有做出较大的改变,只不过相对于hoare法,挖坑法不需要考虑为什么最终相遇位置比key小以及为什么左边做key,右边先走。

    挖坑法实现快排分为以下步骤:

    1、定义变量key保存最左边(右边)的值,此位置留出空位。

    2、定义下标L和R,L往左走,R往右走。

    3、R(L)先走,找到比key小(大)的位置停下。

    4、把R(L)下标所对应的值放入先前的空位,R(L)位置留出新的空位。

    5、再让L(R)向后(前)走,找到比key大(小)的位置停下。

    6、把L(R)下标所对应的值放入先前的空位,L(R)位置留出新的空位。

    7、R(L)继续走,重复上面步骤。

    8、直到L和R相遇,把key的值放入L和R相遇的坑位。

    🎨画图演示:

    ✍🏻代码展示:

    1. //挖坑法
    2. int PartSort2(int* a, int left, int right)
    3. {
    4. //key保存最左边的值
    5. int key = a[left];
    6. //left位置设为空位
    7. int pit = left;
    8. while (left < right)
    9. {
    10. //右边先走,找小于key的值
    11. while (left < right && a[right] >= key)
    12. {
    13. right--;
    14. }
    15. //找到小于key的值就把right下标所对应的值赋给坑位,并把自己设为坑位
    16. a[pit] = a[right];
    17. pit = right;
    18. //左边走,找大于key的值
    19. while (left < right && a[left] <= a[right])
    20. {
    21. left++;
    22. }
    23. //找到大于key的值就把left下标所对应的值赋给坑位,并把自己设为坑位
    24. a[pit] = a[left];
    25. pit = left;
    26. }
    27. //此时L和R相遇,把key的值赋给坑位
    28. a[pit] = key;
    29. return pit;
    30. }
    31. //快速排序
    32. void QuickSort(int* a, int begin, int end)
    33. {
    34. //子区间只有一个值或者不存在就是最小子问题
    35. if (begin >= end)
    36. {
    37. return;
    38. }
    39. int keyi = PartSort2(a, begin, end);
    40. //分成[begin,keyi-1]和[keyi+1,end]两个左右区间进行递归
    41. QuickSort(a, begin, keyi - 1);
    42. QuickSort(a, keyi + 1, end);
    43. }

    前后指针法 

    🤔基本思想:

    前后指针法思路相对于前两种会有所不同。

    前后指针法实现快排分为以下步骤:

    🌜最左边为key:

    1、定义变量key保存最左边的值。

    2、定义下标prev指向第一个位置,cur指向下一个位置。

    3、若cur指向的值小于key,prev和cur均向后移。

    4、当cur指向的值大于key,prev不动,cur继续往后移。

    5、当cur指向的值小于key,prev后移一位,prev和cur指向的值进行交换,cur++。

    6、重复上述步骤,当cur越界时,交换此时的prev和key的值。

    🎯:通过对上面步骤的分析我们知道在整个过程中prev所指向的值始终是小于key的,实质上就是让cur遍历数组,把小于key的放前边,大于key的放前边。

    🌛最右边为key:

    1、定义变量key保存最右边的值。

    2、定义下标cur指向第一个位置,prev指向前一个位置。

    3、若cur指向的值小于key,prev和cur均向后移。

    4、当cur指向的值大于key,prev不动,cur继续往后移。

    5、当cur指向的值小于key,prev后移一位,prev和cur指向的值进行交换,cur++。

    6、重复上述步骤,当cur到达数组末端时,prev++,交换此时的prev和key的值。

    🎨画图演示:

    🌜最左边为key

     🌛最右边为key

     ✍🏻代码展示:

     🌜最左边为key

    1. //前后指针法
    2. int PartSort3(int* a, int left, int right)
    3. {
    4. int key = left;
    5. int prev = left;
    6. int cur = prev + 1;
    7. while (cur <= right)
    8. {
    9. if (a[cur] < a[key] && a[++prev] != a[cur])
    10. {
    11. Swap(&a[prev], &a[cur]);
    12. }
    13. cur++;
    14. }
    15. Swap(&a[prev], &a[key]); //cur越界,交换prev和key所指向的值
    16. return prev;
    17. }
    18. //快速排序
    19. void QuickSort(int* a, int begin, int end)
    20. {
    21. //子区间只有一个值或者不存在就是最小子问题
    22. if (begin >= end)
    23. {
    24. return;
    25. }
    26. int keyi = PartSort3(a, begin, end);
    27. //分成[begin,keyi-1]和[keyi+1,end]两个左右区间进行递归
    28. QuickSort(a, begin, keyi - 1);
    29. QuickSort(a, keyi + 1, end);
    30. }

    🌛最右边为key

    1. //前后指针法
    2. int PartSort3(int* a, int left, int right)
    3. {
    4. int key = right;
    5. int prev = left-1;
    6. int cur = left;
    7. while (cur < right)
    8. {
    9. if (a[cur] < a[key] && a[++prev] != a[cur])
    10. {
    11. Swap(&a[prev], &a[cur]);
    12. }
    13. cur++;
    14. }
    15. Swap(&a[++prev], &a[key]); //cur到数组末尾,prev++,交换prev和key所指向的值
    16. return prev;
    17. }
    18. //快速排序
    19. void QuickSort(int* a, int begin, int end)
    20. {
    21. //子区间只有一个值或者不存在就是最小子问题
    22. if (begin >= end)
    23. {
    24. return;
    25. }
    26. int keyi = PartSort3(a, begin, end);
    27. //分成[begin,keyi-1]和[keyi+1,end]两个左右区间进行递归
    28. QuickSort(a, begin, keyi - 1);
    29. QuickSort(a, keyi + 1, end);
    30. }

    快速排序特性总结 

    📝:快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序。

    🍒:时间复杂度:O(N*logN)

    🍒:空间复杂度:O(logN)

    🍒:稳定性:不稳定

    ⭐时间复杂度分析:

    最好情况:每次选的key都是中位数,此时key左边序列和后边序列相同。时间复杂度为O(N*logN)

     最坏情况:每次选的key都是最大或者最小的数。时间复杂度为O(N^2)

     当我们要排序的数组为有序或者是接近有序的时候就是最坏情况,因为最坏情况要多次递归,如果我们要排序的数据量很大以至于递归次数过多导致栈溢出。这时候我们就需要将快排进行优化一下。

    快排优化 

    三数取中

    🤔基本思想:

    为了解决快速排序存在的弊端,我们可以一次性选出三个数—我们取数组最左边、最右边和中间的数。我们比较三个数的大小,选出既不是最大的也不是最小的那个数为key,这样针对数组本来就是有序或者接近有序情况就能有效解决,同时也不影响随机数组。对快速排序进行了一个整体优化。

    ⭐:三数取中对hoare法、挖坑法、前后指针法皆适用。

    我们以前后指针法举例:

    1. //三数取中
    2. int GetMidIndex(int* a, int left, int right)
    3. {
    4. int mid = (left + right) / 2;
    5. if (a[left] < a[mid])
    6. {
    7. if (a[mid] < a[right])
    8. return mid;
    9. else if (a[left] < a[right])
    10. return right;
    11. else
    12. return left;
    13. }
    14. else
    15. {
    16. if (a[right] > a[left])
    17. return left;
    18. else if (a[mid] > a[right])
    19. return mid;
    20. else
    21. return right;
    22. }
    23. }
    24. //前后指针法
    25. int PartSort3(int* a, int left, int right)
    26. {
    27. int midi = GetMidIndex(a, left, right);
    28. Swap(&a[midi], &a[left]);
    29. int key = left;
    30. int prev = left;
    31. int cur = prev+1;
    32. while (cur <= right)
    33. {
    34. if (a[cur] < a[key] && a[++prev] != a[cur])
    35. {
    36. Swap(&a[prev], &a[cur]);
    37. }
    38. cur++;
    39. }
    40. Swap(&a[prev], &a[key]); //cur越界,交换prev和key所指向的值
    41. return prev;
    42. }
    43. //快速排序
    44. void QuickSort(int* a, int begin, int end)
    45. {
    46. //子区间只有一个值或者不存在就是最小子问题
    47. if (begin >= end)
    48. {
    49. return;
    50. }
    51. int keyi = PartSort3(a, begin, end);
    52. //分成[begin,keyi-1]和[keyi+1,end]两个左右区间进行递归
    53. QuickSort(a, begin, keyi - 1);
    54. QuickSort(a, keyi + 1, end);
    55. }

    小区间优化 

    🤔基本思想:

    对于快排特殊情况:每次选的key都是中位数,然后将数组分为平均的两份,继续分成4份、8份.......这个过程其实就是二分,和二叉树的样子差不多,而快排递归调用的简化图其实就类似于二叉树。假设数组的数据量为1000,那么递归调用就需要走logN层也就是10层,而当我们递归的层次越深,需要的递归次数也就越多,而小区间优化就是为了解决这里的问题,针对最后的小区间我们进行其他排序算法,插入排序就是一种很好的选择,这样就减少了大量的递归次数。

    ✍🏻代码展示:

    1. //直接插入排序
    2. void InsertSort(int* a, int n)
    3. {
    4. //i的取值范围为[0,n-2]
    5. for (int i = 0; i < n - 1; i++)
    6. {
    7. //单趟排序
    8. int end = i;
    9. int tmp = a[end + 1];
    10. while (end >= 0)
    11. {
    12. if (tmp < a[end]) //插入数字小于所比较数字
    13. {
    14. a[end + 1] = a[end];//将所比较数字往后挪
    15. end--;
    16. }
    17. else
    18. {
    19. break;
    20. }
    21. a[end + 1] = tmp;
    22. }
    23. }
    24. }
    25. //三数取中
    26. int GetMidIndex(int* a, int left, int right)
    27. {
    28. int mid = (left + right) / 2;
    29. if (a[left] < a[mid])
    30. {
    31. if (a[mid] < a[right])
    32. return mid;
    33. else if (a[left] < a[right])
    34. return right;
    35. else
    36. return left;
    37. }
    38. else
    39. {
    40. if (a[right] > a[left])
    41. return left;
    42. else if (a[mid] > a[right])
    43. return mid;
    44. else
    45. return right;
    46. }
    47. }
    48. //前后指针法
    49. int PartSort3(int* a, int left, int right)
    50. {
    51. int midi = GetMidIndex(a, left, right);
    52. Swap(&a[midi], &a[left]);
    53. int key = left;
    54. int prev = left;
    55. int cur = prev+1;
    56. while (cur <= right)
    57. {
    58. if (a[cur] < a[key] && a[++prev] != a[cur])
    59. {
    60. Swap(&a[prev], &a[cur]);
    61. }
    62. cur++;
    63. }
    64. Swap(&a[prev], &a[key]); //cur越界,交换prev和key所指向的值
    65. return prev;
    66. }
    67. //快速排序
    68. void QuickSort(int* a, int begin, int end)
    69. {
    70. //子区间只有一个值或者不存在就是最小子问题
    71. if (begin >= end)
    72. {
    73. return;
    74. }
    75. //小区间用插入排序算法
    76. if (end - begin + 1 <= 10)
    77. {
    78. InsertSort(a + begin, end - begin + 1);
    79. }
    80. else
    81. {
    82. int keyi = PartSort3(a, begin, end);
    83. //分成[begin,keyi-1]和[keyi+1,end]两个左右区间进行递归
    84. QuickSort(a, begin, keyi - 1);
    85. QuickSort(a, keyi + 1, end);
    86. }
    87. }

    快排非递归

    🤔基本思想:

    上面的快排我们都使用的是递归,对于递归存在的最大问题还是栈溢出,那么我们能不能不用递归,用一种别的办法去实现快排呢?接下来我们来看看新的思路—用栈实现快排非递归。

    所谓快排递归无非就是把数组切割成小的部分再排、再切割、继续排,继续切割,把一个数组切割成若干个极小的部分,而如何控制切割部分的大小,其实就是函数的参数begin和end,begin和end限制了切割后数组的大小。递归调用需要开辟新的栈帧,而所存储的也正是排序过程中所要控制的区间,递归改非递归重要的就是我们应如何用一种新的方式存储所要控制的区间。而这里,栈就派上了用场。

    例如我们要用非递归排序下面数组:

    我们创建栈先存入数组的begin和end下标:0和5

    入完这两个值后取出这两个值分别赋值给left和right,然后利用前后指针法进行排序并返回keyi值,keyi值为2,此时a[keyi]已经排到正确位置。

     接下来我们以keyi为分界线,将数组分为两个区间:[left,keyi-1]、[keyi+1,right]。此时我们再把这两块区间入栈。

     我们根据以上思路,取出栈尾两个元素赋值给left和right,继续排,直到栈为空。

    ✍🏻代码展示:

    1. //快排非递归
    2. void QuickSort3(int* a, int begin, int end)
    3. {
    4. ST st;
    5. StackInit(&st);
    6. //第一块区间入栈
    7. StackPush(&st, begin);
    8. StackPush(&st, end);
    9. while (!StackEmpty(&st))//栈不为空就继续
    10. {
    11. int right = StackTop(&st);
    12. StackPop(&st);
    13. int left = StackTop(&st);
    14. StackPop(&st);
    15. //使用前后指针法排序
    16. int keyi = PartSort3(a, left, right);
    17. //[left,keyi-1] [keyi+1,right]
    18. if (left < keyi - 1)
    19. {
    20. StackPush(&st, left);
    21. StackPush(&st, keyi - 1);
    22. }
    23. if (keyi + 1 < right)
    24. {
    25. StackPush(&st, keyi + 1);
    26. StackPush(&st, right);
    27. }
    28. }
    29. StackDestory(&st);
    30. }

  • 相关阅读:
    MyBatis在循环内查询序列值重复解决方法
    4月04日,每日信息差
    c语言中文件的定义
    如何选择一个好的简历模板
    EditorConfig + ESLint + Prettier 实现代码规范化
    单核CPU如何执行多线程
    三、虚拟机的迁移和删除
    从零实现FFmpeg6.0+ SDL2播放器
    2023 极术通讯-汽车“新四化”路上,需要一片安全山海
    51单片机应用从零开始(四)
  • 原文地址:https://blog.csdn.net/weixin_60718941/article/details/125978155