二分探索、ソートアルゴリズムと対数
c2251393 August 4, 2013
1 Introduction
這次的主題是二分搜跟排序,這兩項都是演算法世界非常入門的東西。雖然說是 入門,但絕不簡單,他們背後精美的想法絕對是我們要好好認識並學習的。
2 Binary Search
講二分搜之前先講一個小時候大概玩過的遊戲叫做” 終極密碼”(或叫猜數字)。現 在數字範圍是[1,106],有沒有辦法在最多23次的猜測之內猜到密碼。當然,是對於 任何可能的密碼。
有一個方法是這樣的,每次都盡量讓密碼存在的範圍砍半,也就是每次詢問範圍 中間的那個值大/小於目標值,當然如果等於的話就 WIN 了,這樣就大概可以在
20 21次內找到密碼。因為 106 只能一直除 2除大概 20次就會變成 1了,也就是範
圍大小變成1。用比較數學的方式來說就是 ⌈log2106⌉= 20。
如果你懂了上面那個方法,那你就大概懂了二分搜的精隨,每次透過檢驗中間元 素然後將解的範圍砍半,砍到解的範圍只剩下唯一的元素就是答案了。但不是甚麼 時候都可以把範圍砍半,除非你可以確定那被砍半的範圍中不會有答案。
I
現在給你一個由小到大排好的一個未知的不重複數列 A,你要找出 x 在哪裡,你 可以用以下的演算法:
Algorithm 1Binary Search
1: procedure binary search(A[], l, r, x)
2: if l > r then
3: return NULL
4: end if
5: m← l+r2
6: if A[m] == xthen
7: return m
8: else if A[m]> x then
9: return binary search(A, l, m−1, x)
10: else
11: return binary search(A, m+ 1, r, x)
12: end if
13: end procedure
來 思 考 一 下 這 為 甚 麼 正 確 吧, 既 然 給 的 數 列 A 是 由 小 排 到 大 的, 所 以 要 是 A[m] < x 則 x 絕不可能存在於範圍 [l, m],所以接下來只要去找 [m+ 1, r] 就可以 了,同理A[m]> x。
那麼要是 A 排得亂七八糟的話呢? 那這個演算法就爛了,因為你沒辦法砍範圍,
例如要是A[m]< x不代表你可以確定 x不在範圍[l, m] 裡。所以你會發現二分搜之 能去搜有順序的東西,因為這樣才能確保你砍範圍這件事是正確的。
除了二分搜一個東西在哪裡,我們也可以去二分搜題目的答案。例如我要問你 log230 大概是多少,第一個想到的是去找對數表,然後弄出答案來,可是對數表不 常見,所以我們可以枚舉答案,從 1 開始算 2k 是多少、2k+1 是多少,直到大於等
於30,可是這裡我們可以用二分搜加速,直接二分搜答案取代枚舉答案。
II
Exercise!!
1. 時間複雜度分析
請問 Algorithm 1 的時間複雜度為何? 2. 數值分布上界 (upper 棒)
給你一個由小排到大的數列 A,找出x 最後一次出現的地方。
3. 數值分布下界 (lower 棒)
給你一個由小排到大的數列 A,找出x 第一次出現的地方。
4. 猜數字 (JOI 2011 day 2, HOJ 118)
互動題: 有一個長度為 n(n ≤ 100) 的序列 A,A 為一個 1 到 n 的排列。每次
可用 Answer(x[])詢問一個序列,函式會回傳有幾個位置的數字猜對了。求在
700 次詢問內能回答正確。
5. Cave (IOI 2013 day 2)
互動題: もも被拓也哥監禁起來了,但もも想逃出去。監獄的構造是這樣的,
連續 n(n≤5000) 個門將牢房與外面阻隔。門的鑰匙剛好被拓也哥忘在牢房裡
了,這鑰匙是這樣的,上面有 n 個按鈕,分別對應到不同的門,而有的門是按 鈕按下去了才會開,有的是不按才會開。每次可用 trycombination(x[]) 來詢問 一種按鈕按法組合,函式會回傳目前距離牢房最近的那扇關起來的門。因為時 間緊迫,拓也哥快回來了,所以你只能詢問 70000 次,最後你要回傳一種按鈕 按法組合以及每個按鈕分別對應到哪一扇門的序列。
3 Sorting Algorithm
要講排序演算法前先來定義一下甚麼是排序問題好了。
定義: 給你 N 筆資料,以及資料間大小的定義 (比較大小的方式),請將他們由小到 大輸出
從古自今有很多人想出了很多解決這問題的方法,我們將介紹其中有名的幾個。
3.1 如夢似幻的 N ! 亂猜
也就是Monkey Sort,每次把整個序列隨機亂排,並O(N)檢查是否以排序完成,
RP()高的話一次就 WIN了,但也有可能會試了 N! 才正確,滿夢幻的。
3.2 基礎的 O(N
2) 排序法
由於O(N!) 的sort 太夢幻了,所以人們進一步想出了 O(N2) 的 sort,接下來會 介紹3 種 sort,這三種 sort 的核心概念不外乎是找一個合適的元素,然後塞進去排
III
列好的序列裡。
1. Selection sort
每次花 O(N) 的時間找到不在排序好的序列裡的最小值,然後 O(1) 插到排序 好的序列的後面,由於要插入 N 次,所以複雜度是滿的 O(N2),算是相當直 觀的 sort。
2. Insertion sort
從第一個元素開始,O(N) 把該元素插進去排序好的序列 (從後 (大) 往前找,
直到找到第一個比自己小的元素,然後插在他後面 wwww),如果原本序列已 經排好了,那每次插入都直接放在後面 (這操作可以是 O(1) 常數時間完成),
所以雖然最壞情況是 O(N2),但也有可能是 O(N),如何把元素插進排序好的 序列值得初學者思考。
3. Bubble sort
從頭開始,每次不斷往後把最大值兩個兩個 (第 i 個跟第 i+1 個) 交換到後面 去,有點類似 Selection sort,只是這演算法實際寫起來比 Selection sort 好寫。
3.3 了不起的 O(N lg N ) 排序法
因為人類是貪心的,所以比 O(N2) 更快的排序法便在這樣的情況下降臨了 (?):O(NlgN) 排序法出現了。接下來也會介紹3 種 sort,除了 Heap sort 之外的兩 種 sort 的核心概念並不是 O(N2) 的那些,而是運用了 Divide & Conquer這一種演 算法設計技巧設計出來的排序法,換句話說,就是透過把序列拆成兩半遞迴去排序,
然後再合體。
1. Merge sort
把序列砍半,排序好左半邊跟右半邊然後兩邊合併起來,合併的方法有點 運 用 到 Selection sort 的 想 法, 每 次 把 最 小 值 插 進 排 序 好 的 序 列 後 面, 但 是你可能想這樣是否要花 O(N2),但其實只要花 O(N) 就好了,因為你要 找目前未排序的最小值你只要 O(1) 比對兩坨序列的最前面那個元素,然 後把比較小的 O(1) 直接插入即可,然後做 O(N) 次。時間複雜度可以透 過解遞迴 T(N) = 2 ×T(N2) +O(N) = O(NlgN),或是畫出遞迴樹算出 每 層 總 共 要 花 O(N) 做 合 併, 然 後 總 共 有 O(lgN) 層 要 做, 所 以 複 雜 度
=O(N)×O(lgN) =O(NlgN)。
2. Quick sort
跟Merge sort不一樣,Quick sort不是直接砍半,而是找一個元素當軸(Pivot),
然後把比較小的都丟軸的左邊,比較大的丟軸的右邊,這樣子可以 O(N) 完 成 (細節可以自己想想看,不難),然後遞迴去排序這兩坨。這演算法最快的情 況是剛好找到中間值,然後可以剛好砍兩半,這樣複雜度就跟 Merge sort 一
IV
樣是 O(NlgN) 了。但若是很不幸的每次都是找到最大 (最小) 值,那這樣複 雜度就變成了 T(N) =T(N −1) +O(N) = O(N2)(因為要做 N 層,每層都花
O(N))。但經過數學方法算出來,在一般的情況下 Quick sort 時間複雜度是
Θ(NlgN)。當然Quick sort有很多優化的方向,例如當遞迴到一定深度的時候 使用 Insertion sort,或是優化選擇軸的方法等等。是說 STL 的 std::sort() 就是使用優化過的 Quick sort(Intro sort)。
3. Heap sort
這演算法的概念很簡單,把所有東西塞到一個謎樣的資料結構裡,然後從資料 結構拿出最小值輸出。實現這個願望的資料結構叫做 Heap,他可以 O(lgN) 插入元素、O(1) 查詢最小值、O(lgN) 刪除最小值,Heap會在以後的培訓課 程裡講到,所以今天暫時不談了。
3.4 題外話 : 基於比較的排序的最低複雜度
數學密度超標注意!
我們這裡說的最低複雜度是排序一個相異序列在最差情況下的最低複雜度 (不然
Insertion sort 就可以 O(N) 完勝了)。假設 N 個元素在還沒排序前,可能的正確結
果有N!種(因為還沒判斷,每一種都有可能),經過一次比較後可能的正確結果就會
變成 N!
2! 種,依次類推,經過 m 次比較後可能的結果只剩下 N!
(2!)m 種,直到 N!
(2!)m ≤1 的時候我們終於確定了排序的正確結果。此時 m =O(lgN!) = O(NlgN),所以在 最壞情況下比較的次數不會小於O(NlgN)。
3.5 神秘的 O(N ) 排序法
以上講的 sort 都是基於比較的排序演算法,但當然也有不基於比較的排序演算 法喔! 這些不基於比較的演算法通常複雜度會有一些神秘但重要的常數在裡面。
1. Counting sort
開個陣列 (index 是值) 直接記錄每個值出現的次數,時間複雜度是O(N +C),
C 是值範圍。
2. Radix sort
將待排數字以某種進位法表示,空的位數補零,由左到右或由右到左對位數進 行排序 (通常是使用 Counting sort)。複雜度是 O((logBC)×(N +B)),B 是 進位法,C 是數字範圍。
V
Exercise!!
1. 逆序數對 (TIOJ 1080)
給你 N(≤105)個數字的數列 a,問有幾組(i, j) :i < j∧ai > aj。 2. 第 K 大數(TIOJ 1364)
給你長度 N 的數列跟K,O(N) 找出第 K 大數。
3. Bubble sort 交換次數計算
計算出長度 N(≤105) 的數列使用Bubble sort 排序的交換次數。
VI
各排序演算法虛擬碼
Algorithm 2Selection sort
1: procedure Selection sort(A[], N)
2: fori←0 to N −1 do
3: forj ←i+ 1 toN −1 do
4: if A[i]> A[j]then
5: Swap(A[i], A[j])
6: end if
7: end for
8: end for
9: end procedure
Algorithm 3Insertion sort
1: procedure Insertion sort(A[], N)
2: fori←0 to N −1 do
3: k ←A[i]
4: forj ←i down to 0 do
5: if A[j−1]> k then
6: A[j]←A[j−1]
7: else
8: break
9: end if
10: end for
11: end for
12: end procedure
VII
Algorithm 4Bubble sort
1: procedure Bubble sort(A[], N)
2: fori←N −1 down to1 do
3: forj ←1 to i do
4: if A[j−1]> A[j]then
5: Swap(A[j−1], A[j])
6: end if
7: end for
8: end for
9: end procedure
Algorithm 5Counting sort
1: procedure Counting sort(A[], N, C)
2: create cnt[C]
3: fori←0 to N −1 do
4: cnt[A[i]]←cnt[A[i]] + 1
5: end for
6: k ←0
7: fori←1 to C do
8: forj ←1 to cnt[i] do
9: A[k]←i
10: k←k+ 1
11: end for
12: end for
13: end procedure
VIII
Algorithm 6Merge sort
1: procedure Merge sort(A[], l, r)
2: if l ≥r then
3: return
4: end if
5: mid← ⌊l+r2 ⌋
6: Merge sort(A[], l, mid)
7: Merge sort(A[], mid+ 1, r)
8: Merge(A[], l, mid, r)
9: end procedure
10:
11: procedure Merge(A[], l, mid, r)
12: create B[r−l+ 1]
13: i←l, j ←mid+ 1, k ←0
14: while i≤mid∧j ≤r do
15: if A[i]< A[j]then
16: B[k]←A[i]
17: i←i+ 1
18: else
19: B[k]←A[j]
20: j ←j+ 1
21: end if
22: k ←k+ 1
23: end while
24: while i≤mid do
25: B[k]←A[i]
26: i←i+ 1
27: k ←k+ 1
28: end while
29: while j ≤r do
30: B[k]←A[j]
31: j ←j+ 1
32: k ←k+ 1
33: end while
34: fori←0 to k−1 do
35: A[l+i]←B[i]
36: end for
37: end procedure
IX
Algorithm 7Quick sort
1: procedure Quick sort(A[], l, r)
2: if l < r then
3: mid←P artition(A[], l, r)
4: Quick sort(A[], l, mid)
5: Quick sort(A[], mid+ 1, r)
6: end if
7: end procedure
8:
9: function Partition(A[], l, r)
10: pivot←A[r], split←l
11: fori←l tor−1do
12: if A[i]≤pivotthen
13: swap(A[split], A[i])
14: split←split+ 1
15: end if
16: end for
17: swap(A[split], A[r])
18: return split
19: end function
X