第0篇:學習資料結構和演算法的框架思維

噠宰的自我修養發表於2020-12-27

學習資料結構和演算法的框架思維

⼀、資料結構的儲存⽅式:

資料結構的儲存⽅式只有兩種:陣列(順序儲存)和連結串列(鏈式儲存)。

這句話怎麼理解,不是還有雜湊表、棧、佇列、堆、樹、圖等等各種資料結 構嗎?

我們分析問題,⼀定要有遞迴的思想,⾃頂向下,從抽象到具體。你上來就 列出這麼多,那些都屬於「上層建築」,⽽陣列和連結串列才是「結構基礎」。 因為那些多樣化的資料結構,究其源頭,都是在連結串列或者陣列上的特殊操 作,API 不同⽽已。

⽐如說「佇列」、「」這兩種資料結構既可以使⽤連結串列也可以使⽤陣列實 現。⽤陣列實現,就要處理擴容縮容的問題;⽤連結串列實現,沒有這個問題, 但需要更多的記憶體空間儲存節點指標。

」的兩種表⽰⽅法,鄰接表就是連結串列,鄰接矩陣就是⼆維陣列。鄰接矩 陣判斷連通性迅速,並可以進⾏矩陣運算解決⼀些問題,但是如果圖⽐較稀 疏的話很耗費空間。鄰接表⽐較節省空間,但是很多操作的效率上肯定⽐不 過鄰接矩陣。

雜湊表」就是通過雜湊函式把鍵對映到⼀個⼤陣列⾥。⽽且對於解決雜湊 衝突的⽅法,拉鍊法需要連結串列特性,操作簡單,但需要額外的空間儲存指 針;線性探查法就需要陣列特性,以便連續定址,不需要指標的儲存空間, 但操作稍微複雜些。

」,⽤陣列實現就是「堆」,因為「堆」是⼀個完全⼆叉樹,⽤陣列存 儲不需要節點指標,操作也⽐較簡單;⽤連結串列實現就是很常⻅的那種 「樹」,因為不⼀定是完全⼆叉樹,所以不適合⽤陣列儲存。為此,在這種 連結串列「樹」結構之上,⼜衍⽣出各種巧妙的設計,⽐如⼆叉搜尋樹、AVL 樹、紅⿊樹、區間樹、B 樹等等,以應對不同的問題。

瞭解 Redis 資料庫的朋友可能也知道,Redis 提供列表、字串、集合等等 ⼏種常⽤資料結構,但是對於每種資料結構,底層的儲存⽅式都⾄少有兩 種,以便於根據儲存資料的實際情況使⽤合適的儲存⽅式。

綜上,資料結構種類很多,甚⾄你也可以發明⾃⼰的資料結構,但是底層存 儲⽆⾮陣列或者連結串列,⼆者的優缺點如下:

**陣列**由於是緊湊連續儲存,可以隨機訪問,通過索引快速找到對應元素,⽽ 且相對節約儲存空間。但正因為連續儲存,記憶體空間必須⼀次性分配夠,所 以說陣列如果要擴容,需要重新分配⼀塊更⼤的空間,再把資料全部複製過 去,時間複雜度 O(N);⽽且你如果想在陣列中間進⾏插⼊和刪除,每次必 須搬移後⾯的所有資料以保持連續,時間複雜度 O(N)。

**連結串列**因為元素不連續,⽽是靠指標指向下⼀個元素的位置,所以不存在陣列 的擴容問題;如果知道某⼀元素的前驅和後驅,操作指標即可刪除該元素或 者插⼊新元素,時間複雜度 O(1)。但是正因為儲存空間不連續,你⽆法根 據⼀個索引算出對應元素的地址,所以不能隨機訪問;⽽且由於每個元素必 須儲存指向前後元素位置的指標,會消耗相對更多的儲存空間。

⼆、資料結構的基本操作:

對於任何資料結構,其基本操作⽆⾮遍歷 + 訪問,再具體⼀點就是:增刪 查改。

資料結構種類很多,但它們存在的⽬的都是在不同的應⽤場景,儘可能⾼效 地增刪查改。話說這不就是資料結構的使命麼?

如何遍歷 + 訪問?我們仍然從最⾼層來看,各種資料結構的遍歷 + 訪問⽆ ⾮兩種形式:線性的和⾮線性的。

線性就是 for/while 迭代為代表,⾮線性就是遞迴為代表。再具體⼀步,無非以下幾種框架:

陣列遍歷框架,典型的線性迭代結構:

void traverse(int[] arr) 
{ 
	for (int i = 0; i < arr.length; i++) 
	{ 
		// 迭代訪問 arr[i] 
	} 
}

連結串列遍歷框架,兼具迭代和遞迴結構:

