当前位置:   article > 正文

数据结构-C语言-排序(3)

数据结构-C语言-排序(3)

        代码位置:test-c-2024: 对C语言习题代码的练习 (gitee.com)

一、前言:

1.1-排序定义:

        排序就是将一组杂乱无章的数据按照一定的规律(升序或降序)组织起来。(注:我们这里的排序采用的都为升序)

1.2-排序分类:

常见的排序算法:

1.3-算法比较:

1.4-目的:

        今天,我们这里要实现的是快速排序

1.5-快速排序定义:

        通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,然后分别对这两部分记录继续进行排序,以达到整个序列有序。

二、快速排序-key的选择:

2.1-直接在left和right中选择:

        这种选择方法具有局限性如果排序的序列已经为升序的情况下,根据快速排序的定义,我们可知,快速排序的话,会确定一个值的位置也就是key,这个值的作用就是把数据分割成独立的两部分,一部分是比他大,一部分是比他小,而如果是升序的情况下直接选择left,选出的left的值是最小的,也就是说右面的部分是N-1个数据,而如果是用递归的方式实现的快排,那么就需要递归N次,也就是建立N个栈才能实现最终排序的操作,如果数据量大就很有可能出现栈溢出的情况。

该情况下递归的图片如图所示:

2.2-随机选择key:

        随机选择key,也就是说,在数组下标范围内,随机生成一个下标,采用这个下标位置的数据值作为key这样的情况下,我们就大大降低了选出的key是最小值的情况。能有效地减少栈溢出的情况。

2.3-三数取中:

        三数取中就是在left 、midi((right+left)/2) 、right三个下标位置上的数据之间选择出这三个数据中的中间数。这样就避免了key为最小值的情况。

2.4-代码:

  1. void Swap(int* p, int* q) //交换函数
  2. {
  3. int tem = *p;
  4. *p = *q;
  5. *q = tem;
  6. }
  1. //直接选取法
  2. int keyi = left;
  3. //随机选keyi
  4. int randi = left + (rand() % (right - left));
  5. Swap(&a[randi], &a[left]);
  6. int keyi = left;
  7. //三数取中
  8. int midi = GetMidNumi(a, left, right);
  9. if (midi != left)
  10. Swap(&a[midi], &a[left]);
  11. int keyi = left;
  1. int GetMidNumi(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. if (a[left] < a[right])
  11. {
  12. return left;
  13. }
  14. else
  15. {
  16. return right;
  17. }
  18. }
  19. if (a[left] < a[mid])
  20. {
  21. if (a[left] > a[right])
  22. {
  23. return left;
  24. }
  25. if (a[right] > a[mid])
  26. {
  27. return mid;
  28. }
  29. else
  30. {
  31. return right;
  32. }
  33. }
  34. }

三、快速排序-Hoare:

3.1-思路:

Hoare快速排序的思路就是:

        如果key定义的是left,那么就首先从右边找小,找到以后再从左边出发找大,找完以后后将他们俩的数据调换,然后接着进行下一次先右后左找小再找大,直到left小于right为止。同理若定义key在right处那么就先左后右,处理方式与left类似。        

        这样排完一趟后,我们能将大于key的数分布在右边,小于key的数分布在左边,最后,我们只需要按照这个思路递归下去,就实现快速排序啦。

        注意:在递归时,如果出现left>=right的情况下,我们就需要返回,否则就会死循环。

左边做key为什么相遇位置一定比key小?

3.2-过程图:

