阿里第二輪面試:手寫Java二叉樹

阿里面試

如今不少公司在招聘開發崗位的時候,都會事先在招聘信息中註明面試者應當具有的知識技能,並且在面試的過程當中,有部分對於技能掌握程度有嚴格要求的公司還會要求面試者手寫代碼,這個環節很考驗面試者的基礎功底和實力!node

這不,前些天一個朋友去阿里面試的時候,在二面過程當中就被要求使用Java實現二叉樹,王二Dog因爲沒有準備這方面的知識,沒有答上來,而後就讓回家等通知了。面試

因此有利用給王二Dog講解二叉樹的機會,我總體梳理了下二叉樹常見的面試點,發出來供你們一塊兒交流學習。但願對你的面試有所幫助。數據結構

二叉樹

二叉樹是遞歸數據結構,其中每一個節點最多能夠有2個子節點。ide

常見類型的二叉樹是二叉搜索樹,其中每一個節點的值大於或等於左子節點值,而且小於或等於右子節點中的節點值。post

這是這種二叉樹的直觀表示:學習

阿里第二輪面試:手寫Java二叉樹

對於實現,咱們將使用 Node 類來存儲 int 值並保存對每一個子節點的引用:測試

class Node {
    int value;//本節點的值
    Node left;//左邊的子節點
    Node right;//右邊的子節點

    Node(int value) {
        this.  value = value;
        right = null;
        left = null;
    }
}

而後,讓咱們添加樹的根節點,一般稱爲 this

public class BinaryTree {
    Node root;
    // ...}

讓咱們一塊兒來實現下

如今,讓咱們看看能夠在二叉樹上執行的最多見操做有哪些?code

插入元素

咱們要介紹的第一個操做是插入新節點blog

首先,咱們必須找到咱們想要添加新節點的位置,以便對樹進行排序。咱們將從根節點開始遵循這些規則:

  • 若是新節點的值低於當前節點的值,咱們轉到左子節點
  • 若是新節點的值大於當前節點的值,咱們將轉到右子節點
  • 節點當前爲null時,咱們已到達葉節點,咱們能夠在該位置插入新節點

首先,咱們將建立一個遞歸方法來進行插入:

private Node addRecursive(Node current, int value) {
    if (current == null) {
        return new Node(value);
    }
    if (value < current.value) {
        current.left = addRecursive(current.left, value);
    } else if (value > current.value) {
        current.right = addRecursive(current.right, value);
    } else {
        // value already exists
        return current;    
    }    
    return current;
}

接下來,咱們將建立一個遞歸方法來建立根節點:

public void add(int value) {
    root = addRecursive(root, value);
}

如今讓咱們看看如何使用此方法從咱們的示例中建立樹:

private BinaryTree createBinaryTree() {
    BinaryTree bt = new BinaryTree();

    bt.add(6);
    bt.add(4);
    bt.add(8);
    bt.add(3);
    bt.add(5);
    bt.add(7);
    bt.add(9);

    return bt;
}
查找元素

如今讓咱們添加一個方法來檢查樹是否包含特定值。

和之前同樣,咱們首先建立一個遍歷樹的遞歸方法:

private boolean containsNodeRecursive(Node current, int value) {
    if (current == null) {
        return false;
    }
    if (value == current.value) {
        return true;
    }
    return value < current.value
      ? containsNodeRecursive(current.left, value)
      : containsNodeRecursive(current.right, value);
}

在這裏,咱們經過將其與當前節點中的值進行比較來搜索該值,而後根據該值繼續在左或右子節點中繼續查找。

接下來,咱們讓建立一個公共方法來查找:

public boolean containsNode(int value) {
    return containsNodeRecursive(root, value);
}

如今,讓咱們建立一個簡單的測試來驗證樹真的包含插入的元素:

@Test
public void givenABinaryTree_WhenAddingElements_ThenTreeContainsThoseElements() {
    BinaryTree bt = createBinaryTree();

    assertTrue(bt.containsNode(6));
    assertTrue(bt.containsNode(4));
    assertFalse(bt.containsNode(1));
}
刪除元素

另外一種常見操做是從樹中刪除節點。

首先,咱們必須以與以前相似的方式找到要刪除的節點:

private Node deleteRecursive(Node current, int value) {
    if (current == null) {
        return null;
    }
    if (value == current.value) {
        // Node to delete found
        // ... code to delete the node will go here
    }
    if (value < current.value) {
        current.left = deleteRecursive(current.left, value);
        return current;
    }
    current.right = deleteRecursive(current.right, value);
    return current;
}

一旦咱們找到要刪除的節點,就有3種主要的不一樣狀況:

