王道 数据结构 第八章 排序 希尔排序开始

一、希尔排序

思路

先将待排序表分割成若干形如L[i,i+d,i+2d,…,i+kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止
即先追求表中元素部分有序,再逐渐逼近全局有序

过程

image
image
image
image
第三趟排序时,整个表已呈现出“基本有序”,对整体再进行一次“直接插入排序”

代码

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)

稳定性:不稳定

image

适用性:仅适用于顺序表,不适合链表

二、冒泡排序

思路

从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即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; //若未发生交换,说明整个序列有序
	}
}

性能分析

时间复杂度

image
每次交换都需要移动元素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)

稳定性:不稳定

image

四、简单选择排序

思路

每一次循环找出最小的,与数组前面的数进行交换

代码

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)——小根堆

建立大根堆

思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
检查当前结点是否满足根大于等于左、右,若不满足,将当前结点与更大的一个孩子互换(建立小根堆时,将当前结点与更小的一个孩子互换)
若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断下坠)

插入元素

对于小根堆,新元素放到表尾,与父结点对比,若新元素比父结点小,则将二者交换。新元素就这样一路上升,直到无法上升为止

删除元素

被删除的元素用堆底元素代替,然后让该元素不断下坠,直到无法下坠为止

基于堆进行排序

每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换),并将待排序元素序列再次调整为大根堆(小元素不断下坠)

效率分析

image

时间复杂度 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个辅助队列

稳定性:稳定

应用

image
基数排序擅长解决的问题:
①数据元素个数可以方便地拆分为d组,且d较小
②每组关键字的取值范围不大,即r较小
③数据元素个数n较大

八、外部排序

磁盘中以"块"为单位进行存储,每块大小为1KB
image
将数据读入内存并进行归并排序后,再写入磁盘
构造初始归并块段:需要16次"读"和"写"操作

知识梳理与总结

简单选择排序,能够取出当前无序序列中最(小or大)值与第一位置的元素互换位置。

堆排序每趟总能选出一个最值位于根节点。

冒泡排序总是两两比较选出一个最值位于数组前面。

快速排序选出的枢轴在一趟排序中就位于了它最终的位置

插入排序(直接、二分)不一定会位于最终的位置,因为不确定后面插入的元素对于前面的元素是否产生影响。

希尔排序(本质也是插入排序)只在子序列中直接插入排序。所以不能确定。

二路归并排序除非在缓存区一次放入所有的序列(这样得不偿失),否则不能确定最终位置。

所以能够在一趟结束后,就选出一个元素在其最终的位置上的排序是否就只有 :
简单选择排序、快速排序、冒泡排序、堆排序

posted @   安河桥北i  阅读(406)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示