388,先序遍歷構造二叉樹

資料結構和演算法發表於2020-09-23

想了解更多資料結構以及演算法題,可以關注微信公眾號“資料結構和演算法”,每天一題為你精彩解答。也可以掃描下面的二維碼關注
在這裡插入圖片描述

返回與給定先序遍歷相匹配的二叉搜尋樹的根結點。


示例:

輸入:[8,5,1,7,10,12]

輸出:[8,5,10,1,7,null,12]

在這裡插入圖片描述


問題分析

我們知道先序遍歷的順序是:根節點→左子樹→右子樹二叉搜尋樹的特點是當前節點左子樹的值都小於當前節點,當前節點右子樹的值都大於當前節點。比如我們在下面的搜尋二叉樹中插入節點4
在這裡插入圖片描述
在這裡插入圖片描述

原理很簡單,我們來看下如果插入一個結點的時候程式碼該怎麼寫

//data是插入的結點
private static void addTreeNode(TreeNode root, int data) {
    TreeNode node = new TreeNode(data);
    TreeNode p = root;
    while (true) {
        //如果要插入的結點data比結點p的值小,就往p結點的左
        //子節點找,否則往p的右子節點找
        if (p.val > data) {
            //如果p的左子節點等於空,直接放進去
            if (p.left == null) {
                p.left = node;
                break;
            } else {
                p = p.left;
            }
        } else {
            //如果p的右子節點等於空,直接放進去
            if (p.right == null) {
                p.right = node;
                break;
            } else {
                p = p.right;
            }
        }
    }
}

上面程式碼很簡單,插入一個結點的程式碼寫出來了,我們只需要把陣列中的元素全部遍歷一遍然後再一個個插入即可,程式碼如下

public TreeNode bstFromPreorder(int[] preorder) {
    TreeNode root = new TreeNode();
    root.val = preorder[0];
    for (int i = 1; i < preorder.length; i++)
        addTreeNode(root, preorder[i]);
    return root;
}

遞迴方式

上面節點插入的時候我們使用的是while迴圈的方式,這種比較容易理解,但程式碼量相對比較多,我們還可以改為遞迴的方式

private TreeNode addTreeNode(TreeNode root, int val) {
    if (root == null)
        return new TreeNode(val);
    else if (root.val > val)
        root.left = addTreeNode(root.left, val);
    else
        root.right = addTreeNode(root.right, val);
    return root;
}

這種遞迴的方式程式碼會更簡潔一些。如果root為空的話會新建一個節點。否則會一直走下去,他會根據root節點的大小判斷往左走還是往右走,注意這裡的root節點不一定是根節點,在遞迴的時候他是一直變的。


二分法構造

我們知道輸入的資料是二叉樹的先序遍歷,那麼第一個節點肯定是頭結點,比他小的是他左子樹的節點值,比他大的是他右子樹的節點值,我們就拿上面的[8,5,1,7,10,12]來說,8是根節點,比8小的[5,1,7]是他左子樹上的值,比他大的[10,12]是他右子樹上的值。所以可以參照二分法查詢的方式,把陣列分為兩部分,他是這樣的
在這裡插入圖片描述

然後左邊的[5,1,7]我們再按照上面的方式拆分,5是根節點,比5小的1是左子節點,比5大的7是右子節點。同理右邊的[10,12]中10是根節點,比10大的12是右子節點,這樣我們一直拆分下去,直到不能拆分為止,所以結果是下面這樣
在這裡插入圖片描述

我們再來看下程式碼

public TreeNode bstFromPreorder(int[] preorder) {
    return buildBST(preorder, 0, preorder.length - 1);
}

