目录
1. 排序的简介
1.1 排序的概念
- 排序,是将一组数据记录(可以是数字、文字描述、商品信息、院校数据等各类可比较的内容 ),依据某个或多个“关键字”(如商品价格、院校总分、数字大小等 ),按照升序(从小到大)或降序(从大到小 )的规则重新排列的操作。简单来说,就是让杂乱的数据变得“有序”,方便后续查看、分析与使用。
1.2 排序的应用
(一)购物筛选排序
在电商购物平台,排序是帮我们快速找到心仪商品的“神器” 。比如浏览商品时:
(二)院校排名排序
教育领域里,院校排名是考生和家长了解学校实力的重要参考,排序在这里发挥着关键作用:
(三)排序的深层价值
除了直观的“找商品、选学校”,排序在更广泛领域默默发挥作用:
- - 数据处理与分析:科研、金融、互联网等行业,面对海量原始数据(如股票交易记录、用户行为数据 ),先通过排序梳理规律,后续才能开展聚类分析、趋势预测等深度工作,排序是数据挖掘的“前置步骤” 。
- - 系统优化与效率提升:计算机系统中,文件存储、检索功能依赖排序算法,让数据存储更规整,查找时能通过二分查找等高效方式定位,提升软件、硬件的运行效率 。
- - 逻辑思维培养:学习排序的过程(比如理解冒泡排序、快速排序等算法 ),能锻炼我们梳理规则、优化流程的逻辑思维,这种思维迁移到生活里,面对复杂任务(如规划旅行路线、整理工作文件 ),也能更有条理地拆解、排序,找到高效解决办法 。
- 从日常购物的便捷,到影响人生选择的院校报考,再到推动行业发展的专业应用,排序早已融入生活与工作的方方面面,成为数据时代不可或缺的基础工具,持续帮我们把“无序”变“有序”,让复杂世界变得清晰可寻 。
1.3 常见的排序算法
好的,那么接下来让我们步入正题,在算法世界中我们会用到哪些排序算法呢?以及这些排序算法
是如何帮助我们解决算法问题呢?
上图片是我们在算法中需要用到的常见排序算法,下面小编就各个算法的算法核心逻辑做一个简单
的说明,让大家先有一个简单的了解。
1. 插入排序
- - 直接插入排序:逐个将元素插入已排序序列,像整理手牌,把新牌插入合适位置,数据量小时稳定好用。
- - 希尔排序:是直接插入排序的改进,按间隔分组插入排序,缩小间隔再整体插入,让元素更快归位,提升效率。
2. 选择排序
- - 直接选择排序:每次选未排序里最小(大)的,和起始位置交换,简单但数据量大时效率低,因为总要遍历找极值。
- - 堆排序:利用堆结构选极值,先建堆,再依次取堆顶元素(最大或最小),适合处理大规模数据的排序需求。
3. 交换排序
- - 冒泡排序:相邻元素比较,逆序就交换,重复操作让大(小)元素像冒泡一样“浮”到一端,优化后可提前终止有序序列排序。
- - 快速排序:选基准元素,把数组分成两部分,一部分比基准小,一部分大,递归排序这两部分,平均效率高,常用在一般数据排序。
4. 归并排序
- 归并排序:把数组拆成小数组,分别排序后,再合并成有序数组,基于分治思想,稳定且适合处理大规模数据,尤其是外部排序(数据存磁盘,内存装不下时 )。
接下来在这篇文章中,小编先讲关于第一个常见的算法---插入排序。
2. 插入排序
2.1 直接插入排序
1. 基本思想
- 直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
- 当插入第 i(i>=1)个元素时,前面的 arr[0] , arr[1] , … , arr[i-1] 已经排好序,此时用 arr[i] 的排序码与 arr[i-1] , arr[i-2] , … 的排序码顺序进行比较,找到插入位置即将arr[i] 插入,原来位置上的元素顺序后移。
2. 代码实现(升序)
基本思想已经了解, 那么小编下面直接对直接插入排序的代码进行讲解:
//直接插入排序
void InsertSort(int* arr, int n)
{
for (int i = 0;i < n - 1;i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
//升序: >
//降序: <
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
arr[end + 1] = tmp;
}
}
}
小编从以下从函数参数、变量含义、循环逻辑等角度,逐行拆解直接插入排序代码,帮你彻底搞懂
它的运行过程:
1. 函数参数与整体作用
void InsertSort(int* arr, int n)
- int* arr :
接收一个整型数组的指针(或理解为数组首地址),代表要排序的数组。排序操作会直接修改这个数组的内容,实现“原地排序”。
- int n :
表示数组的元素个数,告诉函数要处理多少个数据,避免越界访问。- 功能:
用直接插入排序算法,把 arr 数组里的 n 个元素,从小到大排列(以升序为例)。
2. 外层循环: for (int i = 0; i < n - 1; i++)
for (int i = 0;i < n - 1;i++) { // 内层逻辑... }
- 作用:划分“已排序区间”和“未排序区间”。
- 直接插入排序的核心思想是:先让数组的前 i+1 个元素有序(逐步扩大“已排序区间”),再把后面的元素逐个插入到合适位置。
- 初始时, i=0 ,认为数组的第 0 个元素( arr[0] )是“已排序区间”(只有 1 个元素,天然有序);
- 每次循环 i++ ,“已排序区间”向右扩展 1 个位置,要处理的“新元素”是 arr[i+1] (因为 end = i , tmp = arr[end+1] )。
- 循环条件 i < n-1 :因为最后一次处理的是倒数第 2 个元素( arr[n-2] ),插入后整个数组就有序了,无需处理到 i = n-1 (否则会越界)。
3. 变量定义
int end = i; int tmp = arr[end + 1];
- int end = i :
切记 : end 是已排序区间的最后一个元素下标。初始时,“已排序区间”是 arr[0] ~ arr[i] ,所以最后一个元素下标是 i 。
- int tmp = arr[end + 1] :
tmp 存储的是待插入的元素值(即“未排序区间”的第一个元素 arr[i+1] )。用临时变量保存,是为了后续移动元素时,避免覆盖导致数据丢失。
4. while 循环:找插入位置 + 移动元素
while (end >= 0) { if (arr[end] > tmp) { arr[end + 1] = arr[end]; end--; } else { break; } arr[end + 1] = tmp; }
- 循环条件 end >= 0 :
保证在“已排序区间”内查找( end 从 i 往前遍历,直到 end = 0 (end 是已排序区间的最后一个元素下标) ,覆盖所有已排序元素)。
- if (arr[end] > tmp) :
判断已排序区间的当前元素 arr[end] 是否大于待插入元素 tmp :
- 如果成立( arr[end] > tmp ) : 说明 tmp 应该插在 arr[end] 的前面。此时要把 arr[end] 向后移动一位( arr[end + 1] = arr[end] ),然后 end-- ,继续向前比较。如果不成立( arr[end] <= tmp ):说明找到插入位置了, break 跳出循环。
- arr[end + 1] = tmp :
把 tmp 放到正确的插入位置( end + 1 )。
- 如果是因为 arr[end] <= tmp 跳出循环, end + 1 就是 tmp 该插入的位置;
- 如果是因为 end < 0 跳出循环(比如 tmp 比已排序区间所有元素都小), end + 1 = 0 ,也能正确插入到数组开头。
3. 代码运行流程示例
为了更直观,用具体数据走一遍流程,看直接插入排序如何工作:
以初始数组 arr = [5, 3, 4, 6, 2] 为例:
值: 5 3 4 6 2
索引:0 1 2 3 4
从数组的第2个元素(索引i=1)开始,每次将当前元素(arr[i])插入到它左边已排好序的子序列
中,使子序列始终保持有序。
数组 arr = [5, 3, 4, 6, 2] , n = 5 (数组长度),代码进入 for 循环, i 从 0 开始,到 3 结束
(因为 i < n - 1 ,即 i 取 0 、 1 、 2 、 3 )。
第 1 轮循环: i = 0
1. 初始化变量
- end = i = 0
- tmp = arr[end + 1] = arr[1] = 3 (暂存待插入元素 3 )
此时数组: [5, 3, 4, 6, 2]
2. 进入 while 循环
条件 end >= 0 成立( end = 0 ),执行循环体:
- 判断 arr[end] > tmp → arr[0] = 5 > 3 ,条件成立
- 执行 arr[end + 1] = arr[end] → arr[1] = arr[0] = 5 ,数组变为 [5, 5, 4, 6, 2]
- 执行 end-- → end = -1
- 再次判断 end >= 0 → 不成立,跳出 while 循环
- 执行 arr[end + 1] = tmp → end = -1 ,所以 arr[0] = 3 ,数组变为 [3, 5, 4, 6, 2]
第 2 轮循环: i = 1
1. 初始化变量
- end = i = 1
- tmp = arr[end + 1] = arr[2] = 4 (暂存待插入元素 4 )
此时数组: [3, 5, 4, 6, 2]
2. 进入 while 循环
条件 end >= 0 成立( end = 1 ),执行循环体:
- 判断 arr[end] > tmp → arr[1] = 5 > 4 ,条件成立
- 执行 arr[end + 1] = arr[end] → arr[2] = arr[1] = 5 ,数组变为 [3, 5, 5, 6, 2]
- 执行 end-- → end = 0
- 再次判断 end >= 0 成立( end = 0 ),执行循环体:
- 判断 arr[end] > tmp → arr[0] = 3 <= 4 ,条件不成立,执行 break ,跳出 while 循环
- 执行 arr[end + 1] = tmp → end = 0 ,所以 arr[1] = 4 ,数组变为 [3, 4, 5, 6, 2]
第 3 轮循环: i = 2
1. 初始化变量
- end = i = 2
- tmp = arr[end + 1] = arr[3] = 6 (暂存待插入元素 6 )
此时数组: [3, 4, 5, 6, 2]
2. 进入 while 循环
条件 end >= 0 成立( end = 2 ),执行循环体:
- 判断 arr[end] > tmp → arr[2] = 5 <= 6 ,条件不成立,执行 break ,跳出 while 循环
- 执行 arr[end + 1] = tmp → end = 2 ,所以 arr[3] = 6 ,数组无变化(仍为 [3, 4, 5, 6, 2] ,因为本身已满足顺序 )
第 4 轮循环: i = 3
1. 初始化变量
- end = i = 3
- tmp = arr[end + 1] = arr[4] = 2 (暂存待插入元素 2 )
此时数组: [3, 4, 5, 6, 2]
2. 进入 while 循环
条件 end >= 0 成立( end = 3 ),执行循环体:
- 判断 arr[end] > tmp → arr[3] = 6 > 2 ,条件成立
- 执行 arr[end + 1] = arr[end] → arr[4] = arr[3] = 6 ,数组变为 [3, 4, 5, 6, 6]
- 执行 end-- → end = 2
- 再次判断 end >= 0 成立( end = 2 ),执行循环体:
- 判断 arr[end] > tmp → arr[2] = 5 > 2 ,条件成立
- 执行 arr[end + 1] = arr[end] → arr[3] = arr[2] = 5 ,数组变为 [3, 4, 5, 5, 6]
- 执行 end-- → end = 1
- 再次判断 end >= 0 成立( end = 1 ),执行循环体:
- 判断 arr[end] > tmp → arr[1] = 4 > 2 ,条件成立
- 执行 arr[end + 1] = arr[end] → arr[2] = arr[1] = 4 ,数组变为 [3, 4, 4, 5, 6]
- 执行 end-- → end = 0
- 再次判断 end >= 0 成立( end = 0 ),执行循环体:
- 判断 arr[end] > tmp → arr[0] = 3 > 2 ,条件成立
- 执行 arr[end + 1] = arr[end] → arr[1] = arr[0] = 3 ,数组变为 [3, 3, 4, 5, 6]
- 执行 end-- → end = -1
- 再次判断 end >= 0 → 不成立,跳出 while 循环
- 执行 arr[end + 1] = tmp → end = -1 ,所以 arr[0] = 2 ,数组变为 [2, 3, 4, 5, 6]
最终结果 : 经过 i = 0 到 i = 3 四轮循环,数组完成升序排序,结果为: [2, 3, 4, 5, 6]
4. 降序
- 要将直接插入排序代码改为降序排序,只需修改比较条件,让“大的元素往后移、小的元素往前插”,核心改动是把 if (arr[end] > tmp) 换成 if (arr[end] < tmp) 。
5. 复杂度
1. 时间复杂度
时间复杂度由 比较次数 和 移动次数 共同决定(这两个操作是算法的核心耗时步骤),具体取决
于数组的初始状态:
- 最好情况(数组已完全有序,如升序数组 [1,2,3,4,5] )
- 比较次数:每轮插入时,待插入元素 tmp 只需与已排序区间的最后一个元素比较 1 次(因arr[end] <= tmp ,直接跳出循环)。
总比较次数: 1 + 1 + ... + 1 (共 n-1 轮)→ 约 n 次。
- 移动次数:无需移动任何元素(因 tmp 直接放在已排序区间末尾)。
总移动次数:0 次。
- 时间复杂度:O(n)
- 最坏情况(数组完全逆序,如升序目标下的 [5,4,3,2,1] )
- 比较次数:每轮插入时,待插入元素 tmp 需与已排序区间的所有元素比较(从末尾一直比较到开头)。
第 1 轮(插入第 2 个元素):比较 1 次;
第 2 轮(插入第 3 个元素):比较 2 次;
...
第 n-1 轮:比较 n-1 次;
总比较次数:1 + 2 + ... + (n-1) = n(n-1)/2 次(约 n²/2 次)。
- 移动次数:每次比较后都需要移动元素(因 arr[end] > tmp 恒成立),移动次数与比较次数相同(每比较 1 次就移动 1 次,最后还要插入 tmp ,额外 1 次移动)。
总移动次数:n(n-1)/2 + (n-1) = (n+1)(n-1)/2 → 约 n²/2 次。
- 时间复杂度:O(n²)
- 平均情况(数组随机无序)
此时比较和移动的次数介于最好和最坏情况之间,随机数据中,每个元素的插入成本平均是它所在轮次已排序区间长度的一半,累加后整体接近 n²/4 ,通过数学推导,总操作次数约为 n²/4 次(仍为二次方级别)。
- 时间复杂度:O(n²)
2. 空间复杂度
算法中仅使用了 常数个额外变量(如 end 记录索引、 tmp 暂存待插入元素),与数组规模 n 无关,因此:
- 空间复杂度:O(1)(原地排序,不占用额外空间)
总结:
- 时间复杂度:最好 O(n),最坏和平均 O(n²);
- 空间复杂度:O(1)。
特点:对 小规模数据 或 部分有序数据 效率较高(接近 O(n)),但对大规模无序数据效率较低(二次方增长)。
以上便是直接插入排序的所有内容。下面我们来看一下建立在直接插入排序基础上的另一种排序—
希尔排序。
2.2 希尔排序
1. 概念
希尔排序(Shell Sort)是一种改进的插入排序算法,由计算机科学家Donald Shell在1959年提出。它的核心思想是通过“分组”减少数据的无序程度,先将整个序列按一定间隔(称为“增量”)分成多个子序列,对每个子序列进行直接插入排序;然后逐步缩小增量,重复分组和排序操作;当增量缩小到1时,整个序列变为一个子序列,此时再进行一次直接插入排序,最终使序列有序。
希尔排序说白了就是“分阶段整理”的排序方法。
你可以把它想象成整理一堆打乱的书:先不一本本细排,而是按“每隔几本书”分成几摞(比如每隔4本分一摞),每摞内部先大致排好;然后缩小间隔(比如改成每隔2本分一摞),再把每摞排得更整齐;最后间隔变成1(也就是一整摞),再细致排一遍,就全有序了。
核心就是:先通过“大间隔分组”让元素快速归位,减少后续细排的工作量,比直接从头细排快得多。
2. 基本思想
- 希尔排序的基本思想可以概括为:通过“逐步缩小间隔”来分组,让元素先在远距离上“粗略归位”,再在近距离上“精细调整”,最终高效完成排序。
- 具体来说,就是先按较大的间隔把序列分成多个子序列,每个子序列内部做简单排序(让元素离最终位置更近);然后缩小间隔,重复分组排序;直到间隔变成1(整个序列为一个子序列),最后做一次排序,就能快速得到有序结果。
- 这么做的目的是避免直接插入排序中“元素只能一步步挪动”的低效问题,让元素能“跳着”靠近目标位置,从而提速。
3. 代码实现(升序)
//希尔排序
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1; //3 2 1
//堆每组进行直接排序
for (int i = 0;i <n-gap;i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end = end - gap;
}
else
{
break;
}
arr[end + gap] = tmp;
}
arr[end + gap] = tmp;
}
}
}
这段代码是希尔排序的具体实现,核心逻辑是通过“动态调整增量( gap )”分组,对每组执行类似
直接插入排序的操作。下面围绕代码的参数、变量、循环逻辑逐行解读:
1. 函数参数
void ShellSort(int* arr, int n)
- int* arr :待排序的数组(指针),排序后会直接修改原数组(原地排序)。
- int n :数组中元素的个数(用于确定初始增量和循环范围)。
2. 核心变量: gap (增量)
int gap = n;
gap 是希尔排序的核心变量,代表“分组间隔”:
- 初始值为 n (数组长度),通过 gap = gap / 3 + 1 逐步缩小(例如 n=10 时, gap 会依次变为 4→2→1 )。
- 当 gap=1 时,就是一次完整的直接插入排序(此时数组已接近有序,效率很高)。
- gap / 3 + 1 的作用:确保 gap 最终能缩小到1(避免无限循环),且比单纯的 gap/2 减少不必要的排序轮次,效率更优。
3. 外层循环:控制 gap 逐步缩小
while (gap > 1) { gap = gap / 3 + 1; // 调整增量(如 10→4→2→1) // 内层循环:按当前 gap 分组并排序 ... }
- 作用:不断缩小 gap ,直到 gap=1 时退出循环(此时最后一轮排序已完成)。
- 每轮循环会用当前 gap 对数组分组,执行一次“分组插入排序”,让数组更接近有序。
4. 中层循环:遍历所有分组的“起始位置”
for (int i = 0; i < n - gap; i++)
- 作用:遍历每组中“未排序部分的第一个元素”的索引 i 。
- i < n - gap 的原因:每组中最后一个元素的索引是 n - gap (再往后加 gap 会超出数组范围)。
- 例如: gap=4 、 n=10 时, i 从0到5( 10-4=6 ,不包含6),对应每组的起始位置。
5. 内层循环:对当前组执行“插入排序”
这部分逻辑和直接插入排序类似,只是比较和移动的步长是 gap 而非1:
int end = i; // 记录当前组中“已排序部分的最后一个元素”的索引 int tmp = arr[end + gap]; // 待插入的元素(当前组中未排序部分的第一个元素) while (end >= 0) { if (arr[end] > tmp) // 已排序元素 > 待插入元素,需要后移 { arr[end + gap] = arr[end]; // 已排序元素向后移动 gap 位 end -= gap; // 继续向前比较(步长为 gap) } else { break; // 找到插入位置,退出循环 } } arr[end + gap] = tmp; // 将待插入元素放到正确位置
举例说明(以 gap=4 、数组 [9,1,5,8,3,7,4,6,2,0] 为例):
- i=0 时, end=0 , tmp = arr[0+4] = arr[4] = 3 (待插入元素)。
比较 arr[0] (9)和 tmp (3):9>3 → arr[4] = 9 (9后移4位), end 变为 -4 (退出循环),最后 arr[0] = 3 (3插入到正确位置)。
-这一步实际是对“第0组”(元素索引0、4、8)执行插入排序,让组内元素有序。
6. 循环之间的关系
- - 外层循环 控制 gap 从大到小(决定分组间隔)。
- - 中层循环 遍历每个分组的起始位置(确保所有组都被处理)。
- - 内层循环 对单个分组执行插入排序(核心操作,让组内元素有序)。
- 三者协同工作:每轮用当前 gap 分组,每组内插入排序, gap 逐渐缩小,直到数组完全有序。
7. 总结:
- 1. 初始 gap = n ,进入循环。
- 2. 计算新 gap (如 n=10 → gap=4 )。
- 3. 按 gap=4 分组(每组元素索引相差4),对每组执行插入排序。
- 4. 缩小 gap 为 2 ,重复分组排序。
- 5. 缩小 gap 为 1 ,执行一次直接插入排序(此时数组已接近有序,快速完成)。
- 6. gap=1 退出循环,排序完成。
- 通过这种“先粗略分组排序,再精细调整”的方式,效率远高于直接插入排序,尤其适合中大型数组。
4. 代码运行流程示例
咱们仍以数组 [5, 3, 4, 6, 2] 为例,用希尔排序一步步实现升序(从小到大)排列。
先明确希尔排序的步骤核心:
希尔排序的关键是“先分组排序,再缩小间隔,最后整体排序”。对这个长度为5的数组,我们用最常见的“增量策略”:初始间隔(gap)为数组长度的一半(5/2=2,取整数),之后每次间隔减半(2/2=1),直到间隔为1。
第一步:确定初始间隔(gap=2)
- 分组规则:间隔为2,即数组中索引相差2的元素分为一组。
- 原数组索引: 0:5 、 1:3 、 2:4 、 3:6 、 4:2
- 分组结果:
- 第1组:索引0、2、4 → 元素 [5, 4, 2]
- 第2组:索引1、3 → 元素 [3, 6]
对第1组 [5, 4, 2] 执行插入排序(组内升序)
插入排序的思路:从第二个元素开始,把它插入到前面已排序部分的正确位置。
- 组内元素顺序(按原索引): 5 (索引0)→ 4 (索引2)→ 2 (索引4)1. 处理第二个元素 4 (索引2):
- 前面已排序的元素是 5 (索引0)。
- 比较 4 和 5 : 4 < 5 ,所以把 5 往后移2位(移到索引2的位置),此时组内元素变为 [4, 5, 2] (原数组暂时变为 [4, 3, 5, 6, 2] )。
2. 处理第三个元素 2 (索引4):
- 前面已排序的元素是 4 (索引0)、 5 (索引2)。
- 先比较 2 和 5 (索引2): 2 < 5 ,把 5 往后移2位(移到索引4的位置),组内变为 [4, 2, 5] (原数组暂时变为 [4, 3, 2, 6, 5] )。
- 再比较 2 和 4 (索引0): 2 < 4 ,把 4 往后移2位(移到索引2的位置),组内变为 [2, 4, 5] (原数组暂时变为 [2, 3, 4, 6, 5] )。对第2组 [3, 6] 执行插入排序(组内升序)
- 组内元素顺序(按原索引): 3 (索引1)→ 6 (索引3)。
- 比较 3 和 6 : 3 < 6 ,不需要移动,组内保持 [3, 6] (原数组不变,仍为 [2, 3, 4, 6, 5] )。
第1轮排序后(gap=2)的数组
经过上面两组排序,数组变为: [2, 3, 4, 6, 5] 。此时数组比原来更有序了,但还没完全排好。第二步:缩小间隔(gap=1)
间隔减半后, gap=2/2=1 。此时间隔为1,整个数组就是一个组(索引0-4的元素 [2, 3, 4, 6, 5] ),这一步其实就是直接插入排序。
对整个数组 [2, 3, 4, 6, 5] 执行插入排序
从第二个元素开始,依次把元素插入到前面已排序部分的正确位置:
1. 处理元素 3 (索引1):
- 前面已排序的是 2 (索引0), 3 > 2 ,不需要移动,数组保持 [2, 3, 4, 6, 5] 。
2. 处理元素 4 (索引2):
- 前面已排序的是 [2, 3] , 4 > 3 ,不需要移动,数组保持 [2, 3, 4, 6, 5] 。
3. 处理元素 6 (索引3):
- 前面已排序的是 [2, 3, 4] , 6 > 4 ,不需要移动,数组保持 [2, 3, 4, 6, 5] 。
4. 处理元素 5 (索引4):
- 前面已排序的是 [2, 3, 4, 6] ,比较 5 和 6 (索引3): 5 < 6 ,把 6 往后移1位(移到索引4的位置),数组暂时变为 [2, 3, 4, 5, 6] 。
- 再比较 5 和 4 (索引2): 5 > 4 ,不需要继续移动,插入完成。第2轮排序后(gap=1)的数组
数组变为: [2, 3, 4, 5, 6] ,已经完全升序,排序结束。
总结整个过程:
1. gap=2时:分组 [5,4,2] 和 [3,6] 排序后,数组变为 [2,3,4,6,5] (初步有序)。
2. gap=1时:对整个数组执行插入排序,最终得到 [2,3,4,5,6] (完全有序)。
通过先“大间隔分组粗略排”,再“小间隔整体精细排”。
5. 降序
在希尔排序的代码中,实现降序只需要修改比较元素时的判断条件这一行代码。
以升序代码中“内层循环的比较判断”为例:
升序的核心判断(插入时找比当前元素小的位置):
if (arr[end] > tmp) // 若已排序元素比待插入元素大,就后移
降序只需把判断符号反过来:
if (arr[end] < tmp) // 若已排序元素比待插入元素小,就后移
为什么改这一行?
希尔排序的分组、循环逻辑(控制gap、遍历元素)完全不变,核心差异是“插入规则”:
- 升序:要把小元素往前插(所以当已排序元素比它大时,需要移动大元素)。
- 降序:要把大元素往前插(所以当已排序元素比它小时,需要移动小元素)。
其他代码(如gap计算、循环范围、元素移动的步长)都不需要改,只改这一个比较符号即可。
6. 复杂度
1. 时间复杂度
希尔排序的时间复杂度分析比较复杂,因为它依赖于间隔序列(gap序列)的选择,不同的间隔序列会导致不同的时间复杂度。
- 1. 复杂度的核心影响因素
- 希尔排序的本质是“分组插入排序”,通过不断缩小间隔(gap),最终在gap=1时执行一次完整的插入排序。其复杂度由两点决定:
- 间隔序列的选择(如初始gap多大、每次如何缩小);
- 每一轮分组插入排序的复杂度(随gap减小而变化)。
- 2. 单轮分组插入排序的复杂度
- 以某一轮的间隔为 gap 为例:
- - 数组被分成 gap 个组,每个组的元素数量约为 n/gap (n为数组长度);
- - 对每个组执行插入排序,单个组的插入排序复杂度为 O(k²) (k为组内元素数);
- - 因此, gap 个组的总复杂度为 gap × O((n/gap)²) = O(n²/gap) 。
- 3. 不同间隔序列的总复杂度
- 总复杂度是每一轮(不同gap)的复杂度之和。以下是几种常见间隔序列的分析:
- (1). 最初的希尔间隔(gap = n/2,每次减半)
- - 间隔序列为: n/2, n/4, ..., 1 (如n=10时,gap=5→2→1)。
- - 每一轮的复杂度为 O(n²/gap) ,总和为:
- O(n²/(n/2)) + O(n²/(n/4)) + ... + O(n²/1)
- = O(2n) + O(4n) + ... + O(n²)
- - 最终复杂度:最坏情况O(n²)(和直接插入排序相同,但实际效率更高,因为前期分组已让数组基本有序)。
- (2). 希巴德间隔(gap = 3^k - 1,如1, 4, 13, 40...)
- - 间隔序列增长更快,且每次缩小为前一次的1/3左右。
- - 数学证明显示,这种序列的最坏情况复杂度为O(n^(3/2))(约n¹.⁵),远优于希尔间隔。
- (3). 其他优化间隔(如Sedgewick序列等)
- - 更优的间隔序列(如9×4^k - 9×2^k + 1)可进一步降低复杂度,最坏情况接近O(n^1.3),但实现较复杂。
总结:
希尔排序的时间复杂度没有固定值,核心结论是:
- - 依赖于间隔序列,最坏情况在O(n²)到O(n^1.3)之间;
- - 平均复杂度通常在 O(n^1.25) 到 O(n^1.5) 之间(实际应用中效率远高于直接插入排序);
- - 空间复杂度为O(1)(只需要一个临时变量存储待插入元素,原地排序)。
简单来说,间隔序列设计得越好(让数组更快地接近有序),希尔排序的效率就越高。日常使用中,若不特别指定间隔序列,默认按“n/2减半”处理,此时复杂度以 O(n²) 为上限,但实际表现远优于直接插入排序。
2. 空间复杂度
希尔排序的空间复杂度是O(1),属于原地排序算法。
原因很简单:希尔排序的整个排序过程,只需要一个临时变量来辅助元素的交换或插入(比如在分组内进行插入排序时,临时存储待插入的元素),不需要额外开辟与数组长度相关的存储空间(比如额外的数组、栈、队列等)。
无论数组规模n多大,所需的额外空间都是固定的、不随n变化的常数,因此空间复杂度为O(1)。
3. 总结
下面我们在编译器上对上面的两个排序和冒泡排序(冒泡排序小编之前有讲过所以直接用来测试)进行一个测试:
我们可以看出,排序之前的数据是乱序的,经过排序之后全部变为了有序数组。从而也验证了我们在实现上述排序是正确的。
但是小编感觉上面两种排序均有一定的难度,所以一定要深入理解,不断的揣摩其中各个变量和循环的用途。才能掌握并熟练应用。以上便是关于插入排序的全部内容。感谢大家的观看!