当前位置:   article > 正文

手撕初阶数据结构之---排序

手撕初阶数据结构之---排序

 

1.排序概念及运用

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

常见的排序算法

直接插入排序的时间复杂度是O(N^2)

这个是最差的情况下,就是大的在前面,小的在后面

希尔排序就是直接插入排序的优化版本

将时间复杂度优化为O(N^1.3)

时间选择排序是雷打不动的O(N^2)

堆排序是O(N logN)

冒泡排序是O(N^2)

在数组有序的情况下我们能优化成O(N),但是这种情况很少见

 

2.常见排序算法

1.插入排序

1.直接插入排序

当插⼊第 i(i>=1) 个元素时,前⾯的 array[0],array[1],…,array[i-1] 已经排好序,此时⽤ array[i] 的排序码与 array[i-1],array[i-2],… 的排序码顺序进⾏⽐较,找到插⼊位置即将 array[i] 插⼊,原来位置上的元素顺序后移

 

现在我们有一堆的数字,现在我们一个一个进行比较,只要我们发现后面的值比前面的值大,那么我们就把前面的值放到当前的位置,继续往前进行比较,那么这么就会导致前面遍历过的内容都是有序的,将后面乱序的数字一个个插入到前面有序的序列中,使其变得有序

这种思想和打牌是一样的

给出一个数组10 9 8 7 6 5 4 3 2 1

现在我们要变为升序

我们创建变量tmp

先将9存在tmp中,将9与10进行比较,如果9比10小的话,那么就将10挪到9的位置

然后用tmp为第一个位置重新赋值,那么第一个数字就是9

然后我们前两个数字就是有序的数列

然后依次从后面拿数字到前面进行比较

  1. //2 5 8 1 5 6
  2. //直接插入排序
  3. void InsertSort(int* arr, int n)
  4. {
  5. //i最后一次是n-2,对应数组中倒数第二个数字
  6. for (int i = 0; i < n-1; i++)
  7. {
  8. //i不能小于n,假设n=10
  9. //那么i<9
  10. //end=8时,那么tmp就是9,那么就存在越界行为了
  11. int end = i;//end0开始
  12. int tmp = arr[end + 1];//有序区间的后一个数字
  13. while (end>=0)
  14. {
  15. //比较
  16. if (arr[end] > tmp)//end位置的数据大于tmp,那么tmp往前走
  17. {
  18. arr[end + 1] = arr[end];//end位置的数据往后挪
  19. end--;
  20. }
  21. else//end位置的数小于tmp位置的,我们就跳出循环
  22. {
  23. break;
  24. }
  25. }
  26. //此时的end就小于0,跳出循环
  27. arr[end + 1] = tmp;
  28. }
  29. }

在内层循环中

随着i的变化,交换的情况最差就是1 +2 +3 +(n-1)

那么内层循环的时间复杂度就是O(N)

外层循环的时间复杂度也是O(N)

那么这个直接插入法的时间复杂度就是O(N^2)

数组有序且为降序的情况下时间复杂度最差

最差的情况下时间复杂度为O(N^2)

如果是有序为升序下,那么时间复杂度就是O(N)

数组为降序序列时,直接插入排序能得到优化?

将N个数据划分成很多个段,每个段单独进行直接插入排序

那么这样我们就能减少时间复杂度了

那么我们将直接插入排序我们就得到了希尔排序

2.希尔排序

数组为降序序列时,直接插入排序能得到优化?

将N个数据划分成很多个段,每个段单独进行直接插入排序

那么这样我们就能减少时间复杂度了

那么我们将直接插入排序我们就得到了希尔排序

1.对每一段进行预排序

2.直接插入排序

预排序使数组接近有序

希尔排序法⼜称缩⼩增量法。希尔排序法的基本思想是:先选定⼀个整数(通常是gap = n/3+1),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。