/* 基本的單連結串列節點 */ 
class ListNode 
{ 
	int val; 
	ListNode next;
}

void traverse(ListNode head) 
{ 
	for (ListNode p = head; p != null; p = p.next) 
	{ 
	// 迭代訪問 p.val 
	} 
}

void traverse(ListNode head) 
{ 
	// 遞迴訪問 head.val traverse(head.next)
}

⼆叉樹遍歷框架,典型的⾮線性遞迴遍歷結構:

/* 基本的⼆叉樹節點 */ 
class TreeNode 
{ 
	int val; 
	TreeNode left, right; 
}

void traverse(TreeNode root) 
{ 
	traverse(root.left) 
	traverse(root.right) 
}

你看⼆叉樹的遞迴遍歷⽅式和連結串列的遞迴遍歷⽅式,相似不?再看看⼆叉樹 結構和單連結串列結構,相似不?如果再多⼏條叉,N 叉樹你會不會遍歷?
⼆叉樹框架可以擴充套件為 N 叉樹的遍歷框架:

/* 基本的 N 叉樹節點 */ 
class TreeNode 
{ 
    int val; 
    TreeNode[] children; 
}

void traverse(TreeNode root) 
{ 
	for (TreeNode child : root.children) 
	traverse(child) 
}

N 叉樹的遍歷⼜可以擴充套件為圖的遍歷,因為圖就是好⼏ N 叉棵樹的結合 體。你說圖是可能出現環的?這個很好辦,⽤個布林陣列 visited 做標記就 ⾏了,這⾥就不寫程式碼了。

所謂框架,就是套路。不管增刪查改,這些程式碼都是永遠⽆法脫離的結構, 你可以把這個結構作為⼤綱,根據具體問題在框架上新增程式碼就⾏了,下⾯ 會具體舉例。

三、演算法刷題指南

⾸先要明確的是,資料結構是⼯具,演算法是通過合適的⼯具解決特定問題的方法。也就是說,學習演算法之前,最起碼得了解那些常⽤的資料結構,瞭解 它們的特性和缺陷。

那麼該如何在 LeetCode 刷題呢?
直接說具體的建議:

先刷⼆叉樹,先刷⼆叉樹,先刷⼆叉樹!

在這裡插入圖片描述
⼤部分⼈對資料結構相關的演算法⽂章不感興趣,⽽是更關⼼動規回溯分治等等技巧。
為什麼要先刷⼆叉樹呢?
因為⼆叉樹是最容易培養框架思維的,⽽且⼤部分演算法技巧,本質上都是樹的遍歷題。

刷⼆叉樹看到題⽬沒思路?

其實⼤家不是沒思路,只是沒有理解我們說的「框架」是什麼。不要⼩看這⼏⾏破程式碼,⼏乎所有⼆ 叉樹的題⽬都是⼀套這個框架就出來了。

void traverse(TreeNode root) 
{ 
	// 前序遍歷 
	traverse(root.left) 
	// 中序遍歷 
	traverse(root.right) 
	// 後序遍歷 
}

⽐如說我隨便拿⼏道題的解法出來,不⽤管具體的程式碼邏輯,只要看看框架 在其中是如何發揮作⽤的就⾏。

LeetCode 124 題難度 Hard,讓你求⼆叉樹中最⼤路徑和,主要程式碼如下:

int ans = INT_MIN; 
int oneSideMax(TreeNode* root) 
{ 
	if (root == nullptr) 
		return 0; 
	int left = max(0, oneSideMax(root->left)); 
	int right = max(0, oneSideMax(root->right)); 
	ans = max(ans, left + right + root->val); 
	return max(left, right) + root->val; 
}

你看,這就是個後序遍歷嘛。

LeetCode 105 題難度 Medium,讓你根據前序遍歷和中序遍歷的結果還原 ⼀棵⼆叉樹,很經典的問題吧,主要程式碼如下:

TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd, Map<Integer, Integer> inMa p) 
{
	if(preStart > preEnd || inStart > inEnd) 
	    return null; 
	    TreeNode root = new TreeNode(preorder[preStart]); 
	    int inRoot = inMap.get(root.val); 
	    int numsLeft = inRoot - inStart;

		root.left = buildTree(preorder, preStart + 1, preStart + numsLeft , inorder, inStart, inRoot - 1, inMap); 
		root.right = buildTree(preorder, preStart + numsLeft + 1, preEnd, inorder, inRoot + 1, inEnd, inMap); 
		return root; 
}

不要看這個函式的引數很多,只是為了控制陣列索引⽽已,本質上該演算法也 就是⼀個前序遍歷。

LeetCode 99 題難度 Hard,恢復⼀棵 BST,主要程式碼如下:

