快速排序 是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值(key),按照该基准值将待排序集合分割成两个子序列,左子序列中所有元素小于基准值,右子序列中所有元素据大于基准值,然后最左右子序列反复重复该过程,直到所有元素都排列在相应位置为止。
首先,我们需要明白的一点是,先写一个单趟排序,再把单趟排序重复多次,这样来写排序会更加方便快捷不易出错
所以,从快速排序的单趟实现,开始讲起
现有一数组 6 1 2 7 9 3 4 5 10 8
begin end
key
pivot
我们一般选择,数组第一个元素作为我们的基准值 key,在此为6
我们单趟操作要实现的目标是,将6送到中间,同时6的左边都是比6小的值,6的右边都是比6大的值,假设我们完成了目标
得到了一个这样的数组 5 1 2 4 3 6 9 7 10 8
我们会发现6是排名第6小的,那么也就是说6已经处于它在升序排序中的位置了,这里可能会有点绕,我们可以拿有序数组来对比,
1 2 3 4 5 6 7 8 9 10,有序数组中6也排在第六位
那,如何让6的左边都小于它本身,右边都大于它本身
就要用到以下的挖坑法
6 1 2 7 9 3 4 5 10 8
begin end
key
pivot
begin前端,end尾端,pivot坑,key基准值,只有key存的是数值,其余存的都是下标
首先把 key赋上6
然后将pivot的值赋为0
也就是6的位置是第一个坑,这个坑可以开始接受小于key的数据了
end先开始工作,开始从尾部找小于key的数据,找到后将该数据抛到坑里
然后end的位置就变成了新的坑
begin再开始工作,开始从头部找大于key的值,然后丢到坑里
以begin为下标的地方又变成新的坑,依次操作
当begin=end时交换停止,说白了就是左右两边数据按照key值左右交换
写完单趟就要用分治思想,来实现整体排序
把大问题划分成一个个不可分割的子问题
例如,举一个最小的情况 两个数中一个数为key,让其右边比key大,或左边比key小,这两个数就有序了
以下是代码实现
void QuickSort(int* a, int left,int right)
{
if (left >= right)//left>right表示没有左区间存在,left=right表示只有一个值
{
return;
} 一轮递归的结束条件
int begin = left, end = right;
int pivot = begin;
int key = a[begin];
while (begin < end)
{
//选择从左端开始做坑
//右边找小放到左边
while (begin < end && a[end] >= key)
{
end--;//end持续--可能会导致end到达到begin前面发生交错,把已经处于正确位置的数,又扔到后面的坑中所以while条件里要加一条begin<end
}
//选出小的放到左边的坑中,自己就变成了新的坑位
a[pivot] = a[end];
pivot = end;
//key的左边目标是全是小于key,所以从左边找大,放到右边
while (begin < end && a[begin] <= key)
{
begin++;
}
a[pivot] = a[begin];
pivot = begin;//我成为新的坑
}
pivot = begin;
a[pivot] = key;
//left区间 坑 pivot right区间
//[left,pivot-1] pivot [pivot+1,right]
//左子区间和右子区间有序,我们就有序了,如何让他们有序呢? 分治递归 二叉树前序遍历,根,左子树,右子树
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot+1, right);
}
void TestQuickSort()
{
int a[] = { 3,5,2,7,8,6,1,9,4,0 };
QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
PrintfArry(a, sizeof(a) / sizeof(int));
}
int main()
{
TestQuickSort();
}
关于快速插入排序的时间复杂度
首先看单趟,我们从左边不断找大的向右边抛,从右边不断找小的向左边抛,最差的情况是,来回总共抛了N次
总体看,我们拿一个具体的理想情况来估算,假如我们每次选的key,它的顺序大小刚好在正中间则可以将它视为一个二叉树,N个数的话,总共有log2N层,但是,第一层往下,每一层都会分成左右两部分,则进行操作的次数为 2*n/2+2*n/2=n............依次类推,每一层进行的操作次数还是n次
然后总有,N层,所以时间复杂度为 N*logN
快排什么情况下最坏?时间复杂度又是多少?
有序的情况下最坏
假如是顺序,我们的算法并不能检测已经是顺序,所以,每次只会排最左边的值,把它排有序,然后,右边的值从最后遍历到最开头,第一次遍历N,第二次遍历N-1,.....................直到1
快速排序,有着致命的缺陷,假如是有序,则它的时间复杂度为O(N^2),会变得非常非常慢
官方用三数取中法来选取key,不再单纯的选最左边,或最右边来作为key,而是选用三个数
中间那个数来做为key,这样就避免了选用最大或最小值来做key
以下是添加了,三数取中函数GetMidIndex(),后的代码
//三数取中,三个数中取中间那个
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;//下标取平均,这样写效率更高一点,循环右移说白了,就是二进制中每一位的权重除以2,加和成十进制数就还是整体除以2
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[right] > a[left])
{
return right;
}
else
{
return left;
}
}
else//a[left]>a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] > a[left])
{
return left;
}
else
{
return right;
}
}
}
void QuickSort(int* a, int left,int right)
{
if (left >= right)//left>right表示没有左区间存在,left=right表示只有一个值
{
return;
}
int index= GetMidIndex(a, left, right);//找出中间值的下标
//为了不打乱下面的代码结构,我们选择将,中间值下标找出来,并将该处存放的数值与开头存放的数值互换
Swap(&a[left], &a[index]);
int begin = left, end = right;
int pivot = begin;
int key = a[begin];
while (begin < end)
{
//选择从左端开始做坑
//右边找小放到左边
while (begin < end && a[end] >= key)
{
end--;//end持续--可能会导致end到达到begin前面发生交错,把已经处于正确位置的数,又扔到后面的坑中所以while条件里要加一条begin<end
}
//选出小的放到左边的坑中,自己就变成了新的坑位
a[pivot] = a[end];
pivot = end;
//key的左边目标是全是小于key,所以从左边找大,放到右边
while (begin < end && a[begin] <= key)
{
begin++;
}
a[pivot] = a[begin];
pivot = begin;//我成为新的坑
}
pivot = begin;
a[pivot] = key;
//left区间 坑 pivot right区间
//[left,pivot-1] pivot [pivot+1,right]
//左子区间和右子区间有序,我们就有序了,如何让他们有序呢? 分治递归 二叉树前序遍历,根,左子树,右子树
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot+1, right);
}
void TestQuickSort()
{
int a[] = { 3,5,2,7,8,6,1,9,4,0 };
QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
PrintfArry(a, sizeof(a) / sizeof(int));
}
int main()
{
TestQuickSort();
}
实测的话,快速插入排序和堆排序速度差不多
快速插入排序的特性总结
1.快速插入排序综合性能强,使用场景多
2.时间复杂度只有O(N*logN)
官方c语言库中,对数组的排序选择的是快排
如果,我们把最后的几层递归调用消除掉,即进行小区间优化,则效率会更加高
假如,我们有一百万个数,最后两层,就会调用80万次左右的递归,那不如在划分成很小的区间后,直接用其他排序来完成小区间的排序,我选择用插入排序,下面是部分需要修改的地方,
至于插入排序的实现,我之前的博客有写过可以直接copy过来用。
void QuickSort(int* a, int left,int right)
{
if (left >= right)//left>right表示没有左区间存在,left=right表示只有一个值
{
return;
}
int index= GetMidIndex(a, left, right);//找出中间值的下标
//为了不打乱下面的代码结构,我们选择将,中间值下标找出来,并将该处存放的数值与开头存放的数值互换
Swap(&a[left], &a[index]);
int begin = left, end = right;
int pivot = begin;
int key = a[begin];
while (begin < end)
{
//选择从左端开始做坑
//右边找小放到左边
while (begin < end && a[end] >= key)
{
end--;//end持续--可能会导致end到达到begin前面发生交错,把已经处于正确位置的数,又扔到后面的坑中所以while条件里要加一条begin<end
}
//选出小的放到左边的坑中,自己就变成了新的坑位
a[pivot] = a[end];
pivot = end;
//key的左边目标是全是小于key,所以从左边找大,放到右边
while (begin < end && a[begin] <= key)
{
begin++;
}
a[pivot] = a[begin];
pivot = begin;//我成为新的坑
}
pivot = begin;
a[pivot] = key;
//left区间 坑 pivot right区间
//[left,pivot-1] pivot [pivot+1,right]
//左子区间和右子区间有序,我们就有序了,如何让他们有序呢? 分治递归 二叉树前序遍历,根,左子树,右子树
if (pivot - 1 - left > 10)
{
QuickSort(a, left, pivot - 1);
}
else
{
InsertSort(a + left, pivot - 1 - left + 1);//InsertSort函数要求传入,数组的地址和元素个数
}
if (right - (pivot + 1) > 10)
{
QuickSort(a, pivot+1,right);
}
else
{
InsertSort(a + pivot + 1, right - (pivot + 1) + 1);
}
}