1. 程式人生 > >【資料結構與演算法】一、基本

【資料結構與演算法】一、基本

一、絮絮叨叨

計劃寫一系列資料結構與演算法的部落格:

  1. 一是給自己立個flag——堅持做完,
  2. 二是記錄自己的學習過程,總結和分享知識

1、Why?

  1. 面試 =》考查基礎 =》資料結構與演算法
  2. 工作 =》有助於理解、使用框架;優化程式,提升效率、效能
  3. 鍛鍊邏輯思維能力 =》提升個人競爭力

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)