它是在直接插⼊排序算法的基础上进⾏改进⽽来的,综合来说它的效率肯定是要⾼于直接插⼊排序算法的。

  1. //希尔排序
  2. //时间复杂度为O(N^1.3)
  3. void ShellSort(int* arr, int n)
  4. {
  5. int gap = n;
  6. while (gap>1)//控制gap的变化
  7. {
  8. gap =gap/3+1;//保证最后一次gap一定为1
  9. //当我们的gap为1的时候那么就是直接插入排序
  10. for (int i = 0; i < n - gap; i ++)//每一组的几个数据之间进行比较
  11. {
  12. /*
  13. 如果最后一次的话i为n-2,那么end为n-2
  14. 那么tmp=n-2+gap 那么就会存在越界现象
  15. 所以我们的条件是i<n-gap
  16. 那么end=n-gap-1
  17. tmp=n-gap-1+gap=n-1
  18. 那么tmp最后一次取到的数据就是最后一个数据
  19. */
  20. int end = i;
  21. int tmp = arr[end + gap];
  22. while (end >= 0)
  23. {
  24. if (arr[end] > tmp)//那么说明tmp小,那么小的就要往前面放
  25. {
  26. arr[end + gap] = arr[end];//我们现将end这个位置的数挪到end+gap这个位置
  27. end -= gap;
  28. }
  29. else//就说明tmp对应的数大于end对应的数,那么对于跳出循环的代码我们是相当于tmp是不动的
  30. {
  31. break;
  32. }
  33. }
  34. //跳出循环之后我们的tmp应该放在end+gap这个位置
  35. arr[end + gap] = tmp;
  36. }
  37. }
  38. }
  39. /*
  40. 9 1 2 5 7 4 8 6 3 5
  41. 现在我们的gap为3
  42. 那么就是隔三个为一组
  43. 那么第一组就是9 5 8 5
  44. 因为5小于9
  45. 那么先将9放到5的位置,然后end-=gap
  46. 因为一开始的end0,那么end-=gap之后那么end就变为了-3
  47. 然后因为end<0那么跳出循环
  48. 然后我们end+gap=-3+3=0
  49. 那么arr[3]这个位置就放tmp的值
  50. 这个位置就是第一个位置
  51. 那么就完成了endend+gap这两个位置的数据的置换
  52. 5 1 2 9 7 4 8 6 3 5
  53. 我们外循环的条件改为i+=gap
  54. 每次递增gap
  55. 那么我们的end就到了9的位置,tmp到了8的位置
  56. 因为tmp小于end
  57. 那么我们就将tmp位置的数覆盖为end的值,tmp位置上面就变成了9
  58. 然后end-=gap
  59. 此时的end=3,经过了end-=gap操作之后end=0
  60. 我们再拿出tmp中的8end对应的5进行比较,那么我们就break了循环
  61. 那么我们就将tmp里面的8放到end+gap 的位置
  62. 那么就是下标为3的位置
  63. 5 1 2 8 7 4 9 6 3 5
  64. 那么我们再次进入循环
  65. end就走到了9的位置,tmp就走到了end+gap就是最后的5的位置了
  66. 因为59小,那么先将9覆盖到tmp 的位置
  67. 然后end-=gap
  68. 那么end就走到了现在8的位置
  69. 然后再次进入循环,那么8于tmp中的5进行比较
  70. 那么5<8
  71. 那么8放到9的位置,9的位置就是end+gap
  72. 8放完之后end-=gap
  73. 那么end就回到了0
  74. 那么55进行比较
  75. 那么我们就break出来
  76. 然后我们将tmp中存的5放到end+gap这个位置
  77. 5 1 2 5 7 4 8 6 3 5 9
  78. 我们之前的第一组是9 5 8 5 就是每隔gap个就是一组
  79. 那么现在就是5 5 8 9
  80. 那么现在我们控制的是一组
  81. 那么我们还要在外面进行一个for循环
  82. 我们定义的gap是3
  83. 那么就是定义的3
  84. 第一组是9 5 8 5
  85. 第二组是1 7 6
  86. 第三组是2 4 3
  87. 那么第二组换完就是1 6 7
  88. 那么第二组换完就是2 3 4
  89. 那么经过这么一趟排序
  90. 我们的数组就变成了5 1 2 5 6 3 7 4 9
  91. 那么数组又接近有序了
  92. 然后gap/3=1
  93. 那么就是1个为1
  94. 那么就变成了直接插入排序了
  95. 那么对于希尔排序来说,预排序的条件是gap>1
  96. 直接插入排序的话是gap==1
  97. 那么最后我们的一次直接插入排序我们就得到了1 2 3 4 5 5 6 7 9
  98. 那么通过预排序小的在前,大的在后
  99. 那么我们通过直接插入排序的话就可以减少次数
  100. 我们现在从4层循环优化成3层循环
  101. 现在我们是
  102. 第一次我们先排第一组的前两个数据
  103. 因为将之前的for循环删除了
  104. 那么现在我i++
  105. 那么我们接着排第二组的前两个数据
  106. 然后i++
  107. 那么我们就接着排第三组的前两个数据
  108. 然后我们i++
  109. 我们就进行第一组的第三个数据和前两个数据进行比较
  110. */

希尔排序是三层循环

n/3

n/3/3

……

除到1

log3 n=x

x是次数

那么3^x=n

如果n=9的话,那么x=2

 

假设一组里面的数据是8 5 2

那么我们的交换次数最差是3次

8和5进行交换一次

2和8 和5分别交换一次

那么总共就是三次

当gap=1的时候,数组基本有序,直接插入排序时间复杂度为O(N)

我们推荐在使用希尔排序的时候使用gap=gap/3

如果是gap/2的话那么循环的次数就太多了

2.选择排序

1.直接选择排序

  1. 在元素集合 array[i]--array[n-1] 中选择关键码最⼤(⼩)的数据元素

  2. 若它不是这组元素中的最后⼀个(第⼀个)元素,则将它与这组元素中的最后⼀个(第⼀个)元素交换

  3. 在剩余的 array[i]--array[n-2](array[i+1]--array[n-1]) 集合中,重复上述步骤,直到集合剩余 1 个元素

  1. //直接选择排序优化版本
  2. //直接选择排序的时间复杂度是O(N ^ 2)
  3. void SelectSort02(int* arr, int n)
  4. {
  5. int begin=0;//指向第一个数据
  6. int end = n - 1;//指向最后一个数据
  7. //遍历整个数组找到最小的数保存到mini中
  8. while (begin < end)//当begin=end的时候那么这个时候我们就调整完了整个数组了
  9. {
  10. //一开始将最大的和最小的都定义在begin这个位置
  11. int mini = begin, maxi = begin;
  12. for (int i = begin+1; i <=end; i++)
  13. {
  14. //起始的条件取决于begin,我们每次从begin+1这个位置进行找大的和找小的数据
  15. if (arr[i] > arr[maxi])
  16. {
  17. maxi = i;//重新定义最大的值的下标
  18. }
  19. if (arr[i] < arr[mini])
  20. {
  21. mini = i;//重新定义最小的值的下标
  22. }
  23. }
  24. //跳出循环之后我们用最小值和begin这个位置的值进行交换
  25. //最大值和end进行交换
  26. //优化:避免maxi begin都在同一个位置,begin和mini交换之后,maxi数据变成了最小的数据
  27. if (maxi == begin)
  28. {
  29. //因为此时的maxi和begin指向的是同一个数字
  30. //那么我们直接为maxi重新赋值
  31. //我们将maxi的值赋值到mini这个位置
  32. maxi = mini;
  33. }
  34. /*
  35. 9 5 1
  36. 在这个数组中,我们的maxi和begin指向的都是同一个数
  37. 那么就满足了这个条件语句了
  38. 此时的maxi指向的是9
  39. mini指向的是1
  40. 那么我们将maxi重新进行赋值,复制为mini
  41. 那么此时的maxi和mini同时指向的是1
  42. 我们在下面的交换函数中
  43. 我们先将mini和begin进行交换
  44. 那么就是将19进行交换
  45. 那么现在的数字就是
  46. 1 5 9
  47. 那么此时我们就完成了最小值的交换了
  48. 然后进行最大值的交换
  49. 此时的end和maxi都指向的是9
  50. 那么相当于不进行操作
  51. 那么最后我们就得到了我们想要的排序了
  52. */
  53. Swap(&arr[mini], &arr[begin]);
  54. Swap(&arr[maxi], &arr[end]);
  55. //交换完成之后我们继续往中间进行找大小的操作
  56. begin++;
  57. end--;
  58. }
  59. }
  60. /*
  61. 在外层的while循环中,
  62. n n-2 n-4 n-6 …………2
  63. 每次减少首尾两个数据进行比较
  64. 那么while循环的时间复杂度就是O(N)
  65. 内层的for循环
  66. n-1 n-2 n-3 ………… 1
  67. 这里的for循环时间复杂度也是O(N)
  68. 那么这个直接选择排序的时间复杂度是O(N^2)
  69. 和冒泡排序是一样的
  70. */

