直接插入
描述: 从原始序列第二个元素起,依次选取并插入到前面合适的位置,直到序列最后一个元素完成插入。
java代码
public void insertSort(int[] arr) {
int temp, j;
for (int i = 1; i < arr.length; i++) { // 从第二个元素起
temp = arr[i]; // 取出待排元素
// 待排元素与它前面的元素从后往前比较,根据比较结果判断前面元素是否后移
for (j = i; j > 0 && temp < arr[j - 1]; j--) {
arr[j] = arr[j - 1];
}
// 每次满足循环条件执行元素后移时也执行了j--,此时j所指的位置就是前面元素后移之后空出来的那个位置
arr[j] = temp;
}
}
算法分析
- 时间复杂度 O ( n 2 ) Ο(n^2) O(n2),最好情况n个初始元素顺序,每个元素只和前面元素比较一次,共比较n-1次,不移动;最坏情况逆序,每个元素都要和前面所有元素比较,共比较 ∑ i = 1 n − 1 i ≈ n 2 2 \sum_{i=1}^{n-1}i \approx \frac{n^2}{2} ∑i=1n−1i≈2n2次,每次比较后元素都会移动,所以也移动了 n 2 2 \frac{n^2}{2} 2n2次。平均情况下比较次数和移动次数都是 n 2 4 \frac{n^2}{4} 4n2次,时间复杂度 O ( n 2 ) Ο(n^2) O(n2)。
- 空间复杂度 O ( 1 ) Ο(1) O(1),排序过程只需要一个辅助空间temp。
- 排序稳定,第二个for循环的循环条件可以看出排序前后没有改变相等元素的相对位置。
- 顺序存储结构、链式存储结构都适用。
- 由时间复杂度的分析可看出当初始元素基本有序时,时间复杂度趋近 O ( n ) Ο(n) O(n),基本取决于初始元素个数n,当初始元素无序程度高且个数n又较大时,时间复杂度较高,趋近 O ( n 2 ) Ο(n^2) O(n2),不宜采用。
二分插入
描述: 二分插入排序,也叫折半插入排序,只是将上面直接插入中的从后往前顺序查找换为二分查找。
java代码
public void binaryInsertSort(int[] arr) {
int low, mid, high, temp;
for (int i = 1; i < arr.length; i++) { // 从第二个元素开始
low = 0;
high = i - 1; // low、high为初始有序区间上下标
temp = arr[i]; // 取出待排元素
// while循坏得到待排元素应该插入的位置下标low,为什么该插入这个位置后面解释
while (low <= high) {
mid = (low + high) / 2;
if (temp < arr[mid]) { // 通过与有序区中间位置元素的比较重置比较区间
high = mid - 1;
} else {
low = mid + 1;
}
}
// 将前面有序区中下标low及后面的所有元素后移一位
for (int j = i; j > low; j--) {
arr[j] = arr[j - 1];
}
arr[low] = temp; // 插入空出的下标为low的位置
}
}
关于while循环后下标low处就是元素要插入的位置,是因为while循环条件里的low=high这种情况最后一定会出现,出现时有low=high=mid,这时temp与下标为low的元素比较,如果小于,则执行high=mid-1,low不变,此时low指的位置就是temp要插入的位置。如果大于等于,则执行的是low=mid+1,其实也就是low=low+1,表示将temp插在原来下标low元素的后面,所以无论比较结果如何,返回的low值就是temp要插入的位置。从这儿也可以看出相等元素排序前后相对位置并没有发生相对改变,所以折半插入排序是一种稳定排序。(第11行的判断条件改成temp<=arr[mid]就不属于稳定排序了)
算法分析
- 时间复杂度 O ( n 2 ) Ο(n^2) O(n2),折半插入与直接插入相比一般情况下只减少了比较次数,而移动次数并没有改变。
- 空间复杂度 O ( 1 ) Ο(1) O(1),排序过程只需要一个辅助空间temp保存每次取出的元素。
- 排序稳定。
- 只能用于顺序结构,不能用于链式结构。
- 适用于初始元素无序且个数n较大的情况。
希尔排序
描述: 希尔排序,也叫缩小增量排序,从上面可以知道,直接插入排序的时间复杂度与两个因素有关,一是初始元素的个数n,二是初始元素的无序程度。当初始元素的无序程度较高时,时间复杂度趋近于
O
(
n
2
)
Ο(n^2)
O(n2),n越大,时间复杂度越高,所以说直接插入排序不适合初始元素无序程度高且个数n又大的情况。相反,当初始元素基本有序时,时间复杂度趋近于
O
(
n
)
Ο(n)
O(n),此时直接插入排序就是一种很理想的排序算法。而希尔排序正是这样的一种思想。算法步骤如下:
假设选取的增量序列为T4>T3>T2>T1=1
- 对初始元素按T4距离逻辑分组,组内直接插入排序。
- 按T3距离逻辑分组,组内直接插入排序。
- 以此类推,直到增量1,也就是对所有元素进行一次直接插入排序。
java代码
public void shellSort(int[] arr) {
int[] init = new int[] { 5, 3, 1 }; // 保存增量序列
int temp, j;
for (int k = 0; k < init.length; k++) { // 根据每个增量逻辑分组
// 组内还是直接插入排序,只是将原来相邻元素的距离“1”换成“init[k]”
for (int i = init[k]; i < arr.length; i += init[k]) {
temp = arr[i];
for (j = i; j >= init[k] && temp < arr[j - init[k]]; j -= init[k]) {
arr[j] = arr[j - init[k]];
}
arr[j] = temp;
}
}
}
关于增量的选取, 应该依次递减且相互互质,并且最后一个增量值必须为1。这是为了避免某次分在同一组已经排序过的元素本次又分在同一组,再次排序浪费时间。比如下面这种情况
算法分析
- 时间复杂度比直接插入排序 O ( n 2 ) Ο(n^2) O(n2)要低,具体跟选取的增量序列有很大关系。比直接插入要低是因为直接插入每次移动只消除一个逆序对,而希尔排序当增量大于1时每次移动一般消除了多个逆序对,减少了元素的比较和移动次数。当选取Hibbard增量序列 2 k − 1 2^k-1 2k−1 时(k=1,2,3…),最坏情况下的时间复杂度为 O ( n 3 / 2 ) Ο(n^{3/2}) O(n3/2),平均时间复杂度猜想是 O ( n 5 / 4 ) Ο(n^{5/4}) O(n5/4)。
- 空间复杂度 O ( 1 ) Ο(1) O(1),排序过程只需要一个辅助空间temp保存取出的元素。
- 排序不稳定,因为元素都是在自己的逻辑分组内跳跃移动。
- 只能用于顺序结构,不能用于链式结构。
- 总的比较次数和移动次数都比直接插入排序要少,初始元素个数越多效果越明显,所以适合初始元素无序且个数n较大的情况。
两个定理
定理1:任意N个不同元素组成的序列平均具有
N
(
N
−
1
)
4
\frac{N(N-1)}{4}
4N(N−1)个逆序对。
定理2:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度
Ω
(
N
2
)
Ω(N^2)
Ω(N2)。意思是最好也只能到
N
2
N^2
N2数量级这个复杂度,N是元素个数。
从定理2可以看出,以交换相邻元素排序的时间复杂度与逆序对总数有关,这也解释了希尔排序的比较和移动次数低于直接插入排序的原因。(分组元素逻辑相邻)