一、快速排序的基本思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。下面我们将实现快速排序的(三种)递归算法和非递归算法
以下的代码我们都以升序的方式进行排序
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right) {
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,所以我们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。
二、代码实现——三种递归
1.hoare版本(左右指针法)
代码如下:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void QuickSort(int* a, int left, int right)//快排之左右指针
{
if (left >= right)
{
return;
}
int begin = left, end = right;
int keyi = begin;//每次取最左边的元素的下标为key(保存下标即可,如果保存元素,后面则无法进行交换)
while (begin < end)//外层循环需要保证begin<end,内层循环也要保证,否则可能将会越界
{
while (begin < end && a[end] >= a[keyi])//右边找小
{
--end;
}
while (begin < end && a[begin] <= a[keyi])//左边找大
{
++begin;
}
Swap(&a[end], &a[begin]);//通过指针交换两个位置的元素
}
Swap(&a[begin], &a[keyi]);
//每次将这个key位置的元素放到begin的位置后,a[key]的左边都比它小,右边都比它大,所以自己就不需要再调整了
//本次结束就将整个序列元素分为[left, begin-1] [begin] [begin+1,right] 3个区间
QuickSort(a, left, begin - 1);//递归左区间
QuickSort(a, begin + 1, right);//递归右区间
}
在上述代码中,值得我们注意的是:假设取最左边的数为key,一定要从右边开始找小,如果先从左边找大,就可能会导致排序失败。
接下来,我们画图分析这代码中的诸多细节:
递归展开图:
2.挖坑法
代码如下:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void QuickSort(int* a, int left, int right)//快排之挖坑法
{
if (left >=right)
return;
int begin = left, end = right;
int pivot = left;
int key = a[left];
while (begin < end)
{
while (begin < end && a[end] >= key)//右边找小,如果大,end--
{
end--;
}
a[pivot] = a[end];
pivot = end;//找到后把值放到坑位,自己再形成新的坑位
while (begin < end && a[begin] <= key)//左边找大,如果小,begin++
{
begin++;
}
a[pivot] = a[begin];
pivot = begin;//找到后把值放到坑位,自己再形成新的坑位
}
pivot = begin;
a[pivot] = key;
//本次结束,形成了[left,poivt - 1] [pivot] [pivot + 1, right]
//进行子区间排序->递归
QuickSort(a, left, pivot - 1);//递归左区间
QuickSort(a, pivot + 1, right);//递归右区间
}
3.前后指针法
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void QuickSort(int* a, int left, int right)//
{
if (left >= right)
{
return;
}
int prev = left;//左边第一个
int cur = left + 1;//当前位置,从左边第二个开始
int key = left;//取最左边的为key
while (cur <= right)//最右边的元素也需要调整,所以这里有等于
{
if (a[cur] < a[key] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[prev], &a[key]);
//本次结束,形成了[left,prev] [prev] [prev + 1, right]
QuickSort(a, left, prev - 1);//递归左区间
QuickSort(a, prev + 1, right);//递归又区间
}
三、代码实现——快排之非递归
对于快排的非递归,我们就需要用到数据结构——栈
代码实现:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int QuickSort(int* a, int left, int right)//hoare版本(左右指针法)
{
int key = left;
while (left < right)
{
while (left < right && a[right] >= a[key])//右边找小
{
right--;
}
while (left < right && a[left] <= a[key])//左边找大
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[left], &a[key]);//left和right相等,再跟key交换
return left;//left就是不需要调整的元素的位置
}
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);//先入左
StackPush(&st, right);//先入右
while (!StackEmpty(&st))
{
int end = StackTop(&st);//获取栈顶元素,是右
StackPop(&st);//出栈
int begin = StackTop(&st);//获取栈顶元素,是左
StackPop(&st);//出栈
//mid是不需要调整的元素的位置
int mid = QuickSort(a, begin, end);
//结束后把区间分为[begin,mid-1] [mid] [mid+1,end]
if (mid + 1 < end)//因为我们先对左边进行排序,所以先入右边,最后取出来的就左边
{
StackPush(&st, mid + 1);
StackPush(&st, end);
}
if (mid - 1 > begin)//再入左边
{
StackPush(&st, begin);
StackPush(&st, mid - 1);
}
}
}
画图分析是如果入栈出栈顺序:
总结
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定