第0篇:學習資料結構和演算法的框架思維
學習資料結構和演算法的框架思維
⼀、資料結構的儲存⽅式:
資料結構的儲存⽅式只有兩種:陣列(順序儲存)和連結串列(鏈式儲存)。
這句話怎麼理解,不是還有雜湊表、棧、佇列、堆、樹、圖等等各種資料結 構嗎?
我們分析問題,⼀定要有遞迴的思想,⾃頂向下,從抽象到具體。你上來就 列出這麼多,那些都屬於「上層建築」,⽽陣列和連結串列才是「結構基礎」。 因為那些多樣化的資料結構,究其源頭,都是在連結串列或者陣列上的特殊操 作,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 ?
從框架上看問題,就是像我們這樣基於框架進⾏抽取和擴充套件,既可以在看別 ⼈解法時快速理解核⼼邏輯,也有助於找到我們⾃⼰寫解法時的思路⽅向。
當然,如果細節出錯,你得不到正確的答案,但是隻要有框架,你再錯也錯 不到哪去,因為你的⽅向是對的。
但是,你要是⼼中沒有框架,那麼你根本⽆法解題,給了你答案,你也不會 發現這就是個樹的遍歷問題。
這種思維是很重要的,動態規劃詳解中總結的找狀態轉移⽅程的⼏步流程, 有時候按照流程寫出解法,說實話我⾃⼰都不知道為啥是對的,反正它就是 對了。。。
這就是框架的⼒量,能夠保證你在快睡著的時候,依然能寫出正確的程式; 就算你啥都不會,都能⽐別⼈⾼⼀個級別。
四、總結幾句:
資料結構的基本儲存⽅式就是鏈式和順序兩種,基本操作就是增刪查改,遍 歷⽅式⽆⾮迭代和遞迴。
刷演算法題建議從「樹」分類開始刷,結合框架思維,把這⼏⼗道題刷完,對 於樹結構的理解應該就到位了。這時候去看回溯、動規、分治等演算法專題, 對思路的理解可能會更加深刻⼀些。
相關文章
- 資料結構和演算法-學習筆記(一)資料結構演算法筆記
- 1-2 學習資料結構和演算法的用途資料結構演算法
- 為什麼要學習資料結構和演算法?資料結構演算法
- 大話資料結構-思維導圖資料結構
- 資料分析應學習邏輯思維和分析方法
- 學習資料結構與演算法心得資料結構演算法
- 如何學習資料結構和演算法——大佬文章彙總資料結構演算法
- 資料結構和演算法學習筆記七:圖的搜尋資料結構演算法筆記
- 《資料結構與演算法之美》為什麼要學習資料結構和演算法 (讀後感)資料結構演算法
- 資料結構與演算法學習-陣列資料結構演算法陣列
- 學習JavaScript資料結構與演算法 (一)JavaScript資料結構演算法
- 資料結構與演算法學習-開篇資料結構演算法
- 資料結構學習筆記-佛洛依德演算法資料結構筆記演算法
- 資料結構和演算法學習筆記十六:紅黑樹資料結構演算法筆記
- 資料結構和演算法學習筆記九:最短路徑資料結構演算法筆記
- 大資料時代,從零學習資料思維大資料
- 資料結構與演算法學習總結--遞迴資料結構演算法遞迴
- 資料結構與演算法學習-連結串列下資料結構演算法
- 資料結構與演算法學習-連結串列上資料結構演算法
- 我是如何學習資料結構與演算法的?資料結構演算法
- 資料結構學習資料結構
- 資料結構和演算法資料結構演算法
- 你真的會學習嗎?從結構化思維說起
- Java的資料結構和演算法Java資料結構演算法
- JavaScript 的資料結構和演算法JavaScript資料結構演算法
- 我們為什麼要學習資料結構和演算法?(一)資料結構演算法
- SpringCloud 學習總結(思維導圖)SpringGCCloud
- 資料結構與演算法-學習筆記(二)資料結構演算法筆記
- 資料結構與演算法-學習筆記(16)資料結構演算法筆記
- 資料結構與演算法學習筆記01資料結構演算法筆記
- 學習javascript資料結構與演算法(六)——圖JavaScript資料結構演算法
- 演算法與資料結構學習路線圖演算法資料結構
- 資料結構學習之樹結構資料結構
- 《資料結構與演算法之美》資料結構與演算法學習書單 (讀後感)資料結構演算法
- 資料結構學習心得資料結構
- 資料結構和演算法-堆資料結構演算法
- JavaScript資料結構和演算法JavaScript資料結構演算法
- 聊聊資料結構和演算法資料結構演算法