《劍指offer》Java學習錄:樹(面試題6:重建二叉樹)

在數據結構中,我們把存在邏輯上的起點和終點的數據結構,成爲線性的數據結構。例如鏈表、棧和隊列等都是線性的數據結構。

樹是衆所周知的非線性數據結構。它在邏輯上沒有終點,且不以線性方式存儲數據。他們按層次組織數據。

樹的定義

(tree) 是被稱爲 結點(node)實體的集合。結點通過(edge)連接。每個結點都包含值或數據(value/date),並且每個結節點可能有子結點,也可能沒有。

樹的首結點叫根結點(即root結點)。如果這個根結點和其他結點所連接,那麼根結點是父結點(parent node),與根結點連接的是子結點child node)。

所有的結點都通過(edge)連接,它負責管理節點之間的邏輯關係。

葉子結點leaves)是樹末端,它們沒有子結點。

樹的高度(height)和深度(depth):

  • 樹的高度是到葉子結點(樹末端)的長度
  • 結點的深度是它到根結點的長度

二叉樹

二叉樹是樹結構中特殊且常用的類型,每個節點最多有兩個子節點,被稱作左孩子和右孩子(你可以叫做左節點和右節點)。

二叉樹實現(Java/C++)

在實現二叉樹時,我們只需要注意,一個節點中有三個屬性:數據、左節點、右節點。

Java實現

public class BinaryTree {
    public BinaryTree left;
    public BinaryTree right;
    public String value;

    public BinaryTree(BinaryTree left, BinaryTree right, String value) {
        this.left = left;
        this.right = right;
        this.value = value;
    }

    public BinaryTree(String value) {
        this(null, null, value);
    }

    /**
     * 將給定的新左節點值插入到當前節點中:
     * 1. 如果當前節點沒有左節點,新節點爲當前節點的左節點。
     * 2. 如果當前節點有左節點,新節點爲當前節點的左節點,原左節點作爲新節點的左節點。
     *
     * @param currentNode 插入左節點的父節點,即當前節點
     * @param value 新左節點的值
     */
    public void insertLeft(BinaryTree currentNode, String value) {
        if (currentNode == null) {
            return;
        }

        BinaryTree newLeftNode = new BinaryTree(value);
        if (currentNode.left != null) {
            BinaryTree leftNode = currentNode.left;
            newLeftNode.left = leftNode;
        }
        currentNode.left = newLeftNode;
    }

    public void insertRight(BinaryTree currentNode, String value) {
        if (currentNode == null) {
            return;
        }

        BinaryTree newLeftNode = new BinaryTree(value);
        if (currentNode.right != null) {
            BinaryTree leftNode = currentNode.right;
            newLeftNode.right = leftNode;
        }
        currentNode.right = newLeftNode;
    }
}

我們可以利用BinaryTree來構造一個二叉樹:

public class Main {
    public static void main(String args[]) {
        BinaryTree node_a = new BinaryTree("a");
        node_a.insertLeft(node_a, "b");
        node_a.insertRight(node_a, "c");

        BinaryTree node_b = node_a.left;
        node_b.insertRight(node_b, "d");

        BinaryTree node_c = node_a.right;
        node_c.insertLeft(node_c, "e");
        node_c.insertRight(node_c, "f");
    }
}

該二叉樹如下圖:
在這裏插入圖片描述

C++實現

二叉樹的遍歷

樹的遍歷有兩種方式,深度優先搜索(DFS)和廣度優先搜索(BFS)。

在Wikipedia中,被描述如下:

DFS是用來遍歷或搜索樹數據結構的算法。從根節點開始,在回溯之前沿着每一個分支儘可能遠的探索。

BFS是用來遍歷或搜索樹數據結構的算法。從根節點開始,在探索下一層鄰居節點前,首先探索同一層的鄰居節點。

深度優先搜索(Depth-First Search)

DFS從根節點開始,在回溯之前沿着每一個分支儘可能遠的探索。
在這裏插入圖片描述

