OSDN Git Service

Merge branch 'master' into basic
authorXeonacid <h.dwwwwww@gmail.com>
Mon, 19 Oct 2020 09:07:03 +0000 (17:07 +0800)
committerGitHub <noreply@github.com>
Mon, 19 Oct 2020 09:07:03 +0000 (17:07 +0800)
1  2 
docs/basic/quick-sort.md

@@@ -1,10 -1,8 +1,12 @@@
 -快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),简称快排,是一种被广泛运用的排序算法。
 +本页面将简要介绍快速排序。
 +
 +## 简介
 +
 +快速排序(英语:Quicksort),又称分区交换排序(英语:partition-exchange sort),简称快排,是一种被广泛运用的排序算法。
  
- ## 工作原理
+ ## 基本原理与实现
+ ### 原理
  
  快速排序的工作原理是通过 [分治](./divide-and-conquer.md) 的方式来将一个数组排序。
  
  
  第三步中的序列已经分别有序且第一个序列中的数都小于第二个数,所以直接拼接起来就好了。
  
 -### 性质
 +### 线性找第 k 大的数
 +
 +找第 $k$ 大的数(K-th order statistic),最简单的方法是先排序,然后直接找到第 $k$ 大的位置的元素。这样做的时间复杂度是 $O(n\log n)$ ,对于这个问题来说很不划算。事实上存在时间复杂度 $O(n)$ 的解法。
 +
 +考虑快速排序的划分过程,在快速排序的「划分」结束后,数列 $A_{p} \cdots A_{r}$ 被分成了 $A_{p} \cdots A_{q}$ 和 $A_{q+1} \cdots A_{r}$ ,此时可以按照左边元素的个数( $q - p + 1$ )和 $k$ 的大小关系来判断是只在左边还是只在右边递归地求解。
 +
 +可以证明,在期望意义下,程序的时间复杂度为 $O(n)$ 。
 +
 +## 性质
 +
 +### 稳定性
 +
 +快速排序是一种不稳定的排序算法。
  
 --  **稳定性:** 快速排序是一种不稳定的排序方法;
 --  **时间复杂度:** 快速排序的最佳时间复杂度和平均时间复杂度为 $O(n\log n)$ ,最坏时间复杂度为 $O(n^2)$ 。然而,实践中几乎不可能达到最坏情况,且因为快速排序的内存访问遵循局部性原理,多数情况下快速排序的表现大幅优于堆排序等其他复杂度为 $O(n \log n)$ 的排序算法。[^ref1]
 +### 时间复杂度
 +
 +快速排序的最优时间复杂度和平均时间复杂度为 $O(n\log n)$ ,最坏时间复杂度为 $O(n^2)$ 。
  
- 实践中几乎不可能达到最坏情况,且因为快速排序的内存访问遵循局部性原理,多数情况下快速排序的表现大幅优于堆排序等其他复杂度为 $O(n \log n)$ 的排序算法。[^ref1]
- 在选择 $m$ 的过程中,使用 [Median of Medians](https://en.wikipedia.org/wiki/Median_of_medians) 算法就可以保证最坏时间复杂度为 $O(n\log n)$ 。但是其过于复杂,实践中一般不使用。
- ## 代码实现
- ### C++[^ref2]
+ ### 实现(C++)[^ref2]
  
  ```cpp
  struct Range {
@@@ -87,18 -117,51 +134,16 @@@ void quick_sort(T arr[], const int len
  
  内省排序将快速排序的最大递归深度限制为 $\lfloor \log_2n \rfloor$ ,超过限制时就转换为堆排序。这样既保留了快速排序内存访问的局部性,又可以防止快速排序在某些情况下性能退化为 $O(n^2)$ 。
  
 -2000 年 6 月,SGI C++ STL 的 stl_algo.h 中 sort()函数的实现采用了内省排序算法。
 +从 2000 年 6 月起,SGI C++ STL 的 `stl_algo.h` 中 `sort()` 函数的实现采用了内省排序算法。
  
- ## 三路快速排序
+ ## 线性找第 k 大的数
  
三路快速排序(英语:3-way Radix Quicksort)是快速排序和 [基数排序](radix-sort.md) 的混合。它的算法思想基于 [荷兰国旗问题](https://en.wikipedia.org/wiki/Dutch_national_flag_problem) 的解法
找第 $k$ 大的数(K-th order statistic),最简单的方法是先排序,然后直接找到第 $k$ 大的位置的元素。这样做的时间复杂度是 $O(n\log n)$ ,对于这个问题来说很不划算
  
与原始的快速排序不同,三路快速排序在随机选取分界点 $m$ 后,将待排数列划分为三个部分:小于 $m$ 、等于 $m$ 以及大于 $m$ 
我们可以借助快速排序的思想解决这个问题。考虑快速排序的划分过程,在快速排序的「划分」结束后,数列 $A_{p} \cdots A_{r}$ 被分成了 $A_{p} \cdots A_{q}$ 和 $A_{q+1} \cdots A_{r}$ ,此时可以按照左边元素的个数( $q - p + 1$ )和 $k$ 的大小关系来判断是只在左边还是只在右边递归地求解
  
- 三路快速排序在处理含有多个重复值的数组时,效率远高于原始快速排序。
- 三路快速排序的最优时间复杂度为 $O(n)$ 。
+ 可以证明,在期望意义下,程序的时间复杂度为 $O(n)$ 。
  
 -### 实现(C++)
 -
 -```cpp
 -// 模板的T参数表示元素的类型,此类型需要定义小于(<)运算
 -template <typename T>
 -// arr为查找范围数组,rk为需要查找的排名(从0开始),len为数组长度
 -T find_kth_element(T arr[], int rk, const int len) {
 -  if (len <= 1) return arr[0];
 -  // 随机选择基准(pivot)
 -  const T pivot = arr[rand() % len];
 -  // i:当前操作的元素
 -  // j:第一个等于pivot的元素
 -  // k:第一个大于pivot的元素
 -  int i = 0, j = 0, k = len;
 -  // 完成一趟三路快排,将序列分为:小于pivot的元素 | 等于pivot的元素 |
 -  // 大于pivot的元素
 -  while (i < k) {
 -    if (arr[i] < pivot)
 -      swap(arr[i++], arr[j++]);
 -    else if (pivot < arr[i])
 -      swap(arr[i], arr[--k]);
 -    else
 -      i++;
 -  }
 -  // 根据要找的排名与两条分界线的位置,去不同的区间递归查找第k大的数
 -  // 如果小于pivot的元素个数比k多,则第k大的元素一定是一个小于pivot的元素
 -  if (rk < j) return find_kth_element(arr, rk, j);
 -  // 否则,如果小于pivot和等于pivot的元素加起来也没有k多,则第k大的元素一定是一个大于pivot的元素
 -  else if (rk >= k)
 -    return find_kth_element(arr + k, rk - k, len - k);
 -  // 否则,pivot就是第k大的元素
 -  return pivot;
 -}
 -```
 -
  ## 参考资料与注释
  
  [^ref1]:  [C++ 性能榨汁机之局部性原理 - I'm Root lee !](http://irootlee.com/juicer_locality/)