基礎排序 & 二分搜尋法

蔡銘軒 @ Sprout 2020 C/C++語法班

基礎排序篇

為什麼要排序?

1. 因為第二次階段考會考

2. 因為排序很基本、很常用、很重要

P.S. 以下提到“排序”都預設是將陣列裡的元素按照由小大到大的順序排好

基礎排序的基本想法

如果有兩個數字a, b在陣列裡,a比b大但a卻出現在b的前面,我們就把他們交換

void swap(int &a, int &b) {
    int c = a;
    a = b;
    b = c;
}

一直交換下去,總有一天會排序好。

那麼我們要怎麼有系統地交換元素呢?

Bubble Sort

在上Code之前,先來看看它的概念吧!

概念:

每次操作從index小的元素開始往後,依次檢查相鄰的兩個元素,如果大小順序顛倒,就交換!

不斷重複這樣的步驟,直到排序完成。

來看點例子更好懂!

Bubble Sort Visualizer

Bubble Sort

void bubble_sort(int arr[], int n) {
    // 最多跑n - 1次迴圈就會sort完整個陣列
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - i - 1; j++)  
            // 如果相鄰元素順序不對,就交換 
            if (arr[j] > arr[j + 1])
                swap(arr[j], arr[j + 1]); 
}

時間複雜度?

外層的迴圈:

內層的迴圈:

交換:

總共:

O(n)
O(n)
O(1)
O(n^2)

best case/worst case都是                 ...

O(n^2)

Bubble Sort

void bubble_sort(int arr[], int n) {
    // 最多跑n - 1次迴圈就會sort完整個陣列
    for (int i = 0; i < n - 1; i++) {
    	bool has_swap = false; // 用來紀錄這一次迴圈有沒有發生交換
        for (int j = 0; j < n - i - 1; j++)  
            // 如果相鄰元素順序不對,就交換 
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
                has_swap = true; // 發生交換了
            }
        if (!has_swap) // 如果沒有發生交換,代表已經排好了
        	break;
    }
}

Best case: 

Worst case:

O(n)
O(n^2)

Selection Sort

在上Code之前,先來看看它的概念吧!

概念:

把陣列分成已排序未排序兩部分(一開始全部都是未排序)。

每次操作從未排序的部分找出最小值,把它跟未排序的第一個元素交換(效果相當於把這個元素接在已排序部分的最後面),並將這個元素歸類為已排序

重複直到排序完成。

來看點例子更好懂!

Selection Sort Visualizer

Selection Sort

時間複雜度?

外層的迴圈:

內層的迴圈:

交換:

總共:

O(n)
O(n)
O(1)
O(n^2)

best case/worst case都是                ...

O(n^2)
void selection_sort(int arr[], int n) {
  // 最多做n - 1次迴圈就會排序好
  for (int i = 0; i < n - 1; i++) {
    int min_idx = i; // 用來記錄未排序部分的最小值
    for (int j = i + 1; j < n; j++)
      if (arr[j] < arr[min_idx]) // 發現更小的數值,更新
      	min_idx = j;
      swap(arr[min_idx], arr[i]); // 把未排序部分的最小值放到對的位置
  }
}

Insertion Sort

在上Code之前,先來看看它的概念吧!

概念:

把陣列分為已排序未排序兩部分(一開始第一個元素是已排序,其餘為未排序)。

每次操作選擇未排序部分的第一個元素,看看他應該被插在已排序部分的哪個位置,把它放到那裡去!

想想看你玩撲克牌時怎麼整牌的,或許可以幫助理解

來看點例子更好懂!

Insertion Sort Visualizer

Insertion Sort

void insertion_sort(int arr[], int n) {
    // 最多跑n - 1次迴圈就會sort完整個陣列
    for (int i = 1; i < n; i++) {
    	key = arr[i]; // 這一輪要插入的數字
        j = i - 1;
        // 用while迴圈一邊平移元素,一邊找key該插在哪裡
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key; //將key放入該放的位置
    }
}

時間複雜度?

外層的迴圈:總是

內層的迴圈:最佳              最差

交換:

總共:最佳               最差

O(n)
O(n)
O(1)
O(1)
O(n)
O(n^2)

小練習

Sample Solution

#include <iostream>
void bubble_sort(int arr[], int n) {
  for (int i = 1; i < n - 1; i++) {
    	for (int j = 1; j < n - 1; j++) {
          if (arr[j] < arr[j + 1])
            	std::swap(arr[j], arr[j + 1]);
        }
    }
}
int main() {
    int n, arr[10000 + 5];
    std::cin >> n;
    for (int i = 0; i < n; i++)
    	std::cin >> arr[i];
    bubble_sort(arr, n);
    for (int i = 0; i < n; i++) {
    	std::cout << arr[i];
        if (i != n - 1)
        	std::cout << ' ';
        else
        	std::cout << std::endl;
    }
    return 0;
}

小總結

以上介紹三種非常基本,基於交換的排序演算法

