当前位置:   article > 正文

几种排序算法详解以及比较(8种)_多种排序算算法对比研究

多种排序算算法对比研究

目录

1. 比较类排序

1.1 插入类排序

1.1.1 直接插入排序

1.1.2 希尔排序 对插入排序的优化

1.2 选择类排序

1.2.1 选择排序

1.2.2 堆排序

1.3 交换类排序

1.3.1 冒泡排序

1.3.2 快速排序

1.4 归并排序

2. 非比较类排序

2.1 计数排序


排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
稳定指的是:相同的数,在拍完序之后,其相对位置不发生变化。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1. 比较类排序

1.1 插入类排序

1.1.1 直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一 个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

  1. void InsertSort(int* a, int n)
  2. {
  3. for (int i = 0; i < n-1; i++)
  4. {
  5. //大问题分解成小问题
  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. //a[end + 1] = tmp;
  20. break;
  21. }
  22. }
  23. a[end + 1] = tmp;
  24. }
  25. }

直接插入排序特性总结:
时间复杂度分析:
最好情况:数组已经排好序,时间复杂度O ( n ) 
最坏情况:逆序,每个元素都要与前面所有元素都比较一次,等差数列求和。时间复杂度为O(n2)
对于插入排序来说,接近有序的序列排序是比较快的。
空间复杂度O ( 1 ) O(1)O(1)
 

