• 数据结构—排序算法汇总(插入排序,交换排序,选择排序,二路归并排序,基数排序和外部排序)


    在没有特殊说明的情况下,默认排序结果是升序。 

    1、插入排序

    1.1 直接插入排序算法

    对于直接插入排序,设A[n]是一个有n个数的待排序数组,现假设要排序的位置是i,其执行的过程为(第0号位置不适用,从1号开始):

    1、首先保存第i号元素,把第i号元素放在第0号位置,充当哨兵的作用,这样可以减少循环条件,相对来说会优化一点点;

    2、1— i-1位置的元素都已排好序,将第i个位置的元素与前i-1个元素依次比较,直到找到小于等于自己的元素停止,假设位置是k。

    3、将第k+1—i-1位置上的所有元素后移,空出的第k+1个位置放第i个元素。

    具体代码:

    1. //直接插入排序
    2. void InsertSort(int a[],int n)
    3. {
    4. int j;
    5. for(int i=2;i<=n;i++)
    6. {
    7. if(a[i]<a[i-1]) //当前元素比其前驱小,将a[i]插入有序表中
    8. {
    9. a[0]=a[i]; //保存当前元素,充当哨兵
    10. for(j=i-1;a[j]>a[0];j--) //从后往前寻找待插入位置
    11. {
    12. a[j+1]=a[j]; //向后移动
    13. }
    14. a[j+1]=a[0]; //将第i个元素插入
    15. }
    16. }
    17. }

    空间复杂度:O(1);

    时间复杂度:

    最好情况:O(n);       //原始表中有序,每次插入一个元素,只需要比较一次,而无需移动;

    最坏情况:O(n);       //原始表也是有序,但是是逆序。总的比较次数达到最大,移动次数也达到最大。

    平均时间复杂度:O(n^2);     

    稳定性:每次都是从后往前移动元素,不会出现相同元素相对位置发生变化的情况,稳定。

    直接插入排序即适合顺序存储,也适合链式存储的线性表。但是链式存储只是减少了移动的次数,比较的次数仍然回达到平方的数量级,时间复杂度仍然是平方级。

    但其适用于待排序元素基本有序。 

    1.2  折半插入排序

    折半插入排序是针对直接插入排序算法的一个小改进,其改进之处在于,由于在插入第i个元素时,前i-1个元素是排好序的,所以在查找这步中,我们可以使用折半查找,找到待插入位置,然后再移动。

    具体代码:

    1. //折半插入排序
    2. void InsertSort(int a[],int n)
    3. {
    4. int i,j,low,high,mid;
    5. for(i=2;i<=n;i++)
    6. {
    7. a[0]=a[i];
    8. low=1,high=i-1;
    9. while(low<=high) //折半查找的思想,寻找待插入位置
    10. {
    11. mid=(low+high)/2;
    12. if(a[mid]<=a[0])
    13. {
    14. low=mid+1;
    15. }
    16. else
    17. high=mid-1;
    18. }
    19. for(j=i-1;j>=high+1;j--) //将low —i-1位置的元素后移
    20. {
    21. a[j+1]=a[j];
    22. }
    23. a[high+1]=a[0]; //插入到low的位置
    24. }
    25. }

    时间复杂度:折半查找虽然减少了比较的次数,但是移动的次数还是没变,因此,折半查找的时间复杂度仍为O(n^2);

    折半查找在处理的时候,特别注意了元素值相等的情况,当a[mid]=a[i]的时候,我们继续往后寻找合适的位置,这样做的好处就是可以保证插入排序的稳定性。

    2、希尔排序 

    希尔排序是针对直接插入排序的一种改进。因为直接排序在数组相对有序的情况下,时间复杂度比较小,所以希尔排序的思想就是:先将待排序表分割成若干个形如:a[i,i+d,i+2d……]的“特殊”子表,即把相隔某个增量的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素都呈现出“基本有序”时,再对全体记录进行一次直接插入排序。

    其实希尔排序只不过是增加了一个增量,每次比较的是同一增量的元素,每轮排序完成后,增量减半,但是其基本思想就是直接插入排序。要注意希尔排序的移动的判定条件。

    具体代码:

    1. //希尔排序
    2. void ShellSort(int a[],int n)
    3. {
    4. int d; //增量
    5. for(d=n/2;d>=1;d/=2) //不断缩短增量排序
    6. {
    7. for(int i=d+1;i<=n;i+=d) //前面元素是排好序的,从d+1开始
    8. {
    9. if(a[i]<a[i-d])
    10. {
    11. a[0]=a[i]; //这里的a[0]不是起到哨兵的作用,只是保存第i个元素
    12. for(int j=i-d;j>0&&a[0]<a[j];j-=d) //注意判定条件,将同一个子表中符合条件的元素后移
    13. {
    14. a[j+d]=a[j];
    15. }
    16. a[j+d]=a[0]; //插入元素
    17. }
    18. }
    19. }
    20. }

    时间复杂度:最坏情况下(当d取1时:O(n);当n在某个特定范围时,时间复杂度约为:O(n^1.3)。

    通过排序过程可以看出,希尔排序是个不稳定的排序算法。并且由于它需要使用增量d找到属于同一个子表的元素,所以它不能使用链表这种不具备随机访问特性的存储结构。

     3、冒泡排序

    冒泡排序是一种基于交换的思想,每一趟排序将最大值或者最小值排到前面,每一趟结束后, 每一个位置的元素就是其最终所在的位置。

    1. //冒泡排序
    2. void BubbleSort(int a[],int n)
    3. {
    4. for(int i=0;i<n-1;i++)
    5. {
    6. bool flag=false; //表示本趟冒泡排序是否发生交换的标志
    7. for(int j=0;j<n-1-i;j++)
    8. {
    9. if(a[j]>a[j+1]) //若逆序
    10. {
    11. swap(a[j],a[j+1]); //则交换
    12. flag=true;
    13. }
    14. }
    15. if(flag==false) //本趟遍历没有发生交换,说明已经有序
    16. return ;
    17. }
    18. }

    4、快速排序 

    快速排序是基于分而治之的思想:每一次在待排序的表中选择一个数作为基准,通过一趟排序将待排序表划分为两个部分,每一次排完序,比基准小的元素都在左边,比基准大的元素都在右边,然后左右两边递归进行排序,直到每个部分只有一个元素或者为空位为止。

    在该代码中,假设每次都以第一个元素作为基准进行划分。

    具体代码:

    1. #include <iostream>
    2. using namespace std;
    3. int s[100];
    4. int Partition(int a[],int low,int high)
    5. {
    6. int p=a[low]; //选择基准元素
    7. while(low<high)
    8. {
    9. while(low<high&&a[high]>=p) //把基准后面比基准大的元素往前移
    10. high--;
    11. a[low]=a[high];
    12. while(low<high&&a[low]<p) //把基准前面比基准小的元素往后移
    13. low++;
    14. a[high]=a[low];
    15. }
    16. a[low]=p; //把基准放在最终位置
    17. return low; //返回基准位置
    18. }
    19. void QuickSort(int a[],int low,int high)
    20. {
    21. if(low<high)
    22. {
    23. int mid=Partition(a,low,high);
    24. QuickSort(a,low,mid-1); //对每次划分的前后部分分别递归处理
    25. QuickSort(a,mid+1,high);
    26. }
    27. }
    28. int main(int argc, char** argv) {
    29. int n;
    30. cin>>n;
    31. for(int i=0;i<n;i++)
    32. cin>>s[i];
    33. QuickSort(s,0,n-1);
    34. for(int i=0;i<n;i++)
    35. {
    36. cout<<s[i]<<" ";
    37. }
    38. return 0;
    39. }

    可以看出,快速排序的算法关键在于划分,那么该算法的性能也就主要取决于划分操作的好坏。快速排序需要利用递归进行,所以需要一个递归工作栈作为辅助空间。

    空间复杂度就是该栈所需的空间,最好情况下是:O(logn);最坏的情况就是表基本有序或者基本逆序。空间复杂度为O(n);平均空间复杂度为:O(logn);

    时间复杂度:最好的情况下:O(nlogn);最坏的情况下:O(n);

    可以推算出,快排是一种不稳定的算法。

    虽然快排每次排序后并不产生有序子序列,但是每次排序后都会把基准放到其最终位置上。

    5、选择排序 

    5.1 简单选择排序 

    算法思想:每一趟排序都是在待排序元素中选取关键字最小(或最大)的元素加入到有序子序列中。

    具体代码:

    1. //简单排序
    2. void SelectSort(int a[],int n)
    3. {
    4. for(int i=0;i<n-1;i++) //n个元素需要n-1趟排序
    5. {
    6. int min=i;
    7. for(int j=i+1;j<n;j++) //每次都是寻找最小值
    8. {
    9. if(a[j]<a[min])
    10. min=j;
    11. }
    12. if(min!=i) //若找到最小值,交换
    13. swap(a[i],a[min]);
    14. }
    15. }

    空间复杂度:O(1);

    时间复杂度:O(n*n);

    该算法不稳定,但其既适用于顺序表,又使用于链表。

    5.2 堆排序 

    堆排序是一个性能比较优秀的算法,它也是基于选择排序的思想,大顶堆:在完全二叉树中,根>=左右结点。小顶堆刚好相反。

    堆排序分为:建堆和输出堆顶元素,再继续调整堆。

    建堆过程(以大顶堆为例):从完全二叉树中的最后一个非终端结点,也就是第n/2个结点 开始,若该结点存在左右孩子,则判断该结点的左右孩子是否比根结点大,若是,则选择最大的一个孩子结点和根结点互换,但是互换后可能会破坏其孩子结点的堆,所以继续采用刚才方法构造下一级堆,直到以该结点为根的子树构成大顶堆为止。

    排序过程(以大顶堆为例):每一趟排序将堆顶元素加入到有序子序列(与待排序元素序列中的最后一个元素交换),并将待排序元素序列再次调整为大顶堆。

    大顶堆的具体代码:

    1. #include <iostream>
    2. using namespace std;
    3. int s[100];
    4. //大根堆排序
    5. void HeadAdjust(int a[],int k,int len)
    6. {
    7. a[0]=a[k]; //先把第k个元素存着,以期后面找到合适的位置放置
    8. for(int i=2*k;i<=len;i*=2) //顺着k较大的子结点向下筛选
    9. {
    10. if(i<len&&a[i]<a[i+1]) //i<len保证有右孩子,在左孩子和右孩子中选一个最大
    11. {
    12. i++;
    13. }
    14. if(a[0]>=a[i]) //根结点本身就是最大
    15. break;
    16. else
    17. {
    18. a[k]=a[i]; //根结点替换为最大的孩子
    19. k=i; //孩子结点的大顶堆有可能被破坏,所以沿着k的孩子结点往下判断
    20. }
    21. }
    22. a[k]=a[0]; //被筛选结点的值放入最终位置
    23. }
    24. void BuiltMaxHeap(int a[],int len)
    25. {
    26. for(int i=len/2;i>=1;i--) //从最后一个非终端结点到根结点调整堆
    27. {
    28. HeadAdjust(a,i,len);
    29. }
    30. }
    31. void HeapSort(int a[],int len)
    32. {
    33. BuiltMaxHeap(a,len); //先建堆
    34. for(int i=len;i>1;i--) //n个元素需要进行n-1趟排序
    35. {
    36. swap(a[i],a[1]); //和堆低元素交换,将最大值后移,实现升序
    37. HeadAdjust(a,1,i-1);
    38. }
    39. }
    40. int main(int argc, char** argv) {
    41. int n;
    42. cin>>n;
    43. for(int i=1;i<=n;i++)
    44. cin>>s[i];
    45. HeapSort(s,n);
    46. for(int i=1;i<=n;i++)
    47. {
    48. cout<<s[i]<<" ";
    49. }
    50. return 0;
    51. }

    小顶堆的分析过程和大顶堆类似,所以,这里直接给出代码。需要注意的是:基于大顶堆的排序是升序,基于小顶堆的排序是降序。

     小顶堆的具体代码:

    1. #include <iostream>
    2. using namespace std;
    3. int s[100];
    4. //小根堆排序
    5. void HeadAdjust(int a[],int k,int len)
    6. {
    7. a[0]=a[k]; //先把第k个元素存着,以期后面找到合适的位置放置
    8. for(int i=2*k;i<=len;i*=2) //顺着k小的子结点向下筛选
    9. {
    10. if(i<len&&a[i]>a[i+1]) //i<len保证有右孩子,在左孩子和右孩子中选一个最小
    11. {
    12. i++;
    13. }
    14. if(a[0]<=a[i]) //根结点本身就是最小
    15. break;
    16. else
    17. {
    18. a[k]=a[i]; //根结点替换为最小的孩子
    19. k=i; //孩子结点的小顶堆有可能被破坏,所以沿着k的孩子结点往下判断
    20. }
    21. }
    22. a[k]=a[0]; //被筛选结点的值放入最终位置
    23. }
    24. void BuiltMinHeap(int a[],int len)
    25. {
    26. for(int i=len/2;i>=1;i--) //从最后一个非终端结点到根结点调整堆
    27. {
    28. HeadAdjust(a,i,len);
    29. }
    30. }
    31. void HeapSort(int a[],int len)
    32. {
    33. BuiltMinHeap(a,len); //先建堆
    34. for(int i=len;i>1;i--) //n个元素需要进行n-1趟排序
    35. {
    36. swap(a[i],a[1]); //和堆低元素交换,将最小值后移,实现降序
    37. HeadAdjust(a,1,i-1);
    38. }
    39. }
    40. int main(int argc, char** argv) {
    41. int n;
    42. cin>>n;
    43. for(int i=1;i<=n;i++)
    44. cin>>s[i];
    45. HeapSort(s,n);
    46. for(int i=1;i<=n;i++)
    47. {
    48. cout<<s[i]<<" ";
    49. }
    50. return 0;
    51. }

    空间复杂度:O(1);

    时间复杂度为:最好,最坏和平均都是O(nlogn);

    可以推算出该算法是不稳定的。 

    6、归并排序 

     归并排序的算法思想:将待排序表中的n个记录视为有n个有序的子表,但是每个子表的长度为1,然后两两归并,得到n/2向上取整个长度为1或2的有序表,继续两两归并,如此重复,直到合并为一个长度为n的有序表为止,这就是2路归并排序。

    具体代码:

    1. #include <bits/stdc++.h>
    2. using namespace std;
    3. #define n 100
    4. int s[n];
    5. int *b=(int *)malloc(sizeof(int)*(n+1)); //辅助数组B
    6. int i,j,k;
    7. void Merge(int a[],int low,int mid,int high)
    8. {
    9. for(k=low;k<=high;k++) //先将表中数据复制到辅助数组中
    10. {
    11. b[k]=a[k];
    12. }
    13. for(i=low,j=mid+1,k=i;i<=mid&&j<=high;)
    14. {
    15. if(b[i]<=b[j])
    16. {
    17. a[k++]=b[i++];
    18. }
    19. else
    20. {
    21. a[k++]=b[j++];
    22. }
    23. }
    24. while(i<=mid) //其中有一个表中还有数据
    25. a[k++]=b[i++];
    26. while(j<=high)
    27. a[k++]=b[j++];
    28. }
    29. void MergeSort(int a[],int low,int high)
    30. {
    31. if(low<high)
    32. {
    33. int mid=(low+high)/2; //从中间划分两个子序列
    34. MergeSort(a,low,mid); //对左侧子序列进行递归排序
    35. MergeSort(a,mid+1,high); //对左侧子序列进行递归排序
    36. Merge(a,low,mid,high); //归并
    37. }
    38. }
    39. int main(int argc, char** argv) {
    40. int m;
    41. cin>>m;
    42. for(int i=1;i<=m;i++)
    43. {
    44. cin>>s[i];
    45. }
    46. MergeSort(s,1,m);
    47. for(int i=1;i<=m;i++)
    48. cout<<s[i]<<" ";
    49. return 0;
    50. }

    归并排序的核心操作是:把数组 内的两个有序序列归并为一个。

    空间复杂度:O(n);虽然递归也需要空间,但是归并树可以看成是一个倒立的二叉树。递归深度<logn;

    时间复杂度:O(nlogn)。

    7、基数排序 

    基数排序不是基于比较和移动的思想,而是基于关键字各位的大小进行排序。

    基数排序的基本思想(以递减序列为例):

    1、初始化:设置r个空队列,Qr-1,Qr-2,……,Q0,按照各个关键字位权重递减的次序(个、十、百),对d各关键字位分别做“分配” 和“收集”。

    2、分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾。

    3、收集:把Qr-1,Qr-2,……,Q0各个队列中的结点依次出队并链接。

    空间复杂度:由于基数排序是用链式存储。需要辅助空间r个队列。所以时间复杂度为O(r)。

    时间复杂度:O(d(n+r));基数排序需要进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(r)。

    可以推算出:基数排序是稳定的。

  • 相关阅读:
    React组件
    数据库锁介绍
    爬虫项目(10):白嫖抓第三方网站接口,基于Flask搭建搭建一个AI内容识别平台
    数据可视化:视觉感知与基本图表
    HiveSQL分位数函数percentile()使用详解+实例代码
    《痞子衡嵌入式半月刊》 第 78 期
    面经-LinkedList与ArrayList比较
    Spring IoC容器生命周期内容梳理
    基于React俄罗斯方块h5小游戏源码响应式支持PC+手机
    【DW组队学习—动手学数据分析】第一章:第三节探索性数据分析-课程学习
  • 原文地址:https://blog.csdn.net/m0_51769031/article/details/125459392