目录
前言
八大排序总结(时间,空间复杂度,稳定性)
常见排序算法的实现
插入排序
希尔排序
选择排序
第一种选择排序?
第二种选择排序
堆排序
冒泡排序
第一种方法:
?第二种方法:
快速排序:
hoare法(左右指针法)
挖坑法:
左右指针法:
快排的时间复杂度O(NlogN)
三数取中法
小区间优化:
经过优化的快排如下:
快排的非递归写法
归并排序
归并的思路:
归并排序的非递归写法:
内排序与外排序
计数排序?
各个排序的稳定性
稳定性的应用:
前言
关于排序你所要了解的概念
排序
:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性
:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j]
,且
r[i]
在
r[j]
之前,而在排序后的序列中,
r[i]
仍在
r[j]
之前,则称这种排 序算法是稳定的;否则称为不稳定的。
内部排序
:数据元素全部放在内存中的排序。
外部排序
:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
时间复杂度:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。详情见博主的文章:
数据结构之【什么是数据结构与时间复杂度详解】_cls的博客-CSDN博客_数据结构怎么看时间复杂度
空间复杂度:空间复杂度(Space Complexity)是指对一个算法在运行过程中临时占用存储空间大小的量度,用o()来表示。空间复杂度的内容 记做S(n)=O(f(n))。
PS:本篇博客排序全都以升序进行
常见排序算法的实现
插入排序
基本思想:直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一 个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想。比如我们斗地主的时候,我们假设摸到的第一张牌是4,第二张牌摸到5我们就会将5放到4的后面,如果下张摸到3,我们则将3放到4的前面.....
最后我们手上的牌就是有序的。
??
思路图如下:
?代码实现:
我们先将问题拆解
1.0先写单趟排序
2.0在写多趟
3.0画图
1.0单趟插入思路图:
插入0
单趟排序的代码:
void InsertSort(int* a, int n)
{
int tmp;
int end = n - 1;
while (end > 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
}
else
{
a[end + 1] = tmp;
break;
}
a[end + 1] = tmp; //例如上图插入0,或者tmp>a[end];
}
}
如果存在tmp>a[end],或者插入的元素是0,都只需将tmp赋给a[end+1]即可
所以可以进行下小优化
void InsertSort(int* a, int n)
{
int tmp;
int end = n - 1;
while (end > 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
}
else
{
break;
}
a[end + 1] = tmp; //例如上图插入0,或者tmp>a[end];
}
}
? 2.0 再由单趟推广到多趟,也就是需要确定每次单趟排序时end与tmp的位置
推广到多趟的代码
void InsertSort(int* a, int n)
{
for (int 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];
end--;
}
else
{
break;
}
a[end + 1] = tmp;
//例如上图插入0;
}
}
}
测试代码:?
void Test1()
{
int a[] = { 4456,87,963,456,124,568,972,962,66202 };
int n = sizeof(a) / sizeof(a[0]);
PrintfArray(a, n);
InsertSort(a, n);
PrintfArray(a, n);
}
效果图如下:?
?
直接插入排序的特性总结:
1.
元素集合越接近有序,直接插入排序算法的时间效率越高
2.
时间复杂度:
O(N^2)
最好是O(N) 接近顺序有序
最坏是O(N^2) 接近逆序
3.
空间复杂度:
O(1)
,它是一种稳定的排序算法
4.
稳定性:稳定
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。
希尔排序可以说是直接插入排序的进阶版,它的关键在于以下两步
1.0?预排序? ?接近有序? 先分组对分组的数据进行插入排序
2.0 直接插入排序
预排序如下:
间隔为gap分为一组,对一组进行插入排序,我们先假设gap是3,分成gap组
?代码实现:
将问题拆解
1.0? 单趟,不知道gap是几,对每组进行排
2.0 多趟
以蓝色这组为例
?单趟排的代码
void ShellSort(int* a, int n)
{
int gap=3;
int end;
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与gap的位置如图
这里给出蓝色组的位置,红色和绿色组同理
?排蓝色这组的代码,其实和直接插入完全一样,仅仅是两个数之间的间距由1变成了gap
int gap=3;
for (int i = 0; i < n - gap; i + gap)
{
int end;
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,1,2开始我们用下面这种方式解决
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;
}
}
?仅仅是将i+=gap改为i++这样就实现了同时可以排序红绿蓝三组(gap为3)
?a [ n-gap ]为6,i最终到达8那个位置就排好了
最后一个问题,gap的取值问题,上述代码我们是取gap=3为例 ,但在实际排序中,给gap一个定值肯定是不行的,数组可能很大也可能很小
经过分析我们可以知道
gap越大,大的和小的数可以更快的挪到对应方向去 gap越大,越不接近有序 gap越小,大的和小的数可以更慢的挪到对应方向去 gap越小,越接近有序 如果gap==1 ,就是直接插入排序
当gap>1的时候就是预排序,gap==1的时候就是直接排序
gap=(gap/3+1)的目的是保证最后一次一定是1
这里gap除5也可以?
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
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;
}
}
}
}
?完整代码
void ShellSort(int* a, int n)
{
//gap>1 预排序
//gap==1 直接排序
int gap=n;
while (gap > 1)
{
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;
}
printf("gap为%d预排后->", gap);
PrintfArray(a, n);
}
}
}
测试代码
void Test2()
{
int a[] = { 4456,87,963,456,124,568,972,962,66202 };
int n = sizeof(a) / sizeof(a[0]);
printf("排序前:");
PrintfArray(a, n);
ShellSort(a, n);
printf("排序后:");
PrintfArray(a, n);
}
效果演示:?
?选择排序
基本思想: 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。
第一种选择排序?
直接选择排序
:
在元素集合
array[i]--array[n-1]
中选择关键码最大
(
小
)
的数据元素
若它不是这组元素中的最后一个
(
第一个
)
元素,则将它与这组元素中的最后一个(第一个)元素交换 ,在剩余的array[i]--array[n-2]
(
array[i+1]--array[n-1]
)集合中,重复上述步骤,直到集合剩余
1
个元素
思路图:
代码实现:
分解子问题
1.0 单趟
2.0多趟
单趟思路,遍历一遍数组选出最小的数的下标,将这个最小的数据放到数组的第一位
假设最小的元素的下标是0
int min=0;
for(int i=0;i<n-1;i++)
{
if(a[i]<a[min])
min=i;
}
Swap(&a[0],&a[min]);
多趟思路,初始元素的位置在改变,遍历的区间数目也在改变
代码实现:
void SelectSort(int* a, int n)
{
for (int j = 0; j < n-1 ; j++)
{
int min = j;
for ( int i = j; i < n; i++)
{
if(a[i] < a[min])
min = i;
}
Swap(&a[j], &a[min]);
}
}
第二种选择排序
思路:遍历区间? [left,right]? 同时选出最小的和最大的,把最小的放在左边,最大的放在右边,比刚才的快一倍,是之前的优化
int left = 0;
int right = n - 1;
while (left < right)
{
int min = left;
int max = left;
for (int i = left; i <=right ; i++)
{
if (a[i] < a[min])
min = i;
if (a[i] > a[max])
max = i;
}
Swap(&a[left], &a[min]);
Swap(&a[right], &a[max]);
left++;
right--;
}
这段代码看似没问题,逻辑上也符合,但它却存在着一个bug
int a[] = { 7,4,5,9,8,2,1 }; 对这个数组是没问题的
int a[] = { 9,7,4,5,9,8,2,1 }; 但对这个数组就产生了问题
??
?问题分析及原因
?改进方法,如果max的下标与left的下标相同时,只需要在第一次交换之后,将最小值min的下标赋给最大值max的下标后即可。
void SelectSort(int *a,int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int min = left;
int max = left;
for (int i = left; i <=right ; i++)
{
if (a[i] < a[min])
min = i;
if (a[i] > a[max])
max = i;
}
Swap(&a[left], &a[min]);
if (left == max)
{
max = min;
}
Swap(&a[right], &a[max]);
left++;
right--;
}
}
?完整代码:
void SelectSort(int *a,int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int min = left;
int max = left;
for (int i = left; i <=right ; i++)
{
if (a[i] < a[min])
min = i;
if (a[i] > a[max])
max = i;
}
Swap(&a[left], &a[min]);
if (left == max)
{
max = min;
}
Swap(&a[right], &a[max]);
left++;
right--;
}
}
堆排序
博主之前有过详解:
传送门:(14条消息) 数据结构之【堆详解】_cls的博客-CSDN博客
(14条消息) 数据结构之【堆排序】_cls的博客-CSDN博客
堆排序肯定要优于直接选择排序【O(N^2)】才会有价值。
那么排升序是用大堆还是小堆?
答案是:建大堆。
升序,为什么不能建小堆?
问题在于选出最小的数字后如何选出次小的数字
? ? 建堆选出最小的数,花了O(N),紧接着选择次小的数,剩下的N-1个数继续建堆,又是O(N), 最终成为N^2{因为剩下数字的父子关系完全乱了,只能重新建堆,效率太低}
? ? 这样建堆排序的时间复杂度就和直接排序一样没有什么实际的意义。不是不可以,而是没有什么价值。
而通过建大堆,
首先选出最大的数字,让它与最后一个数字交换,紧接着选择次大的数,不把最后一个数看做是堆里面的,向下调整就能选出次大的
思路如图:
?同理排降序 就应该建小堆,原因及思路和上面完全一致。
?排升序代码如下:
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(NlogN) [ N+NlogN,N相对于logN太小了可以忽略]
?简单分析下,堆排序和冒泡排序的差别有多大数字小了看不出来啥区别
?假设排100W个数字。
?N^2 需要100亿次
?NlogN 需要 100W*20 次 堆排的优势就显示出来了
完整代码:
#include <stdio.h>
?
void Swap(int* p1, int* p2)
{
?? ?int tmp = *p1;
?? ?*p1 = *p2;
?? ?*p2 = tmp;
}
?
void AdjustDown(int* 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[parent], &a[child]);
?? ??? ??? ?parent = child;
?? ??? ??? ?child = parent * 2 + 1;
?? ??? ?}
?
?? ??? ?else ?//b情况
?? ??? ?{
?? ??? ??? ?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--;
?? ?}
}
?
?
?
int main()
{
?? ?//前提左右字树是小堆
?? ?int a[] = { 15,18,28,34,65,19,49,25,37,27 };
?? ?int n = sizeof(a) / sizeof(a[0]);
?? ?HeapSort(a, n);
?? ?return 0;
}
冒泡排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
思路图:
代码实现,问题拆解
1.0 单趟排? 思想,如果前一个数大于后一个数就交换,最终实现将最大的数据放在数组末尾
for (int i = 0; i < n - 1; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
}
}
?2.0 多趟排 基本思想:已经在末尾数据就不用再去管,控制单趟排序的长度即可,也就是图中绿色框之前的部分
第一种方法:
for (int j = 0; j < n; j++)
{
for (int i = 0; i < n - 1 - j; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
}
}
}
?第二种方法:
for (int end = n;end > 0;--end)
{
for (int i = 0;i < end - 1;i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
}
}
}
?优化
?对一组有序的数而言,它是不进行交换的,因此我们设置一个exchange进行检查,如果没发生交换就证明它是有序的的,直接跳出结束程序。
for (int end = n;end > 0;--end)
{
int exchange = 0;
for (int i = 0;i < end - 1;i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
冒泡排序的特性总结:
1.
冒泡排序是一种非常容易理解的排序
2.
时间复杂度:
O(N^2)
3.
空间复杂度:
O(1)
4.
稳定性:稳定
冒泡排序和插入排序比较:
顺序有序:他俩一样好
接近有序:插入排序好??
快速排序:
? ? ?快速排序是
Hoare
于
1962
年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快排的根本思想还是分治
1.0? 排单趟区间
2.0? 像二叉树一样递归排序它的左右子树
思路图:
将区间按照基准值划分为左右两半部分的常见方式有:
1. hoare
版本
2.
挖坑法
3.
前后指针版本
PS:以下代码博主写了有返回值和无返回值的,两者没任何区别,有返回值只是为了便于复用。
hoare法(左右指针法)
快排首先进行单趟排序
? ?选出一个数字Key,一般是最左边的,或者是最右边的,Key放到它的正确的位置上去,左边的要比Key小,右边的要比Key大
? 选择左边的值作为Key,right先走,right找比key小的值,left找比key大的值,找到之后进行交换,直到他俩相遇,最后再把key与左右相遇地方的值交换
最终key所在的位置就是,他在最后排好序的的一堆数里面的位置,如下图,6最终的位置就是他在最后有序数字中的位置,位于第六个
思路图:
为什么左边做key,要让右边先走?
相遇存在两种情况:左遇到右与右遇到左
如果左边做key,左边先走,最终结果如图
?达不到我们的预期(key的左边都比它小,右边都比它大)
而让右边先走,无论是左遇右,还是右遇左,都可以保证最后结束的位置的值比key的值要小
?左右指针法的代码
void PartSort1(int* a, int n)
{
int left = 0, right = n - 1;
int keyi = left;
while (left < right)
{
while (a[right] >= a[keyi])
{
right--;
}
while (a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[right]);
}
但是以上代码存在一个问题:对这样的一种情况
?这部分代码就会一直进行,造成越界的情况;
?同时,他们之间的等于号也是必不可少的
如果去掉等于号,对这样的情况就会造成死循环
?一直交换左右两边的5.
最终左右指针法的完整代码如下:(无返回值的)
void PartSort1(int* a, int n)
{
int left = 0, right = n - 1;
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[right]);
}
有返回值的
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
单个区间的排序已经完成,之后再进行左右区间的递归,也就是整个快排的过程
?将数组分为? [begin,meeti-1]? meeti? [meeti+1,end] 三部分,递归这两个区间,meet就是每次左右相遇的位置,当这个区间一个值也没有或者只有一个值就结束,
完整的快排代码如下:(如果仅是写一个快排这样写即可)
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int left = begin;
int right = end;
int keyi = left;
while (left < right)
{
//找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
int meeti = left; //或者 int meeti = right; 都是可以的
Swap(&a[keyi], &a[meeti]);
// [begin,meeti-1] meeti [meeti+1,end]
QuickSort(a, begin, meeti - 1);
QuickSort(a, meeti + 1, end);
}
挖坑法:
时间复杂度O(N)
基本思路
先找一个最左边或者最右边的值存起来,
1.0? 我们在这里选择最左边的值作为key将它保存起来,这样他就形成了一个坑,
2.0? 从右边开始找比key小的值,找到之后把它放到刚开始的这个坑里去,这个地方就形成一个新的坑
3.0 再从左边开始找比key大的值,找到之后再把它放到新的坑里去,循环2,3步直到左与右相遇
4.0 最后左右相遇停止时,把key放到最终结束的坑中去。
PS:如果开始找的是最右边的值,就要从左边开始,也就是2 ,3步换下顺序
思路图如下:
代码如下:
int PartSort2(int* a, int left, int right)
{
int key = a[left];
while (left < right)
{
//找小
while (left < right && a[right] >= key)
{
right--;
}
//找到后放到左边的坑中去,右边形成新的坑
a[left] = a[right];
//找大
while (left < right && a[left] <= key)
{
left++;
}
//找到后放到右边的坑中去,左边形成新的坑
a[right] = a[left];
}
//将刚开始保存的数字放到左右相遇的地方的坑中去
a[left] = key;
return left;
}
左右指针法:
思路:这里我们选取最左边的当做key,设置cur与prev,开始一前一后,cur去找比key小的值,找到以后++prev,同时交换prev与cur,直到数组尾
思路图:
代码如下:
int PartSort3(int *a,int left,int right)
{
int key = left;
int cur = key + 1;
int prev = key;
while (cur <= right)
{
if (a[cur] < a[key] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[key],&a[prev]);
return prev;
}
在学习上述三种方法后快排进而可以写成这样:单趟排想用那种方法,就用哪种方法
void QuickSort(int* a, int begin,int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort3(a,begin,end); //想用其他单趟排的方法改下这即可
// [begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
快排的时间复杂度O(NlogN)
理想的快排情况,每次单趟排都是二分,选的key每次都是这组数据的中位数 ,时间复杂度就是O(NlogN)
最坏的情况:即给出的数据是有序的,时间复杂度就是O(N^2)
?针对最坏的情况我们进行以下优化:
1.0 三数取中
2.0 小区间优化
?分析一下对快排影响最大的是选的key,如果key越接近中位数,就越接近二分,效率就越高
三数取中法
三个数分别是一组数据中最左边的数,最右边的数,和中间的那个数,取出这三个数中最小的那个数,这样就可以避免有序的情况。
代码如下:left right都指的是下标
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;
//left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return left;
}
else
{
return right;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
?使用方法:我们在Pastsort中任然是取最左面的一个,只需要在每个Pastsort中加入以下这段代码即可。
int midIndex = GetMidIndex(a, left, right);
Swap(&a[left], &a[midIndex]);
eg:Pastsort1。
int PartSort1(int* a, int left, int right)
{
int midIndex = GetMidIndex(a, left, right);
Swap(&a[left], &a[midIndex]);
int keyi = left;
while (left < right)
{
//找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
小区间优化:
? ? ? 当递归到最后的时候,越递归到最后,递归的次数就越多,因为快排的递归相当于一颗完全二叉树,最后几层执行的次数就是2^n附近,所以为了减少递归的次数,我们可以找其他排序帮助我们。
1.0 如果子区间数据很多,继续选择key单趟,分割子区间分治递归 2.0 如果子区间数据很小,再去递归不太划算用插排代替
PS:小区间优化并不太明显,原因是它虽然减少递归树的最后几层,优化到有较少的递归调用层数,但是插入排序也是有消耗的。
插入排序的传值:因为插排形参传的是数组与个数,所以我们将快排的区间转化成数组与个数。
经过优化的快排如下:
void QuickSort(int* a, int begin,int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort3(a,begin,end);
//1.0 如果子区间数据很多,继续选择key单趟,分割子区间分治递归
//2.0 如果子区间数据很小,再去递归不太划算用插排代替
if (end - begin > 10)
{
// [begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1);
}
}
快排的非递归写法
递归,现在的编译器优化很好,性能不是大的问题 递归最大的问题就是递归的深度太深,程序本身没有问题,但是栈的空间不够,导致栈溢出 只能改成非递归,改成非递归存在两种方式 1.0 直接改成循环 例如斐波那契数 2.0 树遍历非递归和快排非递归,只能借助栈来实现模拟递归的过程
首先我们要明白栈是后进先出的,快排的递归,递归的是一段区间
我们用十个数字举例,假设如图是递归的全过程
? ? ?递归的顺序可以类比为二叉树的前序遍历也就是根左右,我们用栈模拟这个递归的过程,我们先让09入栈对[0 9]区间找key,再入69, 04 因为后进先出,再入34, 01,最后入89, 6,这样就完美模拟了整个递归的过程,如果我们想要递归的顺序是根右左,就先让09 入栈,再入04,69,再入01 34,再入6,89即可,反一下。?
代码实现如下(借助之前实现的栈)
Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//typedef int BOOL;
//#define TURE 1;
//#define FALSE 0;
// 静态的栈
//typedef int STDataType;
//typedef struct Stack
//{
// STDataType array[1000]; //静态的栈就是一个数组
// int top; //栈顶
//
//}Stack;
// 动态的栈
typedef int STDataType;
struct Stack
{
STDataType* a;
int top; //栈顶
int capacity; //容量,方便增容
};
typedef struct Stack Stack;
//同样传指针,形参改变不影响实参,实参传过去是它的拷贝
void StackInit(Stack* pst); //初始化
void StackDestory(Stack* pst); //销毁
//性质决定在栈顶出入数据
void StackPush(Stack* pst, STDataType x); //入栈
void StackPop(Stack* pst); //出栈
//取栈顶的数据
STDataType StackTop(Stack* pst);
// 空返回1,非空返回0
//int StackEmpty(Stack* pst);
bool StackEmpty(Stack* pst); //判断栈是否为空
int StackSize(Stack* pst); //统计栈中的数据
Stack.c
#include"Stack.h"
void StackInit(Stack* pst)
{
assert(pst);
/*pst->a = NULL;
pst->capacity = 0;
pst->top = 0;*/
pst->a = (STDataType*)malloc(sizeof(STDataType)*4);
pst->capacity = 4;
pst->top = 0;
}
void StackDestory(Stack* pst)
{
assert(pst);
free(pst->a);
pst->capacity = pst->top = 0;
}
void StackPush(Stack* pst, STDataType x)
{
if (pst->top == pst->capacity)
{
STDataType* tmp = (STDataType*)realloc(pst->a,sizeof(STDataType)*(pst->capacity * 2));
if (tmp == NULL)
{
printf("realloc失败\n");
exit(-1); //结束程序
}
pst->a = tmp;
pst->capacity *= 2;
}
pst->a[pst->top] = x;
pst->top++;
}
void StackPop(Stack* pst)
{
assert(pst);
assert(!StackEmpty(pst));
pst->top--;
}
STDataType StackTop(Stack* pst)
{
assert(pst);
assert(!StackEmpty(pst));
return pst->a[pst->top - 1];
}
bool StackEmpty(Stack* pst)
{
assert(pst);
return pst->top == 0;
}
int StackSize(Stack* pst)
{
assert(pst);
return pst->top ;
}
//模拟根左右的顺序递归
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);//先入右,再入左
while (!StackEmpty(&st))
{
int left, right;
left = StackTop(&st);//所以先出左,再出右
StackPop(&st);
right = StackTop(&st);
StackPop(&st);
int keyi = PartSort1(a, left, right);
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
StackDestory(&st);
}
//模拟根右左顺序的递归
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int left, right;
right = StackTop(&st);
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
int keyi = PartSort1(a, left, right);
if (left < keyi - 1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
if (keyi + 1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
StackDestory(&st);
}
归并排序
基本思想:
归并排序(
MERGE-SORT
)是建立在归并操作上的一种有效的排序算法
,
该算法是采用分治法(
Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并的思路:
将两段有序的区间归并成一段有序的区间借助第三方数组与俩指针,取两个指针中小的那个数,尾插到下面的数组中去,直到一个区间结束,再把另一个区间剩下的数据尾插到后面
归并的思路图:
归并排序的思路:递归让他子区间有序
归并排序的思路图:(归并排序并不要求两边完全相等)
归并排序代码
void _MergeSort(int* a, int left, int right,int *tmp)
{
//结束条件,区间只有一个或者不存在
if (left >= right)
{
return;
}
int mid = (left + right) >> 1;
//[left,mid] [mid+1,right]
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
//两段有序子区间归并tmp,并且拷贝回去
int begin1 = left, end1 = mid; //第一段区间
int begin2 = mid + 1, end2 = right; //第二段区间
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//归并完成以后拷贝回到原数组
for (int j = left; j <= right; j++)
{
a[j] = tmp[j];
}
//_Merge(a, tmp, left, mid, mid + 1, right);
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
归并排序的非递归写法:
思路:
归并的是一个小组,gap就代表每个小组之间有几个数据,每次归并都是两组两组进行归并
一一归并指的是gap为1 的两个小组进行归并,两两,四四同理
一次循环代表两个区间进行归并,循环要取出间隔为gap的所有区间
针对 [ 10 6 7 1 3 9 4 2 ]这样一个数组
代码如下:
_Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int j = begin1;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//归并完成以后拷贝回到原数组
for ( j ;j <= end2; j++)
{
a[j] = tmp[j];
}
}
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
_Merge(a, tmp, i,i+gap-1,i+gap,i+2*gap-1);
}
gap = gap * 2;
}
free(tmp);
}
但是这段代码只针对的如上数组,数组只有8个数据,因此我们由特殊推广到一般,对一般的数组会存在以下几种问题
1.0? 最后一个小组归并时,第二个小区间不存在,不需要归并了
2.0??最后一个小组归并时,第二个小区间存在,但是第二个小区间不够gap个了
3.0? 最后一个小组归并时,第一个小区间不够gap个了,不需要归并了????????
第二个小区间不存在,只要检查i是否大于n就行?
如图几种问题:
解决方法:
如果第二个小区间不存在就不需要归并了,结束本次循环
如果第二个小区间存在,但是第二个小区间不够gap个,结束的位置就越界了,需要修正一下
代码如下:
_Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int j = begin1;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//归并完成以后拷贝回到原数组
for ( j ;j <= end2; j++)
{
a[j] = tmp[j];
}
}
//对任意个数都成立
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//[i i+gap-1] [i+gap i+2*gap-1]
int begin1 = i, end1 = i + gap - 1, begin2 = i + gap, end2 = i + 2 * gap - 1;
//如果第二个小区间不存在就不需要递归了,结束本次循环
if (begin2 >= n)
{
break;
}
//如果第二个小区间存在,但是第二个小区间不够gap个,结束的位置就越界了,需要修正一下
if (end2 >= n)
{
end2 = n - 1;
}
_Merge(a, tmp, begin1, end1, begin2, end2);
}
gap = gap * 2;
}
free(tmp);
}
例子:
内排序与外排序
内排序:数据量相对少一些,可以放到内存中排序,以上排序都可以内排序
外排序:数据量较大,内存中放不下,数据放到磁盘文件中,需要排序
归并排序既可以用来内排序,又可以用来外排序
对这样的一道题:将10亿个整数放到文件A中进行排序,假设可以给512M的内存,如何进行?
思路:首先想办法算出文件有多大,10亿个整数就是40亿个字节就是4g的内存,给了512M的内存能用,我们就可以考虑将其分成8份,每次读1/8,也就是512M到内存中进行排序,然后写到一个小文件中,再继续读1/8,重复这个过程,接下来就可以进行归并了,因为每个小文件中的数据都是有序的(类比归并有序的区间),这里的归并相当于把归并很深的部分都砍掉,部分进行归并,因为数据量太大了,注意:这里在512M内存中的排序可以用其他排序,但不用归并,因为它有O(N)的空间复杂度
两个文件归并只要求两个都有序就行,所以这里的归并有两种方法:
1.0??两两归并
如图:
?2.0 头两个归并后与相邻的进行归并
如图:?
计数排序?
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1.
统计相同元素出现次数
相关概念:(结合下图看)
绝对排序:每个数字都放到与它本身的值对应的位置,例如:5对应5,2对应2
相对映射:一个值映射到哪个位置,就用它的值减去该数组中最小的值
思路图:
而对这样的数组: 思路及其思路图如下
? ? 当然这种数组也可以用绝对映射,只不过会造成空间浪费,假如给的数字很大,?造成的空间浪费也会很大。
代码实现:
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;//易错
int* count = malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * range);
for (int i = 0; i < n; i++)
{
count[a[i]-min]++; //易错
}
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);
}
时间复杂度O(N+range) 空间复杂度O(range)
计数排序的特点:
? ?只适合一组数据,数据的范围比较的集中,如果范围集中效率是很高的,但是局限性也在这里 并且只适合整数,如果是浮点数和字符串等就不行了。
各个排序的稳定性
?数组中相同的值,排完序以后,相对顺序不变,就是稳定的,否则就是不稳定的。对排序来说,如果他可以通过调整算法做到稳定(对任意的情况都是稳定的)就说它是稳定的,否则就不是稳定的。
稳定性的应用:
看似排序的稳定性好像没啥价值,但在这样一种特殊情况中,就展现了它的价值
考试的时候,提交试卷以后,自动判卷拿到成绩。 成绩按照交卷顺序排到数组中去,然后我们对数组进行排序,进行排名。 要求:分数相同,先交卷的排在前面?
冒泡排序:稳定,冒泡是两个数大的数放后面,只要保证两数相等不换就做到稳定了。?
简单选择排序:不稳定,可能有人会说,面对两个相同的最大数字,选择后一个,两个相同小的数字选择前一个就可以做到稳定,但是对以下情况,就是不稳定的。
直接插入:稳定,面对相等的两个数,直接放在后面
希尔:不稳定,相同的值在预排序的时候分到了不同的组里面
堆排序:不稳定
?归并排序:稳定?两区间归并的时候,遇到相同的值,让第一个组里数先下来,就是稳定的
?快速排序:不稳定
八大排序总结(时间,空间复杂度,稳定性)
|