排序算法专题(一):插入排序 & 希尔排序

目录

一、排序问题的本质

二、稳定排序

[三、插入排序(Insertion Sort)](#三、插入排序(Insertion Sort))

[1 插入排序的核心思想](#1 插入排序的核心思想)

[2 插入排序过程](#2 插入排序过程)

插入3

插入8

插入1

[3 插入排序代码](#3 插入排序代码)

四、复杂度分析

最坏情况

最好情况

五、插入排序的重要特点

[六、希尔排序(Shell Sort)](#六、希尔排序(Shell Sort))

[1 为什么需要希尔排序](#1 为什么需要希尔排序)

[2 希尔排序的核心思想](#2 希尔排序的核心思想)

[3 gap(间隔)](#3 gap(间隔))

七、希尔排序代码

八、为什么代码没有"分组循环"

[九、为什么 gap 要越来越小](#九、为什么 gap 要越来越小)

十、复杂度

十一、稳定性

十二、学习过程中踩过的坑

[1 为什么 i 从 1 开始](#1 为什么 i 从 1 开始)

[2 为什么要保存 tmp](#2 为什么要保存 tmp)

[3 为什么 j -= gap](#3 为什么 j -= gap)

[4 为什么最后必须 gap=1](#4 为什么最后必须 gap=1)

十三、面试常问问题

十四、我的理解总结

结语


Author 👉**:TomGo**

排序专题是数据结构中非常基础但非常重要的一部分。

一开始学的时候,总觉得排序就是"会写代码"。

后来慢慢发现:

复制代码
会写 ≠ 理解

真正重要的是理解:

  • 算法的思想

  • 为什么这样写

  • 复杂度从哪里来

  • 容易踩的坑

这篇博客主要整理:

  • 插入排序

  • 希尔排序

    以及我学习过程中踩过的坑和思考。


一、排序问题的本质

排序其实就是:

重新排列数据,使其满足某种顺序关系

例如:

复制代码
原数组
5 3 8 1 2

排序后
1 2 3 5 8

排序算法通常关注三个重要指标:

指标 含义
时间复杂度 排序需要多少计算量
空间复杂度 是否需要额外空间
稳定性 相同元素的相对顺序是否改变

二、稳定排序

假设数组中有两个值相同的元素:

复制代码
(2,A) (2,B)

如果排序后仍然是:

复制代码
(2,A) (2,B)

说明:

复制代码
排序是稳定的

如果顺序变成:

复制代码
(2,B) (2,A)

说明:

复制代码
排序不稳定

三、插入排序(Insertion Sort)

1 插入排序的核心思想

插入排序其实非常符合人的直觉。

想象你在整理扑克牌:

  • 左边的牌已经排好序

  • 右手拿一张新牌

  • 把它插入到正确的位置

数组中的逻辑也是一样:

复制代码
左边:已排序
右边:未排序

2 插入排序过程

数组:

复制代码
5 3 8 1 2

初始:

复制代码
[5] | 3 8 1 2

默认第一元素有序。


插入3

复制代码
5 | 3 8 1 2

比较:

复制代码
3 < 5

移动:

复制代码
3 5 | 8 1 2

插入8

复制代码
3 5 | 8 1 2

比较:

复制代码
8 > 5

不需要移动。


插入1

复制代码
3 5 8 | 1 2

向左比较:

复制代码
1 < 8
1 < 5
1 < 3

移动:

复制代码
1 3 5 8 | 2

3 插入排序代码

cpp 复制代码
​
void InsertSort(int* a, int n)
{
    for(int i = 1; i < n; i++)
    {
        int tmp = a[i];
        int j = i - 1;

        while(j >= 0 && a[j] > tmp)
        {
            a[j + 1] = a[j];
            j--;
        }

        a[j + 1] = tmp;
    }
}

​

核心动作:

复制代码
移动元素
插入元素

四、复杂度分析

最坏情况

数组完全逆序:

复制代码
9 8 7 6 5

移动次数:

cpp 复制代码
1 + 2 + 3 + ... + n
等差数列

时间复杂度:

复制代码
O(n²)

最好情况

数组已经有序:

复制代码
1 2 3 4 5

每次只比较一次:

复制代码
O(n)

五、插入排序的重要特点

插入排序有一个非常重要的特性:

复制代码
对接近有序的数组非常快

这一点非常关键,因为很多高级排序算法都会利用这一特性。


六、希尔排序(Shell Sort)

1 为什么需要希尔排序

插入排序最大的问题是:

复制代码
元素移动距离太远

例如:

复制代码
9 1 2 3 4 5 6

插入排序:

复制代码
9需要移动6次

这就是效率低的原因。


2 希尔排序的核心思想

希尔排序的思路是:

复制代码
先让远距离元素提前移动

也可以理解为:

复制代码
先粗排
再细排

3 gap(间隔)

希尔排序引入一个变量:

复制代码
gap

例如:

复制代码
gap = 3

数组:

复制代码
9 8 3 7 5 6 4 1

分组:

复制代码
组1:0 3 6
组2:1 4 7
组3:2 5

每一组做:

复制代码
插入排序

七、希尔排序代码

cpp 复制代码
void ShellSort(int* a, int n)
{
    int gap = n;

    while(gap > 1)
    {
        gap /= 2;

        for(int i = gap; i < n; i++)
        {
            int tmp = a[i];
            int j = i - gap;

            while(j >= 0 && a[j] > tmp)
            {
                a[j + gap] = a[j];
                j -= gap;
            }

            a[j + gap] = tmp;
        }
    }
}

八、为什么代码没有"分组循环"

很多人第一次看希尔排序会疑惑:

复制代码
代码没有显式分组
怎么处理每一组?

关键在这句:

复制代码
for (i = gap; i < n; i++)

例如:

复制代码
gap = 3

数组:

复制代码
0 1 2 3 4 5 6 7

i 的变化:

复制代码
3 4 5 6 7

实际上对应:

复制代码
i=3 → 处理组 0 3 6
i=4 → 处理组 1 4 7
i=5 → 处理组 2 5

所以:

所有组是交替处理的

希尔排序并不会单独处理每一组,
而是通过一个线性循环依次处理所有元素,
每个元素都会在自己的 gap 组内进行插入调整,
因此所有组会被"交替地"完成排序。


九、为什么 gap 要越来越小

希尔排序的核心策略:

复制代码
大步调整
小步微调

例如:

复制代码
gap = 4
gap = 2
gap = 1

当:

复制代码
gap = 1

其实就是:

复制代码
一次普通插入排序

但此时数组:

复制代码
已经接近有序

所以效率非常高。


十、复杂度

希尔排序复杂度比较特殊。

取决于:

复制代码
gap序列

一般认为:

复制代码
O(n^1.3 ~ n^1.5)

明显优于:

复制代码
O(n²)

十一、稳定性

插入排序:

复制代码
稳定

因为只移动:

复制代码
大于 tmp 的元素

希尔排序:

复制代码
不稳定

因为:

复制代码
跨 gap 移动

可能改变相同元素顺序。


十二、学习过程中踩过的坑

1 为什么 i 从 1 开始

因为:

复制代码
a[0] 默认有序

2 为什么要保存 tmp

因为移动时:

复制代码
a[i] 会被覆盖

3 为什么 j -= gap

因为需要在:

复制代码
同一组

里移动。


4 为什么最后必须 gap=1

否则只能保证:

复制代码
局部有序

而不能保证:

复制代码
整体有序

十三、面试常问问题

插入排序什么时候最快?

复制代码
数组接近有序

复杂度:

复制代码
O(n)

希尔排序稳定吗?

复制代码
不稳定

原因:

复制代码
跨gap移动

希尔排序为什么比插入排序快?

核心原因:

复制代码
减少元素移动距离

十四、我的理解总结

学习排序之后,我最大的感受是:

复制代码
算法不是记代码
而是理解结构

例如:

插入排序的本质是:

复制代码
维护一个有序区

而希尔排序只是:

复制代码
让元素提前接近正确位置

当理解这一点之后,代码就变得非常清晰。


结语

这是我排序专题的第一篇总结。

后续我还会整理:

  • 冒泡排序

  • 选择排序

  • 快速排序

  • 归并排序

  • 堆排序

希望未来复习时,一眼就能想起这些算法的本质。

------ TomGo


相关推荐
南境十里·墨染春水10 小时前
C++传记(面向对象)虚析构函数 纯虚函数 抽象类 final、override关键字
开发语言·c++·笔记·算法
2301_7971727510 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
有为少年11 小时前
告别“唯语料论”:用合成抽象数据为大模型开智
人工智能·深度学习·神经网络·算法·机器学习·大模型·预训练
比昨天多敲两行11 小时前
C++ 二叉搜索树
开发语言·c++·算法
Season45011 小时前
C++11之正则表达式使用指南--[正则表达式介绍]|[regex的常用函数等介绍]
c++·算法·正则表达式
Tisfy11 小时前
LeetCode 2839.判断通过操作能否让字符串相等 I:if-else(两两判断)
算法·leetcode·字符串·题解
问好眼11 小时前
《算法竞赛进阶指南》0x04 二分-1.最佳牛围栏
数据结构·c++·算法·二分·信息学奥赛
会编程的土豆12 小时前
【数据结构与算法】优先队列
数据结构·算法
minji...13 小时前
Linux 进程信号(二)信号的保存,sigset_t,sigprocmask,sigpending
linux·运维·服务器·网络·数据结构·c++·算法