2.堆排序

HeapSort

  1. //堆排序
  2. //期望空间复杂度是O(1) ,不额外的开辟空间
  3. //时间复杂度是O(n*log n)
  4. void HeapSort(int* arr, int n)
  5. {
  6. //根据数组来建堆,那么我们就需要便利数组
  7. //建小堆---降序
  8. // 向上调整算法建堆
  9. //for (int i = 0; i < n; i++)
  10. //{
  11. // AdjustUp(arr, i);//我们给的参数是i,就是开始 调整数字的 下标,我们是从数组第一个元素进行调整的,i=0开始的
  12. //}
  13. //向下调整算法建堆
  14. //我们需要通过最后一个元素的下标n-1找到他的父节点,从这个点开始进行操作
  15. //父节点:(n-1 -1/2
  16. for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  17. {
  18. AdhustDown(arr, i, n);//我们从i这个位置开始向下调整
  19. //i一开始的位置就是最后一个数据的父节点,我们从这个父节点开始向下操作
  20. }
  21. //循环将堆顶数据跟最后位置的数据(会变化,每次减小一个数据)进行交换
  22. int end = n - 1;//n是数组的有效个数,那么最后一个数据的下标是n-1
  23. while (end > 0)//end会一直--,但是end必须大于0
  24. {
  25. Swap(&arr[0], &arr[end]);//将第一个数据和最后一个数据进行交换
  26. //那么我们就要对现在的剩下的堆进行调整
  27. //我们采用向下调整的方法
  28. AdhustDown(arr, 0, end);//我们从根进行调整,那么第二个参数就是0
  29. //n-1=end,就是我们只需要对剩下的n-1个数据进行向下调整,那么第三个参数就是我们调整的有效数据n-1
  30. end--;
  31. }
  32. }

堆排序使用时需要的向下调整算法AdhustDown以及交换函数Swap

  1. //向下调整
  2. void AdhustDown(int* arr, int parent, int n)
  3. //第一个数据是我们要调整的数组,第二个参数是父节点,就是我们开始进行调整的位置的下标对应的元素,第三个是数组中有效的元素个数
  4. {
  5. //我们已知Parent,那么我们能够通过2i+1或者2i+2找到这个父节点的左右子节点
  6. int child = parent * 2 + 1;//左孩子
  7. while (child < n)//我们不能让child因为++操作导致越界
  8. {
  9. //找左右孩子中最小的那个,我们与父节点进行交换操作
  10. if (child + 1 < n && arr[child] > arr[child + 1])
  11. {
  12. //假设我们的子节点只有一个左节点的话,那么我们就可以通过child+1<n
  13. //这个条件避免child++,避免了越界访问,我们直接让左节点和父节点进行交换
  14. //child+1必须小于n我们才能往下进行调整操作,避免越界访问
  15. //假设左边的是56,右边的是15
  16. child++;//我们进行child++操作,然后child就跑到了15的位置,然后我们将15和父节点进行交换
  17. }
  18. if (arr[child] < arr[parent])//如果子节点小于父节点的话我们就进行交换
  19. {
  20. Swap(&arr[child], &arr[parent]);
  21. //交换完成之后我们让parent走到child的位置
  22. parent = child;
  23. //child走到当前位置的左孩子的位置
  24. child = parent * 2 + 1;
  25. }
  26. else//如果Parent比child小的话,那么我们就没必要向下进行调整了
  27. {
  28. break;//我们直接跳出
  29. }
  30. }
  31. }
  1. //交换函数
  2. void Swap(int* x, int* y)//一定要传地址才能进行交换了
  3. {
  4. int tmp = *x;
  5. *x = *y;
  6. *y = tmp;
  7. }

3.交换排序

1.冒泡排序

  1. //冒泡排序
  2. //时间复杂度为O(N^2)
  3. void BubbleSort(int* arr, int n)//数组以及数组中的有效数据个数
  4. {
  5. for (int i = 0; i < n; i++)
  6. {
  7. int exchange = 0;
  8. for (int j = 0; j < n - i - 1; j++)
  9. {
  10. //<就是降序
  11. if (arr[j] < arr[j + 1])//大的在后面,那么我们就进行交换
  12. {
  13. exchange = 1;
  14. Swap(&arr[j], &arr[j + 1]);
  15. }
  16. }
  17. if (exchange == 0)
  18. {
  19. //那么就说明我们经历了一趟内循环,我们的exchange并没有改变,就说明这个数组可能之前就是有序的
  20. break;
  21. }
  22. }
  23. }

