OSDN Git Service

feat: update sort
authorIr1d <sirius.caffrey@gmail.com>
Thu, 23 Aug 2018 11:39:33 +0000 (19:39 +0800)
committerIr1d <sirius.caffrey@gmail.com>
Thu, 23 Aug 2018 11:39:33 +0000 (19:39 +0800)
closes #133

docs/basic/divide-and-conquer.md
docs/basic/sort.md

index 01e6506..61215d1 100644 (file)
@@ -24,6 +24,8 @@
 # 分治
 分治是一种极为重要的思想。顾名思义,分而治之,就是把大问题化小,再各个击破的过程。
 
+英文名是 divide and conquer.
+
 !!! 例题
     求数列中有多少个逆序对,所谓逆序对,是满足 $i < j$ 而且 $a[i] > a[j]$ 的数对 $(i, j)$ 的个数。
 
index 5fc3b2f..cd95188 100644 (file)
 
 ## 归并排序
 
+归并排序是 [分治](/basic/divide-and-conquer) 地来将一个数组排序。
+
+归并排序分为三个过程:
+1. 将数列划分为两部分(直接分,而不是像快速排序那样要求保证相对大小关系)
+2. 递归到两个子序列中分别进行归并排序
+3. 合并两个子序列
+
+不难发现,归并排序的核心是如何合并两个子序列,前两步都很好实现。
+
+其实合并的时候也不难操作。注意到两个子序列在第二步中已经保证了都是有序的了,第三步中实际上是想要把两个 **有序** 数列合并起来。
+
+```c++
+void merge(int ll, int rr) {
+  // 用来把 a[ll.. rr - 1] 这一区间的数排序。 t 数组是临时存放有序的版本用的。
+  if (rr - ll <= 1) return;
+  int md = ll + (rr - ll >> 1);
+  merge(ll, md); merge(md, rr);
+  int p = ll, q = md, s = ll;
+  while (s < rr) {
+    if (p >= md || (q < rr && a[p] > a[q])) {
+      t[s++] = a[q++];
+      // ans += md - p;
+    } else t[s++] = a[p++];
+  }
+  for (int i = ll; i < rr; ++i) a[i] = t[i];
+}
+```
+
+由于 `||` 是短路运算符,这里面 if 判断的情况是 “第一部分已经完全合并完了” 或者 “两个都没有合并完,且前一个的队首要大于后一个”,这两种情况都是要把后一个子序列的队首放到新序列的当前位置中。
+
+### 逆序对
+
+归并排序还可以用来求逆序对的个数。
+
+所谓逆序对,就是数对 $(i, j)$,满足 $a[i] > a[j]$ 且 $i < j$。
+
+可以用 [树状数组](/data-structure/intermediate/bit)、[线段树](/data-structure/intermediate/segment/) 等数据结构来求,也可以用归并排序来求。
+
+具体来说,上面归并排序中间注释掉的 `ans += md - p` 就是在统计逆序对个数。
+
+是因为,那里把靠后的数放到前面了(较小的数放在前面),所以这个数的原来位置以前的、比它大的数都会和他形成逆序对,而这个个数就是还没有合并进去的数的个数,即为 `md - p`。
+
+### 参考
+
+https://www.geeksforgeeks.org/merge-sort/
+
 ## 快速排序
 
