---
# System prepended metadata

title: Divide and Conquer / Merge Sort
tags: [演算法(交大OCW)]

---

---
title: Divide and Conquer / Merge Sort
tags: 演算法(交大OCW)
---
# 數學歸納法 Induction

這就是 Divide and Conquer 的基礎

## 好處

當你用數學歸納法證明了一個東西，你就無形中建立了一個演算法
因為數學歸納法有要求你每一步要怎麼走要講清楚，而這就是演算法

將一個命題記為 P(n)， n 代表第 n 個步驟；根據證明的形式有兩種歸納法
>命題（英語：proposition）是一個陳述句所表達的判斷，具有真值 by Wiki

## Weak Induction

- Basic Step：要說明 P(0) 為真、是對的
- Inductive Step：假設 P(k) 是對的，進而推導出 P(k+1) 也是對的

其中 P(k) 又叫做 Induction Hypothesis
:::info
因為一次只從一步跳到下一步，所以才叫 weak
:::
## Strong Induction

- Basic Step：要說明 P(0) 為真、是對的
- Inductive Step：假設 P(0)\^P(1)\^...\^P(k) 是對的，進而推導出 P(k+1) 也是對的

:::info
因為一次從很多步跳到下一步，所以才叫 strong
:::

---
:::success
離散數學中會證明兩者等價
:::

## 歸納法小例子 - 貼磁磚

一個 8×8 正方形地板，要鋪磁磚，總共 64 格但有一格有 1 個排水孔不能鋪
請問能不能用一個三角形角塊(Triomino)將整片磁磚鋪完

:::warning
- Triomino請見下面的圖
- 排水孔的位置是任意的
:::

答案是可以的，就是藉由歸納法

最後得到的結論是
:::info
P(n)：任何一個 $2^{n}$ 的正方形地板都可以被 $\frac{2^{n}\times 2^{n}-1}{3}$ 個 Triomino 給貼完 ($n\ge 1$)
:::
:::success
順帶一提，因為 $2^{n}\times 2^{n}-1=(2^{n}+1)(2^{n}-1)$ 並且以 2 = 3 - 1 替換後得到

$$
((3-1)^{n}+1)((3-1)^{n}-1)
$$

當 n 是奇數時，左邊部分會全都剩下 3 的倍數；當 n 是偶數時，右邊部分會全都剩下 3 的倍數
:::
### 1. Basic Step

當 n = 1 時很明顯

