排序、搜尋

INDEX

  • 時間複雜度
  • 各種排序法
  • 搜尋

時間複雜度

時間複雜度

對於一個程式

要如何計算運行時間

假如今天老師出作業 給你兩種方案:

1. 每天算 10 題乘法

2. 第一天算 1 題加法,第二天算 2 題加法,第三天算 3 題加法……

你會選哪一種?

時間複雜度

假設算一題加法需要 5 秒,算一題乘法需要 20 秒

到了第n天:

1. 10 * n * 20 = (200n)秒

2. (n + 1) * n/2 * 5 =

1. 每天算 10 題乘法

2. 第一天算 1 題加法,第二天算 2 題加法,第三天算 3 題加法……

2.5*(n^2+n)

如果n很小 那2會比較輕鬆 比如等於3   (600>30)

但如果n很大 365的話呢

方案2需要的操作"量級"比較多

所以就長期來說 做方案1會比較輕鬆

量級?

今天如果有兩個函數 f(n) g(n)

當他們接近無限大 來比較他們的大小

超級比一比:

•\ 100𝑛\ vs\ 200n\\
•\ 3𝑛^2 + 𝑛 + 20\ vs\ 100𝑛\\
•\ 𝑛^{100}\ vs\ 2^𝑛\\
•\ 𝑛^2\ vs\ 10n\log 𝑛\\
•\ 100𝑛\ vs\ 𝑛!\\
•\ 30 ∗ 2^𝑛\ vs\ 3^𝑛\\

量級?

可以看到數字變大 次方比常數影響得多

複雜度的概念

接著來看次數的量級(複雜度)

並評估時間

• 加減乘除

• 取餘數

• 位運算

• 存取記憶體

• 判斷運算子

• 賦值運算子

...

然後把操作的次數算出來

3n^2+n+20

比如長這樣:

大歐符號 (Big-O)

每次要列式子很麻煩

常數其實相對沒那麼重要

最常用的Big-O就出現了!

寫法:

f(n)是複雜度量級的上界

O(f(n))
• ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∊ 𝑂(𝑛^3)\\ • ex. 𝑓(𝑛) ∊ 𝑂(𝑛^2) ,則 𝑓(𝑛) ∊ 𝑂(𝑛^3)\\ • ex. 1000 ∊ 𝑂(1)

通常會選最接近的上界

(不嚴格<=)

一般會用的就Big-O

其他就聽聽就行

小歐符號 (Little-o)

記為 𝑜(𝑓(𝑛))

其中 𝑓(𝑛) 為複雜度量級的嚴格上界

• ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∉ 𝑜(𝑛^3)\\ • ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∊ 𝑜(𝑛^4)\\ • ex. 𝑓(𝑛) ∊ 𝑜(𝑛^2) ,則 𝑓(𝑛) ∊ 𝑜(𝑛^3)

(嚴格<=)

• ex.\ 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3\ ∊\ Ω(𝑛^3)\\ • ex.\ 𝑓 (𝑛)\ ∊\ Ω (𝑛^2),則 𝑓(𝑛)\ ∊\ Ω(𝑛)

(不嚴格>=)

Little-omega

Big-omega

記為 Ω(𝑓(𝑛))

其中 𝑓(𝑛) 為複雜度量級的下界

記為 ω(𝑓(𝑛))

其中 𝑓(𝑛) 為複雜度量級的嚴格下界

• ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∊ ω(𝑛^2)\\ • ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∉ ω(𝑛^3)

(嚴格>=)

其中 𝑓(𝑛) 為複雜度量級的嚴格上下界 (完全相同!)

• ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∊ Θ(𝑛^3)\\ • ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∉ Θ(𝑛^4)\\ • ex. 3𝑛^3 + 5𝑛^2 + 10𝑛 + 3 ∉ Θ(𝑛^2)

Big-theta

記為 Θ(𝑓(𝑛))

之後演算法的課程一定會一直提到時間複雜度

只是一點點小小的數學

希望不要燒機

各種排序法

各種排序法

氣泡排序 (Bubble Sort)

選擇排序 (Selection Sort)

插入排序 (Insertion Sort)

快速排序 (Quick Sort)

合併排序 (Merge Sort)​

接下來會預設為由小排到大

1. 氣泡排序 (Bubble Sort)

重複掃過陣列

比較相鄰元素

如果前>後

交換兩元素

有點像泡泡浮起來的過程

時間複雜度O(n²)

實作code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 20005
int arr[maxn],n;
void bubble_sort(){
    for(int i=0;i<n-1;i++){
        for(int j=0;j<n-1;j++){
            if(arr[j]>arr[j+1])swap(arr[j],arr[j+1]);
        }
    }
}
signed main(){
    while(cin>>n){
        for(int i=0;i<n;i++){
            cin>>arr[i];
        }
        bubble_sort();
        for(int i=0;i<n;i++){
            cout<<arr[i]<<' ';
        }
        cout<<'\n';
    }
}

2. 選擇排序 (Selection Sort)

