BAT 經典演算法筆試題: 映象二叉樹

碼洞發表於2019-01-07

再過不到 2 個月,網際網路行業就要再次迎來面試高峰了。為了讓大家能順利透過所有面試環節必經的筆試階段,我提前給大夥準備了一套常見的演算法筆試題。這套演算法題來源於 LeetCode,題目都是 BAT、京東頭條滴滴美團等大型網際網路公司都喜歡考的題目。

演算法這個東西很難,縱使你瞭解了其中的邏輯,用程式碼寫出來仍然不是一件容易的事,內部有太多的細節需要處理。為了便於大夥輕鬆理解演算法邏輯,對於所有的題目,我會使用圖文加動畫的形式進行講解,讓讀者可以輕鬆理解演算法邏輯的同時,還可以留下深刻的影像不容易遺忘。


BAT 經典演算法筆試題: 映象二叉樹


好,下面我們開始今天的演算法題:映象二叉樹,就是將一顆二叉樹上的左右節點全部交換,就好比鏡子裡的二叉樹,左右方向是反過來的。

二叉樹節點表示


BAT 經典演算法筆試題: 映象二叉樹


class Node<T{
    T value;
    Node<T> left;
    Node<T> right;

    Node(T value) {
        this.value = value;
    }

    Node(T value, Node<T> left, Node<T> right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }
}


一個引數的構造器是葉子節點,三個引數的構造器是中間節點,看到這裡讀者應該知道這是 Java 語言,我使用了範型 

構造二叉樹


BAT 經典演算法筆試題: 映象二叉樹


Node<Integer> node2 = new Node<>(2);
Node<Integer> node3 = new Node<>(3);
Node<Integer> node1 = new Node<>(1, node2, node3);

// 一次性構造
Node<Integer> node1 = new Node<>(1new Node<>(2), new Node<>(3));

呈現二叉樹結構

如果我們正確寫出了映象演算法,那如何來直觀驗證映象的結構是否正確呢?LeetCode 使用的是單元測試,它使用一系列的單元測試和壓力測試指令碼程式碼來驗證使用者編寫的演算法的正確性和效能。但是我們不要這樣做,因為不直觀。我們選擇對二叉樹的結構內容進行直觀的呈現,如此就可以使用肉眼來進行快速驗證。如何直觀呈現呢?我們使用最簡單的括號表示法,它並不是最直觀的,但是它易於實現。


BAT 經典演算法筆試題: 映象二叉樹


class Node<T{
  T value;
  Node<T> left;
  Node<T> right;

  public String toString() {
    // 葉節點
    if (left == right) {
        return String.format("[%d]", value);
    }
    // 中間節點
    return String.format("(%d, %s, %s)", value, left, right);
  }
}

Node<Integer> node2 = new Node<>(2);
Node<Integer> node3 = new Node<>(3);
Node<Integer> node1 = new Node<>(1, node2, node3);

System.out.println(node1);
System.out.println(node2);
System.out.println(node3);
---------------------
[2]
[3]
(1, [2], [3])

遞迴映象二叉樹

映象二叉樹有兩種演算法,一種是遞迴,一種是迭代。遞迴的演算法簡單易於理解,我們先使用遞迴演算法來求解。遞迴的思想就是深度遍歷,遇到一個節點,先遞迴映象它的左子樹,再遞迴映象它的右子樹,然後再交換自己的左右子樹。如果遇到的是葉子節點,就不必處理了。為了避免無限遞迴,一定要及時設定好遞迴的停止條件,在這裡停止條件就是遇到了葉節點。


BAT 經典演算法筆試題: 映象二叉樹


public void mirrorFrom(Node<T> node) {
    // 葉子結點
    if (node.left == node.right) {
        return;
    }
    // 遞迴映象左子樹
    if (node.left != null)
        mirrorFrom(node.left);
    // 遞迴映象右子樹
    if (node.right != null)
        mirrorFrom(node.right);
    // 交換當前節點的左右子樹
    Node<T> tmp = node.left;
    node.left = node.right;
    node.right = tmp;
}

迭代映象二叉樹

遞迴演算法的優勢在於邏輯簡單,缺點在於每一次遞迴呼叫函式都會增加一個新的函式堆疊,如果樹的深度太深,函式的堆疊記憶體就會持續走高,一不小心就會觸發臭名昭著的異常 StackOverflowException。如果二叉樹分佈比較均勻,那麼樹就不會太深,但是遇到偏向的二叉樹,比如所有的子節點都掛在了右節點上,二叉樹就退化成了線性連結串列,連結串列的長度就是樹的深度,那這顆樹的深度就比較可怕了。


BAT 經典演算法筆試題: 映象二叉樹