void traverse(TreeNode* node) 
{ 
	if (!node) 
		return; 
	traverse(node->left);
 	if (node->val < prev->val)
 	{ 
 		s = (s == NULL) ? prev : s; 
 		t = node; 
 	}
 	prev = node; 
 	traverse(node->right); 
}

這不就是個中序遍歷嘛,對於⼀棵 BST 中序遍歷意味著什麼,應該不需要 解釋了吧。

你看,Hard 難度的題⽬不過如此,⽽且還這麼有規律可循,只要把框架寫 出來,然後往相應的位置加東⻄就⾏了,這不就是思路嘛。

對於⼀個理解⼆叉樹的⼈來說,刷⼀道⼆叉樹的題⽬花不了多⻓時間。那麼 如果你對刷題⽆從下⼿或者有畏懼⼼理,不妨從⼆叉樹下⼿,前 10 道也許 有點難受;結合框架再做 20 道,也許你就有點⾃⼰的理解了;刷完整個專 題,再去做什麼回溯動規分治專題,你就會發現只要涉及遞迴的問題,都是 樹的問題。

再舉例吧,動態規劃詳解說過湊零錢問題,暴⼒解法就是遍歷⼀棵 N 叉樹:
在這裡插入圖片描述

def coinChange(coins: List[int], amount: int):

def dp(n): 
	if n == 0: return 0 
	if n < 0: return -1 

	res = float('INF') 
	for coin in coins:
		subproblem = dp(n - coin) 
		//⼦問題⽆解,跳過 
		if subproblem == -1: continue 
		res = min(res, 1 + subproblem) 
	return res 
	if res != float('INF') 
	else -1 

return dp(amount)

這麼多程式碼看不懂咋辦?直接提取出框架,就能看出核⼼思路了:

# 不過是⼀個 N 叉樹的遍歷問題⽽已 
def dp(n): 
	for coin in coins: 
		dp(n - coin)

其實很多動態規劃問題就是在遍歷⼀棵樹,你如果對樹的遍歷操作爛熟於⼼,起碼知道怎麼把思路轉化成程式碼,也知道如何提取別⼈解法的核⼼思路。

再看看回溯演算法,前⽂回溯演算法詳解⼲脆直接說了,回溯演算法就是個 N 叉 樹的前後序遍歷問題,沒有例外。

⽐如 N 皇后問題吧,主要程式碼如下:
在這裡插入圖片描述

void backtrack(int[] nums, LinkedList<Integer> track) 
{ 
	if (track.size() == nums.length) 
	{ 
		res.add(new LinkedList(track)); 
		return; 
	}
}

for (int i = 0; i < nums.length; i++) 
{ 
	if (track.contains(nums[i])) 
		continue; 
	track.add(nums[i]); 
	// 進⼊下⼀層決策樹 
	backtrack(nums, track); 
	track.removeLast(); 
} 
/* 提取出 N 叉樹遍歷框架 */ 
void backtrack(int[] nums, LinkedList<Integer> track) 
{ 
	for (int i = 0; i < nums.length; i++) 
	{ 
		backtrack(nums, track); 
	}
}

N 叉樹的遍歷框架,找出來了把〜你說,樹這種結構重不重要?

綜上,對於畏懼演算法的朋友來說,可以先刷樹的相關題⽬,試著從框架上看 問題,⽽不要糾結於細節問題。

糾結細節問題,就⽐如糾結 i 到底應該加到 n 還是加到 n - 1,這個陣列的⼤ ⼩到底應該開 n 還是 n + 1 ?

從框架上看問題,就是像我們這樣基於框架進⾏抽取和擴充套件,既可以在看別 ⼈解法時快速理解核⼼邏輯,也有助於找到我們⾃⼰寫解法時的思路⽅向。

當然,如果細節出錯,你得不到正確的答案,但是隻要有框架,你再錯也錯 不到哪去,因為你的⽅向是對的。

但是,你要是⼼中沒有框架,那麼你根本⽆法解題,給了你答案,你也不會 發現這就是個樹的遍歷問題。

這種思維是很重要的,動態規劃詳解中總結的找狀態轉移⽅程的⼏步流程, 有時候按照流程寫出解法,說實話我⾃⼰都不知道為啥是對的,反正它就是 對了。。。

這就是框架的⼒量,能夠保證你在快睡著的時候,依然能寫出正確的程式; 就算你啥都不會,都能⽐別⼈⾼⼀個級別。

四、總結幾句:

資料結構的基本儲存⽅式就是鏈式和順序兩種,基本操作就是增刪查改,遍 歷⽅式⽆⾮迭代和遞迴。

刷演算法題建議從「樹」分類開始刷,結合框架思維,把這⼏⼗道題刷完,對 於樹結構的理解應該就到位了。這時候去看回溯、動規、分治等演算法專題, 對思路的理解可能會更加深刻⼀些。

相關文章