2.快速排序

Hoare版本

 快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素

序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩

于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列

在相应位置上为⽌。

简单意思就是说:我们在数组中去一个数当作基准值,然后,整个数组被分为两个部分

左边的一个部分小于基准值,右边的部分大于基准值

那么这就相当于一个二叉树了

基准值是根节点,左边的是左子树,右边的是右子树

然后重复上面的操作,将左序列和右序列单独取出来,重复上面的操作

数组是左和右这个区间,在这个区间之中我们要找基准值mid

那么我们的左子序列的递归的右边界是mid-1

右子序列:mid+1,right

如何找基准值呢?

我们先将数组中第一个数据定义为基准值

right:从右到左找基准值小的数据

left:从左到右找比基准值大的数据

6 1 2 7 9 3

一开始的基准值Keyi指向的是6

left指向的是1

right指向的是3

那么我们先进行right的行动,找到了3

然后left往右找到了7

然后将两个指针指向的值进行交换

那么我们得到了

6 1 2 3 9 7

然后left指向的是3,right指向的是7

交换完成之后再重复刚才的过程

right左走找到3

left右左找到9

那么right就到了left的左边了

这个时候我们将keyi指向的6和right指向的3进行交换

那么我们得到了3 1 2 6 9 7

此时我们的基准值 在right的位置

right指向的位置的左边都比基准值小,右侧都比基准值大

那么对于左序列的话3 1 2

我们找基准值

right一开始就找到了比基准值小的数,但是left一直++,直到比right大了

然后我们就将right指向的数据和keyi指向的数据进行交换

2 1 3

那么此时的right是基准值

我们再对当前序列进行划分

左子序列就是2 1,右序列为空

我们再对左序列进行判断

我们的right指向的1

left指向的是1

keyi指向的是2

此时的right和left是重合的,那么就不影响我们找基准值,相等的话我们继续进行循环

那么right刚好找到的就是1比2小

right找好了

接下来找left,left++,越界了,已经比right大了,那么我们就跳出循环,

那么我们就将key和right指向的值进行交换

那么最后就得到了

1 2

那么此时我们的左序列就剩一个序列了

right和Left都是空了,就是left和right相等了

那么我们就没必要找基准值了

那么对于这个数组的话,我们的左子序列已经递归完了

那么我们还要递归右子序列

那么我们通过快排不断地找基准值,根据基准值划分左右区间,一直进行递归操作

我们要创建log n个函数栈帧

那么空间复杂度就是O(log n)

一层递归循环的时间复杂度是O(N)

但是我们要递归logN层

时间复杂度是O(N logN)

  1. //块排子方法
  2. //right:从右到左找基准值小的数据
  3. //left:从左到右找比基准值大的数据
  4. //hoare版本找基准值
  5. int _QuickSort(int* arr, int left, int right)//找基准值,返回Int
  6. {
  7. int keyi = left;//一开始的left是最左边的,那么我们就先临时将第一个数据定义为基准值
  8. //我们从left+1,right这个范围进行寻找
  9. left++;
  10. //我们要让right走到left的左边
  11. while (left<=right)//一定要=
  12. //leftright相遇的位置的值比基准值大
  13. {
  14. while (left<=right&& arr[right]>arr[keyi])//对应的值大于基准值
  15. {
  16. right--;//从右往左换写一个数字进行寻找
  17. }
  18. //走到这里说明right找到了比基准值小/等于?
  19. while (left <= right && arr[left] < arr[keyi])
  20. {
  21. left++;//还没有找到比基准值大的数据,那么我们就往右接着找
  22. }
  23. //说明我们的rightleft都找好了
  24. if (left <= right)//交换的前提是left<=right
  25. {
  26. //那么我们将这两个下标对应的数据进行交换操作
  27. Swap(&arr[left++], &arr[right--]);
  28. }
  29. }
  30. //只要lefy大于right了,那么我们就跳出循环走到这里了
  31. //right指向的值和key指向的值进行交换
  32. //跳出循环,那么就是right走到了left的左边,那么我们将right指向的值和keyi指向的值进行交换
  33. Swap(&arr[keyi], &arr[right]);
  34. //那么交换完成之后,那么right指向的数据就是我们的基准值了
  35. return right;
  36. }
  37. //快速排序
  38. void QuickSort(int* arr, int left, int right)//左右位置的下标
  39. {
  40. if (left >= right)//left走到了right的右边或者重叠(只有一个数据的话)
  41. {
  42. return;
  43. }
  44. //找基准值
  45. int keyi = _QuickSort(arr, left, right);
  46. //左子序列:[left,keyi-1]
  47. QuickSort(arr, left, keyi - 1);
  48. //右子序列:[keyi+1,right]
  49. QuickSort(arr, keyi +1 ,right);
  50. }

挖坑法

思路:

创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)

不断地挖坑不断地填坑

一开始我们是不知道坑在哪里的,那么我们就随便定义一个坑

6 1 2 7 9 3 4 5 10 8

我们将6定为坑hole

我们再定义一个基准值key=6指向坑所指向的位置

一开始的left指向6,right指向8

先让right往左走找比基准值小的

找到了5

那么我们直接将5拿挖走去填我们之前定义的坑

那么此时的right的指向的位置就是一个坑

那么数组就变成这样了

5 1 2 7 9 3 4 hole 10 8

接下来left从左往右找比基准值大的

找到了7

那么我们将7挖出来去填坑

那么left这个位置就是一个新的坑

5 1 2 hole 9 3 4 7 10 8

然后right再找小

找到了4

那么我们将4挖出去填坑,那么此时的right成为新的坑

5 1 2 4 9 3 hole 7 10 8

左边继续找大,找到了9

将9挖出来,去填坑

那么此时的left的位置就是坑

5 1 2 4 hloe 3 9 7 10 8