1.1.2 希尔排序 对插入排序的优化

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1 时,所有记录在统一组内排好序。

 1.预排序(间隔为gap的元素分成一组,对每一组进行插入排序,一共gap个组

  • gap越大,大的数和小的数越快挪到对应的位置上去,但是越不接近有序
  • gap越小,大的数和小的数越慢挪到对应的位置上去,但是越接近有序
  • gap为1的时候,就是直接插入排序 ,所以可以对gap来一个循环

2.再进行插入排序

  1. void ShellSort(int* a, int n)
  2. {
  3. int gap = n;
  4. // gap > 1的时候,预排序
  5. // gap == 1的时候,直接插入排序 O(N)
  6. while (gap > 1)
  7. {
  8. gap =( gap/3+1);//不能让gap为0,最后一次gap应该为1;
  9. for (int i = 0; i < n - gap; i++)
  10. {
  11. int end = i;
  12. int tmp = a[end + gap];
  13. while (end >= 0)
  14. {
  15. if (tmp < a[end])
  16. {
  17. a[end + gap] = a[end];
  18. end -= gap;
  19. }
  20. else
  21. {
  22. //a[end + 1] = tmp;
  23. break;
  24. }
  25. }
  26. a[end + gap] = tmp;
  27. }
  28. }
  29. }

1.2 选择类排序

1.2.1 选择排序

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
  1. //选择排序
  2. void SelectSort(int* a, int n)
  3. {
  4. for (int j = 0; j < n; j++)
  5. {
  6. int min = j;
  7. for (int i = j+1; i < n; i++)
  8. {
  9. if (a[i] < a[min])
  10. {
  11. min = i;
  12. }
  13. }
  14. Swap(&a[j], &a[min]);
  15. }
  16. }//可以对选择排序优化,一次遍历同时选出最大和最小两个数

1.2.2 堆排序

堆排序 (Heapsort) 是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

 

堆排序:物理上操作的是一个数组,逻辑上把他看成一个heap。
排升序,要建大堆(建小堆只能找到最小的数,接下来又得建堆找次小的,效率太低)
建大堆的话,把最大的数换到最后,向下调整一次选出次大的数,再往后放

  1. //向下调整算法
  2. void AdjustDwon(int* a, int n, int root)
  3. {
  4. int parent = root;
  5. int child = 2 * parent + 1;
  6. while (child < n)
  7. {
  8. if (child + 1 < n && a[child + 1] > a[child])//先检查越界再访问
  9. child++;
  10. if (a[child] > a[parent]){
  11. Swap(&a[child], &a[parent]);
  12. parent = child;
  13. child = 2 * parent + 1;
  14. }
  15. else{
  16. break;}
  17. }
  18. }
  19. void HeapSort(int* a, int n)
  20. {
  21. //1. 首先建堆(大堆,升序),即物理上把数组形成大堆的样子
  22. for (int i = (n - 1 - 1) / 2; i >= 0; i--)//从最后一次不是叶子节点的元素开始调整
  23. {
  24. AdjustDwon(a, n, i);
  25. }//到这里完成建堆,数组a在逻辑上是一个堆
  26. //2. 将堆顶元素(最大元素)换到最后,完成升序
  27. //在物理上就是将首元素换到最后,再让堆(物理上的数组)的长度-1,并向下调整一次生成新的大堆
  28. //注意,这个新的堆在物理上比上一个堆元素少1了,再把这个堆顶(数组首元素)即次大的元素换到最后,循环最后形成升序。
  29. int end = n - 1;
  30. while (end >= 1)
  31. {
  32. Swap(&a[0], &a[end]);
  33. AdjustDwon(a, end, 0);
  34. end--;
  35. }
  36. }

1.3 交换类排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1.3.1 冒泡排序

  1. void Swap(int* a, int* b)
  2. {
  3. int tmp = *a;
  4. *a = *b;
  5. *b = tmp;
  6. }
  7. void BubbleSort(int* a, int n)
  8. {
  9. for (int i = 0; i < n; i++)
  10. {
  11. int flag = 0; //这里是代码优化
  12. for (int j = 0; j < n - i - 1; j++)
  13. {
  14. if (a[j] > a[j + 1])
  15. {
  16. flag = 1;
  17. Swap(&a[j], &a[j + 1]);
  18. }
  19. }
  20. if (flag == 0)
  21. break;
  22. }
  23. }

flag用来优化,所以我们定义一个flag,当某一趟排序没有发生交换时,就说明数组已经有序,然后跳出循环。

1.3.2 快速排序

快排的原生函数为:

  • c中为qsort:需要4个参数,qsort(排序首地址,排序长度,排序元素长度,cmp)
  • c++中为sort(sort是qsort的优化):2/3个参数,sort(排序首地址,排序尾地址,cmp)(2个参数就是不写cmp,这样默认是升序排列)

下面对快排进行模拟实现:

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

将区间按照基准值划分为左右两半部分的常见方式有:
  • 1. hoare版本
  • 2. 挖坑法
  • 3. 前后指针版本

快速排序的优化:

  • 1. 三数取中法选key
  • 2. 递归到小的子区间时,可以考虑使用插入排序

  1. //快速排序:递归的思想
  2. //优化方法(1)三数取中(选三个数,取中间大的数)
  3. int GetMidIndex(int* a, int left, int right)
  4. {
  5. int mid = (left + right) >> 1;//移位运算,相当于除以2((left + right) / 2)
  6. // left mid right
  7. if (a[left] < a[mid])
  8. {
  9. if (a[mid] < a[right])
  10. return mid;
  11. else if (a[left] > a[right])
  12. return left;
  13. else
  14. return right;
  15. }
  16. else // a[left] > a[mid]
  17. {
  18. if (a[mid] > a[right])
  19. return mid;
  20. else if (a[left] < a[right])
  21. return left;
  22. else
  23. return right;
  24. }
  25. }
  26. //单趟排序方法(1)hoare版本 -- 左右指针法
  27. int PartSort1(int* a, int left, int right)
  28. {
  29. int midIndex = GetMidIndex(a, left, right);//加入优化,三数取中
  30. Swap(&a[left], &a[midIndex]);
  31. int key = left;
  32. while (left < right)
  33. {
  34. //right找小的
  35. while (left < right && a[right] >= a[key])//加一个判断,不然会越界
  36. right--;
  37. //left找大的
  38. while (left < right && a[left] <= a[key])
  39. left++;
  40. Swap(&a[left], &a[right]);
  41. }
  42. Swap(&a[left], &a[key]);
  43. return left;
  44. }
  45. //单趟排序方法(2)挖坑法
  46. //可以画图理解
  47. int PartSort2(int* a, int left, int right)
  48. {
  49. int midIndex = GetMidIndex(a, left, right);//加入优化,三数取中
  50. Swap(&a[left], &a[midIndex]);
  51. int key = left;
  52. while (left < right)
  53. {
  54. // right找小
  55. while (left < right && a[right] >= a[key])
  56. {
  57. --right;
  58. }
  59. // 找到之后,放到左边的坑位中,右边就形成新的坑
  60. a[left] = a[right];
  61. // left找大
  62. while (left < right && a[left] <= a[key])
  63. {
  64. ++left;
  65. }
  66. // 找到之后放到右边的坑位中,左边就形成新的坑
  67. a[right] = a[left];
  68. }
  69. //这里left=right,是一个坑位
  70. a[left] = a[key];
  71. return left;
  72. }
  73. //单趟排序方法(3)前后指针法
  74. int PartSort3(int* a, int left, int right)
  75. {
  76. int midIndex = GetMidIndex(a, left, right);//加入优化,三数取中
  77. Swap(&a[left], &a[midIndex]);
  78. int key = left;
  79. int prev = left;
  80. int cur = left + 1;
  81. while (cur <= right)
  82. {
  83. //while (cur <= right && a[cur] >= a[key])
  84. //{
  85. // cur++;
  86. //}
  87. //if (cur > right)//不要忘记这一步
  88. // break;
  89. //Swap(&a[++prev], &a[cur]);
  90. //cur++;
  91. //或者换成下面
  92. if (a[cur] < a[key] && ++prev != cur)
  93. {
  94. Swap(&a[cur], &a[prev]);
  95. }
  96. ++cur;
  97. }
  98. Swap(&a[key], &a[prev]);
  99. return prev;
  100. }
  101. //快排的时间复杂度:最好为每次的key都二分,为O(N*logN)
  102. //最坏的情况为数组有序时,O(N^2)
  103. //影响快排效率最大的是key,越接近中位数二分,效率越高
  104. //针对性优化:
  105. //优化方法(1)三数取中(选三个数,取中间大的数)(优化效果好)
  106. //优化方法(2)小区间排序优化(效果一般)
  107. void QuickSort(int* a, int begin, int end)
  108. {
  109. if (begin >= end)
  110. return;
  111. int keyi = PartSort3(a, begin, end);//单趟排序之后找到key的位置
  112. //下面递归左右两个区间,[begin, keyi-1],[keyi+1, end];
  113. QuickSort(a, begin, keyi-1);
  114. QuickSort(a, keyi+1, end);
  115. }

1.4 归并排序

归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法( Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

 这显然也是可以分治递归实现。

  1. //归并排序
  2. //时间复杂度,O(N*logN),每一层归并都是N个,一共logN层
  3. //空间复杂度,O(N)
  4. void _Merge(int* a, int* tmp, int left1, int right1, int left2, int right2)
  5. {
  6. int begin1 = left1, end1 = right1;
  7. int begin2 = left2, end2 = right2;
  8. int i = left1;//这里要创建新变量,不能直接用
  9. while (begin1 <= end1 && begin2 <= end2)
  10. {
  11. if (a[begin1] < a[begin2])
  12. tmp[i++] = a[begin1++];
  13. else
  14. tmp[i++] = a[begin2++];
  15. }
  16. while (begin1 <= end1)
  17. tmp[i++] = a[begin1++];
  18. while (begin2 <= end2)
  19. tmp[i++] = a[begin2++];
  20. // 归并完成以后,拷贝回到原数组
  21. for (int i = left1; i <= right2; i++)
  22. {
  23. a[i] = tmp[i];
  24. }
  25. }
  26. void _MergeSort(int* a, int left, int right, int* tmp)
  27. {
  28. if (left >= right)
  29. return;
  30. int mid = (left + right) >> 1;
  31. //将左区间和右区间依次迭代到有序
  32. _MergeSort(a, left, mid, tmp);
  33. _MergeSort(a, mid + 1, right, tmp);
  34. //下面对左区间、右区间进行归并
  35. //按照尾插的思路
  36. _Merge(a, tmp, left, mid, mid + 2, right);
  37. }
  38. void MergeSort(int* a, int n)
  39. {
  40. //用来存放归并时候的临时数组
  41. int tmp = (int*)malloc(sizeof(int) * n);
  42. if (tmp == NULL)
  43. {
  44. exit(-1);
  45. }
  46. _MergeSort(a, 0, n - 1, tmp);
  47. free(tmp);
  48. }

拓展:
内排序和外排序
数量很大时候,要将外存中的数据分割成多个数据在内存中排序
排好的多个数据放到文件中,在从文件中拿数据进行归并
 

2. 非比较类排序

2.1 计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
  • 1. 统计相同元素出现次数
  • 2. 根据统计的结果将序列回收到原来的序列中
  1. //计数排序
  2. //绝对映射、相对映射
  3. //时间复杂度为O(N+range)
  4. //适合,一组数据范围比较集中的数,但是只适合整数
  5. //空间复杂度O(range)
  6. void CountSort(int* a, int n)
  7. {
  8. int max = a[0], min = a[0];
  9. for (int i = 0; i < n; i++)
  10. {
  11. if (a[i] > max)
  12. max = a[i];
  13. if (a[i] < min)
  14. min = a[i];
  15. }//加这个是为了相对映射
  16. //统计每个元素的个数
  17. int range = max - min + 1;
  18. int* count =(int*)malloc(sizeof(int) * range);
  19. memset(count, 0, sizeof(int) * range);
  20. for (int i = 0; i < n; i++)
  21. {
  22. count[a[i] - min]++;
  23. }
  24. //下面对a进行排序
  25. int j = 0;
  26. for (int i = 0; i < range; i++){
  27. while (count[i]--){
  28. a[j++] = i + min; }
  29. }
  30. free(count);
  31. count = NULL;
  32. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/411808
推荐阅读
相关标签
  

闽ICP备14008679号