如上圖所示二叉樹,按照DFS的方式遍歷,輸出順序爲:1-2-3-4-5-6-7。

具體的遍歷步驟如下:

  1. 從根結點(1)開始。輸出
  2. 進入左結點(2)。輸出
  3. 然後進入左孩子(3)。輸出
  4. 回溯,並進入右孩子(4)。輸出
  5. 回溯到根結點,然後進入其右孩子(5)。輸出
  6. 進入左孩子(6)。輸出
  7. 回溯,並進入右孩子(7)。輸出
  8. 完成

當我們深入到葉結點時回溯,這就被稱爲 DFS 算法。 根據根節點輸出順序的不同,又被分爲前序遍歷、中序遍歷、後序遍歷。

前序遍歷

前序遍歷是在DFS的基礎上,按照以下步驟輸出節點:

  1. 輸出當前節點值。
  2. 如果有左子節點,進入該節點,輸出左子節點值。
  3. 如果有右子節點,進入該節點,輸出右子節點值。

簡而言之,節點的輸出順序爲:當前節點-左子節點-右子節點

代碼如下:

/**
 * 前序遍歷
 *
 * @param node 二叉樹的節點
 */
public static void preOrder(BinaryTree node) {
    if (node != null) {
        System.out.println(node.value);
        if (node.left != null) {
            node.left.preOrder(node.left);
        }
        if (node.right != null) {
            node.right.preOrder(node.right);
        }
    }
}

對於,如圖所示的二叉樹:
在這裏插入圖片描述
前序遍歷的輸出結果爲:1-2-3-4-5-6-7

調試代碼如下:

public class Main {
    public static void main(String args[]) {
        BinaryTree node_1 = new BinaryTree("1");
        node_1.insertLeft(node_1, "2");
        node_1.insertRight(node_1, "5");

        BinaryTree node_2 = node_1.left;
        node_2.insertLeft(node_2, "3");
        node_2.insertRight(node_2, "4");

        BinaryTree node_5 = node_1.right;
        node_5.insertLeft(node_5, "6");
        node_5.insertRight(node_5, "7");

        BinaryTree.preOrder(node_1);
    }
}
中序遍歷

和前序遍歷類似,中序遍歷只是將左子節點和當前節點輸出順序互換,也就是:左子節點-當前節點-右子節點。

遍歷輸出代碼如下:

/**
 * 中序遍歷
 *
 * @param node 二叉樹的節點
 */
public static void inOrder(BinaryTree node) {
    if (node != null) {
        if (node.left != null) {
            node.left.inOrder(node.left);
        }
        System.out.println(node.value);
        if (node.right != null) {
            node.right.inOrder(node.right);
        }
    }
}

中序遍歷的輸出結果爲:3-2-4-1-5-5-7

調試代碼和前序遍歷調試代碼類似,只是將遍歷調用改爲:BinaryTree.inOrder(node_1);即可。

後續遍歷

同樣,後序遍歷的輸出順序是:左子節點-右子節點-當前節點。

遍歷輸出代碼如下:

/**
 * 後序遍歷
 *
 * @param node 二叉樹的節點
 */
public static void postOrder(BinaryTree node) {
    if (node != null) {
        if (node.left != null) {
            node.left.postOrder(node.left);
        }
        if (node.right != null) {
            node.right.postOrder(node.right);
        }
        System.out.println(node.value);
    }
}

後序遍歷的輸出結果爲:3-4-2-6-7-5-1

調試代碼和前序遍歷調試代碼類似,只是將遍歷調用改爲:BinaryTree.postOrder(node_1);即可。

篇幅有限,就不再列出C++的相關實現了,都差不多。

廣度優先搜索(Breadth-First Search)

廣度優先搜索,是一層層逐漸深入的遍歷算法。以圖示爲例:
在這裏插入圖片描述

  • 0層:只有節點(1)
  • 1層:有節點(2)和(5)
  • 2層:有節點(3)、(4)、(6)、(7)

BFS算法,就是先遍歷輸出第一層,再遍歷並從左到右輸出第二層,接着第三層……

要實現算法,我們需要一個先入先出的模型-隊列。實現步驟如下:

  1. 將節點(1)入隊。
  2. 從隊列中取出一個節點輸出,並將它的所有子節點從左到右依次入隊。
  3. 重複步驟#2,直到隊列中沒有節點。

代碼如下:

/**
 * 廣度優先搜索
 *
 * @param node 二叉樹的節點
 */
public static void bfsOrder(BinaryTree node) {
    if (node == null) {
        return;
    }

    Queue<BinaryTree> queue = new ArrayDeque<>();
    queue.add(node);
    while (!queue.isEmpty()) {
        BinaryTree currentNode = queue.poll();
        System.out.println(currentNode.value);
        if (currentNode.left != null) {
            queue.add(currentNode.left);
        }
        if (currentNode.right != null) {
            queue.add(currentNode.right);
        }
    }
}

後序遍歷的輸出結果爲:1-2-5-3-4-6-7

調試代碼和前序遍歷調試代碼類似,只是將遍歷調用改爲:BinaryTree.bfsOrder(node_1);即可。

二叉搜索樹

二叉搜索樹又稱爲二叉排序樹或二叉有序數。它的邏輯結構是有序的,特點是:一個節點的值大於其左節點,小於右節點。

這樣的特徵讓二叉搜索樹的查找可以適用於折半查找原理。

二叉搜索樹中的添加節點將不可以手動指定新增節點是插入左節點還是右節點了。新增的節點是當前節點的左節點還是右節點將根據規則決定。

新增節點

下面是二叉搜索樹新增節點的例子:

/**
 * 二叉搜索樹插入新節點
 *
 * @param node 當前樹,注意必須是二叉搜索樹,新增節點後可能是二叉搜索樹
 * @param value 新節點的值
 */
public void insertNode(BinaryTree node, int value) {
    if (node == null) {
        return;
    }

    if (value <= Integer.valueOf(node.value) && node.left != null) {
        node.left.insertNode(node.left, value);
    } else if (value <= Integer.valueOf(node.value)) {
        node.left = new BinaryTree(String.valueOf(value));
    } else if (value > Integer.valueOf(node.value) && node.right != null) {
        node.right.insertNode(node.right, value);
    } else {
        node.right = new BinaryTree(String.valueOf(value));
    }
}

用文字描述就是:

  1. 如果當前節點值大於或等於新節點值,新節點應該放置在當前節點的左子樹中。
  2. 如果當前節點左子樹爲null,則新節點成爲當前節點的左節點。如果當前節點左子樹不爲null,遞歸#1#2。
  3. 如果當前節點值小於新節點值,新節點應該放置在當前節點的右子樹中。
  4. 如果當前節點右子樹爲null,則新節點成爲當前節點的右節點。如果當前節點右子樹不爲null,遞歸#3#4。

搜索

二叉搜索樹因爲是有序,所以它的遍歷搜索將變得簡單。步驟如下:

  1. 從根節點開始,給定值小於當前節點值嗎?
  2. 如果小於,接下來進入左子樹遍歷查找,如果大於將進入右子樹查找。
  3. 如果相等,恭喜你,你找到了給定值。

代碼如下:

/**
 * 二叉搜索樹查找節點是否存在
 *
 * @param node
 * @param value
 * @return
 */
public boolean findNode(BinaryTree node, int value) {
    if (node == null) {
        return false;
    }
    if (value < Integer.valueOf(node.value) && node.left != null) {
        return node.left.findNode(node.left, value);
    }
    if (value > Integer.valueOf(node.value) && node.right != null) {
        return node.right.findNode(node.right, value);
    }
    return value == Integer.valueOf(node.value);
}

刪除

二叉搜索樹中,比較複雜的算法是刪除指定節點。它需要考慮三種情況,1、刪除的節點沒有子節點,2、刪除的節點只有一個節點,3、刪除的節點有兩個節點。

第一種情況:沒有子節點
在這裏插入圖片描述
這是最簡單的一種情況,直接刪除就好。

