聊聊演算法——BFS和DFS

甲由崽發表於2020-05-08

 

如果面試位元組跳動和騰訊,上來就是先撕演算法,阿里就是會突然給你電話,而且不太在意是週末還是深夜,

別問我怎麼知道的,想確認的可以親自去試試。說到演算法,直接力扣hard三百題也是可以的,但似乎會比較傷腦,

有沒一些深入淺出系列呢,看了些經典的演算法,發現其實很多演算法是有框架的,今天就先說下很具代表的樹

演算法BFS和DFS,再來點秒殺題。

 

作者原創文章,謝絕一切轉載,違者必究。

準備:

Idea2019.03/JDK11.0.4

難度: 新手--戰士--老兵--大師

目標:

  1. 理解BFS和DFS框架
  2. 框架應用擴充套件

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大小判斷是否需要新增一個空陣列,隨後將節點加入與層變數對應的陣列中,理解演算法的輔助輸出如下:

總結:這裡說了三套演算法框架,請問看官掌握了嗎?

全文完!


我的其他文章:

 

只寫原創,敬請關注

 

相關文章