定义
作为20世纪十大算法之一,快速算法被广泛应用于计算机工程的各个方面。快速排序主要有两个优点,其一它是原地排序,空间消耗基本忽略不计,其二是算法时间复杂度为O(nlogn)
,排序效率很高。
快速排序是一种递归算法。它将待排序数据集合基于某个基准值按照特定的原则分成左右两个子区间,然后分别将左右两个子区间继续递归快速排序。在基于基准值将待排序数据集合分成左右两个区间时,该基准值已经找到了其在最终排序结果中的位置,当最终两个子区间完成排序后,整个数据集合也完成排序。
快速排序是一种随机化算法。针对一些特殊的数据集合,例如已排序的数据集合,值完全相等的数据集合等,如果基于固定位置取基准值,会让算法的时间复杂度退化成O(n^2)
,严重降低排序的性能。因此,需要随机选择基准值,避免算法性能严重退化。
切分 partition
切分可分为双路切分和三路切分。
双路切分
在快速排序中,切分是将待排序数据基于某个基准值分成左右两个子区间。以从小到大排序为例,完成切分后基准值左区间内所有数据都小于等于基准值,右区间内所有数据都大于等于基准值。这样,基准值就已经位于最终排序结果中的位置。
切分代码如下:
std::ptrdiff_t partition(std::vector<T>& data, std::ptrdiff_t left, std::ptrdiff_t right)
{
// 随机化处理,随机选择区间内任一一个值作为切分的基准值,
// 注意:rd_类型为random_device的成员变量,具体见下基于双路切分的排序代码
std::ptrdiff_t random = left + rd_() % (right - left + 1);
std::swap(data[left], data[random]);
// 双路切分,循环不变量为data[left + 1, i - 1] <= data[left], data[j + 1, right] >= data[left]
std::ptrdiff_t i = left + 1;
std::ptrdiff_t j = right;
while (true) {
while ((i <= j) && (data[i] < data[left])) {
++i;
}
while ((i <= j) && (data[j] > data[left])) {
--j;
}
if (i >= j) {
break; // 如果i == j,那么i和j同时指向的值等于基准值,可以直接退出
}
std::swap(data[i], data[j]);
++i;
--j;
}
// 因为data[j + 1, right] >= data[left], 那么data[left] >= data[j],
// 此时交换data[left]和data[j],然后将j作为基准值在最终排序结果中的索引值返回
std::swap(data[left], data[j]);
return j;
}
快速排序算法代码如下:
template<typename T>
class quickSort {
public:
void sort(std::vector<T>& data)
{
sort(data, 0, static_cast<std::ptrdiff_t>(data.size()) - 1);
}
private:
void sort(std::vector<T>& data, std::ptrdiff_t left, std::ptrdiff_t right)
{
if (left >= right) {
return;
}
std::ptrdiff_t index = partition(data, left, right);
sort(data, left, index - 1);
sort(data, index + 1, right);
}
std::ptrdiff_t partition(std::vector<T>& data, std::ptrdiff_t left, std::ptrdiff_t right)
{
// 具体实现见上的双路切分代码
}
private:
std::random_device rd_;
};
双路切分排序复杂度
快速排序是一种随机算法,理论上其时间复杂度最差为O(n^2)
,但是在数据量超过一定很小的规模后,其概率是极其极其小的,基本不可能出现一种情况让算法时间复杂度达到最差。那么,应该基于数学期望的角度去看待,由于递归的深度期望值为O(logn)
,每层的处理为O(n)
,快速排序时间复杂度整体为O(nlogn)
。
空间复杂度期望为O(logn)
,与递归深度有关。
三路切分
在双路切分中,将待排序数据基于某个基准值分成左右两个子区间。那么,能不能把分成三个区间呢?以从小到大排序为例,左区间内所有数据都小于基准值,中间区间内所有数据都等于基准值,右区间内所有数据都大于基准值。
切分代码如下:
std::pair<std::ptrdiff_t, std::ptrdiff_t> partition(std::vector<T>& data, std::ptrdiff_t left, std::ptrdiff_t right)
{
// 随机化处理,随机选择区间内任一一个值作为切分的基准值
// 注意:rd_类型为random_device的成员变量,具体见下基于三路切分的排序代码
std::ptrdiff_t random = left + rd_() % (right - left + 1);
std::swap(data[left], data[random]);
// 三路切分,循环不变量为data[left + 1, lt] < data[left], [lt + 1, i - 1] == data[left], [gt, right] > data[left]
std::ptrdiff_t lt = left;
std::ptrdiff_t gt = right + 1;
std::ptrdiff_t i = left + 1;
while (i < gt) {
if (data[i] < data[left]) {
++lt;
std::swap(data[lt], data[i]);
++i;
} else if (data[i] > data[left]) {
--gt;
std::swap(data[gt], data[i]);
} else {
++i;
}
}
std::swap(data[left], data[lt]);
return std::make_pair(lt, gt - 1);
}
快速排序算法代码如下:
template<typename T>
class quickSort {
public:
void sort(std::vector<T>& data)
{
sort(data, 0, static_cast<std::ptrdiff_t>(data.size()) - 1);
}
private:
void sort(std::vector<T>& data, std::ptrdiff_t left, std::ptrdiff_t right)
{
if (left >= right) {
return;
}
auto index_pair = partition(data, left, right);
sort(data, left, index_pair.first - 1);
sort(data, index_pair.second + 1, right);
}
std::pair<std::ptrdiff_t, std::ptrdiff_t> partition(std::vector<T>& data, std::ptrdiff_t left, std::ptrdiff_t right)
{
// 具体实现见上的双路切分代码
}
private:
std::random_device rd_;
};
三路切分排序复杂度
快速排序是一种随机算法,应该基于数学期望的角度去看待,由于递归的深度期望值为O(logn)
,每层的处理为O(n)
,快速排序时间复杂度整体为O(nlogn)
。空间复杂度期望为O(logn)
,与递归深度有关,但是由于递归处理每次都会产生一个pair,耗费空间更多,而且在重复元素较少的情况下交换次数相对双路切分更多,因此其排序耗时更多。但是如果待排序数据中的数据完全重复相等,那么算法的时间复杂度为O(n)
。
切分应用
由于双路切分后基准值就已经位于最终排序结果中的位置,因此双路切分常常可以用于Select-K的问题,避免对整个数据集合进行排序。三路切分可应用经典的荷兰国旗问题,适合将数据集合分成三部分。