王道 数据结构 第八章 排序 希尔排序开始
一、希尔排序
思路
先将待排序表分割成若干形如L[i,i+d,i+2d,…,i+kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止
即先追求表中元素部分有序,再逐渐逼近全局有序
过程
第三趟排序时,整个表已呈现出“基本有序”,对整体再进行一次“直接插入排序”
代码
void Shell_Sort(int a[],int n) //希尔递增排序,无哨兵
{
int i,j,d;
for(d = n/2;d>=1;d=d/2) //增量以2的倍数递减
{
for(i = d+1;i<=n;++i) //插入排序默认子序列的第一个数为有序
{
if(a[i] < a[i-d]) //当前非有序序列的第一个数比有序序列小
{
a[0] = a[i];//临时存储要插入的数,不是哨兵
for(j = i-d;j>0 && a[0]<a[j];j=j-d) //移动元素,给插入的数腾出空间
{
a[j+d] = a[j];
}
a[j+d] = a[0];//循环结束时j存在偏移量d,因此正确位置为j+d处插入该数
}
}
}
}
性能分析
时间复杂度
和增量序列d1,d2,d3…的选择有关,目前无法用数学手段证明确切的时间复杂度
最坏O(n^ 2),当n在某个范围内时,可达O(n^1.3)
空间复杂度:O(1)
稳定性:不稳定
适用性:仅适用于顺序表,不适合链表
二、冒泡排序
思路
从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换他们,直到序列比较完,称这样的过程为“一趟”冒泡排序。
优化:若某一趟比较没有发生任何交换,则说明整个序列为有序的
代码
void Bubble_Sort(int a[],int n) //冒泡排序
{
for(int i=0;i<n-1;i++) //每一趟排序会把最小元素交换到数组的i位置
//最后一个数不需要排序
{
bool f = false;//是否发生交换
for(int j = n-1;j>i;j--) //从后往前的比较与交换
{
if(a[j] < a[j-1]) //逆序则交换,相同或顺序不交换,保障稳定性
{
swap(a[j],a[j-1]);
f = true;
}
}
if(f == false) return; //若未发生交换,说明整个序列有序
}
}
性能分析
时间复杂度
每次交换都需要移动元素3次
最好情况:O(n)
最坏情况:O(n ^ 2)
平均情况:O(n ^ 2)
空间复杂度:O(1)
稳定性:稳定
适用性:适用于顺序表、链表
三、快速排序
思路
在待排序表L[1…n]中仍取一个元素pivot作为枢轴(基准),通常一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1……n],使得左序列中所有元素小于pivot,右序列中所有元素大于等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为一次"划分"。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
代码
void qsort(int a[],int l,int r)
{
if(l>=r) return;
int i = l-1,j = r+1,pivot = a[(l+r)/2];
while(i<j)
{
do i++;while(a[i]<pivot);
do j--;while(a[j]>pivot);
if(i < j) swap(a[i],a[j]);
}
qsort(a,l,j);
qsort(a,j+1,r);
}
性能分析
若每一次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高
若每一次选中的“枢轴”将待排序序列划分为恨不均匀的两个部分,则会导致递归深度增加,算法效率变低
若初始序列有序或逆序,则快排的性能最差(因为每次的pivot选择为靠边的元素)
时间复杂度
最好情况:O(nlogn)
最坏情况:O(n^2)
空间复杂度
最好情况:O(logn)
最坏情况:O(n)
稳定性:不稳定
四、简单选择排序
思路
每一次循环找出最小的,与数组前面的数进行交换
代码
void select_sort(int a[],int n)
{
for(int i=0;i<n-1;i++)
{
int Min = i;
for(int j = i+1;j < n;j++)
{
if(a[j] < a[Min]) Min = j;
}
if(Min!=i) swap(a[i],a[Min]);
}
}
性能分析
时间复杂度
最好情况:O(n^2)
最坏情况:O(n^2)
空间复杂度
O(1)
稳定性:不稳定
适用性:顺序表、链表
五、堆排序
什么是堆?
若n个关键字序列L[1……n]满足下列某一条性质,则称为堆:
1.若满足:L(i)≥L(2i)且L(i)≥L(2i+1)(1≤i≤n/2)——大根堆
2.若满足:L(i)≤L(2i)且L(i)≤L(2i+1)(1≤i≤n/2)——小根堆
建立大根堆
思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
检查当前结点是否满足根大于等于左、右,若不满足,将当前结点与更大的一个孩子互换(建立小根堆时,将当前结点与更小的一个孩子互换)
若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断下坠)
插入元素
对于小根堆,新元素放到表尾,与父结点对比,若新元素比父结点小,则将二者交换。新元素就这样一路上升,直到无法上升为止
删除元素
被删除的元素用堆底元素代替,然后让该元素不断下坠,直到无法下坠为止
基于堆进行排序
每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换),并将待排序元素序列再次调整为大根堆(小元素不断下坠)
效率分析
时间复杂度 O(nlogn)
建堆的过程,关键字对比次数不超过4n,建堆时间复杂度 = O(n)
排序时间复杂度 = O(nlogn)
空间复杂度 O(1)
空间复杂度 = O(1)
稳定性:不稳定
六、归并排序
归并:把两个及以上已经有序的序列合并成一个
n路归并:n合1,每选出一个元素需要对比关键字n-1次
代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
int a[N],tmp[N],n;
void merge_sort(int a[],int l,int r)
{
if(l>=r) return;//区间元素个数不大于1,不需要排序
int mid = l+r>>1;
merge_sort(a,l,mid),merge_sort(a,mid+1,r);//折半拆分一个区间
int i = l,j = mid+1,k = 0;
while(i<=mid && j<=r) //合并后的两个序列一定是有序的,双指针算法进行选择
{
if(a[i] <= a[j]) tmp[k++] = a[i++];
else tmp[k++] = a[j++];
}
while(i<=mid) tmp[k++] = a[i++];//两个序列长度可能不一致,因此把剩余的加入临时数组
while(j<=r) tmp[k++] = a[j++];
for(int i=l,j=0;i<=r;i++,j++) //排序完毕,将临时数组赋值给原数组,区间[l,r]排序完成
{
a[i] = tmp[j];
}
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
merge_sort(a,0,n-1);
for(int i=0;i<n;i++)
{
cout<<a[i]<<" ";
}
return 0;
}
效率分析
时间复杂度:O(nlogn)
n个元素进行2路归并排序,归并趟数 = log2 n,每趟归并时间复杂度为O(n),则算法时间复杂度为O(nlogn)
空间复杂度:O(n)
稳定性:稳定的
七、基数排序
思想
假设长度为n的线性表中每个结点aj的关键字由d元组()组成
其中,0≤kj≤r-1,r称为"基数"
基数排序得到递减序列的过程如下:
初始化:设置r个空队列
按照各个关键字位权重递增的次序(个、十、百),对d个关键字位分别做"分配"和"收集"
时间复杂度:O(d(n+r))
一趟分配O(n),一趟收集O(r),总共d趟分配、收集,总的时间复杂度 = O(d(n+r))
空间复杂度:O(r)
需要r个辅助队列
稳定性:稳定
应用
基数排序擅长解决的问题:
①数据元素个数可以方便地拆分为d组,且d较小
②每组关键字的取值范围不大,即r较小
③数据元素个数n较大
八、外部排序
磁盘中以"块"为单位进行存储,每块大小为1KB
将数据读入内存并进行归并排序后,再写入磁盘
构造初始归并块段:需要16次"读"和"写"操作
知识梳理与总结
简单选择排序,能够取出当前无序序列中最(小or大)值与第一位置的元素互换位置。
堆排序每趟总能选出一个最值位于根节点。
冒泡排序总是两两比较选出一个最值位于数组前面。
快速排序选出的枢轴在一趟排序中就位于了它最终的位置
插入排序(直接、二分)不一定会位于最终的位置,因为不确定后面插入的元素对于前面的元素是否产生影响。
希尔排序(本质也是插入排序)只在子序列中直接插入排序。所以不能确定。
二路归并排序除非在缓存区一次放入所有的序列(这样得不偿失),否则不能确定最终位置。
所以能够在一趟结束后,就选出一个元素在其最终的位置上的排序是否就只有 :
简单选择排序、快速排序、冒泡排序、堆排序
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了