數據結構的存儲方式
- 數據結構的存儲方式只有兩種:
- 數組: 順序存儲
- 鏈表: 鏈式存儲
- 散列表, 棧, 隊列, 堆, 樹, 圖都是通過數組和鏈表的數據結構基礎實現的數據結構上層建築
常用數據結構
- 散列表: 通過散列函數將鍵映射到一個大數組裏
- 解決散列衝突的方法:
- 拉鍊法:
- 需要鏈表特性
- 操作簡單
- 但是需要額外的存儲空間存儲指針
- 線性探查法:
- 需要數據特性
- 使用數組是爲了方便連續尋址
- 不需要用來存儲指針的額外存儲空間,但是操作相對複雜一些
- 拉鍊法:
- 解決散列衝突的方法:
- 隊列和棧兩種數據結構既可以使用數組也可以使用鏈表實現:
- 數組: 要處理擴容問題
- 鏈表: 沒有擴容問題,但是需要更多的內存存儲節點指針
- 堆: 堆是一個用數組實現的樹
- 堆是一個完全二叉樹
- 用數組存儲不需要節點指針
- 操作簡單
- 樹: 常見的樹是用鏈表實現的
- 不一定是完全二叉樹,所以不適合用數組存儲
- 通過鏈表可以實現不同巧妙設計的樹來處理不同的問題:
- 二叉搜索樹
- AVL樹
- 紅黑樹
- 區間樹
- B樹
- 圖有兩種表示方式:
- 鄰接矩陣: 二維數組
- 判斷連通性迅速
- 可以通過進行矩陣運算來解決相關問題
- 但是在圖比較稀疏的情況下會耗費空間
- 鄰接表: 鏈表
- 節省空間
- 但是圖相關操作的效率比鄰接矩陣低下
- 鄰接矩陣: 二維數組
數組和鏈表比較
數組
- 優點:
- 數組是緊湊連續存儲
- 可以隨機訪問
- 通過索引快速定位元素
- 相對節約存儲空間
- 缺點:
- 因爲數組連續存儲,內存空間必須一次性分配足夠
- 如果數組需要需要擴容,則需要重新分配一塊更大的空間,再把數據全部複製過去,時間複雜度爲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叉樹節點
*/
class TreeNode {
int val;
TreeNode[] children;
}
void traverse(TreeNode root) {
// 遞歸訪問各個子樹
for (TreeNode child : children) {
traverse(child);
}
}
- N叉樹的遍歷又可以拓展爲圖的遍歷. 因爲圖就是好幾個N叉樹的結合體
算法題目
- 數據結構是工具,算法是通過合適的工具解決特定問題的方法:
- 學習算法之前,需要了解常用的數據結構的特性和缺陷
- 從二叉樹題目開始研究算法問題:
- 二叉樹最容易培養框架思維
- 大部分算法問題,本質上都是樹的遍歷問題
- 先研究樹相關問題,試着從框架上看問題:
- 基於框架進行抽取和擴展
- 既可以看別人解法時快速理解核心邏輯
- 也有助於自己寫解法時找到思路方向
二叉樹
- 二叉樹的基本框架:
void traverse(TreeNode root) {
// 前序遍歷
...
traverse(root -> left);
// 中序遍歷
...
traverse(root -> right);
// 後序遍歷
...
}
- 題目 : 求二叉樹中的最大路徑和
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)); ins ans = max(ans, left + right + root -> val); return max(left, right) + root ->val; }
這就是一個後序遍歷
- 題目 : 根據前序遍歷和中序遍歷的結果還原一棵二叉樹
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 = intRoot - 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; }
函數中的多個參數是爲了控制數組的索引,本質上該算法就是一個前序遍歷
- 題目 : 恢復一棵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) }
這就是一箇中序遍歷
- 可以發現 : 只要涉及遞歸的問題,都是樹的問題
動態規劃
- 大部分動態規劃問題就是在遍歷一棵樹:
- 掌握好樹的遍歷操作
- 熟練怎麼把思路轉換成代碼
- 熟練提取別人解法的核心思路
- 動態規劃中的湊零錢問題: 直接的解法就是遍歷一棵N叉樹
def coinChange(coin: 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: dep(n - coin)
回溯算法
- 回溯算法就是個N叉樹的前後遍歷問題
- N皇后問題:
void backTrack(int[] nums, List<Integer> track) { if (track.size == nums.length) { res.add(new LinkList(track)); return; } for (int id= 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, List<Integer> track) { for (int i = 0; i < nums.length; i++) { backTrack(nums, track); } }
算法框架思維總結
- 數據結構的基本存儲方式:
- 鏈式
- 順序
- 數據結構的基本操作:
- 增
- 刪
- 改
- 查
- 數據結構的遍歷方式:
- 迭代
- 遞歸
- 算法題目:
- 結合框架思維,從樹開始研究算法題
- 理解掌握樹的結構之後,再去看回溯算法,動態規劃和分治算法