快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
void QuickSort(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
int keyi = PartSort(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi+1, end);
}
我们可以明显的发现上述主框架与与二叉树前序遍历规则非常像,因此我们只需分析如何按照基准值来对区间中数据进行划分的方式即可。
Hoare版本
基本流程
1.选取最左侧元素为基准值keyi,变量Right先从数组最右侧元素出发与基准值keyi比对,直到找到一个小于keyi的数停下来
2.此时Left变量从最左侧出发与基准值keyi比对,直到找到一个大于keyi的数停下来
3.此时交换变量Left与变量Right在数组内所对应的值
4.重复步骤1,让变量Right继续走到小于基准值keyi的地方,Left走到大于基准值keyi的地方,再交换
5.接着重复步骤1,Right变量遇到3停止,Left移动与Right相遇,此时交换相遇处与基准值keyi处所对应的值
6.此时这一轮交换完毕,我们可以看到数组中6的左边均为比6小的数字,右边均为比6大的数字,以6为分界点拆分成两个序列,接下来我们再分别处理左右两个子序列即可。
7.对于左序列,将最左侧作为基准值,再次重复以上步骤处理
8.再继续处理3左侧的子区间得到
9.处理完之后2的位置已经合适,1只有一个数,故也不需要处理,此时再同样的方式处理3右侧序列、6右侧序列
10.此时数组已排序完成。
代码实现
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
// Right找小
while (left < right && a[right] >= a[keyi])
//这里要限制住,否则会有数组越界的风险、且要注意这里要加=,否则可能会遇到死循环
--right;
// Left找大
while (left < right && a[left] <= a[keyi])
++left;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
我们要明确的是:
在上述例子中,让Right变量先走就可以保证Left与Right相遇的位置一定是比keyi要小的
挖坑法
该方法与Hoare的版本其实效率差不了多少,但是更好理解,其步骤较为自然顺理成章。
相比Hoare法不需要理解为什么相遇位置比keyi小,且要左边做key让Right先走
基本流程
1.将最左侧的元素key保存起来,因为元素已被保存故该位置处元素可以被覆盖,就相当于“挖了一个坑”,此时Right先走找小于key的数,找到后将这个数填入坑位,然后这个小于key的数的位置就形成了一个新的坑位
2.Left再走找比key大的数字,将其填入坑位,这个数的位置就形成了一个新的坑位
3.Right再走找小,填坑,挖新坑
4.Left同步骤
5.Right再继续找小,继续填坑挖坑
6.Left继续往前走,直至Left与Right相遇,此时将key填入坑位,此时这一轮交换完毕,我们可以看到数组中6的左边均为比6小的数字,右边均为比6大的数字,以6为分界点拆分成两个序列,接下来我们再分别处理左右两个子序列即可。
7.同理处理左右子序列即可排序完成。
代码实现
int PartSort2(int* a, int left, int right)
{
int key = a[left];//要保存值,并不是下标
int pit = left;//坑位
while (left < right)
{
// 右边先走,找小
while (left < right && a[right] >= key)
{
--right;
}
a[pit] = a[right];
pit = right;
// 左边走,找大
while (left < right && a[left] <= key)
{
++left;
}
a[pit] = a[left];
pit = left;
}
a[pit] = key;
return pit;
}
前后指针法
基本流程
1.初始时,prev指针指向序列开头,cur指针指向prev的后一个位置
2**.cur指针找比key小的数,找到后prev++,交换prev与cur对应的值**
注意这里要仔细理解找到小、prev++,交换的这个流程。
cur指针在1,2时prev++,交换后就相当于与本身交换故不变
【prev与cur的关系】
1.cur还没找到比key大的数字时,prev紧跟着cur一前一后
2.cur遇到比key大的值后,cur继续往前走,直到找到比key小的数字后,二者之间隔着一段比key大的值的区间
3.cur再走,遇到4比key小,prev++,交换
4.cur再走,遇到5比key小,prev++,交换
5.cur走到数组结束后,将prev处的值与key交换,此时一轮交换已完毕。
6.再对其左右侧子序列完成上述步骤即可排序完成。
代码实现
// 前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = left+1;
while (cur <= right)
{
if (a[cur] < a[keyi] && a[++prev] != a[cur])//防止自己跟自己交换
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
对于快排的优化
快排的时间复杂度:O(N*logN)
最好的情况:每次选key都是中位数 | O(N*logN)
最坏情况:每次选到的key都为最大/最小的那个数|O(N*N)
三数取中
我们针对最坏情况进行优化:
三数取中:选既不为最大也不为最小的那个数
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;//对于求平均数的优化
// left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
// 前后指针法
int PartSort3(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);//三数取中
Swap(&a[midi], &a[left]);//将其放在最左侧去当key
int keyi = left;
int prev = left, cur = left+1;
while (cur <= right)
{
if (a[cur] < a[keyi] && a[++prev] != a[cur])//防止自己跟自己交换
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
小区间优化
此外,当区间很小时,不要再使用递归划分的思路让其有序,而是直接使用插入排序对小区间排序,从而减少递归调用提高效率
void QuickSort2(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
// 小区间直接插入排序控制有序
if (end - begin + 1 <= 30)//自己控制小区间为多小
{
InsertSort(a + begin, end - begin + 1);//调用插入排序,注意这里的元素个数控制
}
else
{
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
}
快排的非递归
原理
要利用栈来改为非递归,先把第一个要处理的区间下标入栈,先入左再入右,故先出右,再利用单趟排函数逐次缩小区间入栈,直至最小规模子问题结束,排序完成
模拟实现
假设已定义好一个栈
现有数组:
先把begin与end压入栈(注意是下标)
定义left与right变量存储出栈左右区间
此时利用单趟排函数变keyi缩小区间,此时我们使用Hoare版本实现可以得到:
此时再将keyi(此时为6)左侧的子区间与右侧的子区间再入栈:
left与right变量存储出栈左右区间:
注意此时再次利用单趟排函数缩小区间:
此时再将keyi(此时为9)左侧的子区间与右侧的子区间再入栈(此时右侧为最小规模子问题故不入栈):
left与right变量存储出栈左右区间:
此时再调用单趟排函数得到:(此处7 8 10 均为最小规模子问题 全部返回)
到这里最初keyi右侧的数组元素全部排列完毕,此时栈内:
再按上述右侧步骤依次走完即可排序左侧子区间
故排序完成。
void QuickSort3(int* a, int begin, int end)
{
ST st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);//先出右,后出左
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);
// [left,keyi-1][keyi+1,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);
}
其实非递归借用了栈,也是在模拟递归的方法来依次缩小区间排序