
排序算法系列上篇:本文将带大家从最基础的插入排序开始,逐步深入到希尔排序,通过扑克牌整理的直观类比,结合真实代码实现和调试经验,彻底掌握这两种排序算法的核心思想与工程应用。文末还有LeetCode实战题解析和STL底层原理彩蛋!
在开始学习具体算法之前,让我们先思考一个问题:为什么计算机科学中需要这么多不同的排序算法?
在日常生活中,排序无处不在:
每种场景对排序的要求不同:
面试高频考点:LeetCode 912. Sort an Array 是排序算法的经典面试题,要求实现一个能处理任意整数数组的排序函数。这道题看似简单,却能考察候选人对不同排序算法的理解和选择能力。
直接插入排序就像我们整理扑克牌的过程:假设左手已经拿了一部分有序的牌,右手从牌堆中取出一张新牌,然后从右向左依次比较,找到合适的位置插入,使得左手的牌始终保持有序。

具体步骤:
#include <iostream>
using namespace std;
// 直接插入排序
void InsertSort(int* a, int n) {
for (int i = 0; i < n - 1; i++) {
int end = i; // 已排序序列的最后一个元素下标
int tmp = a[end + 1]; // 待插入的元素
// 从后向前比较,找到插入位置
while (end >= 0) {
if (a[end] > tmp) {
// 元素后移
a[end + 1] = a[end];
end--;
} else {
break; // 找到插入位置
}
}
// 插入元素
a[end + 1] = tmp;
}
}
// 打印数组
void PrintArray(int* a, int n) {
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main() {
int a[] = {5, 3, 9, 6, 2, 4, 7, 1, 8};
int n = sizeof(a) / sizeof(a[0]);
cout << "排序前: ";
PrintArray(a, n);
InsertSort(a, n);
cout << "排序后: ";
PrintArray(a, n);
return 0;
}直接插入排序在以下场景表现优异:
上数据结构课时,我第一次实现插入排序遇到了一个经典bug。当时的代码是这样的:
void buggyInsertSort(int* a, int n) {
for (int i = 1; i < n; i++) {
int key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j] = key; // 错误!应该是a[j+1] = key;
}
}测试用例{5, 2, 4, 6, 1, 3}排序后变成了{2, 2, 4, 5, 1, 3},明显有问题。我在实验室debug了整整一个晚上,最后发现当j--后变成-1时,a[j] = key会导致数组越界访问。
小经验:在算法实现中,边界条件是最容易出错的地方。建议在循环结束时添加打印语句,帮助定位问题。例如:
cout << "Current index: " << j << endl;这个教训让我在之后的编程中格外注意边界条件。
LeetCode 147. Insertion Sort List 要求对单链表进行插入排序。这道题比数组更复杂,因为链表不能随机访问,需要维护多个指针。
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
ListNode* insertionSortList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* dummy = new ListNode(0); // 哑节点,简化边界处理
ListNode* curr = head;
while (curr) {
// 保存下一个节点
ListNode* next = curr->next;
// 在已排序部分找到插入位置
ListNode* prev = dummy;
while (prev->next && prev->next->val < curr->val) {
prev = prev->next;
}
// 插入当前节点
curr->next = prev->next;
prev->next = curr;
// 移动到下一个节点
curr = next;
}
return dummy->next;
}解题思路:
面试技巧:当面试官问到链表排序时,除了插入排序,还可以讨论归并排序(时间复杂度O(NlogN))等更优方案,展示你的算法知识广度。
希尔排序(Shell Sort)是直接插入排序的改进版,由Donald Shell在1959年提出。它的核心思想是:先将整个待排序序列分割成若干子序列,分别进行直接插入排序,当整个序列基本有序时,再对全体元素进行一次直接插入排序。
这种"分而治之"的策略大大提高了排序效率。希尔排序的关键在于gap序列的选择:

#include <iostream>
using namespace std;
// 希尔排序
void ShellSort(int* a, int n) {
int gap = n;
while (gap > 1) {
// 推荐写法:除3
gap = gap / 3 + 1;
// 对每个分组进行插入排序
for (int i = 0; i < n - gap; i++) {
int end = i;
int tmp = a[end + gap];
// 组内插入排序
while (end >= 0) {
if (a[end] > tmp) {
a[end + gap] = a[end];
end -= gap;
} else {
break;
}
}
a[end + gap] = tmp;
}
}
}
// 打印数组
void PrintArray(int* a, int n) {
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main() {
int a[] = {5, 3, 9, 6, 2, 4, 7, 1, 8};
int n = sizeof(a) / sizeof(a[0]);
cout << "排序前: ";
PrintArray(a, n);
ShellSort(a, n);
cout << "排序后: ";
PrintArray(a, n);
return 0;
}gap序列的选择对希尔排序的性能影响很大。常见的gap序列有:
PDF中推荐的gap = gap/3 + 1是一种实践中表现不错的序列,能够达到O(n^1.3)的时间复杂度。
外层循环: 外层循环的时间复杂度可以直接给出为:O(log2 n) 或者O(log3 n) ,即O(log n)
内层循环:

假设⼀共有n个数据,合计gap组,则每组为n/gap个;


因此,希尔排序在最初和最后的排序的次数都为n,即前⼀阶段排序次数是逐渐上升的状态,当到达 某⼀顶点时,排序次数逐渐下降⾄n,⽽该顶点的计算暂时⽆法给出具体的计算过程
因此
在一次算法作业中,我使用了以下gap序列实现:
void wrongShellSort(int* a, int n) {
int gap = n / 2; // 错误的gap序列
while (gap > 0) {
for (int i = gap; i < n; i++) {
int temp = a[i];
int j = i;
while (j >= gap && a[j - gap] > temp) {
a[j] = a[j - gap];
j -= gap;
}
a[j] = temp;
}
gap /= 2; // 可能导致死循环
}
}测试用例{1, 2, 3, 4, 5}时一切正常,但当输入{5, 4, 3, 2, 1}时,程序竟然卡死了!通过打印gap值,我发现当n=5时,gap序列为2, 1, 0, 0, 0...,而循环条件gap > 0在gap=0时应该退出,但因为整数除法,gap始终为0,导致死循环。
调试心得:希尔排序的gap序列设计需要非常谨慎。推荐使用
gap = gap/3 + 1,它能保证gap最终会变成1,避免死循环。另外,在循环中添加打印语句cout << "Current gap: " << gap << endl;能帮助快速定位问题。
有趣的是,希尔排序在Linux内核中也有应用。在/kernel/sched/core.c文件中,调度器使用希尔排序对运行队列进行排序,因为:
这证明了即使是"古老"的算法,在特定场景下仍有其价值。

选择建议:
答案放在评论区喽
中篇将深入探讨选择排序家族(直接选择、堆排序) 为什么堆排序升序要建大堆?冒泡排序有哪些优化技巧?
下期亮点:通过一个LeetCode 215. Kth Largest Element in an Array的真实案例,展示堆排序在解决Top K问题时的强大优势,以及如何在面试中优雅地实现它!
系列导航:
互动时间:你在学习插入排序或希尔排序时遇到过哪些问题?欢迎在评论区分享你的调试经验!