重複掃過迴圈
找最小值

丟到左邊

時間複雜度O(n²)

實作code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 20005
int arr[maxn],n;
void selection_sort(){
    for(int i=0;i<n-1;i++){
        int mnindex=i;
        for(int j=i+1;j<n;j++){
            if(arr[j]<arr[mnindex])mnindex=j;
        }
        swap(arr[i],arr[mnindex]);
    }
}
main(){
    while(cin>>n){
        for(int i=0;i<n;i++){
            cin>>arr[i];
        }
        selection_sort();
        for(int i=0;i<n;i++){
            cout<<arr[i]<<' ';
        }
        cout<<'\n';
    }
}


3. 插入排序 (Insertion Sort)

將陣列分成兩堆 已排序/未排序

每次將未排序的插入已排序的正確位置

時間複雜度O(n²)

實作code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 20005
int arr[maxn],n;
void insertion_sort(){
    for (int i=1; i<n;i++) {
        int key = arr[i];
        int j=i-1;
        while (key<arr[j] && j>=0) {
            arr[j+1]=arr[j];
            j--;
        }
        arr[j+1]=key;
    }
}
main(){
    while(cin>>n){
        for(int i=0;i<n;i++){
            cin>>arr[i];
        }
        insertion_sort();
        for(int i=0;i<n;i++){
            cout<<arr[i]<<' ';
        }
        cout<<'\n';
    }
}



4. 快速排序 (Quick Sort)

分治概念

選定一個pivot(軸心)

將比pivot小的丟左邊大的丟右邊

時間複雜度不穩定

平均:O(n log n)

最差:O(n²)

不斷切割陣列 直到每個值都到了正確位置

利用遞迴

4. 快速排序 (Quick Sort)

在選擇pivot上

有各種不同寫法

code是選最右邊一位

不同選法會導致複雜度發生改變

使用random可以讓分割比較平均

想想看甚麼情況會讓時間複雜度變成O(n²)

實作code

// C++ Program to demonstrate how to implement the quick
// sort algorithm
#include <bits/stdc++.h>
using namespace std;

int partition(vector<int> &vec, int low, int high) {

    // Selecting last element as the pivot
    int pivot = vec[high];

    // Index of elemment just before the last element
    // It is used for swapping
    int i = (low - 1);

    for (int j = low; j <= high - 1; j++) {

        // If current element is smaller than or
        // equal to pivot
        if (vec[j] <= pivot) {
            i++;
            swap(vec[i], vec[j]);
        }
    }

    // Put pivot to its position
    swap(vec[i + 1], vec[high]);

    // Return the point of partition
    return (i + 1);
}

void quickSort(vector<int> &vec, int low, int high) {

    // Base case: This part will be executed till the starting
    // index low is lesser than the ending index high
    if (low < high) {

        // pi is Partitioning Index, arr[p] is now at
        // right place
        int pi = partition(vec, low, high);

        // Separately sort elements before and after the
        // Partition Index pi
        quickSort(vec, low, pi - 1);
        quickSort(vec, pi + 1, high);
    }
}

int main() {
    vector<int> vec = {10, 7, 8, 9, 1, 5};
    int n = vec.size();
	
  	// Calling quicksort for the vector vec
    quickSort(vec, 0, n - 1);

    for (auto i : vec) {
        cout << i << " ";
    }
    return 0;
}

還沒教到分治遞迴

但概念大概看一下

5. 合併排序 (Merge Sort)

分治概念

把問題拆分成小問題

時間複雜度O(n log n)

不斷切割陣列 每次合併時將陣列排好

5. 合併排序 (Merge Sort)

分:每次對半切

治:將兩陣列合併 依照順序

實作code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 1000005
int arr[maxn],n;
void mergesort(int l,int r){
    if(l==r)return;
    int mid=(l+r)/2;
    mergesort(l,mid);
    mergesort(mid+1,r);
    int n1=mid-l+1,n2=r-mid;
    int L[n1+1],R[n2+1];
    for(int i=0;i<n1;i++)L[i]=arr[l+i];
    for(int i=0;i<n2;i++)R[i]=arr[mid+i+1];
    int x=0,y=0,z=l;
    while(x<n1 && y<n2){
        if(L[x]<R[y]){
            arr[z++]=L[x++];
        }
        else arr[z++]=R[y++];
    }
    while(x<n1)arr[z++]=L[x++];
    while(y<n2)arr[z++]=R[y++];
}
main(){
    int n;
    while(cin>>n){
        for(int i=0;i<n;i++)cin>>arr[i];
        mergesort(0,n-1);
        for(int i=0;i<n;i++)cout<<arr[i]<<' ';
    }
}

還沒教到分治遞迴

但概念大概看一下

補充

C++函式 std::sort 

時間複雜度:最差 O(n log n) 

實作原理:

sort() 底層為 intro sort,是以 Quick Sort 為核心的混合型排序法。

網路上電神簡報

 

補充

題目

如果你想實作看看

檢驗自己的程式對不對

搜尋

搜尋