所以下面我來介紹第二種演算法 —— 迭代演算法。迭代的基本思想就是將遞迴演算法轉換成迴圈演算法,用一個 for 迴圈來交換所有節點的左右子樹。我們需要再重新理解一下演算法的目標,這個目標非常簡單,就是遍歷整顆二叉樹,將遍歷途中遇到的所有中間節點的左右指標交換一下。

那如何設計這個迴圈呢?一個很明顯的方法是分層迴圈,第一次迴圈處理第 1 層二叉樹節點,也就是唯一的根節點。下一個迴圈處理第 2 層二叉樹節點,也就是根節點的兩個兒子。如此一直處理到最底層,迴圈的終止條件就是後代節點沒有了。所以我們需要使用一個容器來容納下一次迴圈需要處理的後代節點。


BAT 經典演算法筆試題: 映象二叉樹


public MirrorBinaryTree<T> mirrorByLoop() {
    // 空樹不必處理
    if (root == null) {
        return this;
    }
    // 當前迴圈需要處理的節點
    LinkedList<Node<T>> expandings = new LinkedList<>();
    expandings.add(root);
    // 沒有後臺節點就可以終止迴圈
    while (!expandings.isEmpty()) {
        // 下一次迴圈需要處理的節點
        // 也就是當前節點的所有兒子節點
        LinkedList<Node<T>> nextExpandings = new LinkedList<>();
        // 遍歷處理當前層的所有節點
        for (Node<T> node : expandings) {
            // 將後代節點收集起來,留著下一次迴圈
            if (node.left != null) {
                nextExpandings.add(node.left);
            }
            if (node.right != null) {
                nextExpandings.add(node.right);
            }
            // 交換當前節點的左右指標
            Node<T> tmp = node.left;
            node.left = node.right;
            node.right = tmp;
        }
        // 將後代節點設定為下一輪迴圈的目標節點
        expandings = nextExpandings;
    }
    return this;
}

完整程式碼

下面的完整程式碼可以複製過去直接執行,如果讀者還是不夠明白歡迎在留言區及時提問。

package leetcode;

import java.util.LinkedList;

public class MirrorBinaryTree<T{

    static class Node<T{
        T value;
        Node<T> left;
        Node<T> right;

        Node(T value) {
            this.value = value;
        }

        Node(T value, Node<T> left, Node<T> right) {
            this(value);
            this.left = left;
            this.right = right;
        }

        public String toString() {
            if (left == right) {
                return String.format("[%d]", value);
            }
            return String.format("(%d, %s, %s)", value, left, right);
        }

    }

    private Node<T> root;

    public MirrorBinaryTree(Node<T> root) {
        this.root = root;
    }

    public MirrorBinaryTree<T> mirrorByLoop() {
        if (root == null) {
            return this;
        }
        LinkedList<Node<T>> expandings = new LinkedList<>();
        expandings.add(root);
        while (!expandings.isEmpty()) {
            LinkedList<Node<T>> nextExpandings = new LinkedList<>();
            for (Node<T> node : expandings) {
                if (node.left != null) {
                    nextExpandings.add(node.left);
                }
                if (node.right != null) {
                    nextExpandings.add(node.right);
                }
                Node<T> tmp = node.left;
                node.left = node.right;
                node.right = tmp;
            }
            expandings = nextExpandings;
        }
        return this;
    }

    public MirrorBinaryTree<T> mirrorByRecursive() {
        mirrorFrom(root);
        return this;
    }

    public void mirrorFrom(Node<T> node) {
        if (node.left == node.right) {
            return;
        }

        if (node.left != null)
            mirrorFrom(node.left);
        if (node.right != null)
            mirrorFrom(node.right);

        Node<T> tmp = node.left;
        node.left = node.right;
        node.right = tmp;
    }

    public String toString() {
        if (root == null) {
            return "()";
        }
        return root.toString();
    }

    public static void main(String[] args) {
        Node<Integer> root = new Node<>(
                1
                new Node<>(
                        2
                        new Node<>(4), 
                        new Node<>(
                                5
                                new Node<>(8),
                                null)),
                new Node<>(
                        3
                        new Node<>(
                                6
                                null
                                new Node<>(9)), 
                        new Node<>(7)));
        MirrorBinaryTree<Integer> tree = new MirrorBinaryTree<>(root);
        System.out.println(tree);
        tree.mirrorByRecursive();
        System.out.println(tree);
        tree.mirrorByLoop();
        System.out.println(tree);
    }

}
---------------
(1, (2, [4], (5, [8], null)), (3, (6null, [9]), [7]))
(1, (3, [7], (6, [9], null)), (2, (5null, [8]), [4]))
(1, (2, [4], (5, [8], null)), (3, (6null, [9]), [7]))


擴充套件思考:為什麼鏡子裡面左右是反過來的,但是上下不是?這不是一道程式設計題,但是確實不容易回答

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31561269/viewspace-2374664/,如需轉載,請註明出處,否則將追究法律責任。

相關文章