對於一個程式
要如何計算運行時間
假如今天老師出作業 給你兩種方案:
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 題加法……
如果n很小 那2會比較輕鬆 比如等於3 (600>30)
但如果n很大 365的話呢
方案2需要的操作"量級"比較多
所以就長期來說 做方案1會比較輕鬆
今天如果有兩個函數 f(n) g(n)
當他們接近無限大 來比較他們的大小
超級比一比:
可以看到數字變大 次方比常數影響得多
接著來看次數的量級(複雜度)
並評估時間
• 加減乘除
• 取餘數
• 位運算
• 存取記憶體
• 判斷運算子
• 賦值運算子
...
然後把操作的次數算出來
比如長這樣:
每次要列式子很麻煩
常數其實相對沒那麼重要
最常用的Big-O就出現了!
寫法:
f(n)是複雜度量級的上界
通常會選最接近的上界
(不嚴格<=)
其他就聽聽就行
記為 𝑜(𝑓(𝑛))
其中 𝑓(𝑛) 為複雜度量級的嚴格上界
(嚴格<=)
(不嚴格>=)
記為 Ω(𝑓(𝑛))
其中 𝑓(𝑛) 為複雜度量級的下界
記為 ω(𝑓(𝑛))
其中 𝑓(𝑛) 為複雜度量級的嚴格下界
(嚴格>=)
其中 𝑓(𝑛) 為複雜度量級的嚴格上下界 (完全相同!)
記為 Θ(𝑓(𝑛))
之後演算法的課程一定會一直提到時間複雜度
只是一點點小小的數學
希望不要燒機
氣泡排序 (Bubble Sort)
選擇排序 (Selection Sort)
插入排序 (Insertion Sort)
快速排序 (Quick Sort)
合併排序 (Merge Sort)
接下來會預設為由小排到大
重複掃過陣列
比較相鄰元素
如果前>後
交換兩元素
有點像泡泡浮起來的過程
時間複雜度O(n²)
#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';
}
}
重複掃過迴圈
找最小值
丟到左邊
時間複雜度O(n²)
#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';
}
}
將陣列分成兩堆 已排序/未排序
每次將未排序的插入已排序的正確位置
時間複雜度O(n²)
#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';
}
}
分治概念
選定一個pivot(軸心)
將比pivot小的丟左邊大的丟右邊
時間複雜度不穩定
平均:O(n log n)
最差:O(n²)
不斷切割陣列 直到每個值都到了正確位置
利用遞迴
在選擇pivot上
有各種不同寫法
code是選最右邊一位
不同選法會導致複雜度發生改變
使用random可以讓分割比較平均
想想看甚麼情況會讓時間複雜度變成O(n²)
// 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;
}還沒教到分治遞迴
但概念大概看一下
分治概念
把問題拆分成小問題
時間複雜度O(n log n)
不斷切割陣列 每次合併時將陣列排好
分:每次對半切
治:將兩陣列合併 依照順序
#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]<<' ';
}
}
還沒教到分治遞迴
但概念大概看一下
如果你想實作看看
檢驗自己的程式對不對
在資料中找尋特定元素或滿足條件的位置
常見方法:暴力搜、雙指針、二分搜、圖搜尋
使用時機就看題目給的條件
以及資料的特性
並注意複雜度會不會爆
又稱線性搜尋法(Linear Search)
時間複雜度O(n)
適用於資料量小、未排序
用for或while迴圈
從頭掃過陣列
如果找到就輸出 沒找到就回傳false或-1
這種簡單東西不太會出現在競程
只要會迴圈就寫得出來
時間複雜度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;
int ans=-1;
while(l<r){
int mid=(l+r)/2;
if(arr[mid]>q)r=mid;
else if(arr[mid]<q)l=mid+1;
else{
ans=mid;
break;
}
}[0,r)
l==r
[l,l)沒意義
小畫家解釋
我建議可以直接背下來
比較少用
int l=0;
int r=n-1;
int ans=-1;
while(l<=r){
int mid=(l+r)/2;
if(arr[mid]>q)r=mid-1;
else if(arr[mid]<q)l=mid+1;
else{
ans=mid;
break;
}
}[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'; // 超出範圍 值不定
}模板題
想法:
對答案二分搜
會發現答案會有單調性
常用於:
兩數相加(已排序陣列)
子序列(陣列只往前不回頭)
滑動區間(需要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");
希望各位沒燒機