c++实现的插入排序及其优化
普通插入排序
原理
每次取一个,按次在前面已排好序的区间中 依次比较找到第一个大于当前待排数据的位置,然后移动原有数据,插入新的数据。
基本性质
- 时间复杂度(主要是看移动次数)
——最好情况:O(n)
——最坏情况和平均:O(n^2) - 空间复杂度
——O(1):原地排序 - 稳定性
——可做到稳定 - 相比冒泡排序的优点
——移动只需一次赋值操作
代码
template<typename T>
void insert_sort0(T *a, int len) {
// 首先检查数据的合法性(TODO 不完善).
if (a == NULL || len <= 1) {
return;
}
int cur = 0; // 当前待排序的数据
int j = 0; // 内层循环(移动数据)计数变量
// 对未排序区间进行遍历
for (int i = 1; i < len; ++i) {
// 记录当前数据值
cur = a[i];
// 在已排序区间中查找插入位置
for (j = i-1; j >= 0; --j) {
if (a[j] > cur) {
// 未找到插入位置,则往后移动已排序数据
a[j+1] = a[j];
}
else {
// 比当前值小,则找到插入位置,结束循环
break;
}
}
a[j+1] = cur;
}
}
优化1:二分查找找插入位置
原理
与普通的相同,只不过在找插入位置时,由于前面的都是已排好序的,所以可以用二分查找来加速。(此处二分查找查找第一个大于待插入数据的位置)
基本性质
- 时间复杂度(主要是看移动次数)
——最好情况:O(n)
——最坏情况和平均:O(n^2) - 空间复杂度
——O(1):原地排序 - 稳定性
——可做到稳定
代码
/**
* @brief 辅助插入排序优化1进行 二分查找[从小到大] (临时)
* @param a:查找数据; len:查找区间(a[0]-a[len]),tar:待查找数据
* @return int:插入位置下标
* @note
* -辅助插入排序优化1,查找待插入位置
* -不完善,不过无所谓,以后写道查找的时候再优化
* -查找的是第一个大于给定值的元素的下标(维持稳定性)
* -待查数据区间已经排好序
*/
template<typename T>
int binary_search_simple(T *a, int len, T tar) {
// 首先检查数据的合法性(TODO 不完善).
if (a == NULL || len < 1) {
return -1;
}
// 特殊条件
if (a[0] > tar) {
return 0;
}
else if (a[len-1] <= tar) {
return len;
}
int low = 0; // 待查最左位置
int high = len-1; // 待查最右位置
int mid = 0; // 待查中间位置
// 查找第一个大于等于给定值的元素
while (low <= high) {
// 待查中间位置【不使用(a+b)/2是防止溢出;位运算比除法效率高;位运算比加法优先级低】
mid = low+((high-low)>>1);
if (a[mid] > tar) {
// 如果mid大于目标,就要看是否是需要的
if (a[mid-1] <= tar) {
return mid;
}
high = mid - 1;
}
else {
low = mid + 1;
}
}
return -1;
}
/**
* @brief 插入排序优化1[从小到大](TODO 不完善)
* @param a:待排序数组; len:待排序元素
* @return void 原地排序,直接修改
* @note
* -时间复杂度:最好 - O(n);最坏 - O(n^2);平均 - O(n^2)
* -空间复杂度:O(1);是原地排序算法
* -稳定性:稳定
* -使用二分查找找插入位置
*/
template<typename T>
void insert_sort1(T *a, int len) {
// 首先检查数据的合法性(TODO 不完善).
if (a == NULL || len <= 1) {
return;
}
T cur = 0; // 当前待排序的数据
int index = -1; // 二分查找到的插入位置
int j = 0; // 内层循环(移动数据)计数变量
// 对未排序区间进行遍历
for (int i = 1; i < len; ++i) {
// 记录当前数据值
cur = a[i];
// 在已排序区间中查找插入位置
index = binary_search_simple(a, i, cur);
// 移动
for (j = i; j > index; --j) {
a[j] = a[j-1];
}
// 插入数据
a[j] = cur;
}
}
优化2:希尔排序(分治)
原理
- 待插入的元素如果需要插入到有序区间的首部时,需要移动大量的数据来腾出空位置供新元素插入。为了解决这个问题,希尔排序算法就产生了。
希尔排序是在直接插入排序的基础上增加了分组的思想:先取较大的间隔来分组,对每组进行插入排序,然后缩小间隔重新分组插入排序。 由于前期工作使得数据变得整体有序,所以总的来说,插入排序的效率是提高的。
间隔一般取: 间隔=向下取整【间隔(初始为整个数据长度)/ 3】+1 ;
性质
- 时间复杂度(主要是看移动次数)
——最好情况:O(n)
——最坏情况和平均:O(logn) - 空间复杂度
——O(1):原地排序 - 稳定性
——不稳定!!!
代码
template<typename T>
void insert_sort2(T *a, int len) {
// 首先检查数据的合法性(TODO 不完善).
if (a == NULL || len <= 1) {
return;
}
T cur = 0; // 当前待排序的数据
int index = -1; // 二分查找到的插入位置
int zu = 0; // 内层循环(分组)计数变量
int i = 0; // 内层循环(每组未排序空间)计数变量
int j = 0; // 内层循环(移动数据)计数变量
// 对gap进行遍历
for (int gap = len/3+1; ; gap = gap/3+1) {
// 分组
for (zu = 0; zu < gap; ++zu)
// 对每组的未排序区间进行遍历
for (i = gap+zu; i < len; i+=gap) {
// 记录当前数据值
cur = a[i];
// 在已排序区间中查找插入位置
for (j = i-gap; j >= 0+zu; j-=gap) {
if (a[j] > cur) {
// 未找到插入位置,则往后移动已排序数据
a[j+gap] = a[j];
}
else {
// 比当前值小,则找到插入位置,结束循环
break;
}
}
a[j+gap] = cur;
}
// gap==1之后就可以结束了
if (gap == 1) {
break;
}
}
}
测试
以20000个整数进行测试,记录时间,同时assert是否与algorithm中sort结果相同。测试代码略,可见完整代码中。
测试结果时间(同时,assert未报错)如下图所示:
可见,优化效率提升还是非常明显的,而且插入排序本身也比冒泡排序要快(详情见【从头学数据结构和算法】冒泡排序及其优化(c++实现))。
看上希尔排序都快赶上sort函数的效率了,但实际上数据量再增大后还是能够看到有差距的。
完整代码
排序及优化及测试 完整代码在:
https://github.com/zhangdanzhu/basic_data-structure_algorithm/blob/master/sort/cpp/insertion_sort.cpp