然后right继续找小

找到了3

把3拿去填坑

那么此时的right的位置就是坑

5 1 2 4 3 hole 9 7 10 8

然后left接着走

然后left和right已经重合了

那么我们将之前的基准值填到坑里面

5 1 2 4 3 6 9 7 10 8

那么基准值6的左边比基准值小,右边比基准制度大

  1. //挖坑版
  2. int _QuickSort2(int* arr, int left, int right)//找基准值,返回Int
  3. {
  4. int hole = left;
  5. int key = arr[hole];
  6. while (left<right)//只要leftright重合那么我们就跳出循环
  7. {
  8. while (left < right && arr[right] > key)//没有找到比基准值小的,后面的条件我们就不用加等号了,因为可能存在这个数组的元素都是一样的情况
  9. {
  10. right -- ;
  11. }
  12. //那么这里我们找到了小的,那么我们就将这个数字拿去填坑,那么此时right指向的位置就是新的坑
  13. arr[hole] = arr[right];
  14. hole = right;
  15. while (left < right && arr[left] < key)//没有找到比基准值小的
  16. {
  17. left++;
  18. }
  19. //走到这里就说明找到了要拿去填坑的数字
  20. arr[hole] = arr[left];
  21. hole = left;
  22. }
  23. //走到这里说明我们的leftright可能重合了
  24. //那么我们将基准值放到坑位
  25. arr[hole] = key;
  26. return hole;//将坑位返回,此时的坑位就是基准值
  27. }
lomuto前后指针

创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。

定义两个变量prev和cur

cur指向位置的数据跟key值比较,若arr[cur]<arr[keyi]

那么prev向后走一步并和cur交换数据

若arr[cur]≥arr[keyi]

那么cur继续往后走

直到cur越界,那么我们将此时的prev和key的值进行交换

那么此时prev指向的位置就是我们要的基准值

  1. //lomuto前后指针法
  2. int _QuickSort3(int* arr, int left, int right)//找基准值,返回Int
  3. {
  4. int prev = left, cur = left + 1;
  5. int keyi = left;
  6. while (cur<=right)//当我们的cur越界了我们就不进行循环
  7. {
  8. if (arr[cur] < arr[keyi]&&++prev!=cur)//两个指针相等的话,那么我们就不进行交换
  9. {
  10. //只要cur找到小的,就让prve++然后进行交换
  11. Swap(&arr[cur], &arr[prev]);
  12. }
  13. cur++;
  14. }
  15. //cur越界了
  16. //将之前定义的基准值和现在prev指向的值进行交换
  17. Swap(&arr[keyi], &arr[prev]);
  18. return prev;
  19. }

快排的非递归版本

上面介绍到的都是递归版本的

那么我们接下来就介绍下快排的非递归版本

那么非递归的快速排序的需要借助数据结构:栈

我们将区间right和left依次入栈

那么我们再依次取出来,得到的就是0和5

假设我们经过依次寻找,那么这里的基准值是

那么我们就能得到左区间和右区间

左区间[left,keyi-1]

右区间[keti+1,right]

总结下来我们就是先将区间入栈

先是right再试left

我们再出一次栈

那么我们根据这个区间找基准值

前面介绍的三种方法

我们是right先入栈,left先出栈,那么取出来的时候就是left先出栈,right再出栈

那么第一次的话我们的左区间是[0,2],右区间是[4,5]

我们先将右区间入栈,再将左区间入栈

那么我们进行出栈操作,取到的是left=0,right=2

那么在这个区间我们的keyi=0,这里的是下标

那么我们再重复上面的左右区间的方法继续找基准值

左区间[left,keyi-1] [0,-1]

右区间[keti+1,right] [1,2]

这里的基准值是不需要继续去找基准值的,因为这里的keyi-1已经小于0了

那么我们就不用入栈了

就是非有效区间不入栈

但是右区间是有效的区间,那么我们进行入栈操作

那么我们接着对右区间进行递归操作

我们将left和right取出来

left=1,right=2

我们对这个右区间找基准值

那么我们这里的基准值keyi=1,就是数组中对应的3

那么根据当前的基准值我们又能划分区间了

左区间[left,keyi-1] [1,0] 非有效区间不进行入栈

右区间[keti+1,right] [2,2] 只有一个数据不进行入栈

那么此时我们的左区间所有的内容都递归完了

这里其实是循环,不是递归,我们循环取栈的数据模拟的递归

我们排右区间

我们将栈中的left和right取出

left=4,right=5 我们找到的基准值keyi=4,指向6这个数字

那么我们再次根据这个基准值划分左右区间

左区间[left,keyi-1] [4,3]非有效区间,不入栈

右区间[keti+1,right] [5,5] 只有一个有效数据,不如栈

那么我们的右区间就已经找好基准值了

那么这个数组的右区间已经递归完了