第二種情況:只有一個子節點
在這裏插入圖片描述

這種情況需要做兩步操作:

  1. 刪除指定節點。
  2. 將刪除節點的子節點替換被刪節點的位置。

第三中情況:有兩個子節點
在這裏插入圖片描述

這是最複雜的一種情況,當節點有兩個子節點時,需要從該節點的右子樹開始,找到具有最小值的節點。用這個節點替換掉被刪除節點的位置。

代碼如下:

/**
 * 二叉搜索樹刪除節點
 *
 * @param node 當前節點
 * @param value 指定被刪除節點的值
 * @param parent 當前節點父節點
 * @return 成功返回true 失敗返回false
 */
public boolean removeNode(BinaryTree node, Integer value, BinaryTree parent) {
    if (node != null) {
        if (value < Integer.valueOf(node.value) && node.left != null) {
            return node.left.removeNode(node.left, value, node);
        } else if (value < Integer.valueOf(node.value)) {
            return false;
        } else if (value > Integer.valueOf(node.value) && node.right != null) {
            return node.right.removeNode(node.right, value, node);
        } else if (value > Integer.valueOf(node.value)) {
            return false;
        } else {
            if (node.left == null && node.right == null && node == parent.left) {
                parent.left = null;
                node.clearNode(node);
            } else if (node.left == null && node.right == null && node == parent.right) {
                parent.right = null;
                node.clearNode(node);
            } else if (node.left != null && node.right == null && node == parent.left) {
                parent.left = node.left;
                node.clearNode(node);
            } else if (node.left != null && node.right == null && node == parent.right) {
                parent.right = node.left;
                node.clearNode(node);
            } else if (node.right != null && node.left == null && node == parent.left) {
                parent.left = node.right;
                node.clearNode(node);
            } else if (node.right != null && node.left == null && node == parent.right) {
                parent.right = node.right;
                node.clearNode(node);
            } else {
                node.value = String.valueOf(node.right.findMinValue(node.right));
                node.right.removeNode(node.right, Integer.valueOf(node.right.value), node);
            }
            return true;
        }
    }
    return false;
}

/**
 * 查找二叉搜索樹中的最小值坐在的節點
 * 
 * @param node 二叉搜索樹節點
 * @return 返回node樹中,最小值所在的節點
 */
public Integer findMinValue(BinaryTree node) {
    if (node != null) {
        if (node.left != null) {
            return node.left.findMinValue(node.left);
        } else {
            return Integer.valueOf(node.value);
        }
    }
    return null;
}

/**
 * 清空n節點
 *
 * @param node 需要被清空的節點
 */
public void clearNode(BinaryTree node) {
    node.value = null;
    node.left = null;
    node.right = null;
}

面試題 6:重建二叉樹

題目

輸入某二叉樹的前序遍歷和終須遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如,輸入前序遍歷序列{1, 2, 4, 7, 3, 5, 6, 8}和中序遍歷序列{4, 7, 2, 1, 5, 3, 8, 6},則重建出如下圖所示二叉樹並輸出它的頭結點。
在這裏插入圖片描述
二叉樹節點的定義如下:

struct BinaryTreeNode {
    int m_nValue;
    BinaryTreeNode *m_pLeft;
    BinaryTreeNode *m_pRight;
};

分析

在二叉樹的前序遍歷序列中,第一個數字是樹的根節點。單在中序遍歷序列中,根節點在序列的中間,左子樹位於根節點的左邊,右子樹位於根節點右邊。
在這裏插入圖片描述
如圖,中序遍歷序列中,有3個數字是左子樹節點的值,因此左子樹總共有3個左子節點。所以,我們可以知道在前序遍歷序列中,根節點後面的3個數字就是3個左子樹節點的值,其它的是右子樹的值。這樣,我們就在前序遍歷和中序遍歷兩個序列中,分別找到了左右子樹對應的子序列。

接下來,我們只需要遞歸處理左子樹和右子樹就行了。

解:Java

結合前面的Java代碼,實現代碼如下:

