插入排序
插入排序的基本思想:每次将一个待排序序列按其关键字大小插入到前面已经排好序的子序列中,知道全部记录插入完成。主要有直接插入排序、折半插入排序和希尔排序。
直接插入排序(Straight Insertion Sort)
直接插入排序的基本思想:首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。直接插入算法的核心思想是取未排序区间中的第一个元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
#include <stdio.h>
#include <stdlib.h>
// 直接插入排序c实现,a表示数组,n表示数组大小
/**
* Author: gamilian
*/
void straight_insertion_sort(int a[], int n) {
if (n <= 1)
return;
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value)
a[j+1] = a[j]; // 数据移动
else
break;
}
a[j+1] = value; // 插入数据
}
}
# 直接排入排序python实现
"""
Author: gamilian
"""
def straight_insertion_sort(a):
""" 直接插入排序
args:
a: List[int]
"""
length = len(a)
if length <= 1:
return
for i in range(1, length):
value = a[i]
j = i-1
while j >= 0:
if a[j] > value:
a[j + 1] = a[j]
else:
break
j -= 1
a[j + 1] = value
算法的稳定性:在直接插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以直接插入排序是稳定的排序算法。
空间复杂度:从实现过程可以很明显地看出,直接插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法。
时间复杂度:如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为 O(n)。注意,这里是从尾到头遍历已经有序的数据。如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为 O(n^2)。我们在数组中插入一个数据的平均时间复杂度是 O(n)。所以,对于直接插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n^2)。
二分插入排序(Binary Insertion Sort)
二分插入排序的基本思想:直接插入排序需要先查找出待插入元素的插入位置,然后给插入位置腾出空间,将待插入元素插入,我们通过边插入边移动来实现上述操作。二分插入排序则将查找插入位置与移动元素的操作分离,先二分查找出元素待插入位置,然后统一地移动待插入位置之后的所有元素。
#include <stdio.h>
#include <stdlib.h>
// 二分插入排序c实现,a表示数组,n表示数组大小
/**
* Author: gamilian
*/
void binary_insertion_sort(int a[], int n){
int i, j, left, right;
if (n <= 1)
return;
for (int i = 1; i < n; ++i)
{
int value = a[i];
int j = i -1;
left = 0;
right = i;
while (left < right){ //二分查找插入位置最终left为插入位置
int mid = left + (right - left) / 2; //防止整形溢出
if (a[mid] < value)
left = mid + 1; //下一轮搜索区间是 [mid + 1, right]
else if (a[mid] == value){
left = mid + 1; //相同值插入到后面,保持算法稳定性
break;
}
else
right = mid;
}
for(; j >= left; --j)
a[j + 1] = a[j]; //统一移动元素,空出插入位置
a[left] = value;
}
}
# 二分排入排序python实现
"""
Author: gamilian
"""
def binary_insertion_sort(a):
""" 二分插入排序
args:
a: List[int]
"""
length = len(a)
if length <= 1:
return
for i in range(1, length):
value = a[i]
j = i - 1
left = 0
right = i
while left < right:
mid = (left + right) >> 1
if a[mid] < value:
left = mid + 1
elif a[mid] == value:
left = mid -1
else:
right =mid
while j >= left:
a[j + 1] = a[j]
j -= 1
a[left] = value
算法的稳定性:在二分插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以二分插入排序是稳定的排序算法。
空间复杂度:从实现过程可以很明显地看出,二分插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法。
时间复杂度:二分插入排序元素比较的次数与待排序数组的初始状态无关,取决于元素的个数n,约为nlogn次,但是元素的移动次数并未改变,依赖于待排序数组的初始状态。如果要排序的数据已经是有序的,我们并不需要搬移任何数据。所以这种情况下,最好是时间复杂度为 O(nlogn)。如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为 O(n^2)。我们在数组中插入一个数据的平均时间复杂度是 O(n)。所以,对于二分插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n^2)。
对于数据量不是很大的情况下,二分插入排序往往能表现的比直接插入排序更出色。
希尔排序(Shell Sort)
希尔排序的基本思想:也称递减增量排序算法,将整个序列按照相距某个“增量”进行拆分,然后逐个对子序列进行直接插入排序,使得得到的结果基本有序,最后对基本有序的序列进行一次直接插入排序,使得整个序列有序。
算法的主要步骤如下:
- 选择一个最初步长d1,将原始序列分为几个区域,针对每个区域分别进行插入排序
- 第1步完成后,将步长取半,再将第1步排序后的序列又分为几个区域,分别进行插入排序
- 依次类推,直到步长为1时,此时序列几乎已经按升序排好序了,再进行插入排序(插入排序针对几乎已经排好序的序列,效率高)
#include <stdio.h>
#include <stdlib.h>
// 希尔排序c实现,a表示数组,n表示数组大小
/**
* Author: gamilian
*/
void shell_sort(int a[], int n){
for (int d = n / 2; d >= 1; d /= 2) //拆分整个序列,元素间距为d(也就是增量)
for (int i = d ; i < n; i++){ //子序列直接插入排序
int value = a[i];
int j = i - d;
for (; j >= 0 ;j -= d){
if (a[j] > value)
a[j + d] = a[j];
else
break;
}
a[j + d] = value;
}
}
# 希尔排序python实现
"""
Author: gamilian
"""
def shell_sort(a):
""" 希尔排序
args:
a: List[int]
"""
length = len(a)
if length <= 1:
return
d = length // 2 # 初始步长
while d > 0:
# 最后一次步长为1(即普通的插入排序),然后整个希尔排序结束
# 普通的插入排序算法中步长是 1 ,把插入排序中的步长 1 替换为d
for i in range(d, length):
value = a[i]
j = i - d
while j >= 0:
if value < a[j]:
a[j + d] = a[j]
else:
break
j -= d
a[j + d] = value
# 得到新的步长
d = d // 2
算法的稳定性:虽然一次直接插入排序是稳定的,不会改变相同元素的相对顺序,但需要进行多次直接插入排序,在不同的直接插入排序过程中,相同值的元素被划分到不同子序列时,可能改变它们的相对次序,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
空间复杂度:从实现过程可以很明显地看出,直接插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法。
时间复杂度:最好情况时间复杂度为 O(n),最坏情况时间复杂度为 O(n^2)。 当n在特定的某个范围时,希尔排序的时间复杂度约为O(n^1.3)。