基本目标
本章会先介绍递归版本的快速排序,
然后是hoare版本基准值寻找方法,挖坑法,lomuto的前后指针法,
接着会介绍用栈实现的非递归版本的快速排序,
以及为解决有大量重复值三路划分的基准值寻找方法,
最后是实际生产中比较强大的自省排序。
快速排序(递归版本)
对于快速排序来说它的基本思想是任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列在相应位置上为⽌。
总体来烧他是根据基准值不断划分比基准值小的序列,和比基准值大的序列,让最终达到一个有序的序列。
它的主题框架是
void quicksort(int * arr,int left,int right)
{
if(left>=right)
{
return;
}
int keyi = _quicksort(arr,left,right);
quicksort(arr,left,keyi-1);
quicksort(arr,keyi+1,right);
}
hoare版本基准值寻找
在该版本中,我们默认让keyi = left;然后让left++,让left从左往右走,寻找比基准值所对应元素大的,找到停下,让right从右往左走,寻找比基准值小,找到停下。让left和right所对应的值交换,接着重复上述过程,直到left>right,此时right所在的位置就是基准值应该在的位置,让它跟基准值的元素进行交换,最后返回right的值作为基准值位置的索引
int _quicksort(int* arr, int left, int right)
{
int keyi = left;
left++;
while (left <= right)
{
while (left <= right && arr[right] > arr[keyi])
{
right--;
}
while (left <= right && arr[left] < arr[keyi])
{
left++;
}
if (left <= right)
{
Swap(&arr[left++], &arr[right--]);
}
}
Swap(&arr[right], &arr[keyi]);
return right;
}
注意left和right等于keyi的值我们也进行交换,这么做是为了防止在有大量重复元素出现时,时间复杂度的提高。
挖坑法
创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新
的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结
束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)。
int _quicksort2(int* arr, int left, int right)
{
int hole = left;
int key = arr[hole];
while (left < right)
{
while (left<right && arr[right]>key)
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] < key)
{
left++;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
lomuto前后指针
默认keyi的位置是left,定义一个prev指针让他指向prev,定义一个 cur指针指向prev+1,让cur从左往右找比keyi小的值,找到了,让prev++,cur和prev交换。再让cur++,没有找到就让cur++,直到cur>right时停止,此时prev所指向的位置就是keyi应该在的位置,让他们两着所对应的值进行交换,返回prev,这就是现在keyi的索引
如果++prev和cur相等就是自己跟自己交换,为了简化次数,将它放在判断条件中
int _quicksort3(int* arr, int left, int right)
{
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
快速排序的非递归版本
该版本所用的基准值寻找方法是lomuto版本的,借用栈的数据结构来进行实现的
我们在进行循环的时候要首先往栈中压入两元素,也就是这个序列的左右索引,然后进入循环,取出两元素,寻找基准值,如果左右序列不是一个有效的序列,也就是它的左边的序列大于等于右边的情况,等于的时候说明它只有一个元素,大于的情况说明它是空的,关于压栈的时候要注意,根据栈的特性后进先出,我们要先压入右边的元素,再去压入左边的元素,这样循环取堆顶的时候,才能是顺序的。
void quicksort2(int* arr, int left, int right)
{
Stack st;
Stackinit(&st);
Stackpush(&st, right);
Stackpush(&st, left);
while (!Stackempty(&st))
{
int begin = stacktop(&st);
Stackpop(&st);
int end = stacktop(&st);
Stackpop(&st);
int keyi = begin;
int prev = begin;
int cur = prev + 1;
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
keyi = prev;
if (begin < keyi - 1)
{
Stackpush(&st, keyi - 1);
Stackpush(&st, begin);
}
if (keyi + 1 < end)
{
Stackpush(&st, end);
Stackpush(&st, keyi + 1);
}
}
stackdestroy(&st);
}
三路划分
三鹿划分的提出是为了解决有大量的重复值的序列而出现,它是hoare版本和lomuto版本的结合,
它的基本思想是
基准值的默认位置选取在left的位置,定义一个key的变量去保存基准值的数值
定义一个cur变量,让它也指向left,
让cur变量从左往右遍历,遇到比key的值小的,让它和left进行交换,cur++,left++
遇到比key的值大的,让它跟right交换,right–
和key的值相等,让cur++
重复上述过程直到cur的值小于等于right
此时和key相等的值就会在序列的中间,left和right的之间
因为left和right的值在其中发生变化,我们要定义两个变量去保存,left和right的初始值,去保证,后续的递归。
void quicksort3(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
int randi = left + (rand() % (right - left + 1));
Swap(&arr[left], &arr[randi]);
int key = arr[left];
int cur = left + 1;
while (cur <= right)
{
if (arr[cur] < key)
{
Swap(&arr[cur++], &arr[left++]);
}
else if (arr[cur] > key)
{
Swap(&arr[cur], &arr[right--]);
}
else
{
cur++;
}
}
//begin left-1||left right||right+1 end
quicksort3(arr, begin, left - 1);
quicksort3(arr, right + 1, end);
}
在这里随机去取基准值是为了让数据划分更加均匀,提高效率。
自省排序
自省排序是对快排的一种优化,对于快排的次数,它类似于二叉树的深度logn,如果运气不好,每次取基准值的时候划分之后,都会让一棵子树特别深,一颗子树没有元素,在深度超过一定程度之后转为堆排序,对于数组长度小于多少的序列,也可以进行小区间优化,让其转为插入排序
参考代码
void IntroSort(int* arr, int left, int right, int depth, int defaultdepth)
{
if (left >= right)
{
return;
}
//数组小于4的小数组,转为插入排序,简化次数
if (right - left + 1 < 2)
{
insertsort(arr + left, right - left + 1);
return;
}
//深度超高2倍的logn,直接转为堆排序
if (depth > 2 * defaultdepth)
{
heapsort(arr + left, right - left + 1);
return;
}
int begin = left;
int end = right;
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[prev], &arr[keyi]);
keyi = prev;
IntroSort(arr, begin, keyi - 1, depth, defaultdepth);
IntroSort(arr, keyi + 1, end, depth, defaultdepth);
}
void quicksort4(int* arr, int left, int right)
{
int depth = 0;
int logn = 0;
int N = right - left + 1;
int i = 0;
for (i = 1; i < N; i *= 2)
{
logn++;
}
IntroSort(arr, left, right, depth, logn);
}
时间复杂度
对于快排来说,它的时间复杂度,它的最好的情况是一颗完全二叉树,logn,最坏的情况,没次划分都有一颗子树是空的,此时的时间复杂度就是n²