对于这5中排序算法的补充:
快速排序链接: link.
归并排序链接: link.
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一 个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想
1. 直接插入排序
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
这个图片就很好的解释了直接插入排序的过程。首先把第一个元素看作是有序的数组,那第二个元素往第一个有序数组中插入,当完成后再把第一和第二个元素看作一个有序数组拿第三个元素往前两个里面插入,依次类推,只到最后一个元素往前面已经变成有序的数组里面插入后,排序完成。
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2) (解释:因为每遍历一遍数组的元素的时间复杂度就是O(N),然后去掉变为O(N-1)…O(1)相加是一个等差数列,结果为O(N))
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
//插入排序
void InsertSort(int* a, int n)
{
assert(a);
//把第一个数当成有序的,然后拿后面的数据和第一个数据比较插入
//一种有n个数,那么最后一个数的下标是n-1,但是我需要最后一步拿这个小标为n-1的数去向前面已经都有序的数组进行插入,那么end最多就只能落在n-2的位置,所以就可以理解为什么这里的i要小于n-1了
for (int i = 0; i < n - 1; ++i)
{
//可以把任何一种排序都分解开来为单步分析在整合整体的过程来思考问题
int end = i;
int tmp = a[end + 1];
while (end >= 0) //当这个tmp和第一个值相比较还是小的话,再次移动end就会发现他已经到-1的位置了,说明他依旧和所有的元素都比较过了
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
//这里跳出来会有两种可能,第一个就是tmp此时大于end下角标所在的元素,break出来
//第二种就是while循环结束end已经到了-1的位置,(也就是比第一个值还要小)
a[end + 1] = tmp;
}
}
2. 希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N1.3—N2)
- 稳定性:不稳定
首先声明:希尔排序的出现就是在直接插入排序的基础上对其进行优化和和提升。
过程分为2步:
①预排序(为了让数组接近有序):把间距为gap的值分为一组,进行插入排序
②进行直接插入排序
预排序过程详解图:
会发现相比于原数组更接近有序一点点。(大的数被放在了后面,小的数被放在了前面,只要你的gap选的够大,那么你的大数往后移动的速度越快,但是相对的也就越不有序,选的gap越小那么你大的数往后移动的速度减慢,小的数往前移动的速度减慢,但是相对于更加的有序。当gap为1的时候就相当于直接插入排序了。)
//希尔排序
void ShellSort(int* a, int n)
{
//gap越大,数跳的快,但是就变的很不有序了
int gap = n;
//这里就能完成间距为gap组的所有排序,这段代码非常的巧,非常的NB
//如果是我们的思想总是认为先去排红色下来排蓝色最后排紫色的过程,也就是写三个for循环去控制每一个颜色的一组
//但是过程并不是这样的
//他把多组同时进行一次排序就排完了,也就是他并没有一次性先去排红色,在调回来排蓝色,而是排完红色第一个直接排蓝色组的第一个
//这里的n-gap可以理解为当你的gap分组第一组中,最后一个值往前一个值插入就结束了
//gap >1 就是预排序
while (gap > 1)
{
//这里如何来选这个gap值呢?
gap = gap / 3 + 1; //保证了最后一次一定时1
//当最后一次gap==1的时候就是直接插入排序
//这里为3组排
for (int i = 0; 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;
}
}
//不管我是比你大或者相等我就放在你end的后面
//还是放在你的第一个位置处
a[end + gap] = tmp;
}
}
}
void TestShellSort()
{
int a[] = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1};
PrintArray(a, sizeof(a) / sizeof(int));
ShellSort(a, sizeof(a) / sizeof(int));
}
int main()
{
TestShellSort();
return 0;
}
看这个过程你就会发现随着gap的不断减小,他的数组越来越接近有序的过程。
然后写一个性能测试,发现在有100000个数进行排序的时候,两个之间的差值就会很大了。但是你会发现当你的数组本来就很接近有序的时候你的直接插入排序就会比希尔排序快很多了,因为它省略了很多的预排序的过程,在直接插入排序的时候不断地break,可能只需要插入一个值就变的有序了,而你的预排序的过程则会消耗大量的时间。
3. 选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。
直接选择排序:
在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素 。(简单点说:我直接把数组的元素遍历一遍,选出最小的数,然后放在第一个位置,然后在把剩下的数遍历一遍选出次小的数,直到最后还剩下一个直接就在最后一个位置)
但是你会发现还是可以改进的,你遍历一遍不但可以选择出来最小的数,而且还可以选出最大的数,所以我把最小的数放在数组的第一个位置处,再把最大的放在最后一个位置处,然后我把数组长度两边同时-1,在遍历重复上述过程,这样就会比直接选择排序的速度快一半。
//直接选择排序
//遍历一遍同时选出最小的数和最大的数,然后把最小的数放在数组的第一个位置,再把最大的数放在数组的最后一个位置
//数组两边同时减减缩短数组的长度,这样就会比直接选择排序的速度快一半
void SelectSort(int* a, int n)
{
assert(a);
//这里一定是找到那个最小数的下标然后和你第一个位置的元素互换位置,然后我再找到最大的数下标和你最后一个位置的数互换
//如果说你找到最小的数直接放在了第一个位置,那么你第一个位置原来的元素就被你选的这个最小的数覆盖了,
//但是你原来放最小数的地方还是最小数的那个值
int begin = 0, end = n - 1;
while (begin < end)
{
//在[begin,end]之间找出最小数和最大数的下标
int mini, maxi;
mini = maxi = begin;
//这一组遍历下来之后就可以找出来这一组中最大的那个数的下标和最小的那个数的小标
for (int i = begin +1 ; i <= end; ++i)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
//但是这里还有一种情况,当你的第一个位置放的就是这个数组的最大值,当你的最后一个位置放的就是你数组的最小值的时候
//第一个位置的下标为maxi,而你最后一个位置的下标为mini,然后交换,你的下一步再一次交换,又回来了,相当于没有交换
//begin和maxi的位置重合了,这里进行一次交换你的最大值也给交换走了
Swap(&a[begin], &a[mini]);
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
4. 堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
// 堆排序
void AdjustDwon(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
//首先你应该找到两个孩子中较大的哪一个
if (child+1 < n && a[child] < a[child + 1])
{
child++;
}
//然后和父亲比较,如果孩子大,就要和父亲进行交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
}
else
{
break;
}
//需要这个过程迭代下去
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int* a, int n)
{
//对于堆排序来说,首先给你的数组,你需要先保证他的左右子树都是满足堆的特点的,大堆的话左右子树都得满足大堆的特点
//需要从倒数第一个不为叶子结点处开始进行调整,一直调整上来,这里就保证了左右子树都是大堆的特点
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDwon(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
--end;
}
}
5. 冒泡排序
是一种交换排序,如果前一个大于第二个数交换,直到把最大的数换到最后一个位置停止.
//冒泡排序
//是一种交换排序,如果前一个大于第二个数交换,直到把最大的数换到最后一个位置停止
void BubbleSort(int* a, int n)
{
int end = n;
while (end > 0) //当你的数组还剩下一个的时候就不需要再继续冒泡了
{
int exchange = 0; //这里引入exchange这个变量非常的厉害,如果你的数组本来就是有序的,此时你在对这个数组不断的进行冒泡的过程就会浪费
//大量的时间,但是一旦你设置这个,当你的数组发生冒泡就给他设置为1,为0的时候说明没有发生冒泡的过程,也就是本来就是有序的
//这个是一组冒泡的代码
for (int i = 1; i < end; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
//要想清楚数组是从后面再往前面缩短,一旦这里break说明这一段有序了,也就整体冒泡结束了
//说明没有发生交换,也就是这一段已经是有序了,不需要在冒泡了
break;
}
end--;
}
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
6. LeetCode第912题—排序数组
链接: link.
这个题你会发现当你使用直接插入或者是选择排序还有冒泡排序的时候,他给你会报错,虽然你跑过了所有的测试用例,但是他会说:超出时间限制也就是说这个题看似简单,实际上他需要你使用希尔排序或者是堆排
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* sortArray(int* a, int n, int* returnSize){
*returnSize = n;
int gap = n;
//gap >1 就是预排序
while (gap > 1)
{
//这里如何来选这个gap值呢?
gap = gap / 3 + 1; //保证了最后一次一定时1,且3为最佳的一个选项,综合下来当以3为gap作为间距来算是最好最优的
//当最后一次gap==1的时候就是直接插入排序
//这里为3组排
for (int i = 0; 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;
}
}
//不管我是比你大或者相等我就放在你end的后面
//还是放在你的第一个位置处
a[end + gap] = tmp;
}
}
return a;
}
选择的是直接插入排序,就会出现超出时间限制的报错。