//陣列的範圍從left到right
private TreeNode buildBST(int[] preorder, int left, int right) {
    if (left > right)
        return null;
    TreeNode root = new TreeNode(preorder[left]);
    //如果left==right說明只有一個元素,沒法再拆分了
    if (left == right)
        return root;
    int i = left;
    //拆分為兩部分,一部分是比preorder[left]大的,一部分是比preorder[left]小的
    while (i + 1 <= right && preorder[i + 1] < preorder[left])
        i++;
    //區間[left + 1,i]所有元素都在root節點的左子樹
    //區間[i + 1,right]所有元素都在root節點的右子樹
    root.left = buildBST(preorder, left + 1, i);
    root.right = buildBST(preorder, i + 1, right);
    return root;
}

先序遍歷

我們還可以參照先序遍歷的方式把陣列元素一個個取出來,也很好理解,直接上程式碼。

int index = 0;

public TreeNode bstFromPreorder(int[] preorder) {
    return bstFromPreorder(preorder, Integer.MAX_VALUE);
}

public TreeNode bstFromPreorder(int[] preorder, int max) {
    if (index == preorder.length || preorder[index] > max)
        return null;
    //把陣列中的元素一個個取出來建立節點
    TreeNode root = new TreeNode(preorder[index++]);
    //左子樹的最大值不能超過root.val
    root.left = bstFromPreorder(preorder, root.val);
    //右子樹的最大值不能超過max
    root.right = bstFromPreorder(preorder, max);
    return root;
}

使用棧來解決

這題解法比較多,再來看最後一種解題思路。我們還可以使用一個棧來維護二叉搜尋樹中的節點,棧中存放的是已經構建好的二叉搜尋樹的結點(但不是全部,有些可能已經出棧了),其中棧中元素從棧底到棧頂是遞減的,我們遍歷陣列的時候如果當前值小於棧頂元素的值,我們直接讓當前值成為棧頂元素節點的左子節點,然後壓棧。

        if (preorder[i] < stack.peek().val) {
               stack.peek().left = node;
        } 

如果當前元素的值大於棧頂元素的值,我們就讓棧頂元素出棧,直到當前元素的值小於棧頂元素的值為止(或者棧為空為止)。而前一個比當前元素值小的節點就是當前元素的父節點。而當前元素是他父節點的右子節點。

            TreeNode parent = stack.peek();
            //棧從棧底到棧頂是遞減的
            while (!stack.isEmpty() && preorder[i] > stack.peek().val) {
                   parent = stack.pop();
            }
            parent.right = node;

解惑:

這裡如果思路不是很清晰的可能會有點疑問,出棧的時候把小於當前元素的值出棧了,如果再遇到比出棧的元素還要小的值那不是完蛋了,因為那個值已經出棧了,找不到了。其實有這個想法是正確的,但這種想法有點多餘了,我們就拿下面的圖來說吧[8,5,1,7,10,12]
在這裡插入圖片描述

比如當我們插入節點7的時候,節點1,5都已經全部出棧,但7後面無論如何都不會再出現比1或者5還小的值了,因為他是二叉搜尋樹,5的右節點的所有值都是比5大的。我們來畫個簡單的圖看下
在這裡插入圖片描述
在這裡插入圖片描述

所以我們看到後面無論走到哪一步都不可能在遇到比出棧元素更小的值了,最後我們再來看下完整程式碼

public TreeNode bstFromPreorder(int[] preorder) {
    Stack<TreeNode> stack = new Stack<>();
    TreeNode root = new TreeNode(preorder[0]);
    stack.push(root);
    for (int i = 1; i < preorder.length; i++) {
        TreeNode node = new TreeNode(preorder[i]);
        //小於棧頂元素的值,說明應該在棧頂元素的左子樹
        if (preorder[i] < stack.peek().val) {
            stack.peek().left = node;
        } else {//大於棧頂元素的值,我們要找到當前元素的父節點
            TreeNode parent = stack.peek();
            //棧從棧底到棧頂是遞減的
            while (!stack.isEmpty() && preorder[i] > stack.peek().val) {
                parent = stack.pop();
            }
            parent.right = node;
        }
        //節點壓棧
        stack.push(node);
    }
    return root;
}

在這裡插入圖片描述

相關文章