1. 快排性能的关键点分析
决定快排性能的关键点是每次单趟排序后,key对数组的分割,如果每次选key基本⼆分居中,那么快 排的递归树就是颗均匀的满⼆叉树,性能最佳。但是实践中虽然不可能每次都是⼆分居中,但是性能 也还是可控的。但是如果出现每次选到最⼩值/最⼤值,划分为0个和N-1的⼦问题时,时间复杂度为 O(N^2),数组序列有序时就会出现这样的问题,我们前⾯已经⽤三数取中或者随机选key解决了这个问题,也就是说我们解决了绝⼤多数的问题,但是现在还是有⼀些场景没解决(数组中有⼤量重复数据 时),例如下面:
多个key值相等,或者全是相同的数。
cpp
int a[] = { 6,1,7,6,6,6,4,9 };
int a[] = { 3,2,3,3,3,3,2,3 };
int a[] = { 3,3,3,3,3,3,3,3 };
2.三路划分算法
这里使用三路划分可以提升当数组中,有大量相同的值时,排序的时间效率,三路划分的核⼼思想有点类似hoare的左右指针和lomuto的前后指针的结合(这里的hoare的左右指针和lomuto的前后指针在小编前面"各种排序方法"文章中有所讲解)。核⼼思想是把数组中的数据分为三段**⽐key⼩的值;跟key相等的值;⽐key⼤的值**,所以叫做三路划分算法。
实现的思想:
-
key默认取left位置的值。
-
left指向区间最左边,right指向区间最后边,cur指向left+1位置。
-
cur遇到⽐key⼩的值后跟left位置交换,换到左边,left++,cur++
-
cur遇到⽐key⼤的值后跟right位置交换,换到右边,right--。
-
cur遇到跟key相等的值后,cur++。
-
直到cur>right结束
排序成这样以后,再递归left之前的数组,和right之后的数组就可以了。
注意:这种排序方法,主要是当数组中有大量相同的数据时,时间效率较高,但如果数组中没有大量相同的数据,这种算法的时间效率甚至还不如,lomuto和hoare版的快速排序。
三路划分针对有⼤量重复数据时,效率很好,其他场景就⼀般,但是三路划分思路还是很有价 值的,有些快排思想变形体,要⽤划分去选数,他能保证跟key相等的数都排到中间去,三路划分的价值就体现出来了。
cpp
void _QuicksortPro(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[left++], &arr[cur++]);
}
else if (arr[cur > key])
{
Swap(&arr[cur], &arr[right--]);
}
else
{
cur++;
}
}
_QuicksortPro(arr, begin, left - 1);
_QuicksortPro(arr, right + 1, end);
}
void Quicksort(int* arr, int left, int right)
{
srand(time(0));
_QuicksortPro(arr, left, right);
}
3.introsort的快排排序(内省排序)
introsort是由DavidMusser在1997年设计的排序算法,C++sgi STL sort中就是⽤的introspective sort(内省排序)思想实现的。内省排序可以认为不受数据分布的影响,⽆论什么原因划分不均匀, 导致递归深度太深,他就是转换堆排了,堆排不受数据分布影响。
首先确定递归深度达到多少时,深度一般为logn的两倍(数据个数取二的对数:2*logn),就停止这种排序转换成堆排序(堆排序,在小编之前"堆的应用"文章中已有详细讲解);
其次当数组长度小于十六时,就直接使用插入排序,简单递归次数;
代码如下:
cpp
//直接插入排序
void InsertSort(int* arr, int size)
{
for (int i = 0; i < size-1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + 1] = arr[end];
}
else {
break;
}
end--;
}
arr[end + 1] = tmp;
}
}
//堆排序
void AdjustDwon(int* a, int n, int root)
{
int parent = root;
int child = 2 * parent + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child += 1;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
int parent = (n - 1 - 1) / 2;
while (parent >= 0)
{
AdjustDwon(a, n, parent);
parent--;
}
int size = n - 1;
while (size > 0)
{
Swap(&a[0], &a[size]);
AdjustDwon(a, size, 0);
size--;
}
}
void IntroSort(int* arr, int left, int right, int depth, int defaultDepth)
{
if (left >= right)
{
return;
}
if (right - left + 1 < 16)
{
InsertSort(arr + left, right - left + 1);
return;
}
if (depth > defaultDepth)
{
HeapSort(arr+ left, right - left + 1);
return;
}
depth++;
int begin = left;
int end = right;
int randi = left + (rand() % (right - left + 1));
Swap(&arr[randi], &arr[left]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <=right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
keyi=prev;
IntroSort(arr, begin, keyi - 1, depth, defaultDepth);
IntroSort(arr, keyi+1, end, depth, defaultDepth);
}
void QuickSort(int *arr,int left,int right)
{
int depth = 0;
int logn = 0;
int size=right-left+1;
for (int i = 1; i < size; i*=2)
{
logn++;
}
IntroSort(arr, left, right, depth,logn*2);
}