蔡銘軒 @ 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;
}
一直交換下去,總有一天會排序好。
那麼我們要怎麼有系統地交換元素呢?
在上Code之前,先來看看它的概念吧!
概念:
每次操作從index小的元素開始往後,依次檢查相鄰的兩個元素,如果大小順序顛倒,就交換!
不斷重複這樣的步驟,直到排序完成。
來看點例子更好懂!
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]);
}
時間複雜度?
外層的迴圈:
內層的迴圈:
交換:
總共:
best case/worst case都是 ...
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:
在上Code之前,先來看看它的概念吧!
概念:
把陣列分成已排序跟未排序兩部分(一開始全部都是未排序)。
每次操作從未排序的部分找出最小值,把它跟未排序的第一個元素交換(效果相當於把這個元素接在已排序部分的最後面),並將這個元素歸類為已排序
重複直到排序完成。
來看點例子更好懂!
時間複雜度?
外層的迴圈:
內層的迴圈:
交換:
總共:
best case/worst case都是 ...
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]); // 把未排序部分的最小值放到對的位置
}
}
在上Code之前,先來看看它的概念吧!
概念:
把陣列分為已排序跟未排序兩部分(一開始第一個元素是已排序,其餘為未排序)。
每次操作選擇未排序部分的第一個元素,看看他應該被插在已排序部分的哪個位置,把它放到那裡去!
想想看你玩撲克牌時怎麼整牌的,或許可以幫助理解
來看點例子更好懂!
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放入該放的位置
}
}
時間複雜度?
外層的迴圈:總是
內層的迴圈:最佳 最差
交換:
總共:最佳 最差
#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;
}
以上介紹三種非常基本,基於交換的排序演算法
這三種排序演算法很容易出現 的時間複雜度,在資料量較大時 表現非常差
有興趣的同學可以自行學習其他更有效率的排序演算法,例如:
Merge Sort:
Quick Sort: worst case ,但平均 ,且實務上非常快!
好用工具介紹:
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;
}
基於比較&交換的排序演算法,最好只能做到 了
也有非基於交換的排序演算法,例如:
Couting Sort:
Radix Sort:
但以上兩種排序法有使用條件的限制,並不是絕對較好。
有興趣的同學可以自行學習!
Q: 給你一個數值隨機分布的陣列,告訴我42有沒有在裡面?
bool find_number(int arr[], int n) {
for (int i = 0; i < n; i++) {
if (arr[i] == 42)
return true;
}
return false;
}
時間複雜度: 。
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;
}
時間複雜度: 。
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);
}
時間複雜度: 。
來看點範例更清楚!
二分搜可以有效率的尋找指定的元素,但使用的前提是元素必須排序過!
或者是說,我們搜尋的對象具有單調性
假設你有一顆蛋,外殼非常堅硬,從 樓以下丟出去都不會破,但從超過 樓的地方丟出去就會破掉。請找出
有單調性嗎?
Yes!
可以使用二分搜嗎?
Yes!
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);
以上介紹基本的二分搜尋法的概念以及用法。
雖然二分搜又快又好用,但通常使用時最困難的地方不是在於它的概念以及程式碼,而是怎麼看出對象有單調性,發現其實可以套用二分搜!
範例:
你有5個盆栽,他們的座標分別是1, 2, 4, 8, 9
你想要種三棵芽芽,你可以把它們種在1, 2, 4這三個盆栽。
如此第一棵跟第二棵的距離是1,第二棵跟第三棵的距離是2。
這樣的種法距離的最小值是1。
你也可以種在1,4,8這三個盆栽,這樣距離的最小值是3。
可以證明無法做到比3還要更大的數字了,因此答案是3。