文章目录
本文以升序排序为例讲解快速排序法。
快速排序是这样的一个过程——通过选取一个中枢,将数组排成为“左侧部分-中枢-右侧部分”的形式,左侧部分的元素皆小于中枢,右侧部分的元素皆大于中枢,然后对左右侧子数组进行相同的操作,直到数组不可划分为止。因此,快速排序的要点有三方面,一是中枢的选取,二是左右划分的方法,三是递归主体。本文对于“左右划分”的阐述只包括双向扫描法,并不包括单向扫描法和填坑法。
一、中枢选取
如果选取的中枢越接近数组的中位数,那划分效果将会更好,以下介绍几种中枢选取方式。
(一) 直接取最左端或最右端元素
这是最简单的方法,但容易遇上所选取中枢逼近数组最值的情况。
size_t
GetPivotIndex(int nums[], size_t start_index, size_t end_index) {
if (start_index > end_index) {
std::swap(start_index, end_index);
}
if (start_index == end_index || end_index == 1 + start_index) {
return start_index;
}
return start_index;
}
size_t
GetPivotIndex(int nums[], size_t start_index, size_t end_index) {
if (start_index > end_index) {
std::swap(start_index, end_index);
}
if (start_index == end_index || end_index == 1 + start_index) {
return start_index;
}
return end_index - 1;
}
(二) 随机选取
size_t
GetPivotIndex(int nums[], size_t start_index, size_t end_index) {
if (start_index > end_index) {
std::swap(start_index, end_index);
}
if (start_index == end_index || end_index == 1 + start_index) {
return start_index;
}
return rand() % (end_index - start_index + 1) + start_index;
}
(三) 三数取中
三数取中是指选取数组的第一个数,最后一个数和最中间的那个数,然后取出这三个数的中位数作为中枢。
size_t // 思路更简洁的版本
GetPivotIndex(int nums[], size_t start_index, size_t end_index) {
if (start_index > end_index) {
std::swap(start_index, end_index);
}
if (start_index == end_index || end_index == 1 + start_index) {
return start_index;
}
size_t left_index = start_index;
size_t right_index = end_index - 1;
size_t pivot_index = left_index + (right_index - left_index) / 2;
if (nums[left_index] > nums[right_index]) {
std::swap(nums[left_index], nums[right_index]);
}
if (nums[left_index] > nums[pivot_index]) {
std::swap(nums[left_index], nums[pivot_index]);
}
if (nums[pivot_index] > nums[right_index]) {
std::swap(nums[pivot_index], nums[right_index]);
}
std::swap(nums[left_index], nums[pivot_index]);
pivot_index = left_index;
return pivot_index;
}
在上述代码中,三个 if 代码块是有顺序讲究的——必须先比较两端的元素,再比较相邻的元素,否则交换后结果有可能出错。
二、左右划分:双向扫描法
(一) 扫描结束时左右指针重合
若想让左右指针在扫描结束时重合,则需要注意几点。
第一,若中枢为最左端元素,则必须让右指针先移动;若中枢为最右端元素,则必须让左指针先移动。
第二,扫描时,左右指针至少要有一个和中枢相等时也能继续移动,否则会是排序出现错误。
第三,扫描完毕后,若中枢为最左端元素,则只有当左指针指向的元素小于中枢时才能交换两者位置;若中枢为最右端元素亦同理。
第四,第二点提及的交换位置完毕后还要更新中枢索引值,并将其返回。
以下为示例程序,笔者选定 0 为中枢的初始索引,即中枢为最左端元素。
size_t
Partition(int nums[], size_t start_index, size_t end_index) {
if (start_index + 1 == end_index || start_index == end_index) {
return start_index;
}
size_t pivot_index = start_index;
size_t left_index = start_index + 1;
size_t right_index = end_index - 1;
while (left_index < right_index) {
// 右指针在和中枢相等时选择继续移动,对应第二个注意点
while (left_index < right_index && nums[pivot_index] <= nums[right_index]) {
right_index--;
}
// 左指针在和中枢相等时选择暂时停止移动,跳出循环
while (left_index < right_index && nums[left_index] < nums[pivot_index]) {
left_index++;
}
std::swap(nums[left_index], nums[right_index]);
}
if (nums[left_index] < nums[pivot_index