Java中在二叉搜尋樹中查詢節點的父節點

banq發表於2024-03-11

二叉搜尋樹(BST)是一種幫助我們有效解決現實世界問題的資料結構。

什麼是二叉搜尋樹?
BST 是一棵樹,其中每個節點最多指向兩個節點,通常稱為左子節點和右子節點。此外,每個節點的值都大於左子節點且小於右子節點。

例如,讓我們想象三個節點:A=2、B=1 和 C=4。因此,一種可能的 BST 將 A 作為根,B 作為其左子節點,C 作為其右子節點。

1、遞迴解決方案
直接解決方案是使用遞迴遍歷樹並提前返回其任何子節點等於目標值的節點。

我們首先在 TreeNode類中定義一個公共方法:

TreeNode parent(int target) throws NoSuchElementException {
    return parent(this, new TreeNode(target));
}

現在,讓我們在TreeNode類中定義Parent()方法的遞迴版本:

TreeNode parent(TreeNode current, TreeNode target) throws NoSuchElementException {
    if (target.equals(current) || current == null) {
        throw new NoSuchElementException(format(<font>"No parent node found for 'target.value=%s' " +
           
"The target is not in the tree or the target is the topmost root node.",
            target.value));
    }
    if (target.equals(current.left) || target.equals(current.right)) {
        return current;
    }
    return parent(target.value < current.value ? current.left : current.right, target);
}

該演算法首先檢查當前節點是否是最頂層的根節點或者該節點是否不存在於樹中。在這兩種情況下,節點都沒有父節點,因此我們丟擲NoSuchElementException。

然後,演算法檢查當前節點子節點是否等於目標節點。如果是,則當前節點是目標節點的父節點。因此,我們返回current。

最後,我們根據目標值使用遞迴呼叫向左或向右遍歷 BST 。

讓我們測試一下我們的遞迴解決方案:

@Test
void givenBinaryTree_whenFindParentNode_thenReturnCorrectParentNode() {
    assertThrows(NoSuchElementException.class, () -> subject.parent(1231));
    assertThrows(NoSuchElementException.class, () -> subject.parent(8));
    assertEquals(8, subject.parent(5).value);
    assertEquals(5, subject.parent(3).value);
    assertEquals(5, subject.parent(7).value);
    assertEquals(3, subject.parent(4).value);
    <font>// assertions for other nodes<i>
}

在最壞的情況下,演算法最多執行n次遞迴操作,每次查詢父節點的成本 為O(1) ,其中n是 BST 中的節點數。因此,時間複雜度為O (n) 。在平衡良好的 BST中,該時間降至 O(log n),因為其高度始終最多為log n。

此外,該演算法使用堆空間進行遞迴呼叫。因此,在最壞的情況下,當我們找到葉節點時,遞迴呼叫就會停止。因此,該演算法最多堆疊h次遞迴呼叫,這使得其 空間複雜度為O(h),其中 h是 BST 的高度。

2、迭代解決方案
幾乎任何遞迴解決方案都有迭代版本。特別是,我們還可以使用堆疊和while迴圈而不是遞迴來找到 BST 的父級。

為此,我們將 iterativeParent()方法新增到TreeNode類:

TreeNode iterativeParent(int target) {
    return iterativeParent(this, new TreeNode(target));
}

上面的方法只是下面輔助方法的介面:

TreeNode iterativeParent(TreeNode current, TreeNode target) {
    Deque <TreeNode> parentCandidates = new LinkedList<>();
    String notFoundMessage = format(<font>"No parent node found for 'target.value=%s' " +
       
"The target is not in the tree or the target is the topmost root node.",
        target.value);
    if (target.equals(current)) {
        throw new NoSuchElementException(notFoundMessage);
    }
    while (current != null || !parentCandidates.isEmpty()) {
        while (current != null) {
            parentCandidates.addFirst(current);
            current = current.left;
        }
        current = parentCandidates.pollFirst();
        if (target.equals(current.left) || target.equals(current.right)) {
            return current;
        }
        current = current.right;
    }
    throw new NoSuchElementException(notFoundMessage);
}