在資料中找尋特定元素或滿足條件的位置

常見方法:暴力搜、雙指針、二分搜、圖搜尋

使用時機就看題目給的條件

以及資料的特性

並注意複雜度會不會爆

1.暴力搜尋法

又稱線性搜尋法(Linear Search)

時間複雜度O(n)

適用於資料量小、未排序

用for或while迴圈

從頭掃過陣列

如果找到就輸出 沒找到就回傳false或-1

這種簡單東西不太會出現在競程

只要會迴圈就寫得出來

2.二分搜尋法

時間複雜度O(log n)

適用於有單調性的資料

類似終極密碼的玩法

每次將陣列切一半

所以複雜度快很多

比賽常用

實作上有些需要注意的地方

容易搞混

單調性

現在要找一個元素

成立:true

不成立:false

擁有單調性的資料會長怎樣

000000111111111

我們要找的答案就會是在01交界的值

111111000000

或

單調性

舉例

今天要找一個數字:5

擁有單調性的資料會長怎樣

0 0 0 1 1 1  1   1   1   1  

這樣就可以一次把左邊或右邊的值全部刪掉

true為>=5的

1 2 3 5 7 9 11 13 15 17

所以如果選到一個值

1:右邊都是true

0:左邊都是false

常見使用時機

找某個值

找滿足條件最大最小值

->一般經典二分搜

->對答案二分搜

實作

有很多流派:

左閉右開、左閉右閉、左開右開...

二分搜常常發生邊界問題

今天講最常見的

左閉右開

左閉右閉

先來講開閉區間是什麼

區間

開區間:不包含左界 不包含右界

(a,b)={x∈R:a<x<b}

半開區間

左開右閉:不包含左界 包含右界

(a,b]={x∈R:a<x≤b}

閉區間:包含左界 包含右界

[a,b]={x∈R:a≤x≤b}

左閉右開:包含左界 不包含右界

[a,b)={x∈R:a≤x<b}

左閉右開

如果要查大小為n的陣列arr

存不存在q

表示方法:[0,n)  ->  0,1,...,n-1

每次mid=(l+r)/2

當arr[mid]>q

代表 q在arr[mid]的左邊

當arr[mid]<=q

代表q在arr[mid]的右邊

左閉右開

code

int l=0;
int r=n;
while(l<r){
  int mid=(l+r)/2;
  if(arr[mid]>x)r=mid;
  else l=mid+1;
}

[0,r)

l==r

[l,l)沒意義

小畫家解釋

我建議可以直接背下來

左閉右閉

比較少用

int l=0;
int r=n-1;
while(l<=r){
  int mid=(l+r)/2;
  if(arr[mid]>x)r=mid-1;
  else l=mid+1;
}

[0,r-1]

l==r

[l,l]包含l

小畫家解釋

函式

C++有幫你寫好的函式

lower_bound/upper_bound

lower_bound(a,b,c):

回傳在 a 位置(包含)到 b 位置(不含)之間第一個大於等於 c 的元素的位置。

upper_bound(a,b,c):

回傳在 a 位置(包含)到 b 位置(不含)之間第一個大於 c 的元素的位置。

注意事項

如果沒找到符合條件的值

回傳b

只有在從小到大的陣列才可以使用

底層是用binary search

用*取值

vector<int>v={3,5,1,2};

int main(){
    sort(v.begin(), v.end());
	cout << *lower_bound(v.begin(), v.end(), 3) << '\n'; // 3
    cout << *lower_bound(v.begin(), v.end(), 2) << '\n'; // 2
    cout << *upper_bound(v.begin(), v.end(), 3) << '\n'; // 5
    cout << *lower_bound(v.begin(), v.end(), 6) << '\n'; // 超出範圍 值不定 
}

實作時間

模板題

進階題目

想法:

對答案二分搜

會發現答案會有單調性

3.雙指針

常用於:

兩數相加(已排序陣列)
子序列(陣列只往前不回頭)
滑動區間(需要O(n))

維護兩個index

並讓他們在資料中移動

兩數和

前提:已排序陣列

今天要來找兩數和為x的index

舉例 要找總和為8的

1 2 4 5 7

l設為左界、r設為右界

如果總和>8 右界往左

如果總和<8左界往右

兩數和

Code

int i = 0, j = n - 1;
while(i < j){
    int sum = a[i] + a[j];
    if(sum == target){
        cout << i << " " << j;
        break;
    }
    else if(sum < target) i++;
    else j--;
}

子序列

前提:子序列 只需要順序相同

給A、B兩字串

判斷B是否為A的子序列

舉例
A : abcde
B : ace

i設為A的第一個index、r設為B的第一個index

每次比對到一樣的j就往後走

子序列

Code

int i = 0, j = 0;
while(i < A.size() && j < B.size()){
    if(A[i] == B[j]) j++;
    i++;
}
cout << (j == B.size() ? "YES" : "NO");

題目

先教到這裡

希望各位沒燒機

排序、搜尋

By wuchanghualeo

排序、搜尋

  • 246