排序---插入排序(Insertion Sort)
一、算法核心思想
插入排序(Insertion Sort)是一种直观且简单的排序算法,其核心思想借鉴了日常生活中"整理手牌"的行为——将一组元素分为"已排序"和"未排序"两部分,每次从"未排序"部分取出一个元素,按照大小顺序插入到"已排序"部分的合适位置,直到所有元素都被插入完毕。
这种算法的本质是通过逐步构建有序序列实现排序:初始时,"已排序"部分仅包含第一个元素;之后,从第二个元素开始,每一步都将当前元素与"已排序"部分的元素逐一比较,找到其正确位置并插入,最终使整个序列有序。
二、算法步骤分解
以数组 [5, 2, 4, 6, 1, 3]
为例,详细说明插入排序的执行步骤:
-
初始状态:
已排序部分:[5]
(第一个元素默认有序)
未排序部分:[2, 4, 6, 1, 3]
-
插入第2个元素(2):
- 比较2与已排序部分的5:2 < 5,需插入到5左侧。
- 将2存入temp中
- 移动元素:将5右移一位,数组变为
[5, 5, 4, 6, 1, 3]
。 - 插入元素:将2放入空位,数组变为
[2, 5, 4, 6, 1, 3]
。 - 此时已排序部分:
[2, 5]
,未排序部分:[4, 6, 1, 3]
。
-
插入第3个元素(4):
- 比较4与5:4 < 5,继续比较前一个元素2:4 > 2,确定插入位置在2和5之间。
- 移动元素:将5右移一位,数组变为
[2, 5, 5, 6, 1, 3]
。 - 插入元素:将4放入空位,数组变为
[2, 4, 5, 6, 1, 3]
。 - 此时已排序部分:
[2, 4, 5]
,未排序部分:[6, 1, 3]
。
-
插入第4个元素(6):
- 比较6与5:6 > 5,无需移动元素,直接插入到已排序部分末尾。
- 数组变为
[2, 4, 5, 6, 1, 3]
。 - 此时已排序部分:
[2, 4, 5, 6]
,未排序部分:[1, 3]
。
-
插入第5个元素(1):
- 依次比较1与6、5、4、2:1小于所有元素,需插入到最左侧。
- 移动元素:将2、4、5、6依次右移一位,数组变为
[2, 2, 4, 5, 6, 3]
。 - 插入元素:将1放入空位,数组变为
[1, 2, 4, 5, 6, 3]
。 - 此时已排序部分:
[1, 2, 4, 5, 6]
,未排序部分:[3]
。
-
插入第6个元素(3):
- 依次比较3与6、5、4:3 < 4,继续比较2:3 > 2,确定插入位置在2和4之间。
- 移动元素:将4、5、6依次右移一位,数组变为
[1, 2, 4, 4, 5, 6]
。 - 插入元素:将3放入空位,数组变为
[1, 2, 3, 4, 5, 6]
。
-
结束:所有元素插入完成,数组已排序。
三、算法特性分析
1. 时间复杂度
- 最好情况:数组已完全有序。此时每个元素只需与前一个元素比较1次(无需移动),总操作次数为 O(n)O(n)O(n)。
- 最坏情况:数组完全逆序。每个元素需要与已排序部分的所有元素比较并移动,总操作次数为 O(n2)O(n^2)O(n2)(求和公式:1+2+...+(n−1)=n(n−1)/21+2+...+(n-1) = n(n-1)/21+2+...+(n−1)=n(n−1)/2)。
- 平均情况:对于随机排列的数组,平均比较和移动次数约为 n2/4n^2/4n2/4,时间复杂度为 O(n2)O(n^2)O(n2)。
2. 空间复杂度
插入排序是原地排序(In-place Sort),仅需额外常数级空间(如临时变量存储当前元素),空间复杂度为 (O(1))。
3. 稳定性
插入排序是稳定排序。当遇到相等元素时,新元素会插入到原有相等元素的右侧,保持其相对顺序不变。例如,对 [3, 2, 2]
排序时,两个2的位置不会交换。
4. 适用场景
- 小规模数据(如 n≤100n \leq 100n≤100):简单直接,无需额外空间。
- 接近有序的数据:此时比较和移动次数少,效率接近 (O(n))。
- 作为复杂算法的子过程:例如归并排序、快速排序中,对小规模子数组使用插入排序可优化性能。
四、C/C++实现代码
以下是插入排序的C++实现,包含完整的排序函数、测试用例及步骤打印:
#include <iostream>
#include <vector>
using namespace std;// 插入排序函数:对数组arr进行升序排序
void insertionSort(vector<int>& arr) {int n = arr.size();// 若数组为空或仅有一个元素,直接返回if (n <= 1) return;// 外层循环:遍历未排序部分(从第2个元素开始)for (int i = 1; i < n; i++) {int current = arr[i]; // 记录当前待插入元素int j = i - 1; // 已排序部分的末尾索引// 内层循环:在已排序部分中找到插入位置// 若已排序元素大于current,将其右移一位while (j >= 0 && arr[j] > current) {arr[j + 1] = arr[j]; // 元素右移j--;}// 将current插入到正确位置arr[j + 1] = current;// 打印当前步骤的排序结果(可选,用于调试)cout << "第" << i << "轮插入后:";for (int num : arr) {cout << num << " ";}cout << endl;}
}int main() {// 测试用例vector<int> arr = {5, 2, 4, 6, 1, 3};cout << "排序前的数组:";for (int num : arr) {cout << num << " ";}cout << endl << endl;// 执行插入排序insertionSort(arr);cout << endl << "排序后的数组:";for (int num : arr) {cout << num << " ";}cout << endl;return 0;
}
代码说明:
- 函数设计:
insertionSort
接收一个vector引用,直接在原数组上修改(原地排序)。 - 外层循环:
i
从1开始,遍历未排序部分的每个元素(arr[1]
到arr[n-1]
)。 - 内层循环:
j
从i-1
开始,向前遍历已排序部分,若arr[j] > current
则右移元素,直到找到current
的插入位置(j+1
)。 - 插入操作:将
current
放入j+1
位置,完成一次插入。
五、优化思路:二分插入排序
插入排序的主要耗时操作是在已排序部分查找插入位置(线性扫描)。若改用二分查找定位插入位置,可将比较次数从 O(n)O(n)O(n) 降至 O(logn)O(\log n)O(logn),优化查找效率。
二分插入排序的核心修改是将内层循环的线性查找替换为二分查找,代码示例如下:
void binaryInsertionSort(vector<int>& arr) {int n = arr.size();for (int i = 1; i < n; i++) {int current = arr[i];int left = 0, right = i - 1;// 二分查找插入位置while (left <= right) {int mid = left + (right - left) / 2;if (current < arr[mid]) {right = mid - 1; // 目标在左半部分} else {left = mid + 1; // 目标在右半部分}}// 找到插入位置left,将元素右移for (int j = i - 1; j >= left; j--) {arr[j + 1] = arr[j];}arr[left] = current; // 插入元素}
}
注意:二分插入排序仅减少比较次数,元素移动次数仍为 O(n2)O(n^2)O(n2),因此时间复杂度仍为 O(n2)O(n^2)O(n2),但实际效率优于普通插入排序(尤其数据量较大时)。
插入排序是一种简单直观的排序算法,其优点是实现简单、空间复杂度低、稳定且对接近有序的数据高效。尽管时间复杂度为 O(n2)O(n^2)O(n2),但其在小规模数据场景中表现优异,且常作为复杂排序算法的优化补充。