通过栈来实现快速排序

  1. //非递归版本的快速排序
  2. //借助数据结构---栈
  3. void QuickSortNonR(int* arr, int left, int right)
  4. {
  5. ST st;
  6. STInit(&st);//栈的初始化
  7. //先将右区间入栈,再就是左区间
  8. SrackPush(&st, right);
  9. SrackPush(&st, left);
  10. //栈只要不为空,那么我们就取栈顶元素
  11. while (!StackEmpty(&st))//取两次,作为左右区间
  12. {
  13. //取栈顶元素--两次
  14. //那么第一次取到的就是left,第二次取到的就是right
  15. int begin = StackTop(&st);
  16. SrackPop(&st);//取完栈顶元素接着进行出栈操作
  17. //取出来的左右区间
  18. int end= StackTop(&st);
  19. SrackPop(&st);
  20. //利用取出来的左右区间创建新的区间
  21. //[begin,end]---找基准值
  22. //双指针找基准值
  23. int prev = begin;
  24. int cur = begin + 1;
  25. int keyi = begin;
  26. while (cur<=end)//当我们的cur越界了我们就不进行循环
  27. {
  28. //要确保prev++完成之后的值不等于cur我们才能进入循环进行操作
  29. if (arr[cur] <= arr[keyi]&&++prev!=cur)
  30. {
  31. Swap(&arr[cur], &arr[prev]);
  32. }
  33. cur++;
  34. }
  35. //跳出循环的时候说明cur已经越界了
  36. Swap(&arr[keyi], &arr[prev]);
  37. //那么此时的prev指向的位置就是基准值的位置
  38. keyi = prev;
  39. //解下来我们根据这个基准值再对左右区间进行操作
  40. //根据基准值划分左右区间
  41. //左区间:[begin,keyi-1]
  42. //右区间:[keyi+1,end]
  43. //如果我们根据这个begin和end找基准值的话那么这两个量是不会越界的
  44. //我们是需要对下面的keyi+1和keyi-1进行判断是不是有效的值
  45. if(keyi + 1<end)//等于end的话就是说明有效数据只有一个
  46. //如果是大于的话,那么就是越界情况了,比end还大
  47. //如果满足这个条件的话那么我们就入栈
  48. {
  49. //先入右区间,
  50. SrackPush(&st, end);//右区间内的右区间
  51. SrackPush(&st, keyi + 1);//右区间内的左区间
  52. }
  53. if (keyi - 1>begin)//如果是[0,-1]就不是一个有效的区间
  54. {
  55. //再入左左区间
  56. SrackPush(&st, keyi - 1);//左区间内的右区间
  57. SrackPush(&st, begin);//左区间内的左区间
  58. }
  59. }
  60. STDestory(&st);//栈的销毁
  61. }
  62. /*
  63. 一开始我们传的left0right5
  64. 我们进行入栈操作
  65. 先入的是5,再入的是0
  66. 入完栈之后,判断栈是否为空
  67. 不为空的话就进入循环取栈顶元素
  68. 先取0,再取5
  69. 那么05组成了一个空间
  70. 那么我们对这个区间找基准值keyi
  71. 第一次的基准值我们找的是3,这里是下标
  72. 那么我们使用双指针法进行基准值的寻找
  73. 找到基准值之后我们根据这个基准值进行新区间的划分
  74. 划分左右区间
  75. 因为keyi=3
  76. 那么左区间就是[0,2] 右区间是[4,5]
  77. 那么这两个区间都是有效区间,那么就都进入了if条件语句中
  78. 那么进行条件语句中将左右区间进行入栈的操作
  79. 先入的是右区间
  80. 再就是左区间
  81. 先入的是5 4
  82. 再就是 2 0
  83. 那么现在这个栈从上到下看就是0 2 4 5
  84. 因为此时的栈不是空的,所以我们还继续循环
  85. 然后我们将栈顶元素取出来[0,2]
  86. 我们再根据这个区间进行基准值的寻找
  87. 那么这个区间的基准值是0
  88. 那么对于[0,2]这个区间的话我们分出的区间是[0,-1]和[1,2]
  89. 那么我们对这段区间进行判断是否为有效的区间
  90. 对于[1,2]满足条件,那么就入栈
  91. 但是[0,-1]这个不满足,那么就不入栈
  92. 那么此时的栈中我们入的是 2 1
  93. 那么此时的栈中元素从上到下看就是2 1 4 5
  94. 然后因为栈不为空,那么继续进行循环
  95. 2 1 取出找基准值
  96. 那么对于 [1,2]这个区间来说的话
  97. 我们又分出两个区间
  98. [1,0]和[2,2]
  99. 两个区间都不是有效的区间
  100. 那么就都不入栈
  101. 那么我们就进行45的出栈了
  102. [4,5]
  103. 我们对[4,5]找基准值
  104. 基准值是4
  105. 那么分出的区间都是无效区间
  106. 那么此时的栈就是空的
  107. 那么我们对于每个区间我们都找好了基准值了
  108. 那么我们就退出了循环
  109. */

4.归并排序

归并排序算法思想:

归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divideand Conquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。 归并排序核⼼步骤:

对于图中这8个数据,我们实现二分,从中间一分为二

如果我们单看分解的那部分,这个图长得很像二叉树

那么我们可以参考二叉树递归的过程对他进行分解

分解到只有一个数据的时候

合并的时候两两一合并

两个有序的数组合并

8个有序数组两两一合并成为4个数组

