排序算法之希尔排序(2)--菜鸟先飞


菜 就多练 练就有效 反反复复 重复重复

好好吃饭 好好休息 好好睡觉 少熬夜 多锻炼 健身 @辞惜忆


1.1 有序 与 无序的理解

第一个数作为天然有序数 0,end

end 表示有序空间最后一个数的下标

end 从0开始 即是从(有序数列中最后一个有序数 与 第一个无序数 进行比较

记住是从后往前找 直到不满足 就在不满足的下个位置 插入

再到后面的无序空间里面用第一个数作为tmp

第一个数作为天然有序数,范围是 0, end

end 表示有序空间的最后一个数的下标,从 0 开始计数。排序时,首先将第一个有序数与第一个无序数(即 aend+1)进行比较。

然后将无序空间的第一个数存入 tmp 作为临时变量。

1.2 tmp 的理解 有两层

第0层 表示要插入的元素 无序数中的第一个

第一层 就是 存入aend+1的值 作为临时变量 当执行**aend = aend+1**时 就是向后覆盖时

aend+1的值就找不到了 无法继续比较

第二层 代码执行过程中 end会改变 使得 end+1变化 这样就会使得aend+1的值发生变化

然而 我们需要比较的 一直是第一个无序位置 (最初的aend+1

tmp 就会完美保留下来那个值

1.3 i 最后一次的控制

总共n个数 下标从0到n-1都是闭区间

复制代码
		int end = i;
		int tmp = a[end + 1];

明显 当end ==n-1 时候 tmp = an 是不是越界了 最大取的是an-1

其次 当end == n-2 时候 tmp= an-1

有序空间为前n-1项 无序的只有第N项 将其插入即可

最大的 i 是 n-2

1.4 优化简写

优化点1 将 两份判断 放入循环

优化点 2 将break 优化为 循环结束

1.5 升序 和 降序 在代码中的理解

1 tmp 存无序数 第一个 用于从已排好的有序数组中

找到不满足判断条件的时候 在该数字后面一个位置插入tmp

2 aend < tmp <==> tmp > aend

aend > tmp <==> tmp < aend

2.1 主体看 aend

结合挪动的代码 就是 aend+1 = aend;

重要理解 -- 将aend 往后挪动

主要看 aend 前面的符号 显示 它是较小的那个 就是表示 把小的数据往后挪动

同样的 大的数据自然就到前面来了并且保持前面有序的前提下 前大后小 就是 降序

2.2 主体看 tmp

总体是将 tmp 向前插入

比 有序空间中的数 大的时候 才将其插入进去 1 有序空间 有序

2 大 才插入 表示该空间里 大的在前面 满足降序的特点

前面部分有序

从tmp = aend+1 让其插入到前面有序的空间里

无论插到什么位置

开头 end == -1时

中间不满足条件在空位插入

亦或 最开始就不满足条件 直接就插到末尾 无需挪动

2 冒泡排序 代码展示 (仅供参考)

第一种

复制代码
void Swap(int* a, int* b) {
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void bubbleSort(int* a, int sz) {
	for(int j = 0;j < sz-1;j++){
		int flag = 0;//优化点1
		for (int i = 1; i < sz-j; i++) {
			if (a[i] < a[i - 1]) {
				Swap(&a[i], &a[i - 1]);
				flag = 1;//优化点1
			}
		}
		if (flag == 0) {
			break;
		}
	}
}

第二种

复制代码
void bubbleSort(int* a, int sz) {
	for(int j = 0;j < sz-1;j++){
		int flag = 0;
		for (int i = 0; i < sz-1-j; i++) {
			if (a[i] > a[i +1]) {
				Swap(&a[i], &a[i + 1]);
				flag = 1;
			}
		}
		if (flag == 0) {
			break;
		}
	}
}

简单理解一下 就是 i+1 的最大值 是 sz-1

所以 i 的最大值 是 sz-2

结合每轮少一个的思路 统一下 就是 i+1 <= sz-j -1 等价于 i+1 < sz-j

i 的最大值 是 sz-j-2 写出循环结束条件是 i < sz -j-1

交换排序的一种

还有一个是 快排 敬请期待

2.1 图解

两两相邻的数 比较交换 像泡泡一样

上面的动图 直观的表示每一次的排序使得 最值 出现在最后面,每次两两交换,使得较大的值,向后挪动,第一趟使得最大值 出现在最后面 ,第二次 第二大的值出现在倒数第二个位置

2.2 交换函数复习 看一眼就会

复制代码
void Swap(int* a, int* b) {
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

交换单独写 方便复用 不要在冒泡排序里面 去写

2.2 案例分析 单趟开始

第一趟

复制代码
for (int i = 1; i < sz; i++) {
	if (a[i] < a[i - 1]) {
		Swap(&a[i], &a[i - 1]);//大的换到后面 是升序
	}
}

将最大值冒到最后面 占到老大的位置 固定不动

第二趟 剩下的n-1个数中 冒出二把手 第二大的数

第三趟 剩下的 n-2个数中 冒出三把手第三大的数

第 j 趟剩下的 n-j 个数中 冒出 j把手第j大的数

仔细学习上图 后 以升序为例子 哦

1每一趟 冒泡排序 会得到未排序中 最大的那个数 到末尾

一趟排好一个 最坏 要n -1 趟

2 每一趟 中包含 很多次的交换

2.3 单趟 与整体的联系

2.3.1 重要认识

任何复杂问题 可以试着拆解为 简单的小问题

困难的事情 分解为可执行的最小动作单元 初学时尽量不要贪快

2.3.2 变化增量 j 与 sz

第一趟

复制代码
for (int i = 1; i < sz; i++) {
	if (a[i] < a[i - 1]) {
		Swap(&a[i], &a[i - 1]);//大的换到后面 是升序
	}
}

第二趟

复制代码
	for (int i = 1; i < sz-1; i++) { 
		if (a[i] < a[i - 1]) {
			Swap(&a[i], &a[i - 1]);//大的换到后面 是升序
		}
	}
 
第三趟 
复制代码
	for (int i = 1; i < sz-2; i++) {
		if (a[i] < a[i - 1]) {
			Swap(&a[i], &a[i - 1]);//大的换到后面 是升序
		}
	}

此时 如果很显然 每趟排好一个数后

2.4 初步代码

复制代码
void bubbleSort(int* a, int sz) {

	for(int j = 0;j < sz-1;j++){
		for (int i = 1; i < sz-j ; i++) {
			if (a[i] < a[i - 1]) {
				Swap(&a[i], &a[i - 1]);//大的换到后面 是升序
			}
		}
	}

}

2.4.1 奇怪的发问

当你发现 下面的代码 同样可以 实现同一的 结果时

没错 就是如此 但是 它是没有排除末尾的有序的数 大量的重复比较

复制代码
	for(int j = 0;j < sz-1;j++){
		for (int i = 1; i < sz; i++) {
			if (a[i] < a[i - 1]) {
				Swap(&a[i], &a[i - 1]);//大的换到后面 是升序
			}
		}
	}

不建议 pass

2,4,2 代码的优化

引入 一个原本的数组就接近 升序的排列 和 更加无序的数组 (总个数一样)

你觉得 那个效率 会更高 ?

答案是 一摸一样 在你看来一眼就可以找到 那个无序的数 并交换 排好后 就结束了****但是 计算机 会严格按照指令 来运行 死脑筋 一根筋 走到南墙也不回头的

没错 但前提是 你已经主观的加了 一个判断了呀 所以这一步 我们优化到代码上

使得代码提前结束 无需重复的 判断 flag 直接起手开大

2.4.3 优化后展示

复制代码
void bubbleSort(int* a, int sz) {
	for(int j = 0;j < sz-1;j++){
		int flag = 0;//优化点1
		for (int i = 1; i < sz-j; i++) {
			if (a[i] < a[i - 1]) {
				Swap(&a[i], &a[i - 1]);
				flag = 1;//优化点1
			}
		}
		if (flag == 0) {
			break;
		}
	}
}

就是 最开始用 标记数 就是 一个记号 来记住哈

这么理解 假设最开始的初始状态是 0

当 发生什么事情 就变身 -- 这里是 交换操作

记住这是 一个方式 可以套用到其他的地方的

如果说循环结束后

没有发生变化 一直是原来的数值 样子

就表示 没有发生交换操作 就是表示****已经有序了

然后我们 给 指令 --- break 就是告诉计算机 好了有序了 排好了 结束循环

break 不就是 休息会嘛 休息一下吧 很好

2.5 时间复杂度计算

优化后 的版本

1 最坏是完全逆序 第一次交换 n-1次 第二次是 n-2 次 直到 1

用 等差数列求和 公式 Sn = (首项 +末项) * 项数 /2

(1+n-1)*(n-1)/2 = (n^2 - n)/2 = n^2/2 - n/2;

时间复杂度 取权重最大的项 就是 O(n^2)

2 最好的情况 就是有序了 但是也是需要走一遍程序

才知道有序了 如果数很多 人力看不出的 别小瞧

就是遍历一遍 而已 n-1 时间复杂度是O(n)

3 平均时间复杂度 是 O(N^2)

3 希尔排序 初认识

希尔排序的底层是 分组插入排序 gap组呗

不要妄自菲薄 我们可以学习到 这种思想 和 思考 方式

然后 传递下去 就是 思想着前辈的思想 站在巨人的肩膀上喽

-----------------------------------------------------------------------------------------------------------------------

希尔排序 = 多轮 预排序 (当gap>1时都算 ) ----- 视作 变体的插入排序 就是一种拓展

+ 加 +

最后一次直接插入排序(当gap==1时)------ 等价于 特殊状状态的 "预排序"

希尔排序 = 多轮预排序 + 最后一次直接插入排序

3.1.1 注意 等会复习看

第一个注意理解的点 i < n-gap

i 最大的时候是 n-1 - gap

int end == i ; 控制的是 起点

主要目的 还是 防止tmp在 访问时候 越界

当 end 最大时 end max = i max = n-1-gap;

int tmp = aend + gap ==> tmp = an-1 满足n个数组 的最大下标极限

但是如果 大于或等于 red (最多小于 ) 都会 使得 tmp = an 或者an+x 都是越界的访问

3.1 预排序 与 直接插入排序的联系(引子)

预排序(在排序算法里)
定义:
在最终排序之前,先做一轮/多轮粗略、不彻底的排序,

把数组变得更接近有序,从而让后续的主排序更快。

分成 gap组 如上图是 int gap = 3; 分成三组 红 绿 蓝

3.1.1 单趟 红色

3.1.1 单趟中的第一次插入

不急 先画图

1 创建5的替身

2 将 9 挪动 aend+gap;

3 最后将 aend+gap 插入到aend的位置

复制代码
	        int gap = 3;
	int end = 0;
	int tmp = a[end + gap];
	//单次
		while (end >= 0) {
			if (tmp < a[end]) {// 5小于9 满足条件
				a[end + gap] = a[end];//挪动
				end -= gap;//
			}
			else {
				break;
			}
		}
		a[end + gap] = tmp;

3.2 总体把关

相关推荐
洛水水1 小时前
【力扣100题】87.只出现一次的数字
数据结构·算法·leetcode
乐观勇敢坚强的老彭1 小时前
2026全国青少年信息素养大赛(Python小学组)复赛复习讲义
python·算法·数学建模
林间码客1 小时前
02数据挖掘:数据属性、类型与相似性度量
人工智能·算法·机器学习
阿标在干嘛1 小时前
从“拍脑袋”到“数据驱动”:政策平台的A/B测试实践
大数据·人工智能·算法·ab测试
iningwei1 小时前
[数据结构]细说0xFFFFFFFFL
数据结构
实在智能RPA1 小时前
气象预警Agent等级判定算法:2026年AI驱动的概率集合预报与自动化闭环实践
人工智能·算法·ai·自动化
风筝在晴天搁浅2 小时前
LeetCode CodeTop 82.删除排序链表中的重复元素Ⅱ
算法·leetcode·链表
189228048612 小时前
NV114固态MT29F16T08EWLEHD6-MES:E
人工智能·算法·缓存·性能优化
不会就选b2 小时前
数据结构之链表OJ题(下)
数据结构·链表