  • 節點沒有子節點 -這是最簡單的狀況; 咱們只須要在其父節點中用 null 替換此節點
  • 節點只有一個子節點 -在父節點中,咱們用它惟一的子節點替換該節點。
  • 節點有兩個子節點 - 這是最複雜的狀況,由於它須要樹重組

讓咱們看看當節點是葉節點時咱們如何實現第一種狀況:

if (current.left == null && current.right == null) {
    return null;
}

如今讓咱們繼續討論節點有一個子節點的狀況:

if (current.right == null) {
    return current.left;
}
if (current.left == null) {
    return current.right;
}

在這裏,咱們返回 非null 子節點,以便將其分配給父節點。

最後,咱們必須處理節點有兩個子節點的狀況。

首先,咱們須要找到將替換已刪除節點的節點。咱們將使用節點的最小節點刪除右側子樹:

private int findSmallestValue(Node root) {
    return root.left == null ? root.value : findSmallestValue(root.left);
}

而後,咱們將最小值分配給要刪除的節點,以後,咱們將從右側子樹中刪除它:

int smallestValue = findSmallestValue(current.right);
current.value = smallestValue;
current.right = deleteRecursive(current.right, smallestValue);
return current;

最後,咱們讓建立刪除的公共方法:

public void delete(int value) {
    root = deleteRecursive(root, value);
}

如今,讓咱們檢查刪除是否按預期工做:

@Test
public void givenABinaryTree  () {
    BinaryTree bt = createBinaryTree();

    assertTrue(bt.containsNode(9));
    bt.delete(9);
    assertFalse(bt.containsNode(9));
}

轉換樹

在此,咱們將看到遍歷樹的不一樣方式,詳細介紹深度優先和廣度優先搜索。

咱們將使用以前使用的相同樹,而且咱們將顯示每一個案例的遍歷順序。

深度優先搜索

深度優先搜索是一種在每一個子節點探索下一個兄弟以前儘量深刻的遍歷。

有幾種方法能夠執行深度優先搜索:in-order, pre-order 和 post-order。

in-order:首先訪問左子樹,而後訪問根節點,最後訪問右子樹:

public void traverseInOrder(Node node) {
    if (node != null) {
        traverseInOrder(node.left);
        System.out.print(" " + node.value);
        traverseInOrder(node.right);
    }
}

若是咱們調用此方法,控制檯輸出:

3 4 5 6 7 8 9

pre-order:首先訪問根節點,而後是左子樹,最後是右子樹:

public void traversePreOrder(Node node) {
    if (node != null) {
        System.out.print(" " + node.value);
        traversePreOrder(node.left);
        traversePreOrder(node.right);
    }
}

若是咱們調用此方法,控制檯輸出:

6 4 3 5 8 7 9

post-order:訪問左子樹,右子樹,最後訪問根節點:

public void traversePostOrder(Node node) {
    if (node != null) {
        traversePostOrder(node.left);
        traversePostOrder(node.right);
        System.out.print(" " + node.value);
    }
}

若是咱們調用此方法,控制檯輸出:

3 5 4 7 9 8 6

廣度優先搜索

這是另外一種常見的遍歷類型,它在展現進入下一級別以前訪問級別的全部節點

這種遍歷也稱爲按級別順序,並從根開始,從左到右訪問樹的全部級別。

對於實現,將咱們使用 隊列 按順序保存每一個級別的節點。咱們將從列表中提取每一個節點,打印其值,而後將其子節點添加到隊列中:

public void traverseLevelOrder() {
    if (root == null) {
        return;
    }
    Queue<Node> nodes = new LinkedList<>();
    nodes.add(root);
    while (!nodes.isEmpty()) {
        Node node = nodes.remove();
        System.out.print(" " + node.value);
        if (node.left != null) {
            nodes.add(node.left);
        }
        if (node.right!= null) {
            nodes.add(node.right);
        }
    }
}

在這種狀況下,節點的順序將是:

6 4 8 3 5 7 9

最後

在本文中,咱們已經瞭解瞭如何在Java中實現已排序的二叉樹及其最多見的操做。你是否從中有所收穫呢?哪怕你能收穫一點點心得,我在此也欣慰了!

「不積跬步,無以致千里」,但願將來的你能成爲:有夢爲馬 隨處可棲!加油,少年!

相關文章
相關標籤/搜索