4个有序数组两两已合并成为2个数组

  1. //归并排序
  2. void _MergeSort(int* arr, int left, int right,int*tmp)//左下标和右下标求中间值,还要将开辟的空间的指针进行接收,进行合并数据的存储
  3. {
  4. //递归结束的条件,就是刚好被分的数组中只剩下一个元素,那么我们就没必要往下进行递归了
  5. if (left >= right)
  6. {
  7. return;
  8. }
  9. //我们需要根据leftright不断进行中间下标的寻找
  10. int mid = (left + right) / 2;
  11. //根据mid得到两个区间,不断地进行二分,将数据分成两组
  12. //[left,mid] [mid+1,right]
  13. _MergeSort(arr, left, mid, tmp);//左区间
  14. _MergeSort(arr, mid + 1, right, tmp);//右区间
  15. //能够走到这里说明我们已经分解完了,那么我们就开始进行合并的操作了
  16. //[left,mid] [mid+1,right]
  17. int begin1 = left, end1 = mid;
  18. int begin2 = mid + 1, end2 = right;
  19. int index = begin1;
  20. //比较两个序列中的数据的大小
  21. //序列中可能是一个数字,也可能是4个数字
  22. while (begin1 <= end1 && begin2 <= end2)//那么此时的这些下标都是有效的
  23. {
  24. if (arr[begin1] < arr[begin2])
  25. {
  26. tmp[index++] = arr[begin1++];
  27. }
  28. else//说明begin2
  29. {
  30. tmp[index++] = arr[begin2++];
  31. }
  32. }
  33. //要么begin1越界,要么begin2越界
  34. while (begin1<=end1)//begin2越界,那么我们就将begin剩下的元素安排到数组中
  35. {
  36. tmp[index++] = arr[begin1++];
  37. }
  38. while (begin2 <= end2)
  39. {
  40. tmp[index++] = arr[begin2++];
  41. }
  42. //[left,mid] [mid+1,right]
  43. //把tmp中的数据拷贝回arr中
  44. for (int i = left; i < right; i++)
  45. {
  46. arr[i] = tmp[i];
  47. }
  48. }
  49. void MergeSort(int* arr, int n)
  50. {
  51. //开辟一个和原数组大小一样的数组用于存储合并后的数组
  52. //动态开辟n个整型空间
  53. int* tmp = (int*)malloc(sizeof(int) * n);
  54. _MergeSort(arr, 0,n - 1,tmp);
  55. free(tmp);
  56. tmp = NULL;
  57. }
  58. /*
  59. 对于这个归并排序的子函数进行解释
  60. 在第一步分解中
  61. 假如我们传的left=0right=7
  62. 数组原先的数据是这样的
  63. 10 6 7 1 3 9 4 2
  64. 那么我们根据左右下标将mid求出
  65. 3
  66. 那么我们就分出了两个区间
  67. [0,3] [4,7]
  68. 然后我们下面又对着两个区间进行递归
  69. 那么我们最后就实现了将这个数组分解的效果了
  70. 那么我们将这么一个完整的数组分成多个单独的序列
  71. 10 6 7 1 3 9 4 2
  72. 那么就是这8个单独的序列
  73. 我们两两合并
  74. 一开始的begin1end1都是0
  75. begin2end2都是1
  76. 那么我们先在第一个while循环中间进行比较并进行赋值到tmp中
  77. 因为10>6那么我们进入到else
  78. 那么tmp[index++] = arr[begin2++];
  79. 将begin2的值赋值到tmp中第一个位置
  80. 然后begin++,index++
  81. 但是现在我们只是将6放到tmp中,10还没有放
  82. 那么我们在后面的while就进行了处理了
  83. 因为我们的begin1此时已经比end1大了,就是越界了
  84. 那么我们就进入到第一个条件语句中
  85. tmp[index++] = arr[begin1++];
  86. 此时的index就是指向的tmp数组中的第二个位置了
  87. 那么我们的第二个位置就复制成之前的begin1
  88. 那么第二个就是10
  89. 然后begin1++
  90. 那么总结:
  91. 在分解为一个个的元素之前,我们的106是一个区间的
  92. 这个区间求中间值分左右区间,合并完成之后我们完成另一个区间的合并
  93. 我们在逐渐的合并过程中,这个begin1 begin2 end1 end2
  94. 都在变化, 当分解之后的第一个合并的时候,begin1end1都是0
  95. begin2end2都是1 那么将610合并之后
  96. 我们就回到了上一级 上一级的左区间已经合并完成,
  97. 但是右区间没有合并,那么我们按照610的步骤合并17
  98. 合并完成之后返回上一级,那么我们就进行现有的(6 10)( 1 7
  99. 这两个区间的合并 此时的begin10end11 begin22
  100. end23 那么我们逐次进行比较并为tmp中进行赋值
  101. 那么最后我们就合并出了 1 6 7 10 那么我们会到上一级
  102. 上一级的左区间合并完成,现在完成右区间的合并后,合并之后就是
  103. 2 3 4 9 那么最后这两个区间合并之后就是1 2 3 4 6 7 9 10
  104. 这个归并排序可以想成二叉树,从叶子到根,从最下面合并到最上面
  105. */

可以想成一个二叉树,我们分解之后进行合并的操作我们可以想象从下面到上面逐次合并

空间复杂度:logn 根据层数来定的

时间复杂度:在归并的子函数中的第一个while循环中就是相当于n个数进行比较

那么这个循环的时间复杂度是O(N)

那么第二个循环的话,进行完第一个循环之后应该是剩下不了几个数据的

那么最大的情况就是n/2

下面的for循环拷贝数组中的值,那么时间复杂度也是n

那么单次递归时间复杂度就是O*(N)

但是递归的深度是logN

那么这里的时间复杂度就是O(N log n)

那么时间效率和快排是一样的

5.非比较排序

计数排序

计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。

操作步骤:

1)统计相同元素出现次数

2)根据统计的结果将序列回收到原来的序列中

我们需要先找到数组中的最大值,然后将数据中的个数变为下标

根据数组中最大值来开辟对应的空间

新数组的大小如何确定?

原数组中最大的值来确定(如果数组中存在负数的话是不行的)(未考虑到最小值)

将负数变为正数,取绝对值是否可行?

正负数绝对值相同的情况下是无法区分正数和负数的

假如我们的给到的值很大,既然我们要用这个当做数组的下标

那么我们是不是要开创这么大的数组呢

其实没有必要的

最大的值-最小的值+1就是这个数组的大小了

举例子:100 101 109 105 101 105

我们数组中的每个元素减去这个数组的最小值,那么不就是这个新数组的下标吗

100-100=0 那么100对应的下标就是0 出现的次数就是1 那么下标为0对应的数据就是1

100对应的下标就是0,下标对应的数据就是100出现的次数

那么其他的也一样

这个计数排序的原理其实就是通过计数

在新的数组中将这个以这个数据出现的次数为元素

我们依次进行打印

比如说在上面那个代码里面

100出现了1次,那么我们打印1次

101出现两次,那么我们打印两次

那么这样我们就达到了排序的目的了