該演算法首先初始化一個堆疊來儲存父候選。那麼主要取決於四個主要部分:
  • 外部while迴圈檢查我們是否正在訪問非葉節點或者父候選堆疊是否不為空。在這兩種情況下,我們都應該繼續遍歷 BST,直到找到目標父代。
  • 內部 while迴圈再次檢查我們是否正在訪問非葉節點。此時,訪問非葉節點意味著我們應該首先向左遍歷,因為我們使用中序遍歷。因此,我們將父候選新增到堆疊中並繼續向左遍歷。
  • 訪問左側節點後,我們從 Deque 中輪詢一個節點,檢查該節點是否是目標的父節點,如果是則返回。如果找不到父母,我們就會繼續向右移動。
  • 最後,如果主迴圈完成而沒有返回任何節點,我們可以假設該節點不存在或者它是最頂層的根節點。

現在,讓我們測試一下迭代方法:

@Test
void givenBinaryTree_whenFindParentNodeIteratively_thenReturnCorrectParentNode() {
    assertThrows(NoSuchElementException.class, () -> subject.iterativeParent(1231));
    assertThrows(NoSuchElementException.class, () -> subject.iterativeParent(8));
    assertEquals(8, subject.iterativeParent(5).value);
    assertEquals(5, subject.iterativeParent(3).value);
    assertEquals(5, subject.iterativeParent(7).value);
    assertEquals(3, subject.iterativeParent(4).value);
    
    <font>// assertion for other nodes<i>
}

在最壞的情況下,我們需要遍歷整棵o樹來找到父節點,這使得迭代解的空間複雜度為O(n)。同樣,如果 BST 是很好平衡的,我們可以在O(log n)中做同樣的事情。

當我們到達葉節點時,我們開始從ParentCandidates堆疊中輪詢元素。因此,用於儲存父候選的附加堆疊最多包含h 個元素,其中 h是 BST 的高度。因此,它也具有O(h)的 空間複雜度。

3、使用父指標建立 BST
該問題的另一個解決方案是修改現有的 BST 資料結構來儲存每個節點的父節點。

為此,我們建立另一個名為ParentKeeperTreeNode的類,其中包含一個名為Parent的新欄位:

class ParentKeeperTreeNode {
    int value;
    ParentKeeperTreeNode parent;
    ParentKeeperTreeNode left;
    ParentKeeperTreeNode right;
    <font>// value field arg constructor<i>
   
// equals and hashcode<i>
}

現在,我們需要建立一個自定義insert()方法來儲存父節點:

void insert(ParentKeeperTreeNode currentNode, final int value) {
    if (currentNode.left == null && value < currentNode.value) {
        currentNode.left = new ParentKeeperTreeNode(value);
        currentNode.left.parent = currentNode;
        return;
    }
    if (currentNode.right == null && value > currentNode.value) {
        currentNode.right = new ParentKeeperTreeNode(value);
        currentNode.right.parent = currentNode;
        return;
    }
    if (value > currentNode.value) {
        insert(currentNode.right, value);
    }
    if (value < currentNode.value) {
        insert(currentNode.left, value);
    }
}

當為當前節點建立新的左或右子節點時,insert() 方法還會儲存父節點。在這種情況下,由於我們正在建立一個新的子節點,因此父節點始終是我們正在訪問的當前節點。

最後,我們可以測試儲存父指標的 BST 版本:

@Test
void givenParentKeeperBinaryTree_whenGetParent_thenReturnCorrectParent() {
    ParentKeeperTreeNode subject = new ParentKeeperTreeNode(8);
    subject.insert(5);
    subject.insert(12);
    subject.insert(3);
    subject.insert(7);
    subject.insert(1);
    subject.insert(4);
    subject.insert(11);
    subject.insert(14);
    subject.insert(13);
    subject.insert(16);
    assertNull(subject.parent);
    assertEquals(8, subject.left.parent.value);
    assertEquals(8, subject.right.parent.value);
    assertEquals(5, subject.left.left.parent.value);
    assertEquals(5, subject.left.right.parent.value);
    <font>// tests for other nodes<i>
}

在這種型別的 BST 中,我們在節點插入期間計算父節點。因此,為了驗證結果,我們可以簡單地檢查每個節點中的父引用。

因此,我們可以在O(1)時間內透過引用立即獲取它,而不是在O(h)中計算每個給定節點的parent()。此外,每個節點的父節點只是對記憶體中另一個現有物件的引用。因此,空間複雜度也是O(1)。

當我們經常需要檢索節點的父節點時,該版本的 BST 非常有用,因為Parent()操作經過了很好的最佳化。

相關文章