3.3-代码:

  1. //Hoare
  2. void QuickSort1(int* a, int left,int right) //快速排序---时间复杂度(O(N*logN))
  3. {
  4. if (left >= right)
  5. return;
  6. int begin = left;
  7. int end = right;
  8. //直接选取法
  9. //int keyi = left;
  10. //随机选keyi
  11. //int randi = left + (rand() % (right - left));
  12. //Swap(&a[randi], &a[left]);
  13. //int keyi = left;
  14. //三数取中
  15. int midi = GetMidNumi(a, left, right);
  16. if(midi!=left)
  17. Swap(&a[midi], &a[left]);
  18. int keyi = left;
  19. while (left < right)
  20. {
  21. //右边找小
  22. while (left<right&&a[right] >=a[keyi])
  23. {
  24. --right;
  25. }
  26. //左边找大
  27. while (left<right&&a[left] <= a[keyi])
  28. {
  29. ++left;
  30. }
  31. Swap(&a[left], &a[right]);
  32. }
  33. Swap(&a[keyi], &a[left]);
  34. keyi = left;
  35. //递归---小区间优化--小区间直接使用插入排序
  36. if (end - begin+1 > 10)
  37. {
  38. QuickSort1(a, begin, keyi - 1);
  39. QuickSort1(a, keyi + 1, end);
  40. }
  41. else
  42. {
  43. InsertSort(a + begin, end - begin + 1);
  44. }
  45. }

四、快速排序-挖坑法:

4.1-思路:

        快速排序,挖坑法的思路就是:

        先将第一个数据存放在临时变量k中,形成一个坑位,然后再数组的右边出发,向左寻找小于key的数,然后将这个数填在以前的坑位上,然后在该处重新生成坑位,接着在从左边向右寻找,找大于ker的数,然后将这个数填在生成的坑位上,然后在该处重新生成坑位,就这样一直循环实现上述操作,直到left与right相遇为止,此时,该坑位就应该填key。。

        这样排完一趟后,我们能将大于key的数分布在右边,小于key的数分布在左边,最后,我们只需要按照这个思路递归下去,就实现快速排序啦。

        注意:在递归时,如果出现left>=right的情况下,我们就需要返回,否则就会死循环。

4.2-过程图:

        

4.3-代码:

  1. //挖坑法
  2. void QuickSort2(int* a, int left, int right) //快速排序---时间复杂度(O(N*logN))
  3. {
  4. if (left >= right)
  5. return;
  6. int begin = left;
  7. int end = right;
  8. //直接选取法
  9. //int keyi = left;
  10. //随机选keyi
  11. //int randi = left + (rand() % (right - left));
  12. //Swap(&a[randi], &a[left]);
  13. //int keyi = left;
  14. //三数取中
  15. int midi = GetMidNumi(a, left, right);
  16. if(midi!=left)
  17. Swap(&a[midi], &a[left]);
  18. int key = a[left];
  19. int hole = left;
  20. while (left < right)
  21. {
  22. while (left < right &&a[right] >= key)
  23. {
  24. --right;
  25. }
  26. a[hole] = a[right];
  27. hole = right;
  28. while (left < right&&a[left] <= key)
  29. {
  30. ++left;
  31. }
  32. a[hole] = a[left];
  33. hole = left;
  34. }
  35. a[hole] = key;
  36. //递归---小区间优化--小区间直接使用插入排序
  37. if (end - begin + 1 > 10)
  38. {
  39. QuickSort2(a, begin, hole - 1);
  40. QuickSort2(a, hole + 1, end);
  41. }
  42. else
  43. {
  44. InsertSort(a + begin, end - begin + 1);
  45. }
  46. }

五、快速排序-前后指针法:

5.1-思路:

快速排序,前后指针法的思路就是:

首先,定义一个prev=left ; cur=left+1。这里我们实现的操作:

        1.cur找到比key小的值,++prove, cur和prev位置的数调换,然后++cur。

        2.cur找到比key大的值,++cur。

说明:

        1.prev要么紧跟着cur(prev下一个位置就是cur)。

        2.prev跟cur中间隔着一段比key大的值。

         就这样,按上述思想进入循环知道,直到cur走到数组末端的下一个位置为止,接下来我们要实行的操作就是将keyi位置的值(key)与prev位置的值交换。这样排完一趟后,我们能将大于key的数分布在右边,小于key的数分布在左边,最后,我们只需要按照这个思路递归下去,就实现快速排序啦。

        注意:在递归时,如果出现left>=right的情况下,我们就需要返回,否则就会死循环。