最大值-最小值就是我们要开辟的空间大小

但是这个适用于数组中存在负数的情况吗?

-5 -5 9 5 1

那么这个数组的大小就是9-(-5)+1=15

那么我们就申请下标为15的空间

下标就是从0到14

那么我们应该将这些数据怎么放?

就是这个数据减去这个数组中最小的值

-5的对应的下标就是-5-(-5)=0

那么这个0对应的数据就是-5出现的次数2

然后放9 9-(-5)=14

那么9对应的下标就是14,那么14对应的就是9出现的次数1

那么我们通过这种思想的话那么空间就不会过多浪费

通过测试,这个计数排序是现有的排序算法中最牛逼的

单位ms

[InsertSort:663 ShellSort:9 SelectSort:4300 HeapSort:5 QuickSort:6 MergeSort:5 BubbleSort:12451 CountSort:1](InsertSort:663 ShellSort:9 SelectSort:4300 HeapSort:5 QuickSort:6 MergeSort:5 BubbleSort:12451 CountSort:1)

  1. //计数排序
  2. void CountSort(int* arr, int n)
  3. {
  4. //我们需要一个count数组来保存我们每个元素的出现次数
  5. //我们先需要直到数组中最大值和最小值,在这之后我们才方便数组的开辟
  6. //根据最大值最小值确定数组大小
  7. int max = arr[0], min = arr[0];
  8. //通过遍历找最小值
  9. for (int i = 0; i < n; i++)
  10. {
  11. if (arr[i] > max)
  12. {
  13. max = arr[i];
  14. }
  15. if (arr[i] < min)
  16. {
  17. min = arr[i];
  18. }
  19. }
  20. int range = max - min + 1;//数组元素的个数
  21. //申请range个整型大小的空间存放每个数据出现的次数
  22. int* count = (int*)malloc(sizeof(int) * range);
  23. if (count == NULL)//开辟失败的话我们就报个错
  24. {
  25. perror("malloc fail!");
  26. exit(1);
  27. }
  28. //初始化,让count数组中现在全是0
  29. memset(count, 0, range * sizeof(int));
  30. //第一个参数是要被填充的数组的指针
  31. //第二个参数是要被填充的数据
  32. //第三个参数就是填充的字节的大小
  33. //我们的想法是将这个新数组中现在存储的数据都是0
  34. //那么我们要填充的字节大小就是rang*sizeof(int)
  35. //现在统计数组中每个数据出现的次数,然后将次数放到对应的下标
  36. for (int i = 0; i < n; i++)
  37. {
  38. count[arr[i] - min]++;
  39. //这个arr[i]-min就是我们我们数据映照在新数组的下标,
  40. //后面的++就能将count这个数对应的下标进行计数
  41. //就是这个数据的出现次数通过这个代码就能存进新数组中
  42. }
  43. //count中的数据往arr中放
  44. //我们现在遍历的是count,数组大小是range
  45. int index = 0;
  46. for (int i = 0; i < range; i++)
  47. {
  48. while (count[i]--)
  49. {
  50. arr[index++] = i + min;
  51. }
  52. }
  53. /*
  54. 100 101 102 105 101 100
  55. 2 2 1 0 0 1 数据出现的次数
  56. 0 1 2 3 4 5
  57. 在上面的for循环中,当i=0时,count[i]=2
  58. 那么arr[0]=100+0
  59. 这里是内循环两次的,因为循环条件是count[i]--
  60. 那么第二次内循环就是arr[1]=100
  61. 那么原数组中的前两个数据我们就打印好了
  62. 当i=1时,我们内循环的次数就是这个数出现的次数
  63. count[i]=2
  64. 那么我们就内循环两次
  65. 那么我们就将下标为23的位置都赋值上101
  66. 那么最后我们的arr中的数据就是这样的
  67. 100 100 101 101 105
  68. */
  69. }

空间复杂度是O(range)

我们在第一个循环中的时间复杂度是O(N)

第二个循环是外层循环是O(range)

内层循环的循环次数还是取决于实际的数据n

那么这里的这个循环的时间复杂度是range+n

那么总的时间复杂度就是O(range+n)

那么我们总结一下计数排序的特性:

计数排序在数据范围集中时,效率很高,但是使用范围及场景有限

只适用于整数排序,不能用小数排序

3.排序算法复杂度及稳定性分析

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的

相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之

前,则称这种排序算法是稳定的;否则称为不稳定的。

6 2 4 9 1 2

1 2 2 4 6 9

对于原先数组的第一个2的话,如果在排序之后这个2还是第一个二的话,那么这两个二的相对次序保持不变

相对次数保持不变的话,那么我们就称这个排序为稳定排序

相反,相对次数发生变化的话,那么这个排序就是不稳定的排序

直接选择排序是不稳定的

通过找大找小,交换的过程中就会导致相对次序的变化

直接插入排序就是将后面待排序的数据一个一个往前面插入,和打扑克牌一样的

这里是不存在相对位置发生改变的

那么直接插入排序是稳定的

希尔排序我们是会对数据进行分组的,每个组进行独立的排序,就可能导致原本在后面的数字排在前面

那么次序就会发生变化,那么希尔排序是不稳定的

举例:5 8 2 5 9

gap=2

那么5 2 9 是一组,8和5一组

我们先排第一组、

排完了就是2 8 5 5 9

再排第二组

2 5 5 8 9

可以发现之前在后面的5跑到前面去了,那么相对次序就发生了改变

堆排序的思想:将堆顶的数据和堆尾的数据进行交换

那么这里的相对位置就发生了交换了

归并排序是从中间位置划分,两两一排序,相对位置是不会发生变化的

快排:在找基准值的过程中会导致数据发生次序的变化,那么快排就是不稳定的

回忆:栈的底层是数组

队列是链表实现的

 

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

闽ICP备14008679号