這三種排序演算法很容易出現                 的時間複雜度,在資料量較大時                          表現非常差

O(n^2)
(n>10^5)

有興趣的同學可以自行學習其他更有效率的排序演算法,例如:

Merge Sort:  

Quick Sort: worst case               ,但平均                        ,且實務上非常快

O(nlogn)
O(n^2)
O(nlogn)

小插曲

好用工具介紹:

std::sort()

#include <iostream>
#include <algorithm>
#include <cstdlib>
void get_random_array(int arr[], int n) {
    srand(127);
    for (int i = 0; i < n; i++)
        arr[i] = rand();
}
int main() {
    int arr[100000 + 5];
    int n = 100000;
    get_random_array(arr, n);
    std::sort(arr, arr + n);
    for (int i = 0; i < n; i++)
    	std::cout << arr[i] << ' ';
    std::cout << std::endl;
    return 0;
}

小總結

基於比較&交換的排序演算法,最好只能做到                        了

O(nlogn)

也有非基於交換的排序演算法,例如:

Couting Sort:  

Radix Sort: 

O(n+k)
O(nlog(\max{a_0,\dots,a_{n-1}}))

但以上兩種排序法有使用條件的限制,並不是絕對較好。

有興趣的同學可以自行學習!

二分搜尋法篇

怎麼搜尋?

Q: 給你一個數值隨機分布的陣列,告訴我42有沒有在裡面?

bool find_number(int arr[], int n) {
	for (int i = 0; i < n; i++) {
    	  if (arr[i] == 42)
        	  return true;
    }
    return false;
}

時間複雜度:               。

O(n)

怎麼搜尋?

Q: 給你一個數值排序過的陣列,告訴我42有沒有在裡面?

bool find_number(int arr[], int n) {
	for (int i = 0; i < n; i++) {
    	  if (arr[i] == 42)
        	  return true;
    }
    return false;
}

來當五分鐘的哈佛學生

二分搜尋法的基本概念

二分搜尋法

int binary_search(int arr[], int n, int value) {
    int left = 0, right = n - 1;
    while (left <= right) {
    	int mid = (left + right) / 2;
        if (arr[mid] == value)
        	return mid;
        else if (arr[mid] > value)
        	right = mid - 1;
        else
        	left = mid + 1
    }
    return -1;
}

時間複雜度:                    。

O(logn)

遞迴二分搜

int binary_search(int arr[], int left, int right, int value) {
    if (left > right)
        return -1;
    int mid = (left + right) / 2;
    if (arr[mid] == value)
        return mid;
    else if (arr[mid] > value)
        return binary_search(arr, left, mid - 1, value);
    else
        return binary_search(arr, mid + 1, right, value);
}

時間複雜度:                    。

O(logn)

二分搜尋法

來看點範例更清楚!

Binary Search Visualization

使用前提

二分搜可以有效率的尋找指定的元素,但使用的前提是元素必須排序過

或者是說,我們搜尋的對象具有單調性

偷一點算法班的例子

假設你有一顆蛋,外殼非常堅硬,從    樓以下丟出去都不會破,但從超過     樓的地方丟出去就會破掉。請找出   

x
x
x

有單調性嗎?

Yes!

可以使用二分搜嗎?

Yes!

小練習

Sample Solution

int left = 1, right = 100;
while (left < right - 1) {
    int mid = (left + right) / 2;
    if (less(mid))
        right = mid - 1;
    else
        left = mid;
}
if (less(right))
    guess(left);
else
    guess(right);

小總結

以上介紹基本的二分搜尋法的概念以及用法。

雖然二分搜又快又好用,但通常使用時最困難的地方不是在於它的概念以及程式碼,而是怎麼看出對象有單調性,發現其實可以套用二分搜!

小挑戰

你有n(2\leq n \leq 10^5)個盆栽在數線上擺成一列,他們的座標分別為\\ x_1, x_2, \dots, x_n (0\leq x_i \leq 10^9) \\ 現在你想要種s(2\leq s \leq n)棵芽芽在這些盆栽裡,但因為這些芽芽\\ 之後都會長成參天大樹,你希望每棵芽芽之間的距離越大越好,才不會影響到\\ 彼此的生長,因此你想要最大化相鄰芽芽距離的最小值。\\ 你能求出這個最大值是多少嗎?

範例:

你有5個盆栽,他們的座標分別是1, 2, 4, 8, 9

你想要種三棵芽芽,你可以把它們種在1, 2, 4這三個盆栽。

如此第一棵跟第二棵的距離是1,第二棵跟第三棵的距離是2。

這樣的種法距離的最小值是1。

你也可以種在1,4,8這三個盆栽,這樣距離的最小值是3。

可以證明無法做到比3還要更大的數字了,因此答案是3。

Sprout 2020 C/C++ Basic Sorting & Binary Search

By JT

Sprout 2020 C/C++ Basic Sorting & Binary Search

  • 1,011