排序
排序算法
计数排序
-
计数排序是一种计算每个数字出现次数后做值域上的前缀和以对各元素排序的排序方式。
-
计数排序是一种非比较排序,即不基于比较的排序。计数排序是稳定的。
-
具体实现:依次枚举每个元素,将其丢进其关键字对应的桶里(这些桶的管辖范围都是 \(1\),也可以认为计数排序就是桶排序的退化)。全部放置完毕后,从小到大遍历每个桶,将桶内元素顺序取出。
-
计数排序的复杂度为 \(O(n+w)\),其中 \(w\) 为关键字值域大小。
基数排序
-
基数排序是一种对排序各元素的各个关键字依次排序的排序方式,或者说思想。
-
基数排序是一种非比较排序。基数排序是稳定的。
-
基数排序需要一种稳定算法来完成内层的对关键字排序。
-
具体实现:
-
枚举各个关键字并排序。
-
应当在枚举完毕后 \(k\) 个关键字后使得数组按照后 \(k\) 个关键字有序。
-
具体来说,就是先对第 \(k\) 关键字排序,然后对第 \(k-1\) 关键字稳定排序,以此类推。
-
正序倒也可以,但是需要的不是稳定排序而是分进不同的桶内,每个桶分别排序,带来的空间复杂度难以忽视。
-
-
实际使用中,内部排序算法通常为计数排序。此时有总复杂度为 \(O(\sum\limits_{i=1}^k(n+w_i))\),其中 \(w_i\) 为第 \(i\) 关键字的值域大小(相当于做了 \(k\) 次计数排序)。
选择排序
-
选择排序是一种不断把最小值换到最前面的排序算法。
-
具体来说,不断从左到右遍历整个序列,第 \(i\) 次遍历时将 \(a_{i\sim n}\) 中的最小值交换到 \(i\) 处(通过不断比较第 \(a_i\) 和当前扫到的元素的大小并交换)。
-
选择排序是一种比较排序,即基于比较的排序。选择排序是不稳定的,因可能在交换时将原本的 \(a_i\) 换到乱七八糟的地方。
-
选择排序 \(k\) 次后,至少前 \(k\) 个位置是正确的。
-
容易看出其复杂度为 \(\Theta(n^2)\)。
冒泡排序
-
冒泡排序是一种不断交换相邻逆序对的排序算法。
-
具体来说,不断从左到右遍历整个序列,交换相邻的逆序对,直到一次遍历中发现没有相邻逆序对。
-
冒泡排序是一种基于比较的排序。冒泡排序是稳定的。
-
冒泡排序的交换次数是总逆序对数。这是显然的。
-
和比较排序恰好相反,冒泡排序 \(k\) 次后,至少后 \(k\) 个数字的位置是正确的。
-
冒泡排序 \(k\) 次后,\(i\) 处元素为 \([1,i+k]\) 间的最小值,除非它已经用于 \(<i\) 的地方(即这个结论必须从前向后递推)。
快速排序
-
快速排序是一种值域随机分治的排序算法。
-
具体来说,不断随机选取值域意义上的分割点 \(mid\),将 \(\leqslant mid\) 的换到前面,\(>mid\) 的换到后面,然后分治,实践中为了方便 \(mid\) 会取一个集合内的元素。
-
快速排序是一种基于比较的排序。快速排序是不稳定的,也是因为交换。
-
容易看出该算法的复杂度是期望 \(O(n\log)\) 的(在 \(mid\) 选取良好时),但有几率退化到上界 \(O(n^2)\),故在实践中很少单独使用,不过其性能因为访问连续性非常优越。
-
应当指出的是快排思想非常利于第 k 小的求解,将双侧递归改为单侧递归即可,复杂度为期望 \(O(n)\)。
归并排序
-
归并排序是一种序列分治的排序算法。
-
具体来说,不断将序列尽量均分为两部分,分治之然后将两个有序序列归并,归并过程使用双指针实现。
-
归并排序是一种基于比较的排序。归并排序是稳定的。
-
容易看出该算法是严格 \(O(n\log)\) 的。
-
归并排序的归并思想是分治的经典运用,在求逆序对、cdq 等时常使用它的形式。
std::sort 与 std::stable_sort
-
前者是内省排序:先用快排,分割次数过多时转局部堆排序防退化,当范围足够小时使用插入排序(注意这一步可能会严重增加比较次数,在 \(cmp\) 不是 \(O(1)\) 时影响较大)。
-
后者在有额外空间的时候是严格归并排序,否则是最优原地后缀排序算法。请注意,这个名字很长的算法的复杂度是 \(O(n\log^2 n)\)。
例题
CF1768D Lucky Permutation
- 见图论杂项-对换环。
冒泡排序
- 当初开的单独的,现在暂时不想整回来。
AGC024B,CF1367F1/2 Flying Sort
-
题意:给出序列 \(a_{1\sim n}\),允许取出 \(a_i\) 后放到开头/结尾,求排到非降的最小次数。
-
数据范围:\(n\leqslant 2\times 10^5\),第一题是排列,第二题的序列不重。
-
首先考虑排列的简单情况。正难则反,与其考虑怎么操作,不如考虑哪些数能不操作:发现提前、不动、提后刚好构成从左到右的三段。
-
则问题变成求最长连续子序列,这里的连续指的是值域上连续,它们就是“不动”部分的长度。\(n-len\) 即为答案。显然可以 dp,但也显然意义不大,不如直接建图,容易看出是森林,求最大深度即可。
-
考虑推广到非排列情况。注意到此时的最长连续子序列也许是可以掐头去尾的,即中间部分的必须完整,但开头和结尾对应的值可以不全在该子序列里,从而允许诸如
5 7 6 5 7
中选5 6 7
的操作。 -
考虑对边分类。若 \(a_i=a_j \land \nexists (i<k<j \land a_k=a_i)\) 或 \(a_i+1=a_j \land \nexists ((k>i \land a_k=a_i) \lor (k<j \land a_k=a_j))\),则为中边,否则为头边或尾边,然后 dfs 时记录是否走过头/尾边(显然只能走一次)。方便起见,我们规定边是 \(j\to i\) 的。
-
但直接这么做复杂度可能不对(点的度数不对了,图的结构破坏了),故考虑只保留中边然后记一下头/尾边的最大延伸,只对中边 dfs 然后加上头/尾边的贡献。具体来说,将最长连续子序列分为头(不完整),中(完整),尾(不完整)三部分,从每个没有中边入度的点 \(i\) 出发开始 dfs,初始长度为 \(nxt_i+1\)(\(nxt\) 即为尾边的极大延伸),在任意时刻可以 \(+pre_{now}\) 对最长连续子序列的长度贡献,显然只有无入度点才有必要有 \(nxt\),无出度点才有必要有 \(pre\)。注意这样做没有考虑到中是空的情况,在求 \(nxt,pre\) 过程中特判就好了。
-
复杂度总为 \(O(n)\)。