-C 函数模板库实现了快速排序,即 `stdlib.h` 当中的 `qsort`。但在 OI 相关比赛当中,更为常见的库排序函数是 C++ `algorithm` 库中的 `std::sort` 函数。C++ 标准并未严格要求此函数的实现算法,具体实现取决于编译器,但他的时间复杂度是 $O(N\log N)$ 的。可以查阅 [std::sort()](http://www.cplusplus.com/reference/list/list/sort/)
+快速排序是 [分治](/basic/divide-and-conquer) 地来将一个数组排序。
+
+快速排序分为三个过程:
+1. 将数列划分为两部分(不是直接分,要求保证相对大小关系)
+2. 递归到两个子序列中分别进行快速排序
+3. 不用合并,因为此时数列已经完全有序
+
+和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。
+
+第三步中的序列已经分别有序且第一个序列中的数都小于第二个数,所以直接拼接起来就好了。
+
+具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。
+
+怎么操作呢?为了保证平均时间复杂度,一般是随机选择一个数 m 来当做两个子数列的分界。
+
+之后,维护一前一后两个指针 p 和 q,依次考虑当前的数是否放在了应该在的位置(前还是后),当前位置放对了之后,再移动指针继续处理,直到两个指针相遇。
+
+如果当前的数没放对呢?比如说如果后面的指针 q 遇到了一个比 m 小的数,那么可以交换 p 和 q 位置上的数,再把 p 向后移一位。
+
+其实,快速排序没有指定应如何具体实现第一步,不论是选择 m 的过程还是划分的过程,都不是只有一种实现方法。
+
+注意,一般我们说的快速排序的时间复杂度是平均为 $O(N\log N)$,最坏是 $O(n^2)$,只不过实践中几乎不可能达到最坏情况。
+
+其实,在选择 m 的过程中使用 [Median of Medians](https://en.wikipedia.org/wiki/Median_of_medians) 算法,就可以保证最坏时间复杂度为 $O(N\log N)$,但是由于j小微复杂,实践中一般不使用。
+
+### STL
+
+C 函数模板库实现了快速排序,即 `stdlib.h` 当中的 `qsort`。
+
+但在 OI 相关比赛当中,更为常见的库排序函数是 C++ `algorithm` 库中的 `std::sort` 函数。
+
+C++ 标准并未严格要求此函数的实现算法,具体实现取决于编译器。
+
+旧版 C++ 标准中仅要求它的 **平均** 时间复杂度是 $O(N\log N)$ 的,但在 C++11 中要求它的 **最坏** 时间复杂度是 $O(N\log N)$ 的。可以查阅 [std::sort()](https://en.cppreference.com/w/cpp/algorithm/sort)
+
+在 [libstdc++](https://github.com/mirrors/gcc/blob/master/libstdc++-v3/include/bits/stl_algo.h) 和 [libc++](http://llvm.org/svn/llvm-project/libcxx/trunk/include/algorithm) 中使用的都是 [Introsort](https://en.wikipedia.org/wiki/Introsort)。
+
+Introsort 限制了快速排序的分治深度,当分治达到一定深度之后,改用最坏时间复杂度为 $O(N\log N)$ 的排序算法(比如堆排序)来给子数组排序。
+
+Introsort 的这个限制使得它的最坏时间复杂度是 $O(N\log N)$ 的。
 
 快速用法:
 ```c++
@@ -31,6 +116,20 @@ std::sort(a, a + n);
 // 这句代码直接修改 a 数组里的元素顺序,使得现在它是从小到大排列的
 ```
 
+### 线性找第 k 大的数
+
+找第 k 大的数,最简单的方法是先排序,然后直接找到第 k 大的位置的元素。这样做的时间复杂度是 $O(N\log N)$,对于这个问题来说很不划算。事实上,我们有 $O(n)$ 的解法。
+
+考虑快速排序的划分过程,在快速排序的 “划分” 结束后,数列 $A[p \cdots r]]$ 被分成了 $A[p \cdots q]$ 和 $A[q+1 \cdots r]$,此时可以按照左边元素的个数($q - p + 1$)和 k 的大小关系来判断是只在左边还是只在右边递归地求解。
+
+可以证明,在期望意义下,程序的时间复杂度为 $O(n)$。
+
+### 参考
+
+https://stackoverflow.com/questions/22339240/what-algorithms-are-used-in-c11-stdsort-in-different-stl-implementations
+
+https://en.cppreference.com/w/cpp/algorithm/sort
+
 ## 计数排序
 
 计数排序可以在 $O(n)$ 的时间内排序,但是它要求所有的数都出现在一定的范围内。
@@ -42,6 +141,6 @@ std::sort(a, a + n);
 
 一般考虑的是某一范围内的整数,但是计数排序也可以和 [离散化](/misc/discrete) 一起使用,来对浮点数、大整数进行计数排序。
 
-## 参考
+### 参考
 
 https://www.geeksforgeeks.org/counting-sort/
\ No newline at end of file