飞道的博客

【数据结构】手撕八大排序算法

390人阅读  评论(0)

作者:一个喜欢猫咪的的程序员

专栏:《数据结构》

喜欢的话:世间因为少年的挺身而出,而更加瑰丽。                                  ——《人民日报》


目录

 1.排序的概念:

 2.八大排序的思路及其细节

2.1直接插入排序

2.2希尔排序

2.3选择排序:

2.4堆排序

2.5冒泡排序

2.6快速排序:

2.6.1hoare版本(递归版本)

2.6.2三数取中

2.6.3挖坑法

2.6.4前后指针法:

2.6.5非递归写法:

2.7归并排序

2.7.1递归写法:

2.7.2非递归写法:

2.8计数排序


 1.排序的概念:

排序就是把集合中的元素按照一定的次序排序在一起。一般来说有升序排列和降序排列2种排序,在算法中有八大基本排序:

算法的优良主要从4个方面进行评测:

  • 1.时间复杂度
  • 2.空间复杂度
  • 3.适用场景
  • 4.稳定性  

 2.八大排序的思路及其细节

2.1直接插入排序

动图演示:

 当前n-1个已是有序的情况下,将第n个元素插入进去排序,插入到顺序正确的位置。 

将此过程一直重复的话可以可以完成上图的情况。(以数组a为例,来观察一下各个过程)

int a[] = {9,1,2,5,7,4,8,6,3,5};

思路: 

 用一个变量end=i,利用tmp记录下end位置的下一个位置a[end+1]的值,如果a[end]>tmp,a[end]=tmp然后end--。最后将tmp的值赋给end+1的位置。

考虑极端情况:

当数组有n个数时,下标最大值为n-1

当end=n-1时,end+1=n(此时造成了越界)


  
  1. void InsertSort(int* a, int n)
  2. { //插入排序
  3. for (int i = 0; i < n - 1; i++)
  4. {
  5. int end = i;
  6. int tmp = a[ end + 1];
  7. while ( end >= 0)
  8. {
  9. if (a[ end] > tmp)
  10. {
  11. a[ end + 1] = a[ end];
  12. end--;
  13. }
  14. else
  15. {
  16. break;
  17. }
  18. }
  19. a[ end+ 1] = tmp; //防止 end=- 1
  20. }
  21. }

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


2.2希尔排序

对插入排序的时间复杂度进行分析,得出了以下结论:

  •  普通插入排序的时间复杂度最坏情况下为O(N2),此时待排序列为逆序,或者说接近逆序。
  •  普通插入排序的时间复杂度最好情况下为O(N),此时待排序列为升序,或者说接近升序。

动图演示:

 待排序列先进行一次预排序,让待排序列变为接近有序的(接近需要的顺序),然后再进行一次直接插入排序。

因为直接插入排序前的待排序列已是接近有序的情况了,因此时间复杂度为O(N),只要控制预排序阶段的时间复杂度不超过O(N^2),那么整体的时间复杂度就比直接插入排序的时间复杂度低了。

 希尔排序法又称缩小增量法。希尔排序法的基本思想是:

设定一个gap=n/2,将相距gap位置的两个数作比较,如果前面的小于后面交换以此循环

问题:为什么gap=n/2?
answer:gap越大,数据挪动得越快;gap越小,数据挪动得越慢。前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数。

注:一般情况下,取序列的一半作为增量,然后依次减半,直到增量为1(也可自己设置)。

  思路:

单趟排序:当a[end]>a[end+gap]时,将end的值赋给end+gap后end-=gap,在end<0时退出循环

当有n个数时,因为比较是相距gap距离的两个数比较,因此循环次数要小于n-gap次

