这里继续整理常见的排序算法.
本文介绍快速排序以及对快速排序的优化
type right by Thomas Alan 光风霁月023 .XDU
1. 常规
//1. 对arr[l...r]部分进行partition操作
// rtn p, arr[l, p - 1] < arr[p]; arr[p + 1, r] > arr[p]
template
TINT32 __partition(T arr[], TINT32 l, TINT32 r)
{
swap(arr[l], arr[rand() % (r - l + 1) + l]); // 增加标准值的随机性
T v = arr[l]; // 选取标准值
// arr[l+1...j] < v, arr[j+1...i) > v, 而arr[j+i]是当前考察的元素
TINT32 idj = l;
// 注意, (l...idj] 位置的元素时刻 < v, idj初始为l, 这样, arr的(l...idj]范围的元素为空
for (TINT32 idx = l + 1; idx <= r; idx++)
{
// 如果 arr[idx] >= v, 那么索引向后移动就可以
if (arr[idx] < v)
{
swap(arr[idj + 1], arr[idx]);
idj++;
// 上面两行也可以用 swap(arr[++idj], arr[idx]); 代替,
// 但是为了看起来方便, 不需要炫技
// 将arr 的 idj + 1位置(一定 > v)的元素和当前考察元素交换位置
}
}
swap(arr[l], arr[idj]);
return idj;
}
//2. 对arr[l...r]部分进行快速排序
template
void __quickSort(T arr[], TINT32 l, TINT32 r)
{
// if (l >= r)
// {
// return;
// }
if (r - l <= 15)
{
// 老样子, 数据少就用插入排序
insertionSort(arr, l, r);
return;
}
TINT32 p = __partition(arr, l, r); // 得到数组arr的[l, p - 1]部分 < arr[p], [p + 1, r]部分 > arr[p]
__quickSort(arr, l, p - 1);
__quickSort(arr, p + 1, r);
}
// 3. 入口
template
void quickSort(T arr[], TINT32 num)
{
srand(time(NULL)); // 设置随即数种子
__quickSort(arr, 0, num - 1);
}
其中, 如果快速排序选取的标准值过于特殊, 会导致分割成的两个部分失衡, 比如对一个近乎有序(升序)的数组进行快速排序, 会导致每次选取的arr[l]这个标准数是最小的数, 数组在partition后, 右边的组就会极其的长, 快速排序就会退化为O(n^2)级别的算法. 所以需要选取一个随机位置的数作为标准数, 增加性, 使其时间效率接近O(nlogn).
2. 改进的快速排序算法
在常规, 我们可以看到, arr数组在partition过程中, [l+1...idj] 部分总是 < v, 而 [idj + 1 ... idx - 1]的部分总是 >= v. 因此, 如果数组中存在着大量重复元素的时候, 数组很容易会被分割为极不平衡的两个部分, 快速排序也会退化为O(n^2)级别的算法. 因此我们可以这样:
存在两个索引idx, idj, 从数组的l端和r端同时开始扫描, 数组的l端存放 < v 的元素, r端存放 > v的元素, 当扫描到不符合要求的元素交换位置, 然后继续扫描, 直到二者重合后错位即idx > idy 后, 停止扫描, 返回中间位置(idj).
template
// 改进后的partition算法
TINT32 __partition2(T arr[], TINT32 l, TINT32 r)
{
swap(arr[l], arr[rand() % (r - l + 1) + l]);
T v = arr[l];
// arr[l+1, idx] <= v; arr(idj...r] >= v
TINT32 idx = l + 1, idj = r;
while (true)
{
// idx不可越界, 如果arr[idx] < v, 那么跳过, 跳过所有 < v 的元素
while (idx <= r && arr[idx] < v)
{
idx++;
}
// idj不可越界, 如果arr[idj] > v, 那么跳过, 跳过所有 > v 的元素
while (idj >= l + 1 && arr[idj] > v)
{
idj--;
}
// idx 和 idj 重合后, 结束
// 由于前两个while的条件是互斥的, 因此在这里进行判断是可以的, 在大while开头不需要判断
if (idx > idj)
{
break;
}
// 交换左右不符合条件的元素, 那么就符合要求了, 同时对索引 ++, --.
swap(arr[idx], arr[idj]);
idx++;
idj--;
}
// 最后将标准值换到最中间
swap(arr[l], arr[idj]);
// 此时arr[idx]是 >= v的值, 可能 > v, arr[idj] == v
return idj;
}
3. 三路快速排序
在2.中, 我们考虑到了 == v 的情况, 在三路快速排序中, 我们把数组分为三个部分, l端部分 < v, r端部分 > v, 中间部分 == v. 这样, 我们只需要考虑 < 和 > v的部分继续递归的快速排序就可以. 这样可以避免对相等的v进行重复操作.
template
// 1. 三路快速排序的算法
void __quickSort3Ways(T arr[], TINT32 l, TINT32 r)
{
// if (l >= r)
// {
// return;
// }
if (r - l <= 15)
{
insertionSort(arr, l, r);
return;
}
// partition
swap(arr[l], arr[rand() % (r - l + 1) + l]);
T v = arr[l];
TINT32 lt = l; // 小于v的最后一个元素的索引, arr[l+1...lt] < v
TINT32 gt = r + 1; // 大于v的最后一个元素的索引, arr[gt...r] > v
TINT32 idx = l + 1; // arr[lt+1...idx] == v;
while (idx < gt)
{
if (arr[idx] < v)
{
// 如果arr[idx] < v, 那么移动到r端, 同时维护lt, arr[lt + 1] == v, 所以只需要交换他和arr[idx]就可以
swap(arr[idx], arr[lt + 1]);
lt++;
idx++;
}
else if (arr[idx] > v)
{
// 如果arr[idx] > v, 那么移动到r端, 同时维护gt
// idx位置的元素换成末尾的元素, 将继续考察这个元素, 所以没有idx++
swap(arr[idx], arr[gt-1]);
gt--;
}
else
{
// 如果arr[idx] == v, 跳过即可
idx++;
}
}
// 将arr[1]这个标准值和arr[lt]这个 < v 的最后一个元素交换位置即可
swap(arr[l], arr[lt]);
// 分别对 < v 和 > v 部分进行递归处理
__quickSort3Ways(arr, l, lt - 1);
__quickSort3Ways(arr, gt, r);
}
template
// 2. 入口
void quickSort3Ways(T arr[], TINT32 num)
{
srand(time(NULL));
__quickSort3Ways(arr, 0, num - 1);
}