![](https://drive.google.com/uc?id=1W6ag41_dELOBgBVNoBZCkI2JZpHwqWlH&export=download)

### 2. Inductive Step

假設 n = k 的時候是對的

n = k+1 的情況其實就是整塊大的正方形可以看成 4 個小塊的正方形
而這 4 個小塊的正方形，就是我們所假設可以拼得出來的正方形(n = k)

此時只要把這 4 個小正方形中，其中 3 個的排水孔對在一起，形成一個 Triomino ，這樣就可以剛好被一個 Triomino 給補起來；剩下的 1 個小正方形，根據我們的假設，是可以拼出來的

因此 k = n + 1 是可以拼出來的，證明完畢

結論，對於任意正整數，P(n)均成立

:::info
這裡再次可以發現數學歸納法的好處，他除了給你證明，他還給告訴你每一步要怎麼走

在這裡就是指，把 3 個小正方形的排水孔對在一起的步驟
:::

#  Divide and Conquer 暖身：Search

不過是指特別情況的 Search；是從一個已排序好的陣列，找出特定值

## Naïve O(n)

直接從頭看到尾，O(n)
這個正確性是顯然的，但就是慢了些

## Binary search O(logn)

每次都選「一個區間」的中間人比較，比完大小就更新區間範圍，然後重複流程

時間就只要 O(logn)

:::info
Binary Search 其實就有使用 DAC，不過屬於比較特殊的情形
:::

![](https://drive.google.com/uc?id=181X_DJ7zUHA0Bn5mP31anxQw-lsfJrSh&export=download)

理論上「Divide」，也就是分成兩半後，這兩半都要「Conquer」；但是因為陣列已經排過序，所以可以直接藉由比較中間人的大小，判斷出另一半不需要「Conquer」

## Divide and Conquer 步驟

- Divide：將輸入拆解成「很多」「性質一樣的」小部件
    - 性質一樣這點很重要；白話上就是「長的一樣」
    - 通常會分成兩半
- Conquer
    - 遞迴地(Recursively)處理每個小部件，直到可以一眼看出答案才停止遞迴
- Combine：
    - 最後，通常也是最難的部分，將每個小部件的解，組合起來

![](https://drive.google.com/uc?id=1tKuphNncNnZlu0iSABAwS8Ubt_FlIiKb&export=download)

:::warning
步驟上來說就是：
1. Enter the Promblem
2. Divide 1 → Conquer 1 → Combine 1，Enter Conquer 1 
3. Divide 2 → Conquer 2 → Combine 2，Enter Conquer 2
4. ...

體會何謂 Recursively
:::

### 複雜度

由於 DAD 通常藉由「遞迴的形式」寫出解法，所以複雜度的分析，也是藉由一個遞迴的關係(Recurrence relation)，表示出上下界

從上面的步驟可以得出

$$
T(Problem)=T(Divide\ 1)+T(Conquer\ 1)+T(Combine\ 1)
$$


## DAC 模板

老師說通常可以用這個模板處理；但是是通常，必要時還是得要依據原則制定內容

![](https://drive.google.com/uc?id=1C_1JUiraQ4svjriaq1BCcXHRxH-mRHQN&export=download)

---

# Merge Sort - John von Neumann 1945

:::success
- 老師說他在計算機架構等方面的成就令眾人所知，殊不知那個年代的人好像都是通才，他在演算法領域也有很多成就
- 老師說如果你的輸入沒有動任何手腳，你排序的依據是 Comparison Based ，也就是以「比較」作為基礎的話，理論上最快就是 nlogn ，不能再更快了
:::

假設有一堆字母，[A,L,O,G,O,R,I,T,H,M,S]，要將他們依照大小排序

![](https://drive.google.com/uc?id=1ecNZqSblPEJA8T-qA_hk-JFtqZUk1c3q&export=download)

- Divide：將這堆字母拆成兩半
    - 拆要拆到最小，直到只剩一個的時候才不拆；這樣才可以直接吐答案

## Pseudo Code

![](https://drive.google.com/uc?id=1E07xwPF4XAUKTDrO1csjbEjTNGeL-nmI&export=download)

- 可以發現 q 使用地板函數(Flooring functino)，所以另一邊是 q+1
    - 使用整數的除法就可以達成
- Mergesort 就是處理遞迴的部分，也就是「Conquer」
- Merge 就是合併的部分，也就是「Combine」
- 可以發現到最後 p = r，就不會進行任何處理

整個步驟流程如下

![](https://drive.google.com/uc?id=1rZnyxAnZOvau_8zIW7brr9Uhyfw90S8L&export=download)

:::info
其實在做的時候不會左右一起做，而是最左半邊先做完
:::


---

# 複雜度分析

將整體的時間記為 T(n)

- D(n) 就是「Divide」的部分
- T(n/2) 就是遞迴地呼叫自己，也就是「Conquer」的部分，並且 n 變成一半的大小
- C(n) 就是「Combine」的部分

因此最後的整體時間就會寫成

$$
T(n)=D(n)+2T(\frac{n}{2})+C(n)
$$

這就是上面所提到的 Recurrence Relation

![](https://drive.google.com/uc?id=1sdc824aUyohU3J8goFxd1UuuAtrldUXg&export=download)

## C(n)

D(n) 只要一個步驟就可以結束，所以是 O(1)

理論上排序可以達到 nlogn 的速度，因此希望這個 C(n) 可以達到 O(n) 的速度

那要如何 Merge 呢，請看下圖

![](https://drive.google.com/uc?id=1vu2Aw1m2OISN3X4BA9Q6vTAHtugolEsW&export=download)

- 圖中 AGLOR 跟 HIMST 是已經排好序的兩個小部件(陣列)
    - 這兩個小部件要額外建立兩個 index ，記錄目前比較到哪裡
- 用一個額外的陣列來存放結果

:::success
- 兩個小部件的 index 一開始就是 0 ，原因是因為我們要從頭開始比較，也就是比較「他們的最小」，到底誰才更小
- 比較出誰比較小後，讓他所在的陣列所持有的 index 加 1，並把比較小的人放進建立好的額外陣列
- 重複上述過程

當然也要補上是否有部件已經空了的檢查
:::

:::info
假設這兩個小部件各自有 n~1~, n~2~ 個人
那最糟糕的情形就是要比較 n~1~ + n~2~ - 1 次，最後一個剩下的人不用比
不過結果來說就是線性時間 O(n)
:::

---

## T(n/2)

那遞迴的部分要怎麼處理？在數學上有些比較難的、比較複雜的，可以直接查資料，不用自己推導
不過這裡所教的目前都很簡單，可以自己 Handle

### case：n ≦ 2

當遞迴到了最後 p = r ，也就是指 n ≦ 2 的時候，甚麼都不會做，因此是常數時間
用一個常數 c 表示：
$$
T(n)\le c
$$
> n = 1 就是 p = r ；n = 2 會呼叫兩個 T(1)；

### case：n > 2

下面在推導的時候，其實不會先寫 O ，因為 O 代表了「精簡後」，會影響結果；所以這裡要先換成另一個常數 c 乘上 n ，寫作 cn

![](https://drive.google.com/uc?id=1Bv-_gqeLZmwY3yd1LwkGVYHJKIv3UTD6&export=download)

於是得到
$$
T(n)\le 2T(\frac{n}{2})+cn
$$

:::warning
眼尖的人應該會發現，當初 q 有用地板函式，那這樣照理來說，不應該是2T(n/2)
因為取地板後，奇數個數會使得兩者的大小不一樣
:::

沒有錯，就正確性來說應該要寫成下面這樣

![](https://drive.google.com/uc?id=1hvLCk3zP8wnneJ0QPAa81Cs2Qb0qZtiY&export=download)

但是如果當 n 很大時，在 Asymptotic Analysis 下，這件事不打緊，不會影響結果
差異非常小，可以直接忽略它；因此如果函式的分析有出現地板根天花板函式(Flooring、Ceiling)，就可以先忽略他們

---

## 兩招處理 Recurrence

### 前置處理
:::info
- 為了找出 Upper Bound，先將 ≦ 換成 =，也就是做最壞的打算
* 並且為了避免要取地板跟天花板，先假設 n 是 2 的某個次方
:::
$$
T(n)= 2T(\frac{n}{2})+cn
$$

## Unrolling the recurrence / Recurrence Tree

:::info
老師說這招很常用
會叫做 Recurrence Tree 是因為會將 T(n) 展開成一個 Tree 的形式
:::

三個步驟：
- Analyzing the first few levels
    - 就是先畫出少少的一兩層，並找到規律
- Identifying a pattern
    - 畫出整個樹的樣子
- Summing over all levels
    - 將每層加起來

### Analyzing the first few levels

將原本的 T(n) 展開個一兩層，找到規律；並且可以知道，總共只要做 logn 次展開

![](https://drive.google.com/uc?id=1Re4X_u_jwxVCepaVNfHYqws7UyGW1ofK&export=download)

### Identifying a pattern 和 Summing over all levels

Tree 的每個 Node 就代表一個小步驟(一個「Conquer」)，最後就是把每個步驟加起來
每次加起來的時候，要 Level by Level 一層一層的加起來

![](https://drive.google.com/uc?id=1oZe-a6mzL8TSSLjQ4BsZ0RT0lGXfTyX7&export=download)

:::warning
因此最後得到的複雜度總和就是 cn × logn 
取 Big O 可以得到 O(nlogn)
:::


---

## Substituting a guess

:::info
利用數學歸納法

先猜一個答案，然後用數學歸納法證明；如果是很有經驗的人，可能很快就找到答案，但是對於經驗不多的人，可能找了很久都沒找到答案
>這不就是通靈嗎
:::

上面所謂的猜答案，就是去猜他的時間複雜度；因為上面我們已經知道是 nlogn了，那這裡我們就猜 nlogn

從上面已知：

$$
Any\ function\ T\ satisfying\ this\ recurrence\ \\
T(n)\le 2T(\frac{n}{2})+cn\ when\ n>2,\ and\ T(2)\le c\\
$$

去猜

$$
is\ bounded\ by\ O(nlog_{2}n),\ when\ n>1
$$

也就是說，我們相信下面這件事

$$
T(n)\le cnlog_{2}n, for\ all\ n\ge 2
$$

n=1 的時候是常數時間

### Base step

已知的部分

$$
k=2,\ T(2)\le c\le 2c = c×2×log_{2}2
$$

### Inductive Step (Strong)

開始猜的部分

$$
T(m)\le cmlog_{2}m\ for\ all\ m<n\ \\
T(\frac{n}{2})\le c(\frac{n}{2})log_{2}(\frac{n}{2})；log_{2}(\frac{n}{2})=log_{2}n-1\ \\
T(n)\le 2T(\frac{n}{2})+cn\le 2c(\frac{n}{2})log_{2}(\frac{n}{2})+cn=cn[log_{2}n-1]+cn=cnlog_{2}n
$$

證明完畢

結論，對於所有 n ≧ 2 均成立

---
:::info
可以注意到，Unrolling 有換掉 ≦ 的步驟，而通靈法則維持 ≦ 的關係
:::


---

# Heap Sort 跟 Merge Sort

通常來說比較常採用 Merge Sort，因為 Heap Sort 基本上依賴陣列取值的方便性
可是如果資料不是以陣列儲存，而是像是 Linked List 之類的話， Heap Sort 就不是一個好選擇
這時候就是 Merge Sort 出場的地方

---

# 久違的手刻 Merge Sort

```c=
#include<stdio.h>
#include<stdlib.h>
// sort a int array
// can easily modify some little parts to sort other kinds of array

// the swap function determin ascending or descending
void swap(int left,int right,int* array);
// merge function merge the two parts of array
void merge(int left,int right,int middle,int* array);
// main part of DAC
void mergeSort(int left,int right,int* array);
void pArray(int* array,int length);
int main(int argc, char const *argv[])
{
    int N = 10;
    int array[10] = {3,2,1,6,5,4,9,8,7,10};
    printf("Before\n");
    pArray(array,N);
    mergeSort(N-N,N-1, array);
    printf("\nAfter\n");
    pArray( array,N);
    return 0;
}
void pArray(int* array,int length){
    for (int i = 0; i < length; i++)
    {
        printf("%d ",array[i]);
    }
    
}
void swap(int left,int right,int* array){
    int temp = array[left];
    array[left] = array[right];
    array[right] = temp;
}
void merge(int left,int right,int middle,int* array){
    // start comparing by head of the two parts
    int head_left = left;
    int head_right = middle + 1;
    // a temp array to store sorted elements
    int* temp = malloc(sizeof(int)*(right-left+1)); 
    int temp_index = 0;

    // comparing the tow parts from each first element
    // put the smaller one to the temp array
    while (head_left <= middle && head_right <= right)
    {
        // "=" makes it stable
        // ascending
        if(array[head_left] <= array[head_right]){
            temp[temp_index] = array[head_left];
            temp_index++;
            head_left ++;
        }else{
            temp[temp_index] = array[head_right];
            temp_index++;
            head_right ++;
        }
    }
    // put remaining elements into temp array
    if(head_left<= middle){
        for (int i = head_left; i <= middle ; i++)
        {
            temp[temp_index] = array[i];
            temp_index++;
        }
    }else{
        for (int i = head_right; i <= right ; i++)
        {
            temp[temp_index] = array[i];
            temp_index++;
        }
    }
    
    // copy elements back to origin array
    
    for (int i = left; i <= right; i++)
    {
        array[i] = temp[i-left];
    }
    
    // don't forget deallocate it
    free(temp);
      
}
void mergeSort(int left,int right,int* array){
    // divide the array to two parts repeatedly
    // in the end it there may be one element in each part, which means right - left = 1
    // or left = right, doesn't need to do anything
    if(right-left == 1){
        // ascending
        if(array[left] > array[right]){
            swap(left,right,array);
        }
        return;
    }else if(right-left == 0){
        return;
    }else{
        int middle = (left+right)/2;
        mergeSort(left,middle,array);
        mergeSort(middle+1,right,array);
        merge(left,right,middle,array);
        return;
    }
}
```
