算法Day01-算法研習指導之算法框架思維

數據結構的存儲方式

  • 數據結構的存儲方式只有兩種:
    • 數組: 順序存儲
    • 鏈表: 鏈式存儲
      • 散列表, 棧, 隊列, 堆, 樹, 圖都是通過數組和鏈表的數據結構基礎實現的數據結構上層建築

常用數據結構

  • 散列表: 通過散列函數將鍵映射到一個大數組裏
    • 解決散列衝突的方法:
      • 拉鍊法:
        • 需要鏈表特性
        • 操作簡單
        • 但是需要額外的存儲空間存儲指針
      • 線性探查法:
        • 需要數據特性
        • 使用數組是爲了方便連續尋址
        • 不需要用來存儲指針的額外存儲空間,但是操作相對複雜一些
  • 隊列和棧兩種數據結構既可以使用數組也可以使用鏈表實現:
    • 數組: 要處理擴容問題
    • 鏈表: 沒有擴容問題,但是需要更多的內存存儲節點指針
  • 堆: 堆是一個用數組實現的樹
    • 堆是一個完全二叉樹
    • 用數組存儲不需要節點指針
    • 操作簡單
  • 樹: 常見的樹是用鏈表實現的
    • 不一定是完全二叉樹,所以不適合用數組存儲
    • 通過鏈表可以實現不同巧妙設計的樹來處理不同的問題:
      • 二叉搜索樹
      • 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);
	}
}

算法框架思維總結

  • 數據結構的基本存儲方式:
    • 鏈式
    • 順序
  • 數據結構的基本操作:
  • 數據結構的遍歷方式:
    • 迭代
    • 遞歸
  • 算法題目:
    • 結合框架思維,從樹開始研究算法題
    • 理解掌握樹的結構之後,再去看回溯算法,動態規劃和分治算法
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章