聊聊算法——BFS和DFS


 

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

別問我怎么知道的,想確認的可以親自去試試。說到算法,直接力扣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大小判斷是否需要添加一個空數組,隨后將節點加入與層變量對應的數組中,理解算法的輔助輸出如下:

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

全文完!


我的其他文章:

 

只寫原創,敬請關注

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM