快速排序
“hoare” 快速排序思想
每次找到一个target位置,一般将target设到第一个位置,然后用left和right两个指针进行搜索,right找比target值小的数值,left找比target值大的数值,然后两者交换。当left和right相交时将target交换到此位置,这样可以保证target的左边都比其值小,右边都比其值大。
由于right找的是比target值小的数值,所以每次让right先走,第一种情况是right找到了比其小的,然后left没找到位置就和right相遇了,此时right位置处的数还没有交换所以还是小于target的数。
第二种情况,right没找到,一直走到left,此时由于left和right上一次已经交换了数字,此时left处的数也是小于target的数。
这样就可以保证最后left和right相交的地方一定是小于target的。
如果target在最右边,就让left先走。
然后对target左边和target右边两个区间进行递归调用,直到无法再进行排序。
以 6-8-4-3-2-9-7
这组数据为例,如图所示
代码如下:
int QuickSortPart(int* a, int left, int right)
{
int target = left;
while (left < right)
{
if (left < right && a[right] >= a[target])
{
right--;
}
if (left < right && a[left] <= a[target])
{
left++
}
Swap(&a[left],&a[right]);
}
Swap(&a[left],&a[target]);
return left;
}
void QuickSort(int*a ,int begin, int end)
{
if (begin >= end)
{
return;
}
int target = QuickSortPart(a, begin, end);
QuickSort(a, begin, target - 1);
QuickSort(a, target + 1, end);
}
挖坑法
挖坑法是在"hoare"的基础上进行了稍微的改变,首先,确定一个key位置,这里我们设key的位置在第一个位置,然后将key位置的数据进行保存,此时key位置就相当于一个坑位,然后两个双指针left和right,left指向头,right指向尾,right找小于key值的数值,找到后就将改数值“填入”刚才的“坑”中,此时坑位到了right现在的位置,然后left向右寻找大于key值的数据,找到后将数值填入“坑”中,重复进行上述操作,知道left和right走到同一个位置。
继续用 6-8-4-3-2-9-7
这组数据为例
代码实现:
int QuickSortPart(int* a, int left, int right)
{
int key = a[left];
int pit = left;
while(left < right)
{
if (left < right && a[left] >= key)
{
right--;
}
a[pit] = a[right];
pit = right;
if (left < right && a[right] <= key)
{
left++;
}
a[pit] = a[left];
pit = left;
}
a[pit] = key;
return pit;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return NULL;
}
int key = QuickSortPart(a, begin, end);
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
双指针法
1.首先我们设置一个prev和cur分别指向第一和第二个位置。
2.当cur处的值找到小于Key的值时,prev++然后交换cur处的数据和prev处的数据,cur++
3.当cur处的值大于等于key的值时,cur++,prev不变换。
4.当cur走出数组时结束排序。
5.交换prev处的值和key的值。
双指针法思想其实也是将小的值放到前面,大的值放到后面,只是通过双指针进行操作。
每次找到小的值就和prev++后处的值进行交换,这是将小的值向左移动,prev处永远可以保持是小于key的值,如果cur处的值大于key就将cur++。
继续用 6-8-4-3-2-9-7
这组数据为例
代码如下:
void QuickSortPart(int* a, int left, int right)
{
int key = left;
int prev = left;
int cur = left + 1;
while(cur <= right)
{
if (a[cur] < a[key] && a[++prev] != a[cur])
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
a[key] = a[prev];
return prev;
}
void QuickSort(int* a, int begin, int end)
{
if (begin <= end)
{
return NULL;
}
int key = QuickSortPart(a, begin, end);
QuickSort(a, begin, key - 1);
QucikSort(a, key + 1, end);
}
三种方法的算法复杂度都为0(N)。
快排总的算法复杂度取决于每次选择key的位置,如果每次选择的key都是中位数,如图情况。
此种情况下快排的算法复杂度为O(N*logN)。
最坏情况是每次选择的key都为最小或最大,这种情况如图所示
这种情况下的算法复杂度为O(N^2)。
并且如果数据量很大的情况下,由于我们在递归调用这有可能使得栈溢出。
所以针对这种最坏的情况我们进行优化,每次选择key时我们遵循“三数取中原则”,每次取最左边的数据最中间的数据还有最右边的数据,用这三个数据进行比较选择中间大小的数据当作key使用,取得key的值和最原代码最左边的数据进行交换,这样可以保证不用过多改动源代码。
代码:
//找到三数中间大小的下标
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return right;
}
else
{
return left;
}
}
}
//快速排序 "hoare"
int QuickSortPart1(int* a, int left, int right)
{
//优化三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
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]);
return left;
}
优化前:栈溢出
优化后:
对于快排还有第二种优化方式,这种方式叫做小区间优化
小区间优化的思想就是因为在快排递归到后面时大多数数据可能已经接近有序,但是我们仍然还需要对其进行区间划分然后排序,所以此时为了减少这些递归的次数,我们可以采用插入排序进行替换。
数据量小切接近有序的情况下我们大多数情况下选择插入排序。
代码如下:
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if (end - begin + 1 < 13)//当递归调用到大约13个数左右就进行替换
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int target = QuickSortPart3(a, begin, end);
QuickSort1(a, begin, target - 1);
QuickSort1(a, target + 1, end);
}
}
最后我们对优化过后的快排和其他的排序性能进行比较:
10000数据量比较
100000数据量比较