gap=n每次取半直到最终取到gap=1时,每次取半都是一次一次单趟排序


  
  1. void ShellSort(int* a, int n)
  2. {
  3. int gap = n;
  4. while (gap > 1)
  5. {
  6. gap = gap/ 2;
  7. for ( int i = 0; i < n - gap; i++) //i++并排运算
  8. {
  9. int end = i;
  10. int tmp = a[end + gap];
  11. while (end >= 0)
  12. {
  13. if (a[end] > tmp)
  14. {
  15. //Swap(&a[end], &a[end + gap]);
  16. a[end + gap] = a[end];
  17. end -= gap;
  18. }
  19. else
  20. break;
  21. }
  22. a[end + gap] = tmp; //防止end<0
  23. }
  24. }
  25. }

希尔排序详细的时间复杂度和空间复杂度:

《数据结构(C语言版)》--- 严蔚敏

《数据结构-用面相对象方法与C++描述》--- 殷人昆

时间复杂度:O ( NlogN )   空间复杂度:O ( 1 )

平均时间复杂度:O ( N^ 1.3 ) 


2.3选择排序:

动图演示:

 思路:

设两个下标begin和end,begin初始化为0,end初始化为n-1

设置最大值和最小值的下标让他们指向begin和end

当a[i]的值比a[begin]小,更新min的值,当a[i]的值比a[begin]大,更新max的值

循环走完后确认了最小值的下标,将a[begin]和a[min]进行交换,以及a[end]和a[max交换]

极端情况:

当最大值为数组的第一个时,max=min

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


  
  1. void SelectSort(int* a, int n)
  2. {
  3. int begin = 0;
  4. int end = n - 1;
  5. while ( begin < end)
  6. {
  7. int mini = begin;
  8. int maxi = end;
  9. for (int i = begin+ 1; i <= end; i++)
  10. {
  11. if (a[i] < a[mini])
  12. {
  13. mini = i;
  14. }
  15. if (a[i] > a[maxi])
  16. {
  17. maxi = i;
  18. }
  19. }
  20. Swap(&a[mini], &a[ begin]);
  21. if (maxi == begin)
  22. {
  23. maxi = mini;
  24. }
  25. Swap(&a[maxi], &a[ end]);
  26. begin++;
  27. end--;
  28. }
  29. }

2.4堆排序

堆排序:(以小堆为例)
堆的分类:

1.升序or降序
2.大堆or小堆


  
  1. void test2()
  2. { //堆排序
  3. int array[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
  4. Heapsort( array, sizeof( array) / sizeof( array[ 0]));
  5. for ( int i = 0; i < sizeof( array) / sizeof( array[ 0]); i++)
  6. {
  7. printf( "%d ", array[i]);
  8. }
  9. printf( "\n");
  10. }

Heapsort函数(堆排序):
int array[] = { 27,15,19,18,28,34,65,49,25,37 };

需将这个数组进行大堆排列,分为两种调整形式:向上调整和向下调整。

向上调整和向下调整的思想可以参考我的例外一篇博客:http://t.csdn.cn/UD52X


  
  1. void Ajustup (HPDataType*a, int child)
  2. { //N*logN
  3.      assert(a);
  4.      //int child = n - 1;
  5.      while (child > 0)
  6.     {
  7.          int parent = (child - 1) / 2;
  8.          if (a[child] > a[parent])
  9.         {
  10.             Swap(&a[child], &a[parent]);
  11.             child = parent;
  12.         }
  13.          else
  14.         {
  15.              break;
  16.         }
  17.     }
  18. }
  19. void Ajustdown (HPDataType* a, int n,int parent)
  20. { //O(N)
  21.      assert(a);
  22.      int child = 2 * parent+ 1;
  23.      while (child<n)
  24.     {
  25.          if (child + 1 < n && a[child] < a[child + 1]) //  <假设左子树大
  26.         {
  27.             child++;
  28.         }
  29.          if (a[child] > a[parent]) //>大堆,<为小堆
  30.         {
  31.             Swap(&a[child], &a[parent]);
  32.             parent = child;
  33.             child = child * 2 + 1;
  34.         }
  35.          else
  36.         {
  37.              break;
  38.         }
  39.     }
  40. }

 向上调整和向下调整具体的时间复杂度是多少呢?

向下调整具体的时间复杂度:

假设树高为h

第h层,有2^(h-1)个节点,需要向下调整0次(直接不算,从第h-1层开始算)。

第h-1层,有2^(h-2)个节点,需要向下调整1层。

第h-2层,有2^(h-3)个节点,需要向下调整2层。

......

第4层,有2^3个节点,需要向下调整h-4层。

第3层,有2^2个节点,需要向下调整h-3层。

第2层,有2^1个节点,需要向下调整h-2层。

第1层,有2^0个节点,需要向下调整h-1层。

 当h高的次数,最多调整层数为:

F(h)=2^0*(h-1)+2^1*(h-2)+2^2*(h-3)+...+2^(h-3)*2+2^(h-2)*1+2^(h-1)*0       ——①

2*F(h)=2^1*(h-1)+2^2*(h-2)+2^3*(h-3)+...+2^(h-2)*2+2^(h-1)*1+2^(h)*0       ——②

有错位相减②-①可得:

F(h)=-2^0*(h-1)+2^1+2^2+....+2^(h-2)+2^(h-1)

F(h)=2^h-1-h                                                                                                           ——③

 当树高为h时,节点总个数N为:

N=2^0+2^1+...+2^(h-2)+2^(h-1)

N=2^h-1                                                                                                                        ——④

有④可得:h=log(N+1)                                                                                            ——⑤

综合③④⑤可得:

F(N)=N-log(N+1)

  •  因此时间复杂度为O(N)

向上调整具体的时间复杂度:
在一层,需要向上调整0次

第二层,向上调整1次

第三层,向上调整2次

...

第h-1层,向上调整h-2次

第h层,向上调整h-1次

F(h)=2^1*1+2^2*2+....+2^(h-1)*(h-1)。

由错位相减可得:

F(N)=2N(1-log(N+1))。

时间复杂度为O(N*logN)
如何实现堆排序
显然向下调整优于向上调整。

 先利用Ajustdown排序好数组,然后再用交换Ajustdown实现小堆。


  
  1. void Heapsort( int*a, int n) //堆排序
  2. { //向上调整
  3. for ( int i = 1; i <n; i++)
  4. {
  5. Ajustup(a, i);
  6. }
  7. //向下调整
  8. for ( int i = (n - 1 - 1) / 2; i >= 0; i--)
  9. {
  10. Ajustdown(a, n, i);
  11. }
  12. int end = n - 1;
  13. while (end> 0)
  14. {
  15. Swap(&a[ 0], &a[end]);
  16. Ajustdown(a, end, 0);
  17. end--;
  18. }
  19. //N*logN
  20. }
  21. void test2()
  22. { //堆排序
  23. int array[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
  24. Heapsort( array, sizeof( array) / sizeof( array[ 0]));
  25. for ( int i = 0; i < sizeof( array) / sizeof( array[ 0]); i++)
  26. {
  27. printf( "%d ", array[i]);
  28. }
  29. printf( "\n");
  30. }

2.5冒泡排序

动图演示:


 

 代码:


  
  1. void BubbleSort(int* a, int n)
  2. {
  3. for (int j = 0; j < n; j++)
  4. {
  5. for (int i = 1; i < n - j; i++)
  6. {
  7. if (a[i] < a[i - 1])
  8. {
  9. Swap(&a[i],& a[i - 1]);
  10. }
  11. }
  12. }
  13. }

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


2.6快速排序:

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

快速排序有三种写法:

1.hoare版本

2.挖坑法

3.前后指针法

以及优化:

1.三数取中

2.6.1hoare版本(递归版本)

动图演示:

 hoare版本思路:

单趟排序,key一般选最左边或者最右边

当key为最左边,右边找小,左边找大,然后交换继续,相遇停止,相遇的值跟key交换

当key为最右边相反

 当左区间有序,右区间有序那整体就ok了,如果左右区间不有序,左右区间就是单趟的子问题

当区间只有一个值,就不排了,返回

 问题:为什么是key为最左边时,右边先走,最右边做key时,左边先走

answer:左边做key,右边先走,可以保证相遇位置比key要小

此时有两种情况:

1.相遇,left是停着(一定>=key),right向后走,相遇的位置是left的位置

2.相遇,right是停着(一定<=key),left向前走,相遇的位置是right的位置

单趟有两个意义

1.分割出左右区间,左比key小,右比key大

2.key到了正确位置(排序后的最终位置)

key以后不用变了,到了正确位置

代码:


  
  1. int Partsort1(int* a, int begin, int end)
  2. { //hoare版本
  3. int mid = GetMidIndex(a, begin, end);
  4. Swap(&a[mid], &a[ begin]);
  5. int left = begin;
  6. int right = end;
  7. int keyi = begin;
  8. while (left < right)
  9. {
  10. while (left < right && a[right] >= a[keyi])
  11. {
  12. right--;
  13. }
  14. while (left < right && a[left] <= a[keyi])
  15. {
  16. left++;
  17. }
  18. Swap(&a[left], &a[right]);
  19. }
  20. Swap(&a[left], &a[keyi]);
  21. keyi = left;
  22. return keyi;
  23. }
  24. void QuickSort(int* a, int begin, int end)
  25. {
  26. if ( begin >= end)
  27. {
  28. return;
  29. }
  30. if ( end- begin+ 1 < 10)
  31. {
  32. InsertSort(a + begin, end - begin + 1);
  33. }
  34. else
  35. {
  36. int keyi=Partsort1(a, begin, end);
  37. //int keyi=Partsort2(a, begin, end);
  38. //int keyi = Partsort3(a, begin, end);
  39. QuickSort(a, begin, keyi - 1);
  40. QuickSort(a, keyi + 1, end);
  41. }
  42. }

时间复杂度:O(NlogN) 

2.6.2三数取中

 每次排序都会将数组分为三个部分:

【left-key-1】【key】【keyi+1-right】

在理想情况下,我们每次进行完单趟排序后,key的左序列与右序列的长度都相同:

 若每趟排序所选的key都正好是该序列的中间值,即单趟排序结束后key位于序列正中间,那么快速排序的时间复杂度就是O(NlogN)。

但事实上可能会遇到极端情况:就是我们每次取到的都是最大值或者最小值,那么快排的时间复杂度达到最低O(N^2)

 可以看到,这种情况下,快速排序的时间复杂度退化为O(N^2)。其实,对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则则效率越高。

为了避免这种极端情况的发生,于是出现了三数取中:
 三数取中,当中的三数指的是:最左边的数、最右边的数以及中间位置的数。三数取中就是取这三个数当中,值的大小居中的那个数作为该趟排序的key。这就确保了我们所选取的数不会是序列中的最大或是最小值了。

代码:


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

2.6.3挖坑法

动图演示:

 思路:

将一开始的left保存起来,然后左边 空出来一个坑,右边先走,右找大,然后将右边的值的数据填进去,找到的位为坑,左边找小将右边的坑填进去,最后一定会在坑的位置相遇

代码:


  
  1. int Partsort2 (int* a, int begin, int end)
  2. { //挖坑法
  3. int mid = GetMidIndex(a, begin, end);
  4. Swap(&a[mid], &a[begin]);
  5. int left = begin;
  6. int right = end;
  7. int keyi = a[left];
  8. int hole = left;
  9. while (left < right)
  10. {
  11. while (left < right && a[right] >= keyi)
  12. {
  13. right--;
  14. }
  15. a[hole] = a[right];
  16. hole = right;
  17. while (left < right && a[left] <= keyi)
  18. {
  19. left++;
  20. }
  21. a[hole]=a[left];
  22. hole = left;
  23. }
  24. a[hole] = keyi;
  25. keyi = left;
  26. return hole;
  27. }
  28. void QuickSort (int* a, int begin, int end)
  29. {
  30. if (begin >= end)
  31. {
  32. return;
  33. }
  34. if (end-begin+ 1 < 10)
  35. {
  36. InsertSort(a + begin, end - begin + 1);
  37. }
  38. else
  39. {
  40. //int keyi=Partsort1(a, begin, end);
  41. int keyi=Partsort2(a, begin, end);
  42. //int keyi = Partsort3(a, begin, end);
  43. QuickSort(a, begin, keyi - 1);
  44. QuickSort(a, keyi + 1, end);
  45. }
  46. }

时间复杂度:O(NlogN) 


2.6.4前后指针法:

动图演示:

 思路:

1、cur找比key小,找到后停下来
2、++prev, 交换prev位置和cur位置的值

代码:


  
  1. int Partsort3(int* a, int begin, int end)
  2. {
  3. int prev = begin;
  4. int cur = begin + 1;
  5. int keyi = begin;
  6. while (cur<= end)
  7. {
  8. /*if (a[cur] < a[keyi])
  9. {
  10. Swap(&a[++prev], &a[cur]);
  11. }*/
  12. if (a[cur] < a[keyi]&&++prev!=cur)
  13. {
  14. Swap(&a[prev], &a[cur]);
  15. }
  16. cur++;
  17. }
  18. Swap(&a[prev], &a[keyi]);
  19. keyi = prev;
  20. return keyi;
  21. }
  22. void QuickSort(int* a, int begin, int end)
  23. {
  24. if ( begin >= end)
  25. {
  26. return;
  27. }
  28. if ( end- begin+ 1 < 10)
  29. {
  30. InsertSort(a + begin, end - begin + 1);
  31. }
  32. else
  33. {
  34. /*int keyi=Partsort1(a, begin, end);
  35. int keyi=Partsort2(a, begin, end);*/
  36. int keyi = Partsort3(a, begin, end);
  37. QuickSort(a, begin, keyi - 1);
  38. QuickSort(a, keyi + 1, end);
  39. }
  40. }

时间复杂度:O(NlogN) 


2.6.5非递归写法:

思路:

通过非递归的方式实现递归的情况的话,递归从底层是先排左边再排右边因此类推,因此写非递归我们从顶层到底层就需要反过来。

借助栈的内存结构让先入的后出,所以要先压begin再压end,取出来的话就是先出右再出左

再先排右边再排左边。

代码:


  
  1. void QuickSortNonR(int* a, int begin, int end)
  2. { //非递归
  3. ST st;
  4. StackInit(&st);
  5. StackPush(&st, begin);
  6. StackPush(&st, end);
  7. while (!StackEmpty(&st))
  8. {
  9. int right = StackTop(&st);
  10. StackPop(&st);
  11. int left = StackTop(&st);
  12. StackPop(&st);
  13. int keyi = Partsort3(a, left, right); //单趟排序
  14. if (keyi + 1 < right)
  15. {
  16. StackPush(&st, keyi+ 1);
  17. StackPush(&st, right);
  18. }
  19. if (left < keyi -1)
  20. {
  21. StackPush(&st, left);
  22. StackPush(&st, keyi - 1);
  23. }
  24. }
  25. StackDestory(&st);
  26. }

2.7归并排序

2.7.1递归写法:

动图讲解:

 思路:

将两端有序序列取各种较小的值比较排序成一个有序序列。

 代码: 


  
  1. void _MergeSort (int* a, int begin, int end, int* tmp)
  2. {
  3. if (begin >= end)
  4. {
  5. return;
  6. }
  7. int mid = (begin + end) / 2;
  8. _MergeSort(a, mid+ 1, end, tmp);
  9. _MergeSort(a, begin, mid, tmp);
  10. int begin1 = begin;
  11. int end1 = mid;
  12. int begin2 = mid+ 1;
  13. int end2 = end;
  14. int i = begin;
  15. while (begin1<=end1&&begin2<=end2)
  16. {
  17. if (a[begin1] < a[begin2])
  18. {
  19. tmp[i++] = a[begin1++];
  20. }
  21. else
  22. {
  23. tmp[i++] = a[begin2++];
  24. }
  25. }
  26. while (begin1 <= end1)
  27. {
  28. tmp[i++] = a[begin1++];
  29. }
  30. while (begin2 <= end2)
  31. {
  32. tmp[i++] = a[begin2++];
  33. }
  34. memcpy(a + begin, tmp + begin, sizeof( int) * (end - begin + 1));
  35. }
  36. void MergeSort (int* a, int n)
  37. {
  38. int* tmp = ( int*)malloc(sizeof( int) * n);
  39. if (tmp == NULL)
  40. {
  41. perror( "malloc fail");
  42. exit(- 1);
  43. }
  44. _MergeSort(a, 0, n- 1, tmp);
  45. free(tmp);
  46. tmp = NULL;
  47. }

2.7.2非递归写法:

极端情况:

越界有三种情况

1.end1越界 begin2越界end2越界

2.begin2越界end2越界

 3.end2越界

 整体拷贝的话会有覆盖丢失

代码:


  
  1. void MergeSortNonrR(int* a, int n)
  2. {
  3. int* tmp = ( int*) malloc( sizeof( int) * n);
  4. if (tmp == NULL)
  5. {
  6. perror( "malloc fail");
  7. exit( -1);
  8. }
  9. int RangN = 1;
  10. while (RangN < n)
  11. {
  12. for ( int i = 0; i < n; i += 2 * RangN)
  13. {
  14. int begin1 = i;
  15. int end1 = i + RangN - 1;
  16. int begin2 = i + RangN;
  17. int end2 = i + 2 * RangN - 1;
  18. int j = i;
  19. if (end1>=n) // 修正区间 ->拷贝数据 归并完了整体拷贝 or 归并每组拷贝
  20. {
  21. end1 = n - 1;
  22. begin2 = n; // 不存在区间
  23. end2 = n - 1;
  24. }
  25. else if (begin2 >= n)
  26. {
  27. begin2 = n;
  28. end2 = n - 1;
  29. }
  30. else if(end2>=n)
  31. {
  32. end2 = n - 1;
  33. }
  34. while (begin1 <= end1 && begin2 <= end2)
  35. {
  36. if (a[begin1] < a[begin2])
  37. {
  38. tmp[j++] = a[begin1++];
  39. }
  40. else
  41. {
  42. tmp[j++] = a[begin2++];
  43. }
  44. }
  45. while (begin1 <= end1)
  46. {
  47. tmp[j++] = a[begin1++];
  48. }
  49. while (begin2 <= end2)
  50. {
  51. tmp[j++] = a[begin2++];
  52. }
  53. memcpy(a + i, tmp + i, sizeof( int) * (end2 - i + 1));
  54. }
  55. RangN *= 2;
  56. }
  57. free(tmp);
  58. tmp = NULL;
  59. }

时间复杂度:O(NlogN) 空间复杂度:O(N) 

2.8计数排序

思路:

 绝对映射:count数组中下标为i的位置记录的是arr数组中数字i出现的次数。
相对映射:count数组中下标为i的位置记录的是arr数组中数字min+i出现的次数。

代码:


  
  1. void CountSort( int* a, int n)
  2. {
  3. int min = a[ 0];
  4. int max = a[ 0];
  5. for ( int i = 1; i < n; i++)
  6. {
  7. if (a[i] > max)
  8. {
  9. max = a[i];
  10. }
  11. if (a[i] < min)
  12. {
  13. min = a[i];
  14. }
  15. }
  16. int range = max - min + 1;
  17. int* CoutA = ( int*)calloc( range, sizeof( int));
  18. if (CoutA == NULL)
  19. {
  20. perror( "calloc fail");
  21. exit(- 1);
  22. }
  23. for ( int i = 0; i < n; i++)
  24. {
  25. CoutA[a[i] - min]++;
  26. }
  27. int k = 0;
  28. for ( int i = 0; i < range; i++)
  29. {
  30. while (CoutA[i]--)
  31. {
  32. a[k++] = i + min;
  33. }
  34. }
  35. free(CoutA);
  36. }

时间复杂度:O(N+range)      空间复杂度:O(range)
 


转载:https://blog.csdn.net/m0_69061857/article/details/128634213
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场