# Algorithm Chapter 9 - Medians and Order Statistics
## Terminology
* 第 $k$ 順序統計量 ($k$-th order statistic): 在一個大小為 $n$ 的集合中第 $k$ 小的元素
* 最小值 (minimum): 第一順序統計量
* 最大值 (maximum): 第 $n$ 順序統計量
* 中位數 (median): 整個集合當中在正中央的位置的元素
* 如果 $n$ 是奇數,則中位數只有一個, $k = \frac{n + 1}{2}$
* 如果 $n$ 是偶數,則中位數有兩個,分別為 $k = \frac{n}{2}$ 和 $k = \frac{n}{2} + 1$
* **在排序或是選擇問題中,我們所說的「中位數」是指比較小的那個**
* 在統計學中,中位數指的是中間兩個數加起來的平均
## The Selection Problem
* `item selection(set A, index i)`
* Input: 集合 $A$ (由 $n$ 個相異元素組成), 以及整數 $i$ ($1 \leq i \leq n$)
* Output: $A$ 當中的某個元素 $x$, 其值正好大於 $i - 1$ 個 $A$ 當中的其它元素,即第 $i$ 小的元素
### Comparison-based Selection
* 可以在 $O(nlgn)$ 的時間內解決
* 將 $n$ 個元素排序,並取出排序好的陣列當中的第 $i$ 個
* 由於排序演算法的時間複雜度上限在 $O(nlgn)$, 所以選擇問題的上限也在 $O(nlgn)$
* 只找最大值或最小值,可以在 $O(n)$ 時間內解決
* 將第一個元素設定為最大 / 最小值
* 往後一個一個看,如果比目前的極值更大 / 更小,則更新極值
* 完成 $n - 1$ 次比較後即可獲得最大 / 最小值
* 時間複雜度為 $T(n) = n - 1$, 屬於 $O(n)$ 時間
```javascript=
function minimum(A, n) {
let min = A[0];
for(let i = 1; i < n; ++i) {
if(min > A[i]) min = A[i];
}
return min;
}
// To find maximum: change > to <
```
* 同時找最大值和最小值,也可以在 $O(n)$ 時間內達成
* 簡單版: 執行上面的程式兩次, $T(n) = 2n + 2$
* 進階版: 可以在 $T(n) = 3 \times \lfloor \frac{n}{2} \rfloor$ 之內找到
* 若 $n$ 為偶數,比較前兩個元素,將最大值設為較大者,最小值設為較小者
* 若 $n$ 為奇數,最大值和最小值設為第一個元素
* 往後每兩個元素一數,互相比較取得較大者和較小者
* 比較最大值和較大者,並取其中較大的值為最大值
* 比較最小值和較小者,並取其中較小的值為最小值
* 比完所有的元素後,即同時取得最大值和最小值
* 每兩個元素為一組,共 $\lfloor \frac{n}{2} \rfloor$ 組,每一組只需要比較三次,時間複雜度大約為 $T(n) = 3 \times \lfloor \frac{n}{2} \rfloor$
```javascript=
function maxAndMin(A, n) {
let max, min;
if(n % 2 == 0) {
max = A[0] > A[1] ? A[0] : A[1];
min = A[0] === max ? A[1] : A[0];
for(let i = 2; i < n; i += 2) {
let bigger = A[i] > A[i + 1] ? A[i] : A[i + 1];
let smaller = A[i] === bigger ? A[i + 1] : A[i];
max = bigger > max ? bigger : max;
min = smaller < min ? smaller : min;
}
}
else {
max = A[0];
min = A[0];
for(let i = 1; i < n; i += 2) {
let bigger = A[i] > A[i + 1] ? A[i] : A[i + 1];
let smaller = A[i] === bigger ? A[i + 1] : A[i];
max = bigger > max ? bigger : max;
min = smaller < min ? smaller : min;
}
}
return {max, min};
}
```
### Randomized Selection
* 使用隨機搜尋法,可以以平均時間複雜度的期望值在 $O(n)$ 內的時間找到任意第 $i$ 小的元素
* 利用隨機快速排序法當中的 `RANDOMIZED-PARTITION` 函數取得隨機的 pivot 值,並且把 input 的陣列分成比 pivot 大和比 pivot 小的兩邊
* 若隨機回傳的 pivot 值 index 已經等於 $i$ 了,則完成搜尋
* 若隨機回傳的 pivot 值 index 小於 $i$, 則遞迴往左半邊的陣列搜尋第 $i$ 小的值
* 若隨機回傳的 pivot 值 index 大於 $i$, 則遞迴往右半邊的陣列搜尋第 $i - k$ 小的值(因為已經確定有 $k$ 個值比 $i$ 小,所以在右邊的陣列當中的第 $i - k$ 小的值就是完整陣列中的第 $i$ 小的值)
* 遞迴直到 pivot 為第 $i$ 小的數字或是 subarray 的大小為 1
* 最差的時間複雜度可高達 $O(n^2)$ (理由同快速排序法),但平均時間複雜度的期望值落在 $O(n)$ 當中,算是很迅速的搜尋演算法
```javascript=
// p = first index in subarray, r = last index in subarray
function randomized_select(A, p, r, i) {
if(p === r) {
return A[p];
}
let q = randomized_partition(A, p, r); // pivot index in array A
let k = q - p + 1; // pivot index in this subarray
if(i === k) {
return A[q];
}
else if (i < k) {
return randomized_select(A, p, q - 1, i);
}
else {
return randomized_select(A, q + 1, r, i - k);
}
}
function partition(A, p, r) {
let random = Math.floor(Math.random() * r + 1);
swap(A, random, r);
let pivot = A[r];
let q = p;
for (let i = p; i < r; ++i) {
if (A[i] <= pivot) {
swap(A, q, i);
q++;
}
}
swap(A, r, q);
return q;
}
```
* 時間複雜度分析: 不知道在工三小(。
### Median of Medians
* 使用 Median of Medians 搜尋法,可以在最差時間複雜度為 $O(n)$ 的情況下找到任意第 $i$ 小的數字
* 將 input 的陣列五個五個一數,共可分成 $\lceil \frac{n}{5} \rceil$ 組(最後一組可能不滿五個)
* 用任意一種排序演算法(通常用泡沫排序或插入排序,因為比較好寫)將五個數字排序,並且挑出當中的中位數。因為每一組都只有五個數字,所以每一組要挑出中位數所花的時間是 $O(1)$
* 總共會挑出 $\lceil \frac{n}{5} \rceil$ 個中位數,遞迴進行上面的步驟直到挑出這 $\lceil \frac{n}{5} \rceil$ 個中位數的中位數
* 呼叫快速排序法當中的 `PARTITION` 函數,並且以這個中位數們的中位數為 pivot, 將 input 陣列分成兩邊
* 若 pivot 值 index 已經等於 $i$ 了,則完成搜尋
* 若 pivot 值 index 小於 $i$, 則遞迴往左半邊的陣列進行搜尋
* 若 pivot 值 index 大於 $i$, 則遞迴往右半邊的陣列進行搜尋
* 遞迴直到找到第 $i$ 小的元素或是 subarray 的長度為 1
```javascript=
function medianOfMedians(A, left, right, i) {
while(true) {
if(left === right) {
return A[left];
}
let pivotIndex = pivot(A, left, right);
pivotIndex = partition(A, left, right, pivotIndex);
if(i === pivotIndex) {
return A[pivotIndex];
}
else if(i < pivotIndex) {
right = pivotIndex - 1;
}
else {
left = pivotIndex + 1;
}
}
}
function pivot(A, left, right) {
if(right - left < 5) {
return insertion_sort(A, left, right);
}
for(let i = left; i <= right; i += 5) {
let subRight = i + 4;
if(subRight > right) {
subRight = right;
}
let median5 = insertion_sort(A, i, subRight);
swap(A[median5], A[left + Math.floor((i - left) / 5)]);
}
return medianOfMedians(A,
left,
left + Math.floor((right - left) / 5),
left + Math.floor((right - left) / 10));
}
function partition(A, left, right, pivotIndex) {
let pivotValue = A[pivotIndex];
swap(A[pivotIndex], A[right]);
let storeIndex = left;
for(let i = left; i < right; ++i) {
if(A[i] < pivotValue) {
swap(A[storeIndex], A[i]);
storeIndex++;
}
}
swap(A[storeIndex], A[right]);
return storeIndex;
}
function insertion_sort(A, left, right) {
let i, j, tmp;
for(i = left + 1; i <= right; ++i){
let tmp = A[i];
for(j = i; j > left && A[j - 1] > tmp; j--) {
A[j] = A[j - 1];
}
A[j] = tmp;
}
return left + Math.floor((right - left) / 2);
}
```
* 時間複雜度證明:
* 將 input 的陣列五個五個一數: $O(n)$
* 將五個一組的數字排序,並且挑出當中的中位數:$O(1) \times \lceil \frac{n}{5} \rceil = O(n)$
* 遞迴進行直到挑出這 $\lceil \frac{n}{5} \rceil$ 個中位數的中位數: $T(\lceil \frac{n}{5} \rceil)$
* 呼叫快速排序法當中的 `PARTITION` 函數將 input 陣列分成兩邊: $O(n)$
* 遞迴直到找到第 $i$ 小的元素: $T(7 \times \frac{n}{10} + 6)$
* 在第 2 步排序完的結果當中,每一組都至少有兩個元素是小於 pivot 的
* 在第 4 步 partition 完之後,每一個在 pivot 組之前的組,都貢獻了三個比 pivot 小的數字(同理,在 pivot 之後的組都貢獻了三個比 pivot 大的),除了不滿五個元素的組和 pivot 組自己
* 不考慮最後那兩組的話,總共至少會有 $3 \times ( \lceil \frac{1}{2} \times \lceil \frac{n}{5} \rceil \rceil - 2) \geq 3 \times \frac{n}{10} - 6$ 的數字比 pivot 小 / 大
* 換句話說,當我們遞迴呼叫 `medianOfMedians` 的時候,頂多只會處理 $n - (3 \times \frac{n}{10} - 6) = 7 \times \frac{n}{10} + 6$ 個數字而已,所以最後一步的複雜度是 $T(7 \times \frac{n}{10} + 6)$
* 寫出遞迴式 $$T(n) = \begin{equation} \begin{cases} O(1) \text{, if } n < 140\\ T(\lceil \frac{n}{5} \rceil) + T(7 \times \frac{n}{10} + 6) + O(n) \text{, if } n \geq 140 \end{cases} \end{equation} $$
* (然後就看不懂ㄌ(幹
* 總之最後, $T(n) = O(n)$, 所以這是線性的搜尋演算法
## Note
* [General 的排序演算法,因為其決策樹的長相,決定了任意的排序演算法都至少得花 $\Omega(nlog_2n)$ 的時間才能完成排序](http://www.quretec.com/u/vilo/edu/2003-04/ATABI_2004k/BigOh/sort_lower.html)
* 線性時間的排序演算法對其 input 的定義域有一定要求,不能排序所有的實數
* 線性時間的搜尋演算法不倚靠排序,所以才能逃脫 $\Omega(nlog_2n)$ 的限制
* 且線性時間的搜尋演算法對 input 的定義域沒有限制,可以對任意的 input 序列做搜尋