插入排序
基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
单趟
当插入第
i
(
i
≤
1
)
i(i\le 1)
i(i≤1)个元素时,前面的array[0],array[1],…,array[i-1]
已经排好序,此时用array[i]
的排序码与array[i-1],array[i-2],…
的排序码顺序进行比较,找到插入位置即将array[i]
插入,原来位置上的元素顺序后移
void InsertSort(int* a, int n)
{
//[0, end]有序,把end+1位置的插入到前面序列
//控制[0,end + 1]有序
int end = ;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
}
else
{
break;
}
}
a[end + 1] = tmp;
}
- 每一次把end的后一个值往前插入,0到end已经有序了,把end的后一个数字插入
- 先用中间变量tmp保存要插入的值,否则向后挪动的过程中会被覆盖掉
- 如果tmp比end位置的值小,就将end的值往后挪动,也就是将end赋给end+1
- 否则弹出
控制end变整体
5小于9,将end赋给end+1
end–
5小于6,end赋给end+1
5大于4,将tmp赋给end+1
void InsertSort(int* a, int n)
{
for (size_t i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
}
else
{
break;
}
--end;
}
a[end + 1] = tmp;
}
}
- 0到n-2有序,所以end落得最后一个位置i是n-2,把后一个位置n-1插入进去
- i从0到n-1的开区间,也就是end从0开始遍历,直到最后一个数字的前一个,也就是end+1指向最后一个
- 整个循环从将第二个数字往前出入,第三个数字往前插入,直到最后一个数字往前插入
- 循环中,如果end+1下标的数字小于end位置的数字,将end位置的数字向后挪动,然后end–,end往前遍历,继续将tmp与之前的数字比较
- 直到tmp不大于end,退出循环
特性总结
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 稳定性:稳定
时间复杂度
最坏
O
(
N
2
)
O(N^{2})
O(N2),逆序
最好
O
(
N
)
O(N)
O(N),顺序有序
空间复杂度
没有开多余的空间
O
(
1
)
O(1)
O(1)
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
- 预排序
分组排,间距为gap分为一组,分为gap组,每组进行排序,用插入排序的思想完成
gap越小,预排序完成之后越接近有序 - 直接插入排序
如果gap是1,就是直接插入排序
多组排序
void ShellSort (int* a, int n)
{
int gap = 3;
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
- tmp保存end的下一个
- end从第一个开始往后挪,如果end>=0,就继续
- i循环代表end走每组里面的gap间距的第i个数字,是每一组数据的插入排序,内层循环是组内进行插入排序,end每次后移gap个下标
- j循环代表end走间距为gap的第j组,外层循环是遍历每个组,gap为几就分为几组
- 一组排完,走另外一组
第一次排第一组9585,5589
第二次排第二组176,167
第三次排第三组243,234
此时整体更接近有序
多组并排
void ShellSort (int* a, int n)
{
int gap = 3;
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
end从0下标开始走到第一组的最后一个,如8
排完第一组的9和5,下来就排第二组的1和7,然后是第三组的2和4
跳着组排,直到遍历完所有的数据
多次预排序
预排序可以有多次
预排序的意义是让大的数更快得到后面去,小的数更快得到前面去,gap越大跳得越快,越不接近有序;gap越小,跳得越慢,越接近有序;gap=1,直接就有序
所以要进行多次预排序,gap逐渐减小
void ShellSort (int* a, int n)
{
int gap = n;
while (gap > 1)
{
//gap /= 2;
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
gap除以3最后需要加1,防止除到小于三以后,答案等于0,这样加1,保证最后gap是1
特性总结
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当 g a p = 1 gap=1 gap=1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算
- 稳定性:不稳定
时间复杂度
当gap很大的时候,如
n
/
3
n/3
n/3,总共有
n
3
\frac{n}{3}
3n个gap组数据,每组有3个数据,逆序的情况下,每组比较三次,第二个数据插入,比1次,第三个数据插入,比2次,总共三次,三个数据即每一组的消耗是3次;这里的间距为gap的这些数据,合计
n
3
⋅
3
=
n
\frac{n}{3}\cdot 3=n
3n⋅3=n
gap很小的时候,如1,这个时候已经很接近有序了,间距为1的这些数据合计是n
gap为中间值的时候,消耗是逐渐变大,再逐渐变小
如
n
/
9
n/9
n/9,假设整个数组逆序,其实之前的预排序肯定会使数据整体偏向有序,
n
/
9
n/9
n/9组数据,每组9个数据,单组合计1+2+3+…+8=36次;总共合计
n
9
⋅
36
=
4
n
\frac{n}9\cdot36=4n
9n⋅36=4n
外面的while循环,除3是
log
3
N
\log_{3}N
log3N,除2是
log
2
N
\log_{2}N
log2N
结论:
O
(
N
1.3
)
O(N^{1.3})
O(N1.3),略差于
O
(
n
⋅
log
2
N
)
O(n\cdot \log_{2}N)
O(n⋅log2N)
选择排序
基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序
- 在元素集合
array[i]--array[n-1]
中选择关键码最大(小)的数据元素 - 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的
array[i]--array[n-2]
(array[i+1]--array[n-1]
)集合中,重复上述步骤,直到集合剩余1个元素
通过遍历选出最小的交换,再选出次小的
优化:遍历一遍选出最小的和最大的,把最小的放左边,最大的放右边
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = 0, maxi = 0;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
if (maxi == begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
从两边开始,遍历找出最小的和最大的,不断往中间走
期间如果maxi等于begin下标的数字,会被mini换走,所以要修正一下
特性总结
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度: O ( N 2 ) O(N^{2}) O(N2)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 稳定性:不稳定
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//找出小的孩子
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
//继续往下调整
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//向下调整建堆
for (int i = (n - 1 - 1)/2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
特性总结
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
时间复杂度
用满二叉树来计算
因为最差的情况就是节点最多的时候
总共有h层
最后一层不需要调整,从第h-1层开始调整,每个节点向下调整一次
合计要调整
T
(
h
)
=
2
h
−
2
+
2
h
−
3
⋅
2
+
⋯
+
2
1
⋅
(
h
−
2
)
+
2
0
⋅
(
h
−
1
)
T(h)=2^{h-2}+2^{h-3}\cdot2+\dots+2^{1}\cdot(h-2)+2^{0}\cdot(h-1)
T(h)=2h−2+2h−3⋅2+⋯+21⋅(h−2)+20⋅(h−1)
每层的数据个数乘以向下移动的层数
等比每项乘以等差每项
Expected node of symbol group type, but got node of type cr
T
(
h
)
=
2
h
−
1
−
h
T(h)=2^{h}-1-h
T(h)=2h−1−h
T
(
h
)
T(h)
T(h)是向下调整建堆,最坏情况下的合计调整次数
换算关于N的式子,得到时间复杂度
T
(
N
)
=
N
−
log
2
(
N
+
1
)
T(N)=N-\log_{2}(N+1)
T(N)=N−log2(N+1)
约等于N