如果面試位元組跳動和騰訊,上來就是先撕演算法,阿里就是會突然給你電話,而且不太在意是週末還是深夜,
別問我怎麼知道的,想確認的可以親自去試試。說到演算法,直接力扣hard三百題也是可以的,但似乎會比較傷腦,
有沒一些深入淺出系列呢,看了些經典的演算法,發現其實很多演算法是有框架的,今天就先說下很具代表的樹
演算法BFS和DFS,再來點秒殺題。
作者原創文章,謝絕一切轉載,違者必究。
準備:
Idea2019.03/JDK11.0.4
難度: 新手--戰士--老兵--大師
目標:
- 理解BFS和DFS框架
- 框架應用擴充套件
1 介紹
BFS和DFS,即“廣度優先”和“深度優先”,如下圖二叉樹前序BFS為 1-2-3-4-5 ,DFS為 1-2-4-5-3,本文中演算法均以此樹為例:
2 演算法理解
2.1 DFS遞迴模式
如下,這寥寥幾行,即完成了二叉樹先序、中序和後序遍歷演算法,這就是演算法框架!
public static void dfs(Node root){ if (root == null){ return; } // 先序遍歷位置 dfs(root.left); // 中序遍歷位置 dfs(root.right); // 後序遍歷位置 }
其他更復雜的場景可以依此來類推,比如多路樹的遍歷,是不是很簡單:
private static class Node { public int value; public Node[] children; } public static void dfs(Node root){ if (root == null){ return; } // 對node做點事情 for (Node child:children ) { dfs(child); } }
我們來具體化一下,用Java實現,似乎一點也不難,通過調整列印root.value的位置,即可實現前中後序遍歷二叉樹了:
public class DFS { private static class Node { public int value; public Node left; public Node right; public Node(int value, Node left, Node right) { this.value = value; this.left = left; this.right = right; } public Node(int value) { this.value = value; } public Node() { } } /** DFS的遞迴實現,程式碼簡單,但如果層次過深可能會導致棧溢位 */ public static void dfs(Node root){ if (root == null){ return; } // 先序遍歷位置 System.out.println(root.value); dfs(root.left); // 中序遍歷位置 dfs(root.right); // 後序遍歷位置 } public static void main(String[] args) { Node root = new Node(1,new Node(),new Node(3)); root.left = new Node(2,new Node(4),new Node(5)); // 遞迴DFS測試 dfs(root); } }
2.2 DFS非遞迴模式
為了將DFS理解的更透徹一點,再說棧方式實現,事實上,前面的遞迴本質上也是棧實現,只是程式碼上沒表現出來,這是第二個框架:
/** 非遞迴,棧方式進行DFS*/ public static void dfs2(Node root){ if (root == null){ return; } Stack<Node> stack = new Stack<>(); stack.push(root); while( !stack.isEmpty()){ Node treeNode = stack.pop(); // System.out.println(treeNode.value); if (treeNode.right != null){ stack.push(treeNode.right); } if (treeNode.left != null){ stack.push(treeNode.left); } } }
以上程式碼解析:先初始化一個棧,然後將根root壓棧,迴圈中,先彈棧,如果彈出元素的子節點非空,則將子節點壓棧,
因讀出是先左後右,故這裡壓棧要先右後左, 看下動圖實現,更好理解:
2.3 BFS佇列模式
對比一下,BFS使用佇列實現,而 DFS使用棧實現,這是第三個框架:
public class BFS { private static class Node{ public int value; public Node left; public Node right; public Node(int value, Node left, Node right) { this.value = value; this.left = left; this.right = right; } public Node(int value) { this.value = value; } public Node() { } } /** 非遞迴,廣度優先演算法是使用佇列*/ private static void bfs(Node root) { if(root == null){ return; } // LinkedList implements Queue Queue<Node> queue = new LinkedList<>(); queue.add(root); while ( !queue.isEmpty()){ Node node = queue.poll(); // System.out.println(node.value); if (node.left != null){ queue.add(node.left); } if (node.right != null){ queue.add(node.right); } } } public static void main(String[] args) { Node root = new Node(1,new Node(),new Node(4)); root.left = new Node(2,new Node(5),new Node(6)); bfs(root); } }
以上程式碼解析:LinkedList 實現了Queue介面,故可以直接作為佇列使用;迴圈體中,子節點入佇列是先左後右,
動畫展示:
3 演算法擴充套件應用
3.1 BST二叉搜尋樹
這裡舉例為節點大於左子節點,且小於右子節點的BST。
查詢一個數是否存在,其實就是DFS的變形:
static boolean searchBST(Node root,int target){ if (root == null) return false; if (root.value == target){ return true; } if(root.value < target){ return searchBST(root.right,target); } if (root.value > target){ return searchBST(root.left,target); } return false; }
插入一個數:
static Node insertBST(Node root,int target){ if (root == null) return new Node(target); // BST中一般不會插入已有的元素 if(root.value < target){ root.right = insertBST(root.right,target); } if (root.value > target){ root.left = insertBST(root.left,target); } return root; }
以上程式碼解析:如果根為空,則直接生成只有一個根節點的BST,如果根不為空,則看要插入的目標值應該在左邊還是右邊。
若在右邊,且右子樹為空,則先生成一個 new Node,然後賦值給右指標,理解 root.right = insertBST(root.right,target);
等價於兩行Node node = new Node(target); root.right = node;
這樣,即實現了插入;若應該在右邊且右子樹非空,
則遞迴下去,直到子節點有為空的節點。
刪除一個數:
static Node deleteBST(Node root,int target){ if (root == null) return null; if (root.value == target){ if(root.left == null) return root.right; if (root.right == null) return root.left; Node node = getMin(root.right); root.right = deleteBST(root.right,node.value); }else if(root.value < target){ root.right = deleteBST(root.right,target); }else if (root.value > target){ root.left = deleteBST(root.left,target); } return root; } // 以找到最小值節點為例:根要小於右子樹,直接迴圈到葉子 private static Node getMin(Node node) { while (node.left != null) node = node.left; return node; }
以上程式碼解析:1.我們先回歸到最簡單模型,根為空,直接返回;刪除只帶有左子節點的根,則左子節點上升為根;刪除只帶有右子節點的根,
則右子節點上升為根;刪除帶有左右子節點的根,則右子節點上升為根(或者左子節點上升為根) 2. 刪除帶有左右子樹的根,則是找到右子樹最
小節點(或者左子樹最大節點),再做遞迴 3.這個演算法不算最優解,更好的解決方案是先將要刪除的根和右子樹最小值(或者左子樹最大值)做交換,
再刪除目標值節點,這樣就可以避免樹結構的過多調整。
3.2 其他樹
秒殺,題一,找出樹的最小/最大深度:
static int minDepth(Node root){ if (root == null) return 0; int leftDepth = minDepth(root.left) + 1; int rightDepth = minDepth(root.right) + 1; return Math.min(leftDepth,rightDepth); } static int maxDepth(Node root){ if (root == null) return 0; int leftDepth = maxDepth(root.left) + 1; int rightDepth = maxDepth(root.right) + 1; return Math.max(leftDepth,rightDepth); }
題二,二叉樹,返回其按層序遍歷得到的結果,即將每相同深度的節點放一個List,再將各層陣列放入另一個List返回:
// 最終結果存放 private static List<List<Integer>> result = new ArrayList<>(); /** BFS 按層輸出二叉樹,每一層為一個陣列放進一個ArrayList */ private static List<List<Integer>> bfs(Node root) { if(root == null){ return null; } // LinkedList implements Queue Queue<Node> queue = new LinkedList<>(); queue.add(root); while ( !queue.isEmpty()){ List<Integer> levelNodes = new ArrayList<>(); // 同一層的節點數量 int levelNum = queue.size(); for (int i = 0; i < levelNum; i++) { Node node = queue.poll(); levelNodes.add(node.value); System.out.println(node.value); if (node.left != null){ queue.add(node.left); } if (node.right != null){ queue.add(node.right); } } result.add(levelNodes); } return result; }
以上程式碼解析:一看就很明顯是BFS演算法框架,只是需要額外記錄每層的節點個數,每次while迴圈將處理相同層節點;每次for迴圈,
將同層的節點放入層記錄List,並同時將其子節點加入佇列;最終返回結果List。
那麼使用DFS是否也可以呢,下面給出了一個演算法,這個演算法很妙,推薦收藏:
// 最終結果存放 private static List<List<Integer>> result = new ArrayList<>(); private static List<List<Integer>> dfs(Node root,int level) { if (root == null) return; if (result.size() < level + 1){ result.add(new ArrayList<>()); } List<Integer> levelList = result.get(level); levelList.add(root.value); // 理解演算法的輔助輸出 System.out.println(result); // 遍歷左右子樹 dfs(root.left,level +1); dfs(root.right,level +1); return result; } // 執行測試 System.out.println(dfs(root,0));
以上程式碼解析:DFS遞迴中附加一個層數變數,於是每遞迴一層,則層數變數會加 1 ,而根的層數變數可以初始化為0,
這樣在遞迴的過程中順帶通過result大小判斷是否需要新增一個空陣列,隨後將節點加入與層變數對應的陣列中,理解演算法的輔助輸出如下:
總結:這裡說了三套演算法框架,請問看官掌握了嗎?
全文完!
我的其他文章:
- 1 微服務通訊方式——gRPC
- 2 分散式任務排程系統
- 3 Dubbo學習系列之十八(Skywalking服務跟蹤)
- 4 Spring優雅整合Redis快取
- 5 SOFARPC模式下的Consul註冊中心
只寫原創,敬請關注