5.2-过程图:

5.3-代码:

  1. //前后指针法
  2. void QuickSort3(int* a, int left, int right) //快速排序---时间复杂度(O(N*logN))
  3. {
  4. if (left>=right)
  5. return;
  6. int begin = left;
  7. int end = right;
  8. //直接选取法
  9. //int keyi = left;
  10. //随机选keyi
  11. //int randi = left + (rand() % (right - left));
  12. //Swap(&a[randi], &a[left]);
  13. //int keyi = left;
  14. //三数取中
  15. int midi = GetMidNumi(a, left, right);
  16. if (midi != left)
  17. Swap(&a[midi], &a[left]);
  18. int keyi = left;
  19. int prev= left;
  20. int cur = left + 1;
  21. while (cur<=right)
  22. {
  23. if(a[cur]<a[keyi]&&++prev!=cur)
  24. Swap(&a[cur], &a[prev]);
  25. cur++;
  26. }
  27. Swap(&a[keyi], &a[prev]);
  28. keyi = prev;
  29. //递归---小区间优化--小区间直接使用插入排序
  30. if (end - begin + 1 > 10)
  31. {
  32. QuickSort3(a, begin, keyi - 1);
  33. QuickSort3(a, keyi + 1, end);
  34. }
  35. else
  36. {
  37. InsertSort(a + begin, end - begin + 1);
  38. }
  39. }

六、递归的问题与优化:

6.1-递归的问题:

        1.效率问题(略有影响)。

        2.深度太深时会栈溢出。

递归过程图:

6.2-小区间优化:

小区间优化的思想就是:

        将递归的最后几层,也就是基本有序的小区间,采用直接插入排序的方法,不再采用递归的方式,这样能够减少递归时所开辟栈。

        因为,递归栈的开辟相当于二叉树,而二叉树的最后一层相当于总数的一半,如果把最后一层省掉,也就是省去了递归所需开辟栈的大概50%的空间。

        所以,我们可以通过小区间优化来减少栈的开辟,在不影响时间复杂度的情况下,也减小了深度太深时会栈溢出的问题。

6.3-小区间优化代码:

  1. //递归---小区间优化--小区间直接使用插入排序
  2. if (end - begin + 1 > 10)
  3. {
  4. QuickSort3(a, begin, keyi - 1);
  5. QuickSort3(a, keyi + 1, end);
  6. }
  7. else
  8. {
  9. InsertSort(a + begin, end - begin + 1);
  10. }

七、递归改非递归:

        由上述可知,通过递归实现快排,具有一定的弊端,也就是栈溢出,所以这里我们可以采取将递归改成非递归的方式来实现快速排序

7.1-方式:

        1.直接改成循环。

        2.使用栈辅助改成循环。

        通过上述代码可观察发现,递归改非递归的第一种方式,我们是实现不了的,所以我们这里需要借助栈来辅助将递归改成循环。

7.2-思路:

        这里的思路就是将递归时的左右区间,存入栈中。然后在循环的过程中,我们只需将区间值出站即可。

        注意:栈的原理是后进先出。

