通知:若是本站對你學習算法有幫助,請收藏網址,並推薦給你的朋友。因爲 labuladong 的算法套路太火,不少人直接拿個人 GitHub 文章去開付費專欄,價格還不便宜。我這免費寫給你看,多宣傳原創做者是你惟一能作的,誰也不但願劣幣驅逐良幣對吧?node
這是很久以前的一篇文章「學習數據結構和算法的框架思惟」的修訂版。以前那篇文章收到普遍好評,沒看過也不要緊,這篇文章會涵蓋以前的全部內容,而且會舉不少代碼的實例,教你如何使用框架思惟。git
首先,這裏講的都是普通的數據結構,咱不是搞算法競賽的,野路子出生,我只會解決常規的問題。另外,如下是我我的的經驗的總結,沒有哪本算法書會寫這些東西,因此請讀者試着理解個人角度,別糾結於細節問題,由於這篇文章就是但願對數據結構和算法創建一個框架性的認識。算法
從總體到細節,自頂向下,從抽象到具體的框架思惟是通用的,不僅是學習數據結構和算法,學習其餘任何知識都是高效的。數據庫
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。數組
數據結構的存儲方式只有兩種:數組(順序存儲)和鏈表(鏈式存儲)。數據結構
這句話怎麼理解,不是還有散列表、棧、隊列、堆、樹、圖等等各類數據結構嗎?app
咱們分析問題,必定要有遞歸的思想,自頂向下,從抽象到具體。你上來就列出這麼多,那些都屬於「上層建築」,而數組和鏈表纔是「結構基礎」。由於那些多樣化的數據結構,究其源頭,都是在鏈表或者數組上的特殊操做,API 不一樣而已。框架
好比說「隊列」、「棧」這兩種數據結構既可使用鏈表也可使用數組實現。用數組實現,就要處理擴容縮容的問題;用鏈表實現,沒有這個問題,但須要更多的內存空間存儲節點指針。數據結構和算法
「圖」的兩種表示方法,鄰接表就是鏈表,鄰接矩陣就是二維數組。鄰接矩陣判斷連通性迅速,並能夠進行矩陣運算解決一些問題,可是若是圖比較稀疏的話很耗費空間。鄰接表比較節省空間,可是不少操做的效率上確定比不過鄰接矩陣。ide
「散列表」就是經過散列函數把鍵映射到一個大數組裏。並且對於解決散列衝突的方法,拉鍊法須要鏈表特性,操做簡單,但須要額外的空間存儲指針;線性探查法就須要數組特性,以便連續尋址,不須要指針的存儲空間,但操做稍微複雜些。
「樹」,用數組實現就是「堆」,由於「堆」是一個徹底二叉樹,用數組存儲不須要節點指針,操做也比較簡單;用鏈表實現就是很常見的那種「樹」,由於不必定是徹底二叉樹,因此不適合用數組存儲。爲此,在這種鏈表「樹」結構之上,又衍生出各類巧妙的設計,好比二叉搜索樹、AVL 樹、紅黑樹、區間樹、B 樹等等,以應對不一樣的問題。
瞭解 Redis 數據庫的朋友可能也知道,Redis 提供列表、字符串、集合等等幾種經常使用數據結構,可是對於每種數據結構,底層的存儲方式都至少有兩種,以便於根據存儲數據的實際狀況使用合適的存儲方式。
綜上,數據結構種類不少,甚至你也能夠發明本身的數據結構,可是底層存儲無非數組或者鏈表,兩者的優缺點以下:
數組因爲是緊湊連續存儲,能夠隨機訪問,經過索引快速找到對應元素,並且相對節約存儲空間。但正由於連續存儲,內存空間必須一次性分配夠,因此說數組若是要擴容,須要從新分配一塊更大的空間,再把數據所有複製過去,時間複雜度 O(N);並且你若是想在數組中間進行插入和刪除,每次必須搬移後面的全部數據以保持連續,時間複雜度 O(N)。
鏈表由於元素不連續,而是靠指針指向下一個元素的位置,因此不存在數組的擴容問題;若是知道某一元素的前驅和後驅,操做指針便可刪除該元素或者插入新元素,時間複雜度 O(1)。可是正由於存儲空間不連續,你沒法根據一個索引算出對應元素的地址,因此不能隨機訪問;並且因爲每一個元素必須存儲指向先後元素位置的指針,會消耗相對更多的儲存空間。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
對於任何數據結構,其基本操做無非遍歷 + 訪問,再具體一點就是:增刪查改。
數據結構種類不少,但它們存在的目的都是在不一樣的應用場景,儘量高效地增刪查改。話說這不就是數據結構的使命麼?
如何遍歷 + 訪問?咱們仍然從最高層來看,各類數據結構的遍歷 + 訪問無非兩種形式:線性的和非線性的。
線性就是 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> inMap) { 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 ?
從框架上看問題,就是像咱們這樣基於框架進行抽取和擴展,既能夠在看別人解法時快速理解核心邏輯,也有助於找到咱們本身寫解法時的思路方向。
固然,若是細節出錯,你得不到正確的答案,可是隻要有框架,你再錯也錯不到哪去,由於你的方向是對的。
可是,你要是心中沒有框架,那麼你根本沒法解題,給了你答案,你也不會發現這就是個樹的遍歷問題。
這種思惟是很重要的,動態規劃詳解中總結的找狀態轉移方程的幾步流程,有時候按照流程寫出解法,說實話我本身都不知道爲啥是對的,反正它就是對了。。。
這就是框架的力量,可以保證你在快睡着的時候,依然能寫出正確的程序;就算你啥都不會,都能比別人高一個級別。
數據結構的基本存儲方式就是鏈式和順序兩種,基本操做就是增刪查改,遍歷方式無非迭代和遞歸。
刷算法題建議從「樹」分類開始刷,結合框架思惟,把這幾十道題刷完,對於樹結構的理解應該就到位了。這時候去看回溯、動規、分治等算法專題,對思路的理解可能會更加深入一些。