【資料結構與演算法】一、基本
一、絮絮叨叨
計劃寫一系列資料結構與演算法的部落格:
- 一是給自己立個flag——堅持做完,
- 二是記錄自己的學習過程,總結和分享知識
1、Why?
- 面試 =》考查基礎 =》資料結構與演算法
- 工作 =》有助於理解、使用框架;優化程式,提升效率、效能
- 鍛鍊邏輯思維能力 =》提升個人競爭力
2、What?
資料結構: Array(陣列)、Stack/Queue(堆疊/佇列)、PriorityQueue(優先佇列)、LinkedList(連結串列)、Tree/Binary Search Tree(樹/二分查詢樹)、HashTable(雜湊表、散列表)、Disjoint Set(並查集)、Heap(堆)、跳錶(SkipList)、Trie、BloomFilter、LRU Cache
演算法: Greedy(貪婪演算法)、Recursion/Backtrace(遞迴/回溯)、Traversal(遍歷)、Breadth-first/Depth-first search(廣度優先/深度優先搜尋)、Divide and Conquer(分治法)、Dynamic Programming(動態規劃)、Binary Search(二分查詢)、Graph(圖)、sort(排序)、字串匹配演算法、雜湊演算法
3、How?
(1)Chunk it up(切碎知識點)==》從書籍、課程中獲得 (2)Deliberate practicing(刻意練習) + 刻意練習:練習缺陷、弱點的地方 + 心理:感覺不舒服、不爽、枯燥 =》堅持住 (3)Feedback(反饋) + ① 即時反饋 + ② 主動型反饋(自己去找) + 高手程式碼(Github、LeetCode…) + 論壇交流 + ③ 被動式反饋(高手指點) + code review + 高手看自己打併給予反饋
4、關鍵點
- 來歷、自身的特點、適用解決的問題、實際應用場景
5、解題步驟
(1) 理解題目 (2)想出 possible solutions
- 關注時間/空間複雜度
- 選擇最優解法
(3) Coding!!! (4)測試多種情況下,檢查codes是否考慮周全
一、資料結構
1、概念
資料結構:是相互之間存在一種或多種特定關係的資料元素的集合 =》儲存結構 演算法:是為求解一個問題需要遵循的、被清楚地指定的簡單指令的結合。
兩者關係: 資料結構為演算法服務,演算法作用於資料結構之上。
二、複雜度分析
- 時間/空間複雜度 ——是效率和資源消耗的度量衡(不依賴於環境)
1、大O時間複雜度表示法
- 所有程式碼的執行時間 T(n) 與每行程式碼執行的次數 n 成正比 ==》可用執行的次數衡量演算法的優劣
T(n) = O(f(n))
=》 大O記法 其中,T(n)表示程式碼執行的時間,n表示資料規模的大小,f(n)表示每行程式碼執行次數的總和。 大O時間複雜度實際上並不具體代表程式碼真正的執行時間,而是代表程式碼執行時間隨資料規模增長的變化趨勢,所以,也叫做漸進時間複雜度。
2、時間複雜度分析
- 只關注迴圈執行次數最多的一段程式碼
- 加法法則:總複雜度等於量級最大的那段程式碼的複雜度
若 T1(n) = O(f(n)), T2(n) = O(g(n)),
則 T1(n) = T1(n) + T2(n) = max(O(f(n)), O(g(n))) = O(max(f(n), g(n)))
- 乘法法則:巢狀程式碼的複雜度等於巢狀內外程式碼複雜度的乘積
3、幾種常見時間複雜度
(1)多項式量級
- O(1)——常數複雜度(Constant Complexity)
int i = 10;
int j = 6;
int sum = i + j;
- O(logn) ——對數複雜度
for(int i = 1; i < n; i = i * 2){
cout<<"Hey~I'm busy looking at:"<<i<<endl;
}
- O(n)——線性複雜度
for(int i = 1; i <= n; i++){
cout<<"Hey~I'm busy looking at:"<<i<<endl;
}
- O(nlogn)——線性對數階
- O(nlogn) 也是一種非常常見的演算法時間複雜度。比如,歸併排序、快速排序的時間複雜度都是 O(nlogn)。
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j = j * 2){
cout<<"Hey~I'm busy looking at:"<<i<<"and"<<j<<endl;
}
}
- O(n2)、O(n3) … O(nk)——平方階、立方階…k次方階
// 平方階複雜度
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
cout<<"Hey~I'm busy looking at:"<<i<<"and"<<j<<endl;
}
}
//k次方階複雜度
for(int i = 1; i <= Math.pow(2,n);i++){
cout<<"Hey~I'm busy looking at:"<<i<<endl;
}
- O(m+n)、O(m*n)——m和n表示兩個資料規模的大小,且無法事先評估m和n的量級大小
(2)非多項式量級
時間複雜度為非多項式量級的演算法問題 =》NP(Non-Deterministic Polynomial)問題
- O(2n)——指數階
- O(n!)——階乘階
for(int i = 1; i <= factorial(n); i++){
cout<<"Hey~I'm busy looking at:"<<i<<endl;
}
4、空間複雜度分析
空間複雜度表示演算法的儲存空間與資料規模之間的增長關係,又稱漸進空間複雜度。 常見的空間複雜度:O(1)、O(n)、O(n2)
void print(int n){
int i = 0;
int *a = new int[n];
for (i; i<n; ++i){
a[i] = i * i; }
for (i = n-1; i>=0; --i){
cout<<a[i]<<endl; }
}
第二行程式碼中,我們申請了一個空間儲存變數i,但是它是常量階的,跟資料規模n沒有關係,所以可以忽略。第三行中,我們申請了一個大小為n的int型別資料。所以整段程式碼的空間複雜度就是O(n)。
5、複雜度比較
複雜度從低到高: O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) <… < O(2n) < O(n!)
三、其他時間複雜度
1、最好/最壞情況時間複雜度(best/wrost case time complexity)
// 目的:在一個無序的陣列中,查詢變數x出現的位置
// n 表示陣列 array 的長度
int fun1( int[] array, int n, int x){
int i = 0;
int pos = -1;
for(; i < n, ++i){
//找到即結束
if( array[i] == x) {
pos = i;
break;
}
}
return pos;
}
複雜度分析:變數 x 可能出現在陣列中的任何位置。如果陣列中第一個元素正好是要查詢的變數 x ,那就不需要遍歷剩下的 n-1 個數據了,這時時間複雜度為 O(1) ,也就是最好情況時間複雜度為 O(1)。 但如果陣列中不存在變數 x ,那麼就需要把整個陣列都辦理一遍,時間複雜度就是 O(n) ,也就是最壞情況時間複雜度為 O(n)。 所以不同情況下,這段程式碼的時間複雜度是不一樣的。
==》引入概念:最好情況時間複雜度、最壞情況時間複雜度、平均情況時間複雜度。
2、平均情況時間複雜度
平均情況時間複雜度:假定各種輸入例項的出現符合某種概率分佈(如均勻獨立隨機分佈)之後,進而估計出的加權平均時間複雜度
複雜度分析: 仍為上面的例子,要查詢的變數x,要麼在數組裡,要麼不在數組裡。這兩種情況對應的概率統計起來很麻煩,假設這兩種情況的概率都為1/2。要查詢的資料在0~n-1這n個位置的概率也是一樣的,為1/n。所以根據概率乘法法則,要查詢的資料出現在0~n-1中任意位置的概率為1/2n。 把每種情況發生的概率也考慮上:
1*1/2n + 2*1/2n + 3*1/2n + ... + n*1/2n + n*1/2 = (3n+1)/4
==》平均情況時間複雜度為O(n)
3、均攤時間複雜度
(1)舉例
目的:陣列元素求和(有限大小)——往陣列中插入資料,當陣列滿後count = n,用for迴圈遍歷陣列求和,並清空陣列。求和之後的sum值當道陣列的第一個位置,然後再進行插入,如果陣列一開始就有空閒空間,直接插入。
int arr[SIZE] = {0};
int n = sizeof(arr)/sizeof(arr[0]);
int count = 0;
int fun2(int val)
{
if(count == n)
{
int sum = 0;
for(int i=0; i<n; i++)
{
sum = sum + arr[i];
}
arr[0] = sum;
count = 1;
}
arr[count] = val;
++count;
}
最好情況時間複雜度:O(1)
最壞情況時間複雜度:O(n)
平均時間複雜度: 假設陣列的長度是n,根據資料插入位置的不同,我們可以分為n種情況,每種情況的時間複雜度為O(1)。除此外,還有一種情況,就是當陣列沒有空閒空間時,這時的時間複雜度為O(n)。而且,這n+1種情況發生的概率一樣,都是1/(n+1)。所以有:
1*1/(n+1) + 1*1/(n+1) + ... + 1*1/(n+1) + n*1/(n+1) = O(1)
(2)對比 fun1( ) 與 func2( ):
- fun1函式在極端情況下,複雜度才為O(1)。但是fun2在大部分情況下,時間複雜度都為O(1),只有個別情況下,複雜度才比較高。
- 對於fun2來說,O(1)時間複雜度的插入和O(n)時間複雜度的插入的出現是有規律的,而且有一定的前後時序關係,一般是O(n)插入後,緊跟著n-1個O(1)的插入操作,迴圈往復。
均攤時間複雜度: 在程式碼執行的所有複雜度情況中絕大部分是低級別的複雜度,個別情況是高級別複雜度且發生具有時序關係時,可以將個別高級別複雜度均攤到低級別複雜度上。基本上均攤結果就等於低級別複雜度。
上例分析:對於每一次的O(n)操作,將時間均攤給n-1個O(1)的操作,這樣均攤下來,總的時間複雜度為O(1)。==》func2()的均攤時間複雜度為O(1)