public static BinaryTree construct(int preOrder[], int inOrder[]) {
    if (preOrder == null || inOrder == null
        || preOrder.length != inOrder.length || preOrder.length <= 0) {
        return null;
    }

    return constructCore(preOrder, inOrder);
}

private static BinaryTree constructCore(int[] preOrder, int[] inOrder) {
    if (preOrder.length == 0 || inOrder.length == 0) {
        return null;
    }
    int rootValue = preOrder[0];
    BinaryTree root = new BinaryTree(rootValue);
    if (preOrder.length == 1) {
        if (inOrder[0] != rootValue) {
            throw new InvalidParameterException("preOrder and inOrder not match");
        }
        return root;
    }
    // 在中序中查找根節點
    int rootInorderIndex = 0;
    while (rootInorderIndex < inOrder.length && inOrder[rootInorderIndex] != rootValue) {
        rootInorderIndex++;
    }
    if (rootInorderIndex > 0) { // 構建左子樹
        root.left = constructCore(Arrays.copyOfRange(preOrder, 1, rootInorderIndex + 1),
                                  Arrays.copyOf(inOrder, rootInorderIndex));
    }
    if (rootInorderIndex < preOrder.length) { // 構建右子樹
        root.right = constructCore(Arrays.copyOfRange(preOrder, rootInorderIndex + 1, preOrder.length),
                                   Arrays.copyOfRange(inOrder, rootInorderIndex + 1, inOrder.length));
    }
    return root;
}

調用實例:

public static void main(String args[]) {
    int[] preOrder = {1, 2, 4, 7, 3, 5, 6, 8};
    int[] inOrder = {4, 7, 2, 1, 5, 3, 8, 6};

    BinaryTree tree = BinaryTree.construct(preOrder, inOrder);
}

解:C++

和Java代碼類似,只不過將數值引用變爲了數組指針。

#include <iostream>

using namespace std;

struct BinaryTreeNode {
    int m_nValue;
    BinaryTreeNode *m_pLeft;
    BinaryTreeNode *m_pRight;
};

BinaryTreeNode *constructCore(int *startPreorder, int *endPreorder, int *stardInorder, int *endInorder) {
    int rootValue = startPreorder[0]; // 前序遍歷第一個值是根節點的值
    BinaryTreeNode *root = new BinaryTreeNode(); // 創建根節點
    root->m_nValue = rootValue;
    root->m_pLeft = root->m_pRight = NULL;
    if (startPreorder == endPreorder) {
        if ((stardInorder == endInorder) && (*stardInorder == *startPreorder)) {
            return root;
        } else {
            throw invalid_argument("Preorder and Inorder not match!");
        }
    }
    // 在中序序列中找到根節點
    int *rootInorder = stardInorder;
    while (rootInorder <= endInorder && *rootInorder != rootValue) {
        rootInorder++;
    }
    if (rootInorder == endInorder && *rootInorder != rootValue) {
        throw invalid_argument("Preorder and Inorder not match!");
    }
    int leftLength = rootInorder - stardInorder;
    int *leftPreorderEnd = startPreorder + leftLength;
    if (leftLength > 0) { // 構建左子樹
        root->m_pLeft = constructCore(startPreorder + 1, leftPreorderEnd, stardInorder, rootInorder - 1);
    }
    if (leftLength < endPreorder - startPreorder) { // 構建右子樹
        root->m_pRight = constructCore(leftPreorderEnd + 1, endPreorder, rootInorder + 1, endInorder);
    }
    return root;
}

BinaryTreeNode *construct(int *preOrder, int *inOrder, int length) {
    if (preOrder == NULL || inOrder == NULL || length <= 0) {
        return NULL;
    }
    return constructCore(preOrder, preOrder + length - 1, inOrder, inOrder + length - 1);
}

int main() {
    int length = 8;
    int preOrder[] = {1, 2, 4, 7, 3, 5, 6, 8};
    int inOrder[] = {4, 7, 2, 1, 5, 3, 8, 6};
    BinaryTreeNode *node = construct(preOrder, inOrder, length);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章