sort
父之齒隨行,兄之齒鴈行,朋友不相踰。《禮記.王制》
sort
「排序」。把一群數字由小到大排好。
排序演算法類型
一、使用循序資料結構,例如array、list,將數字依序放進去,執行排序演算法。
二、使用具備排序功效的資料結構,例如binary search tree、binary heap,將數字整個倒進去、整個倒出來,完成排序。
本文討論第一種類型。第二種類型請見本站文件「binary search tree」。
排序原理
一、對調(比較)。二、放置(索引)。
對調式排序,將小數字往前挪、大數字往後挪。基礎的對調式排序是selection sort。當前最快的對調式排序是pdqsort。
放置式排序,需要額外的記憶體空間來放置數字,但是計算時間遠少於對調式排序。基礎的放置式排序是counting sort。當前最快的放置式排序是radix sort。
英文說法:一、比較式排序演算法comparison-based sorting algorithm、交換排序exchange sort。二、非比較式排序演算法non-comparison-based sorting algorithm、分布排序distribution sort。
等價於數字的東西也可以排序
字元也可以排序。字元就是ASCII碼、就是數字。
指標也可以排序。指標就是記憶體位址、就是數字。
資料也可以排序。只要資料的某個特定欄位是數字,這個欄位稱作鍵值(key)。
等價於數組的東西也可以排序
數組(tuple)也可以排序。其中一種方式:先比較第一維度;如果平手,再比較第二維度;如果又平手,再比較第三維度;以此類推。這個比較方式稱作字典序(lexicographical order)。
字串也可以排序。字串就是一串ASCII碼、就是數組。
英文單字也可以排序。英文單字就是一串字母、字母可以編號、整體宛如數組。英文字典,採用了字典序,排序所有英文單字。
UVa 482
能夠建立全序的東西也可以排序
所有東西均可兩兩比較大小,而且沒有矛盾,稱作全序(total order)。
排序對象沒必要是數字。沒有數字,只有比較;沒有絕對大小,只有相對大小;無法使用放置式排序,只能使用對調式排序。
間接排序
排序會搬動資料,但是大多數時候我們不希望搬動資料。此時可以取出資料的記憶體位址,另外建立指標,對指標進行排序,但是比較對象是指標所指的資料鍵值大小。
也有人使用陣列索引值,道理跟指標相同。
stable
鍵值相同的資料們,原本排在前頭的,排序後仍在前頭;原本排在後頭的,排序後仍在後頭。這稱作「穩定的」排序演算法,鍵值相同、順序不變。
只要是放在陣列的資料,任何一種排序演算法,都可以擴展成穩定的排序演算法。你想到解決方法了嗎?
comparison function(comparator)
對調式排序必須使用「比較函數」。
C標準函式庫的qsort(),其參數cmp就是比較函數。意義是小於,回傳值是int變數,正零負代表< = >。
C++標準函式庫的sort(),其參數cmp就是比較函數。意義是小於,回傳值是bool變數,true false代表< ≮。
C++標準函式庫也有比較函數less(),以便作為參數預設值。
sorting network
「排序網路」。兩兩比較的先後次序圖。
每一種對調式排序演算法,都可以畫出排序網路。但是也有例外,例如quicksort的加速技巧:三個中位數的中位數,必須知道數字多寡,才能決定比較對象,此時就無法畫出排序網路。
事情也可以反過來。設計一個新的排序網路,就得到一種新的對調式排序演算法。
排序網路的深度,即是排序演算法的步驟數量。深度下限,已被證明是Ω(NlogN),不可能更少了。深度最小值,則是NP-complete問題。換句話說,找到一個深度盡量小的排序網路,目前沒有(以後大概也不會有)快速的演算法。
設計排序網路演算法,製造排序演算法,以演算法產生演算法,這件事簡直超酷的。但是很不幸的,這條路很難走。
UVa 1117
排序演算法
| average worst | extra | stable | case case | space | sort ---------------+---------------------+-------+---- searching sort | O(NR) O(NR) | O(N) | ✓ → my favorite selection sort | O(NN) O(NN) | O(1) | ✓ ---------------+---------------------+-------+---- bubble sort | O(NN) O(NN) | O(1) | ✓ gnome sort | O(NN) O(NN) | O(1) | ✓ best insertion sort | O(NN) O(NN) | O(1) | ✓ stable Shell sort | O(NN) O(NN) | O(1) | comparison- merge sort | O(NlogN) O(NlogN) | O(N) | ✓ based Timsort | O(NlogN) O(NlogN) | O(N) | ✓ → sort ---------------+---------------------+-------+---- heapsort | O(NlogN) O(NlogN) | O(1) | splaysort | O(NlogN) O(NlogN) | O(1) | ---------------+---------------------+-------+---- quicksort | O(NlogN) O(NN) | O(N) | best introsort | O(NlogN) O(NlogN) | O(N) | comparison- blockquicksort | O(NlogN) O(NlogN) | O(N) | based pdqsort | O(NlogN) O(NlogN) | O(N) | → sort ---------------+---------------------+-------+---- counting sort | O(N+R) O(N+R) | O(R) | ✓ best radix sort | O((N+β)D) O((N+β)D) | O(β) | ✓ → sort ---------------+---------------------+-------+---- bucket sort | O(N+β) O((N+β)D) | O(βD) | ✓ flashsort | O(N+β) O((N+β)D) | O(βD) | ✓ best samplesort | O(NlogN) O(NN) | O(N) | ✓ parallel IPS⁴o | O(NlogN) O(NN) | O(N) | ✓ → sort ---------------+---------------------+-------+---- sleep sort | O(N+R) O(N+R) | O(N) | ✓ → best of the best
N: input size R: input range β: radix / bucket count (β = 2, generally speaking) D: digits / recursion depth (D = logᵦR) (R = βᴰ)
Searching Sort
搜尋排序。依序枚舉每一個整數,看看陣列裡頭有沒有。
令最小值是A,最大值是B,從最小值到最大值有R = B-A+1個整數。時間複雜度O(RN)。
search
眾裏尋他千百度,驀然回首,那人卻在,燈火闌珊處。《辛棄疾.青玉案》
search
「搜尋」。找出一個數字的位置。
search in array: sequential search
循序搜尋。依序看一遍。時間複雜度O(N)。
search in sorted array: binary search
二元搜尋。divide-and-conquer method。已排序陣列,對半分成兩邊,遞迴搜尋其中一邊。時間複雜度O(logN)。
經典應用:資料恰有兩種性質,明顯地分做兩邊,可用binary search找到分界之處。
UVa 10611 10077
search in sorted array: interpolation search
內插搜尋。binary search加強版。根據陣列左右兩端數字大小,一次內插,作為分割位置。
search in sorted unbound array: doubling search
倍增搜尋。主持人心中有一正整數,我們可以一直猜他心中的正整數,但是他只會回答「太高」或「太低」或「正確」。請問要怎麼猜可以最快猜到他心中的正整數呢?
有個類似的團康遊戲叫做「終極密碼」,常常在綜藝節目出現。「終極密碼」的規則比較不一樣,數字範圍通常只有1到100,而且是很多個人輪流猜,越晚猜出來越好。這裡的猜數字遊戲,數字範圍是1到無限大,只有一個人猜,越早猜出來越好。
言歸正傳。從1開始一個一個往上猜,實在太慢了。比較快的猜法,是將問題分成兩個步驟,第一個步驟是先確定範圍,第二個步驟再來縮小範圍。
確定範圍:可以從1、2、4、8、……這個順序去猜。如果主持人不斷回答太低,我們就不斷往大數字猜,一直到主持人回答太高為止。如果主持人心中的正整數為N,則可以用O(logN)的時間得到一個合理的範圍,N會落在(2ᵏ⁻¹, 2ᵏ]之間。
縮小範圍:可以使用binary search!從(2ᵏ⁻¹, 2ᵏ]之間找出正確的正整數,只需O(logN)時間。
二元搜尋和倍增搜尋相互對立,前者除以二、後者乘以二。
search in sorted arrays: fractional cascading
search in sorted matrix: saddleback search
鞍背搜尋。二維矩陣裡的數字經過排序,往右、往下都呈現嚴格遞增,此時有個很巧妙的搜尋方式,時間複雜度O(X+Y),X與Y分別為矩陣的長與寬。
首先在腦中將矩陣的數字切割為大於n的一邊(右下)與不大於n的一邊(左上)。現在我們所要作的,便是遊走於大與小的邊緣來尋找n!從矩陣的右上角開始,嘗試走到左下角,若走到了大於n的一邊,就立即往不大於n的另一邊移動,反之亦同。
各位可以想想當找到目標數字時,應該往左還是往下走好?當矩陣數字是非嚴格遞增的時候會產生什麼問題?
UVa 12192
select
羽望見良麾蓋,策馬刺良於萬眾之中,斬其首還。《三國志》
select
「選擇」。找到特定名次的數字,例如第k小的數字。
最簡單的方式就是先排序、再搜尋。以下探討更快的方法。
UVa 10041 10107 11875
select in array: quickselect
quicksort只遞迴其中一邊。平均時間複雜度O(N),最差時間複雜度O(N²)。
可以進化成introselect、pdqselect。最差時間複雜度O(N)。
可以直接使用C++標準函式庫的nth_element(),但是細節不盡相同。
select in array: median-of-medians algorithm
找到中位數們的中位數(中中數),將數字分成大的一邊和小的一邊,遞迴找其中一邊。時間複雜度O(N),但是不實用。
1. 五個五個分堆,最後一堆可以沒有滿。 第一堆 ● ● ● ● ● 第二堆 ● ● ● ● ● 第三堆 ● ● ● ● ● 第四堆 ● ● ● ● ● 第五堆 ● ● ● ● ● 第六堆 ● ● ● 2. 每堆各自排序。然後求出每堆的中位數 ○。 小 → 大 ↑ ● ● ○ ● ● 沒 ● ● ○ ● ● 有 ● ● ○ ● ● 順 ● ● ○ ● ● 序 ● ● ○ ● ● ↓ ● ○ ● ← 最後一堆對齊一下比較好看 3. 求出中位數們的中位數 x。遞迴套用此演算法求得 x。 小 → 大 ↑ ● ● ○ ● ● 沒 ● ● ○ ● ● 有 ● ● ○ ● ● 順 ● ● ○ ● ● 序 ● ● x ● ● ← 中位數可能在任何一個地方 ↓ ● ○ ● 4. 將全部的數字分成兩邊,一邊小於 x ,一邊大於等於 x。 小於 x ←| |→ 大於等於 x ... ● ● ● ● ● ● x ● ● ● ● ● ● ● ● ... 5. 看看 k 是在哪一邊。遞迴下去找出答案。
時間複雜度証明 小 → 大 第一堆 ● ● ○ ● ● 第二堆 ● ● ○ ● ● 第三堆 ● ● ○ ● ● 第四堆 ● ● ○ ● ● 第五堆 ● ● x ● ● 第六堆 ● ○ ● 依照中位數們的大小,重新排列每一堆。 小 → 大 第四堆 中 ● ● ○ ● ● 第二堆 位 ● ● ○ ● ● 第五堆 數 ● ● x ● ● ← 中位數 x 變成排在中間 第一堆 小 ● ● ○ ● ● 第三堆 ↓ ● ● ○ ● ● 第六堆 大 ● ○ ● 觀察一定小於等於x的數、一定大於等於x的數。 一定小於 一定大於 等於x的數 等於x的數 ◊ ◊ ◊ ● ● ● ● ○ ● ● ◊ ◊ ◊ ● ● ● ● ○ ● ● ◊ ◊ x ● ● ● ● x ◊ ◊ ● ● ○ ● ● ● ● ◊ ◊ ◊ ● ● ○ ● ● ● ● ◊ ◊ ◊ ● ○ ● ● ◊ ◊ 推得「小於等於 x 的數至少有 3n/10 - 6 個。大於 x 的數至多有 7n/10 + 6 個。」 推得「大於等於 x 的數至少有 3n/10 - 6 個。小於 x 的數至多有 7n/10 + 6 個。」 至多7n/10 + 6 差不多至多7n/10 + 6 小於 x ←| |→ 大於等於 x ... ● ● ● ● ● ● x ● ● ● ● ● ● ● ● ... 分兩邊後, 小於的那一邊至多有 7n/10 + 6 個, 大於等於的那一邊差不多至多有 7n/10 + 6 個。 遞迴下去,總時間複雜度 O(n)。
select in array: Floyd–Rivest algorithm
據說是實務上速度最快的選擇演算法。
select in sorted array
O(1)。
select in sorted arrays
找到中中數,然後每列皆實施binary search,最後所有數字分為大的一邊和小的一邊,遞迴找其中一邊。每回合至少削減一半的列的一半。每列各自建立首尾索引值,記錄剩下的數字。
每回合需時O(XlogY),總共O(logY)回合,時間複雜度O(XlogYlogY)。X是陣列數量,Y是陣列長度。
簡單來說,此演算法是搜尋中中數、分兩邊、遞迴一邊。
select in sorted arrays
找到X個中位數,然後找到最大中位數、最小中位數。每回合削減最大中位數的右半或最小中位數的左半。每回合刪除一列的一半,總共O(XlogY)回合。
以二元搜尋樹儲存X個中位數,每回合可以快速找到最大中位數、最小中位數。總共操作O(XlogY)次,每次操作O(logX),時間複雜度O(XlogXlogY)。X是陣列數量,Y是陣列長度。
select in sorted matrix
切成一列一列,找到中中數。利用saddleback search將矩陣分為大的一邊和小的一邊,遞迴找其中一邊。
每回合需時O(N),總共O(logN)回合,時間複雜度O(NlogN)。
select in sorted matrix
divide-and-conquer method。O(N)。
statistic
extremum
「極值」。細分為最大值maximum、最小值minimum。
找到極值本身,甚至找到出現次數、出現位置。
循序搜尋。時間複雜度O(N),額外的空間複雜度O(1)。
mode
「眾數」。出現最多次的數字。
先排序再搜尋。採用對調式排序,例如merge sort,時間複雜度O(NlogN),額外的空間複雜度O(1)。採用放置式排序,例如counting sort,時間複雜度O(N+R),額外的空間複雜度O(R)。
longest plateau
一串數字當中,連續出現最多次的數字,找到出現次數。
已排序陣列,有更簡單的做法。
majority
「過半數」。過半的數字。
找到其中一個過半數。時間複雜度O(N),額外的空間複雜度O(1)。
distinct count
相異數字個數。
先排序再搜尋。時間複雜度O(NlogN)或O(N+R)。
近似演算法是「Flajolet–Martin sketch」和「HyperLogLog」。
maximum gap
一串數字,排序之後,相鄰兩數的差值,其中最大的差值稱作「最大間隔」。可能同時有許多個「最大間隔」。
flashsort可以找出所有最大間隔。時間複雜度O(N)。
一、找到最大值max、最小值min。 二、建立N-1個桶,每個桶的範圍大小是 D = (max-min)/(N-1)。 三、除了最大值、最小值, 剩下的N-2個數字通通塞入桶, 數字 x 塞到 floor[(x-min)/D]。 四、N-1個桶,N-2個數字,必有空桶。 最大間隔的兩數必定跨過空桶,最大間隔大於D。 最大間隔的兩數不在同一個桶。 五、找到每個桶的最大值bucket[i].max、最小值bucket[i].min。 六、刪除空桶,重新編號。 max { bucket[i+1].min - bucket[i].max } 即是最大間隔。
minimum gap
先排序再搜尋。時間複雜度O(NlogN)或O(N+R)。【尚待確認】
pairwise sum
X+Y
窮舉法。O(N²)。
Fourier transform。O(N + RlogR)。
sort in X+Y
先窮舉,再排序。O(N²logN²) = O(N²logN)。
先排序,再N-way merge。O(N²logN)。
Fourier transform。O(N + RlogR)。
search in X+Y
先排序。想像矩陣add[i][j] = a[i] + b[j],已排序矩陣的搜尋。O(NlogN)。
select in X+Y
先排序。想像矩陣add[i][j] = a[i] + b[j],已排序矩陣的選擇。O(NlogN)。
search in 2D X+Y
找y/x。最差O(Nlog²N),平均Θ(NlogN)。
select in 2D X+Y
找y/x。Θ(NlogN)。
extremum in 2D X+Y
找y/x。必在凸包上。Θ(NlogN)。
pairwise distance
all row minimums
以下介紹三個特殊矩陣,以及其「每一個橫條(直條)的最小值」的演算法。
Monge matrix ⇒ totally monotone matrix ⇒ monotone matrix。從特例到通例,限制從強到弱,數量從少到多,演算法從快到慢。
Monge matrix (concave) [standard form] ↖ + ↘ ≤ ↗ + ↙ 主對角線和,小於等於次對角線和 [row form] ↘ - ↙ ≤ ↗ - ↖ 橫條各處斜率,往上遞增 [column form] ↘ - ↗ ≤ ↙ - ↖ 直條各處斜率,往左遞增 totally monotone matrix [row type] if ↙ ≤ ↘ then ↖ ≤ ↗ 橫條小於等於關係,往上遞增 [column type] if ↗ ≤ ↘ then ↖ ≤ ↙ 直條小於等於關係,往左遞增 monotone matrix [row type] argmin ⤷ 橫條最小值位置,往下遞增 [column type] argmin ⤵ 直條最小值位置,往右遞增
Monge matrix
矩陣當中所有田字四個格子皆滿足不等式:↖ + ↘ ≤ ↗ + ↙。
不等式分成凹凸兩種,大小相反、性質顛倒。以下以凹為主。
concave Monge matrix: a[i][j] + a[i+1][j+1] ≤ a[i][j+1] + a[i+1][j] convex Monge matrix: a[i][j] + a[i+1][j+1] ≥ a[i][j+1] + a[i+1][j]
另一種等價的寫法:所有子矩形的四個端點皆滿足不等式。
a[i₁][j₁] + a[i₂][j₂] ≤ a[i₁][j₂] + a[i₂][j₁] when i₁ < i₂ and j₁ < j₂
移項一下,得到橫條(直條)斜率遞減的形式。彷彿凹函數。
Monge matrix乘以非負倍率,仍是Monge matrix。
Monge matrix相加,仍是Monge matrix。
Monge matrix has "non-negative linearity": (1) X is Monge matrix, k ≥ 0 ⇒ kX is Monge matrix (2) X and Y are Monge matrices ⇒ X+Y is Monge matrix
Monge matrix刪除橫條(直條),仍是Monge matrix。
Monge matrix橫條(直條)加上同一數,仍是Monge matrix。
symmetric Monge matrix = Supnick matrix。恰好沿著主對角線對稱。
convex/concave Monge matrix = submodular/supermodular function。矩陣行列索引值,視作區間左右邊界。
舉個實際範例。例如二維平面上,凸四邊形上下邊相加 ≤ 兩條對角線相加,距離雙向均等,符合Supnick matrix不等式。
Supnick matrix的延伸意義是:不交換最好。例如尋找最佳排列的問題travelling salesman problem、assignment problem,若滿足Supnick matrix,則有高速演算法。
totally monotone matrix
分成凹凸兩種,又細分成橫直兩種,共四種。以下以凹為主。
橫條(直條)一旦出現≤,上方(左方)也都是≤。
concave row version: if ↙ ≤ ↘ then ↖ ≤ ↗ concave column version: if ↗ ≤ ↘ then ↖ ≤ ↙
自然界難以見到totally monotone matrix,其定義是計算學家故意設計的,是從monge matrix的不等式所推導出來的性質。
Monge matrix ⇒ totally monotone matrix的證明:橫條(直條)斜率遞減的形式當中,若左式非負,則右式也非負。
monotone matrix
分成凹凸兩種,又細分成橫直兩種,共四種。以下以凹為主。
每個橫條(直條)的第一個最小值位置,往右(往下)遞增。最小值可能有許多個,所謂的第一個是指索引值最小者。
row version: argmin r₀ ≤ argmin r₁ ≤ argmin r₂ ≤ ... column version: argmin c₀ ≤ argmin c₁ ≤ argmin c₂ ≤ ...
自然界難以見到monotone matrix,其定義是計算學家故意設計的,是從totally monotone matrix的不等式所推導出來的性質。
totally monotone matrix ⇒ monotone matrix的證明:因為上方橫條(左方直條)小於等於關係不變,所以最小值位置只可能一樣、或者更往左(更往上)。
尋找monotone matrix的每個橫條(直條)的第一個最小值
分治法。找到中央橫條的最小值,然後左上矩陣與右下矩陣,分別遞迴求解。時間複雜度O(YlogX)。
尋找totally monotone matrix的每個橫條(直條)的第一個最小值
限制更強了,擁有更快的演算法。分治法,三個步驟。
一、刪除多餘直條,將X×Y矩陣變成X×X方陣:
把矩陣裡面每一個橫條的第一個最小值圈出來。目標是:砍掉不含第一個最小值的直條。砍到變成方陣即可,不必趕盡殺絕。
任意橫條上面,任取兩個數字a[i][j₁] a[i][j₂],比較大小。如果a[i][j₁] ≤ a[i][j₂],表示右邊數字以上,皆不含第一個最小值。如果a[i][j₁] > a[i][j₂],表示左邊數字以下,皆不含第一個最小值。
沿對角線前進,與右邊數字比較大小。如果a[i][i] ≤ a[i][i+1],那麼姑且繼續前進,累積無效區域,使得上三角矩陣都是無效區域。如果a[i][i] > a[i][i+1],那麼對角線當前數字以下是無效區域,恰好跟先前的無效區域,湊出一整個直條,得以刪除。
二、遞迴求解,以找到偶數橫條的最小值:
偶數橫條合併成一個X/2×X矩陣,遞迴求解。
三、內插夾擠,以找到奇數橫條的最小值:
把矩陣裡面每一個橫條的第一個最小值圈出來,從上往下看,第一個最小值的位置從左往右遞增。用偶數橫條的最小值位置,夾擠出奇數橫條最小值的可能位置,然後窮舉搜尋就行了。
此演算法稱作SMAWK algorithm。時間複雜度O(X+Y)。
尋找Monge matrix的每個橫條(直條)的第一個最小值
限制更強了,理應有更簡易的演算法,但是似乎沒人研究?
可以直接使用上述兩個演算法。
UVa 12311