如果面試字節跳動和騰訊,上來就是先撕算法,阿里就是會突然給你電話,而且不太在意是周末還是深夜,
別問我怎么知道的,想確認的可以親自去試試。說到算法,直接力扣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注冊中心
只寫原創,敬請關注