前言
“排序”学习提纲。
代码模板
排序的基本概念
相关概念
关键字可以是主关键字、次关键字和关键字组合等
多关键字可以转换为单关键字
- 稳定性:排序前后,两个或两个以上相同的关键字,相对位置没有变化,称算法是稳定的,反之是不稳定的
若关键字无重复,则排序结果唯一 若关键字有重复,则排序结果不唯一,需要考虑算法的稳定性
判断方式: 若存在相邻地比较和移动/交换关键字,则是稳定的 若存在跳跃地比较和移动/交换关键字,则是不稳定的
类型(依据思想)
- 插入类排序
- 交换类排序
- 选择类排序
- 归并类排序
- 基数类排序
插入类排序:
交换类排序:
选择类排序:
归并类排序:
基数类排序:
类型(依据复杂度)
简单排序:
复杂排序:
类型(依据数据是否完全在内存中)
内部排序:
- 插入类排序
- 交换类排序
- 选择类排序
- 归并类排序
- 基数类排序
基本操作
注意:基数类排序基于多关键字比较
时间复杂度->比较和移动的次数
插入类排序
直接插入排序
void insertSort(vector<ELEM_TYPE> &elems)
{
int i = 0;
int j = 0;
int temp = 0;
for (i = 1; i < elems.size(); ++i)
{
temp = elems[i];
for (j = i - 1; j >= 0; --j)
{
if (temp < elems[j])
{
elems[j + 1] = elems[j];
}
else
{
break;
}
}
elems[j + 1] = temp;
}
return;
}
折半插入排序
void binarySort(vector<ELEM_TYPE> &elems)
{
int i = 0;
int j = 0;
int temp = 0;
int left = 0;
int right = elems.size() - 1;
int mid = 0;
for (i = 1; i < elems.size(); ++i)
{
temp = elems[i];
left = 0;
right = i - 1;
while (left <= right)
{
mid = left + (right - left) / 2;
if (temp == elems[mid])
{
left = mid + 1;
}
else if (temp > elems[mid])
{
left = mid + 1;
}
else if (temp < elems[mid])
{
right = mid - 1;
}
}
for (j = i - 1; j >= right + 1; --j)
{
elems[j + 1] = elems[j];
}
elems[right + 1] = temp;
}
return;
}
希尔排序/缩小增量排序
void shellSort(vector<ELEM_TYPE> &elems)
{
int i = 0;
int j = 0;
int temp = 0;
for (int step = elems.size() / 2; step >= 1; step /= 2)
{
for (i = step; i < elems.size(); ++i)
{
temp = elems[i];
for (j = i - step; j >= 0; j -= step)
{
if (temp < elems[j])
{
elems[j + step] = elems[j];
}
else
{
break;
}
}
elems[j + step] = temp;
}
}
return;
}
交换类排序
冒泡排序/起泡排序 简单版
void bubbleSort1(vector<ELEM_TYPE> &elems)
{
int i = 0;
int j = 0;
int temp = 0;
for (i = 0; i < elems.size() - 1; ++i)
{
for (j = i + 1; j < elems.size(); ++j)
{
if (elems[i] > elems[j])
{
temp = elems[i];
elems[i] = elems[j];
elems[j] = temp;
}
}
}
return;
}
冒泡排序 正版
void bubbleSort2(vector<ELEM_TYPE> &elems)
{
int i = 0;
int j = 0;
int temp = 0;
for (i = 0; i < elems.size() - 1; ++i)
{
for (j = elems.size() - 1; j > i; --j)
{
if (elems[j - 1] > elems[j])
{
temp = elems[j - 1];
elems[j - 1] = elems[j];
elems[j] = temp;
}
}
}
return;
}
冒泡排序 改进版
void bubbleSort3(vector<ELEM_TYPE> &elems)
{
int i = 0;
int j = 0;
int temp = 0;
bool flag = false;
for (i = 0; i < elems.size() - 1; ++i)
{
for (j = elems.size() - 1; j > i; --j)
{
if (elems[j - 1] > elems[j])
{
temp = elems[j - 1];
elems[j - 1] = elems[j];
elems[j] = temp;
flag = true;
}
}
if (flag == false)
{
return;
}
}
return;
}
快速排序
void quickSort(vector<ELEM_TYPE> &elems, int left, int right)
{
int i = left;
int j = right;
if (left < right)
{
ELEM_TYPE privot = elems[left];
while (i < j)
{
while (i < j)
{
if (elems[j] >= privot)
{
--j;
}
else
{
break;
}
}
elems[i] = elems[j];
while (i < j)
{
if (elems[i] <= privot)
{
++i;
}
else
{
break;
}
}
elems[j] = elems[i];
}
elems[i] = privot;
quickSort(elems, left, privot - 1);
quickSort(elems, privot + 1, right);
}
return;
}
堆
概念
- 数据结构中的完全二叉树。满足:任意一个非叶结点的关键字都不小于(或不大于)其左和右孩子结点的关键字
类型
- 大顶/根堆:任意一个非叶结点的关键字都不小于其左和右孩子结点的关键字
- 小顶/根堆:任意一个非叶结点的关键字都不大于其左和右孩子结点的关键字
插入过程
- 在最右下位置插入(满足完全二叉树定义)
- 调整(满足堆定义)
删除过程
- 交换最右下位置和删除位置的关键字(满足完全二叉树定义)
- 从最右下位置删除(满足完全二叉树定义)
- 调整(满足堆定义)
堆排序的过程
对大顶堆:
- 无序序列->完全二叉树(依据层序遍历方法)
- 完全二叉树->大顶堆(依据大顶堆调整方法)
- 大顶堆->有序序列(依据堆排序方法)
层序遍历方法:
大顶堆调整方法:
- 从完全二叉树的最后一个非叶子结点开始,从右到左,从下到上,调整结点
- 交换结点以满足大顶堆定义
堆排序方法:
- 交换树根结点和最右下位置结点,删除最右下位置结点,输出最右下位置结点(最右下位置结点进入有序序列)
- 调整(最右下位置结点被交换到树根结点,可能不满足堆定义)
- 重复1和2步,堆无结点时结束
选择类排序
简单选择排序
void selectSort(vector<ELEM_TYPE> &elems)
{
int i = 0;
int j = 0;
int min = 0;
ELEM_TYPE temp = 0;
for (i = 0; i < elems.size() - 1; ++i)
{
min = i;
for (int j = i + 1; j < elems.size(); ++j)
{
if (elems[min] > elems[j])
{
min = j;
}
}
if (min != i)
{
temp = elems[i];
elems[i] = elems[min];
elems[min] = temp;
}
}
return;
}
堆排序
void heapAdjust(vector<ELEM_TYPE> &elems, int low, int high)
{
int i = low;
ELEM_TYPE temp = elems[low];
for (int j = 2 * i; j <= high; j *= 2)
{
if (j < high && elems[j] < elems[j + 1])
{
++j;
}
if (temp < elems[j])
{
elems[i] = elems[j];
i = j;
}
else
{
break;
}
}
elems[i] = temp;
return;
}
void heapSort(vector<ELEM_TYPE> &elems)
{
ELEM_TYPE temp = 0;
for (int i = (elems.size() - 1) / 2; i >= 1; --i)
{
heapAdjust(elems, i, elems.size() - 1);
}
for (int i = elems.size() - 1; i >= 2; --i)
{
temp = elems[1];
elems[1] = elems[i];
elems[i] = temp;
heapAdjust(elems, 1, i - 1);
}
return;
}
归并类排序
二路归并排序 递归法 分治法
void merge(vector<ELEM_TYPE> &elems, int low, int mid, int high)
{
vector<ELEM_TYPE> temp(elems.size(), 0);
for (int i = low; i <= high; ++i)
{
temp[i] = elems[i];
}
int i = low;
int j = mid + 1;
int k = low;
while (i <= mid && j <= high)
{
if (temp[i] <= temp[j])
{
elems[k] = temp[i];
++k;
++i;
}
else
{
elems[k] = temp[j];
++k;
++j;
}
}
while (i <= mid)
{
elems[k] = temp[i];
++k;
++i;
}
while (j <= high)
{
elems[k] = temp[j];
++k;
++j;
}
return;
}
void mergeSort(vector<ELEM_TYPE> &elems, int low, int high)
{
if (low < high)
{
int mid = low + (high - low) / 2;
mergeSort(elems, low, mid);
mergeSort(elems, mid + 1, high);
merge(elems, low, mid, high);
}
return;
}
基数类排序
基数排序
总结
插入类排序
名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|
直接插入排序 | O(n2) | O(1) | 稳定 | 折半插入排序 | O(n2) | O(1) | 稳定 | 希尔排序 | O(n的1.3次方)~O(n2) | O(1) | 不稳定 |
交换类排序
名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|
冒泡排序 | O(n2) | O(1) | 稳定 | 快速排序 | O(nlogn) | O(logn) | 不稳定 |
选择类排序
名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|
简单选择排序 | O(n2) | O(1) | 不稳定 | 堆排序 | O(nlogn) | O(1) | 不稳定 |
归并类排序
名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|
二路归并排序 | O(nlogn) | O(n) | 稳定 |
基数类排序
名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|
基数排序 | O(d(n+r)) | O(r) | 稳定 |
总表1
名称 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|
直接插入排序 | O(n2) | O(1) | 稳定 | 折半插入排序 | O(n2) | O(1) | 稳定 | 希尔排序 | O(n的1.3次方)~O(n2) | O(1) | 不稳定 | 冒泡排序 | O(n2) | O(1) | 稳定 | 快速排序 | O(nlogn) | O(logn) | 不稳定 | 简单选择排序 | O(n2) | O(1) | 不稳定 | 堆排序 | O(nlogn) | O(1) | 不稳定 | 二路归并排序 | O(nlogn) | O(n) | 稳定 | 基数排序 | O(d(n+r)) | O(r) | 稳定 |
总表2
名称 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|
直接插入排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 | 折半插入排序 | O(nlogn) | O(n2) | O(n2) | O(1) | 稳定 | 希尔排序 | O(n的1.3次方) | O(n的1.3次方)~O(n2) | O(n2) | O(1) | 不稳定 | 冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 | 快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(logn) | 不稳定 | 简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 | 二路归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 | 基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(r) | 稳定 |
记忆
时间复杂度为O(n2):
- 简单排序:直接插入排序,折半插入排序,冒泡排序,简单选择排序
时间复杂度为O(nlogn):
- 复杂排序:希尔排序(可能),快速排序,堆排序
- 归并排序:二路归并排序
时间复杂度特殊:
最好、平均和最坏时间复杂度相同:
最好和平均时间复杂度不同:
最坏和平均时间复杂度不同:
空间复杂度不为O(1):
稳定:
- 简单排序:直接插入排序,折半插入排序,冒泡排序
- 归并排序,基数排序:二路归并排序
不稳定:
- 复杂排序:希尔排序,快速排序
- 选择类排序:简单选择排序,堆排序
一趟排序,能够确定元素的最终位置:
- 交换类排序:冒泡排序,快速排序
- 选择类排序:简单选择排序,堆排序
比较次数和初始序列无关:
移动次数和初始序列无关:
排序趟数和初始序列有关:
应用
数据基本有序:最好时间复杂度小
数据规模小(<10000):简单排序
- 直接插入排序(性能最优;数据规模<=25)
- 折半插入排序(相对直接插入排序,适用数据规模更大)
- 冒泡排序
- 简单选择排序(相对冒泡排序,性能更优)
数据规模小,信息量大(如:数据是数十位的数字->占用空间大->移动时间长):
数据规模大:复杂排序
- 数据规模<=1000:希尔排序
- 数据随机分布:快速排序(性能最优;数据基本有序时,性能最差)
- 空间复杂度小;从大数据规模排序小部分数据:堆排序
- 稳定:归并排序
- 多关键字排序:基数排序
数据规模更大:外部排序
数据规模大,数据位数少:
数据规模大,数据位数多:
其他:
- 堆排序只适用顺序存储方式
- 堆排序建堆时比较次数多,不适用数据规模小
- 归并排序尽量用迭代法而不是递归法
- 基数排序不适用实数类型
- 混合排序使用
外部排序
概念
- 内存作为工作/辅助空间,排序外存数据
- 算法相对复杂
- 时间=读写外存内存时间+内存排序时间+内存归并时间
- 时间->访问外存/输入/输出(I/O)次数
- 优化:减少访问外存/输入/输出(I/O)次数->减少初始归并段数,增多
流程
- 外存数据分段
- 段读入内存
- 段在内存排序,为初始归并段
- 初始归并段写回外存
- 初始归并段读入内存
- 初始归并段在内存归并,为归并段
- 归并段写回外存
另:
- 分段
- 排序
- 归并
排序方法
常用:
子算法:
置换-选择排序:
- 作用:排序时,生成初始归并段时不受内存限制;减少初始归并段数
- 外存段中,每记录读入内存
- 依据排序规则,选择一个最值生成初始归并段。不符合排序规则的最值等待生成下一初始归并段
- 初始归并段长度不同
最佳归并树:
- 作用:归并时,减少访问外存/输入/输出(I/O)次数;适应长度不同初始归并段的归并
- 生成哈夫曼树
- 权:初始归并段的数据规模
- 路径长度:归并次数
- 2:读和写
- 访问外存/输入/输出(I/O)次数 = 带权路径长度(WPL) × 2
败者树:
- 作用:排序和归并时,减少选最值的比较次数/时间复杂度;增多归并路数
对最小值败者树:
- 叶子结点:值为归并段的段首值,旁为归并段的序号
- 非叶子结点:值为归并段的序号,旁为归并段的段首值
- 构造:比较值,构造二叉树。败者(值大者)为双亲结点,胜者(值小者)为双亲结点的双亲结点
- 由树根结点的序号,取值写回外存,取另外值读入内存
- 调整:同构造过程
选择树:堆,败者树
时间复杂度:相对复杂
- 置换-选择排序中,每记录有两次访问外存/输入/输出(I/O)
- 每一趟归并,每记录有两次访问外存/输入/输出(I/O)
- 归并趟数:[log以k为底的m]上取整。k为归并路数,m为初始归并段数=外存记录数÷内存能容纳的记录数
- k路归并败者树:k个记录/叶子结点的二叉树
- k路归并败者树构造的时间复杂度:O(klogk)。k为归并路数
- k路归并败者树的树高:[log以2为底的k]上取整+1。k为归并路数(不包括树根结点层)
- 传统选最值的比较次数:k-1。k为归并路数
- k路归并败者树选最值的比较次数:[log以2为底的k]上取整。k为归并路数
- k路归并败者树选最值的时间复杂度:O(logk)。k为归并路数
空间复杂度:O(1)。未使用辅助空间
总结
“排序”学习提纲。
参考资料
- 《2023版数据结构高分笔记》主编:率辉
- 《2023年计算机组成原理考研复习指导》组编:王道论坛
- 《大话数据结构》作者:程杰
作者的话
- 感谢参考资料的作者/博主
- 作者:夜悊
- 版权所有,转载请注明出处,谢谢~
- 如果文章对你有帮助,请点个赞或加个粉丝吧,你的支持就是作者的动力~
- 文章在描述时有疑惑的地方,请留言,定会一一耐心讨论、解答
- 文章在认识上有错误的地方, 敬请批评指正
- 望读者们都能有所收获
|