背景
直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。
算法思想
在正式讲解算法思想之前,我先给大家讲解一个生活中的例子。不清楚大家是否玩过扑克牌(没玩过也没关系),在打扑克牌的时候我们需要经历洗牌、发牌、整理牌以及出牌四个阶段。而在整理牌这个阶段,大家一般都会按照一定的规则把扑克牌整理好方便自己查看,而最常见的就是按照大小顺序把扑克牌整理好。其实在按大小顺序插牌的过程中我们已经默认使用了直接插入排序这种思想:逐步把一张新抽到的牌插入到已经有序的牌中。
下面了解一下题设:假设我们存在一组n
个数序列
⟨
a
1
,
a
2
,
⋯
,
a
n
⟩
\langle a_{1},a_{2},\cdots,a_{n}\rangle
⟨a1,a2,⋯,an⟩希望输出整个序列的排序
⟨
a
1
′
,
a
2
′
,
⋯
,
a
n
′
⟩
\langle a_{1}^{'},a_{2}^{'},\cdots,a_{n}^{'}\rangle
⟨a1′,a2′,⋯,an′⟩,其中
a
1
′
≤
a
2
′
≤
⋯
≤
a
n
′
a_{1}^{'} \le a_{2}^{'} \le \cdots \le a_{n}^{'}
a1′≤a2′≤⋯≤an′。如果你会打扑克牌并且看懂了上述文字的话,那么你一定会脱口而出:“使用直接插入排序”。
了解了基本的假设和背景之后,下面我们用伪代码的方式来表述一下直接插入排序
的整个过程。在正式表述之前,我们先了解一下伪代码的书写规则。
- 缩进表示块结构
while
,for
和repeat-until
等循环结构以及if-else
与目前流行的各大语言中的那些结构具有类似的解释- 符号
//
表示注释 - 变量若无显式说明都是局部的
- 数组元素通过
数组名[下标]
这样的形式来访问 - 通过
对象.属性
的形式来获取对象的特性
for j = 2:A.length
key = A[j]
// 把A[j]插入到A[1..j-1]之中
i = j - 1
while i > 0 and A[i] > key
A[i+1] = A[i]
i = i - 1
A[i+1] = key
可能伪代码看起来很抽象,但是基本上所有算法基本上都是由伪代码描述的,希望大家适应。下面贴一个例子来帮助大家理解上述过程。首先我们把第一个元素5当作元素个数为1的有序序列,然后依次把第二个、第三个…直到第n个插入到这个有序序列中。
代码
废话少说,show me code。下面我们直接开整!
#include <stdio.h>
#include <stdlib.h>
/** InsertSort(int arr[], int length) -> None
* 插入排序
* @params
* @param arr[] 需要进行排序的数组
* @param length 需要排序的数组长度
* @return none
*/
void InsertSort(int arr[], int length) {
if (length < 1) printf("please enter the correct size!\n");
for (int j = 1; j < length; j++) {
int tmp = arr[j];
int i = j - 1;
while (i >= 0 && arr[i] > tmp) {
arr[i+1] = arr[i];
i -= 1;
}
arr[i+1] = tmp;
}
}
int main() {
int arr[] = {5, 2, 4, 6, 1, 3};
int len = sizeof(arr) / sizeof(arr[0]);
printf("Original array: ");
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
InsertSort(arr, len);
printf("After sort the array: ");
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
算法分析
时间复杂度
对于一个算法,我们首先要做的就是分析其效率,通常情况下我们用算法执行每条语句的运行时间之和来表示。对于上述的直接插入排序,我们假设每条语句执行的代价为 c i c_i ci,并且已知了每条语句的执行次数,那么整个算法执行的代价就为所有单个语句代价的加权求和。在下面的例子中 t j t_j tj表示执行一次循环需要进行交换的次数。
InsertSort 代价 次数
for j = 2 to A.length c1 n
key = A[j] c2 n-1
// 把A[j]插入到A[1..j-1]之中 0 n-1
i = j - 1 c4 n-1
while i > 0 and A[i] > key c5 \sum(t_j) j=2 to n
A[i+1] = A[i] c6 \sum(t_j-1) j=2 to n
i = i - 1 c7 \sum(t_j-1) j=2 to n
A[i+1] = key c8 n-1
我们可以发现,当输入数组已按照从小到大的顺序排好时,整个算法语句执行次数是最少的,因为在这种情况下
∑
j
=
2
n
(
t
j
−
1
)
=
0
\sum_{j=2}^{n} (t_j-1)=0
∑j=2n(tj−1)=0,该最佳情况的运行时间为:
T
n
=
c
1
n
+
c
2
(
n
−
1
)
+
c
4
(
n
−
1
)
+
c
5
(
n
−
1
)
+
c
8
(
n
−
1
)
=
(
c
1
+
c
2
+
c
4
+
c
5
+
c
8
)
n
−
(
c
2
+
c
4
+
c
5
+
c
8
)
T_{n} = c_1 n + c_2 (n-1) + c_4 (n-1) + c_5 (n-1) + c_8 (n-1) \\ =(c_1+c_2+c_4+c_5+c_8)n-(c_2+c_4+c_5+c_8)
Tn=c1n+c2(n−1)+c4(n−1)+c5(n−1)+c8(n−1)=(c1+c2+c4+c5+c8)n−(c2+c4+c5+c8)
我们可以把该运行时间表示成
a
n
+
b
an+b
an+b,其中
a
a
a和
b
b
b依赖于代价于
c
i
c_i
ci,可以说它是n
的线性函数。
但是并不是每一个数组都是已按照规则排好序的,其顺序可以是任意的。因此判断一个算法性能优劣的时候,我们通常探讨其最坏情况下的执行时间。对于上述算法,当传入的数组为逆序时,
∑
j
=
2
n
t
j
=
n
(
n
+
1
)
2
−
1
\sum_{j=2}^{n} t_j=\frac{n(n+1)}{2}-1
∑j=2ntj=2n(n+1)−1,那么整个算法的运行时间就为:
T
n
=
c
1
n
+
c
2
(
n
−
1
)
+
c
4
(
n
−
1
)
+
c
5
(
n
(
n
+
1
)
2
−
1
)
+
c
6
(
n
(
n
+
1
)
2
)
+
c
7
(
n
(
n
+
1
)
2
)
+
c
8
(
n
−
1
)
=
(
c
5
2
+
c
6
2
+
c
7
2
)
n
2
+
(
c
1
+
c
2
+
c
4
+
c
5
2
−
c
6
2
−
c
7
2
+
c
8
)
n
−
(
c
2
+
c
4
+
c
5
+
c
8
)
T_n=c_1n+c_2(n-1)+c_4(n-1)+c_5(\frac{n(n+1)}{2}-1)+c_6(\frac{n(n+1)}{2})+c_7(\frac{n(n+1)}{2})+c_8(n-1) \\ =(\frac{c_5}{2}+\frac{c_6}{2}+\frac{c_7}{2})n^2+(c_1+c_2+c_4+\frac{c_5}{2}-\frac{c_6}{2}-\frac{c_7}{2}+c_8)n-(c_2+c_4+c_5+c_8)
Tn=c1n+c2(n−1)+c4(n−1)+c5(2n(n+1)−1)+c6(2n(n+1))+c7(2n(n+1))+c8(n−1)=(2c5+2c6+2c7)n2+(c1+c2+c4+2c5−2c6−2c7+c8)n−(c2+c4+c5+c8)
我们可以把该运行时间表示成
a
n
2
+
b
n
+
c
an^2+bn+c
an2+bn+c,其中
a
,
b
a, b
a,b和
c
c
c依赖于代价于
c
i
c_i
ci,可以说它是n
的二次函数。
在实际使用中,我们更在意的其实是运行时间的增长率或增长量级,所以我们只用考虑公式中最重要的项(即次数最高的项),因为在n很大的时候,低阶项可以被忽略。那么,对于插入排序,其最差情况下的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
综上,直接插入排序算法的时间复杂度在最好的条件下为
O
(
n
)
O(n)
O(n),在最差的条件下为
O
(
n
2
)
O(n^2)
O(n2)。
空间复杂度
由于我们只使用了一个临时变量 k e y key key来保存需要交换的值,所以直接插入排序的空间复杂度为1。
算法稳定性
可以看到在判断的时候我们并未考虑元素相等的情况,因此也就不会交换数值相同元素的位置,所以直接插入排序是稳定的算法