摘要:本文主要讲各种排序算法,注意它们的时间复杂度
概念
将各元素按关键字递增或递减排序顺序重新排列
评价指标
稳定性: 关键字相同的元素经过排序后相对顺序是否会改变
时间复杂度、空间复杂度
分类
内部排序------数据都在内存中
外部排序------数据太多,无法全部放入内存
一、插入排序
算法思想:每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成
比27大的数往后挪
cpp
void InsertSort() {
int i, j ,temp;
for (i = 1;i <= n;i++)//将各元素插入已排好序的序列中
{
if (A[i] < A[i - 1]){//若A[i]关键字小于前驱
temp = A[i];//用temp暂存A[i]
for (j = i - 1;j >= 0 && A[j] > temp;--j)//检查所有前面已排好序的元素
{
A[j + 1] = A[j];//所有大于temp的元素都向后挪位
}
A[j + 1] = temp;//复制到插入位置
}
}
}
cpp
void InsertSort() {
int i, j;
for (i = 2;i <= n;i++)//依次将A[2]~A[n]插入到前面已排序序列
{
if (A[i] < A[i - 1]) {//若A[i]关键码小于其前驱,将A[i]插入有序表
A[0] = A[i];//复制为哨兵,A[0]不存放元素
for (j = i - 1;A[0] < A[j];--j)//从后往前查找待插入位置
{
A[j + 1] = A[j];//向后挪位}
}
A[j + 1] = A[0];//复制到插入位置
}
}
}
优点:不用每轮循环判断j>=0
空间复杂度:O(1)
时间复杂度:主要来自对比关键字、移动元素 若n个元素,则需要n-1趟处理
最好时间复杂度(全部有序):O(n)
最坏(逆序):O(n^2)
平均O(n^2)
算法稳定性:稳定
优化------折半插入排序
当low>high时折半查找停止,应将[low,i-1]内的元素全部右移,并将A[0]复制到low所指位置
当A[mid]==A[0]时,为了保证算法的"稳定性",应继续在mid 所指位置右边寻找插入位置
cpp
void InsertSort() {
int i,j,low, high, mid;
for (i = 2;i <= n;i++) {//依次将A[2]~A[n]插入前面的已排序序列
A[0] = A[i];//将A[i]暂存到A[0]
low = 1;high = i - 1;//设置折半查找的范围
while (low <= high) {//折半查找
mid = (low + high) / 2;//取中间点
if (A[mid] > A[0])high = mid - 1;//查找左半子表
else low = mid + 1;//查找右半子表
}
for (j = i - 1;j >= high + 1;--j)
A[j + 1] = A[j];//统一后移元素,空出插入位置
A[high + 1] = A[0];//插入操作
}
}
二、希尔排序(Shell sort)
最好情况:原本就有序
比较好的情况:基本有序
希尔排序:先追求表中元素部分有序,再逐渐逼近全局有序
先将待排序表分割成若干形如L[i,i+d,i+2d,...,i+kd]的"特殊"子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止
重点:给出增量序列,分析每一趟排序后的状态
cpp
void ShellSort() {
int d, i, j;
//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
for (d = n / 2;d >= 1;d = d / 2)//步长变化
for (i = d ;i <n;++i)
if (A[i] < A[i - d]) {//需将A[i]插入有序增量子表
A[0] = A[i];//暂存在A[0]
for (j = i - d;j > 0 && A[0] < A[j];j -= d)
A[j + d] = A[j];//记录后移,查找插入的位置
A[j + d] = A[0];//插入
}
}
三、交换排序
1、冒泡排序
从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列比较完。称这样过程为"一趟"冒泡排序。
cpp
void BubbleSort() {
for (int i = 0;i < n - 1;i++) {
bool flag = false;//表示本趟冒泡是否发生交换的标志
for (int j=n;j > i;j--)//一趟冒泡过程
if (A[j - 1] > A[j]) {//若为逆序
swap(A[j - 1], A[j]);//交换
flag = true;
}
if (flag == false)
return;//本趟遍历后没有发生交换,说明表已经有序
}
}
2、快速排序
算法思想:
在待排序表L[1,...n]中任取元素pivot作为枢轴(或基准,通常取首元素),
通过一趟排序将待排序表划分成独立的两部分L[1...k-1]和L[k+1...n], 使得L[1...k-1]中的所有元素小于pivot,L[k+1...n]大于等于,则pivot放在了其最终位置L(k)上,这个过程称为一次"划分"。
然后分别递归地对两个子表重复上述过程,直到每部分内只有一个元素或空为止,即所有元素放在了最终位置上
cpp
//用第一个元素将待排序序列划分为左右两个部分
int Partition( int low, int high) {
int pivot = A[low];//第一个元素作为枢轴
while (low < high) {//用low、high搜索枢轴的最终位置
while (low < high && A[high] >= pivot) --high;
A[low] = A[high];//比枢轴小的元素移动到左端
while (low < high && A[low] <= pivot) ++low;
A[high] = A[low];//大 右
}
A[low] = pivot;//枢轴元素存放到最终位置
return low;//返回存放枢轴的最终位置
}
//快速排序
void QuickSort(int low, int high) {
if (low < high) {//递归跳出的条件
int pivotpos = Partition( low, high);//划分
QuickSort( low, pivotpos - 1);//划分左子表
QuickSort(pivotpos + 1, high);//划分右子表
}
}
算法表现主要取决于递归深度,若每次"划分"越均匀,则递归深度越低。"划分"越不均匀,递归深度越深
三、选择排序
每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列
1、简单选择排序
cpp
void SelectSort() {
for (int i = 0;i < n - 1;i++) {//一共进行n-1趟
int min = i;//记录最小元素位置
for (int j = i + 1;j < n;j++)//在A[i...n-1]中选择最小元素
if (A[j] < A[min]) min = j;//更新最小元素位置
if (min != i) swap(A[i], A[min]);//封装的swap()函数共移动元素3次
}
}
2、堆排序
若满足L(i)>=L(2i)且L(i)>=L(2i+1)(1<=i<=n/2)------大根堆(大顶堆)
思路:
把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
顺序存储的完全二叉树中,非终端结点编号i<=[n/2]
检查当前结点是否满足根>=左、右
若不满足,将当前结点与更大的一个孩子互换
i的左孩子2i, 右孩子2i+1,父结点[i/2]
若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断"下坠")
(这个比较复杂,附视频:8.4_2_堆排序_哔哩哔哩_bilibili)
cpp
void BuildMaxHeap(int len) {
for (int i = len / 2;i > 0;i--)//从后往前调整所有非终端结点,即非叶子结点
HeadAdjust(i,len);
}
cpp
//将以k为根的子树调整为大根堆
void HeadAdjust(int k,int len) {
A[0] = A[k];//A[0]暂存子树的根节点
for (int i = 2*k;i <= len;i*=2) {//从左子树开始,沿key较大的子结点向下筛选
if (i < len && A[i] < A[i + 1])//i < len 保证有右兄弟
i++;//取key较大的子结点的下标
if (A[0] >= A[i]) break;//筛选结束
else {
A[k] = A[i];//将A[i]调整到双亲结点上
k = i;//修改k值,以便继续向下筛选
}
}
A[k] = A[0];//被筛选结点的值放入最终位置
}
结论:一个结点,每"下坠"一层,最多只需对比关键字2次
若树高为h,某结点在第i层,则将这个结点向下调整最多只需要"下坠"h-i层,关键字对比次数不超过 2(h-i)
基于大根堆进行排序
cpp
//堆排序的完整逻辑
void HeapSort(int len) {
BuildMaxHeap(len);//初始建堆
for (int i = len;i > 1;i--) {//n-1趟的交换和建堆过程
swap(A[i], A[1]);//堆顶元素和堆底元素互换
HeadAdjust(1, i - 1);//把剩余的待排序元素整理成堆
}
}
堆排序:每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换)
并将待排序序列中再次调整为大根堆(小元素不断:下坠)
得到"递增序列"
时间复杂度:O(n)+O(nlog2n)=O(nlog2n)
(O(n)建堆,O(nlog2n)排序)
稳定性:不稳定
堆的插入删除
插入:对于小根堆,新元素放到表尾,与父结点对比,若新元素比父结点更小,则将二者互换。新元素就这样一路"上升",直到无法继续上升为止。
删除:被删除的元素用堆底元素替代,然后让该元素不断"下坠",直到无法下坠为止
关键字对比次数
每次"上升"调整只需对比关键字1次
每次"下坠"调整可能需要对比关键字2次,也可能只需对比1次
四、基数排序
要求:得到按关键字"递减"的有序序列
基数排序不是基于"比较"的排序算法
第一趟:以"个位"进行"分配"
第一趟"收集"结束:得到按"个位"递减排序的序列
算法思想
将整个关键字拆分为d位(组)
按照各个关键字位权重递增的次序(如:个、十、百),做d趟"分配"和"收集",若当前处理的关键字位可能取得r个值,则需要建立r个队列
分配:顺序扫描各个元素,根据当前处理的关键字位,将元素插入相应队列。一趟分配耗时O(n)
收集:把各个队列中的结点依次出队并链接。一趟收集耗时O(r)
性能
空间复杂度 O(r)
时间复杂度 O(d(n+r)) (一趟分配O(n),一趟收集O(r))
稳定
擅长处理
1、数据元素的关键字可用方便地拆分为d组,且d较小
2、每组关键字的取值范围不大,即r较小
3、数据元素个数n较大
五、归并排序
归并:把两个或多个已经有序的序列合并成一个
2路合并 k路合并
空间复杂度O(n)
时间复杂度O(nlogn)
稳定
cpp
//B是辅助数组
const int SIZE = sizeof(A) / sizeof(A[0]);
int B[SIZE];
void Merge(int A[], int low, int mid, int high) {
for (int k = low; k <= high; k++)
B[k] = A[k];
int i = low, j = mid + 1, k = i;
while (i <= mid && j <= high) {
if (B[i] <= B[j])
A[k++] = B[i++];
else
A[k++] = B[j++];
}
while (i <= mid) A[k++] = B[i++];
while (j <= high) A[k++] = B[j++];
}
void MergeSort(int A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(A, low, mid);
MergeSort(A, mid + 1, high);
Merge(A, low, mid, high);
}
}