7.3-代码:

  1. #include "Stack.h"
  2. //递归改非递归
  3. void QuickSortNonR(int* a, int left, int right) //快速排序---时间复杂度(O(N*logN))
  4. {
  5. ST ps;
  6. STInit(&ps);
  7. STpush(&ps, right); //入栈
  8. STpush(&ps, left); //入栈
  9. while (!STEmpty(&ps))
  10. {
  11. int begin= STTop(&ps); //取栈顶元素
  12. STPop(&ps); //出栈
  13. int end = STTop(&ps); //取栈顶元素
  14. STPop(&ps); //出栈
  15. int midi = GetMidNumi(a, begin, end);
  16. if (midi != begin)
  17. Swap(&a[midi], &a[begin]);
  18. int keyi = begin;
  19. int prev = begin;
  20. int cur = begin + 1;
  21. while (cur <= end)
  22. {
  23. if (a[cur] < a[keyi] && ++prev != cur)
  24. Swap(&a[cur], &a[prev]);
  25. cur++;
  26. }
  27. Swap(&a[keyi], &a[prev]);
  28. keyi = prev;
  29. if (keyi + 1 < end)
  30. {
  31. STpush(&ps, end); //入栈
  32. STpush(&ps, keyi + 1); //入栈
  33. }
  34. if (keyi - 1 > begin)
  35. {
  36. STpush(&ps, keyi - 1); //入栈
  37. STpush(&ps, begin); //入栈
  38. }
  39. }
  40. for (int i = 0; i <=right; i++)
  41. {
  42. printf("%d ", a[i]);
  43. }
  44. printf("\n");
  45. STDestory(&ps);
  46. }

7.4-栈的代码:

  1. #pragma once
  2. #include <stdio.h>
  3. #include <assert.h>
  4. #include <stdlib.h>
  5. #include <stdbool.h>
  6. typedef int STDataType;
  7. typedef struct Stack
  8. {
  9. STDataType* a;
  10. int top;
  11. int capacity;
  12. }ST;
  13. void STInit(ST* ps); //初始化
  14. void STDestory(ST* ps); //释放销毁
  15. void STpush(ST* ps, STDataType x); //入栈
  16. void STPop(ST* ps); //出栈
  17. int STSize(ST* ps); //栈中元素个数
  18. bool STEmpty(ST* ps); //判断栈空
  19. STDataType STTop(ST* ps); //栈顶元素
  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include "Stack.h"
  3. void STInit(ST* ps) //初始化
  4. {
  5. assert(ps);
  6. ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
  7. if (ps->a == NULL)
  8. {
  9. perror("malloc");
  10. return;
  11. }
  12. ps->capacity = 4;
  13. ps->top = 0; //top是栈顶元素的下一个位置
  14. //ps->top=-1 //top是栈顶元素位置
  15. }
  16. void STDestory(ST* ps) //释放销毁
  17. {
  18. assert(ps);
  19. ps->top = 0;
  20. ps->capacity = 0;
  21. free(ps->a);
  22. ps->a = NULL;
  23. }
  24. void STpush(ST* ps, STDataType x) //入栈
  25. {
  26. assert(ps);
  27. if (ps->top == ps->capacity)
  28. {
  29. STDataType* tem = (STDataType*)realloc(ps->a, sizeof(STDataType) * ps->capacity * 2);
  30. if (tem == NULL)
  31. {
  32. perror("realloc");
  33. return;
  34. }
  35. ps->a = tem;
  36. ps->capacity *= 2;
  37. }
  38. ps->a[ps->top] = x;
  39. ps->top++;
  40. }
  41. void STPop(ST* ps) //出栈
  42. {
  43. assert(ps);
  44. assert(!STEmpty(ps));
  45. ps->top--;
  46. }
  47. int STSize(ST* ps) //栈中元素个数
  48. {
  49. assert(ps);
  50. return ps->top;
  51. }
  52. bool STEmpty(ST* ps) //判断栈空
  53. {
  54. assert(ps);
  55. return ps->top == 0;
  56. }
  57. STDataType STTop(ST* ps) //返回栈顶元素
  58. {
  59. assert(ps);
  60. assert(!STEmpty(ps));
  61. return ps->a[ps->top - 1];
  62. }

八、结语:

        上述内容,即是我个人对数据结构排序中快速排序的个人见解以及自我实现。若有大佬发现哪里有问题可以私信或评论指教一下我这个小萌新。非常感谢各位友友们的点赞,关注,收藏与支持,我会更加努力的学习编程语言,还望各位多多关照,让我们一起进步吧!

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

闽ICP备14008679号