1. Introduction
1.导言
A Binary Search Tree (BST) is a data structure that helps us efficiently solve real-world problems.
二进制搜索树 (BST) 是一种数据结构,可帮助我们高效地解决现实世界中的问题。
In this post, we’ll look at how to solve the problem of finding the parent of a node in a BST.
在本篇文章中,我们将探讨如何解决在 BST 中查找节点父节点的问题。
2. What Is a Binary Search Tree?
2.什么是二叉搜索树?
A BST is a tree where each node points to at most two nodes, often called left and right children. Additionally, each node’s value is greater than the left child and smaller than the right child.
BST 是一种树,其中每个节点最多指向两个节点,通常称为左子节点和右子节点。此外,每个节点的值都大于左子节点,小于右子节点。
For instance, let’s picture three nodes, A=2, B=1, and C=4. Hence, one possible BST has A as the root, B as its left child, and C as its right child.
例如,假设有三个节点,A=2、B=1 和 C=4。因此,一个可能的 BST 是以 A 为根、B 为左子、C 为右子。
In the next sections, we’ll use a BST structure implemented with a default insert() method to exercise the problem of finding the parent of a node.
在接下来的章节中,我们将使用带有默认 insert() 方法的 BST 结构来解决查找节点父节点的问题。
3. The Parent of a Node in a Binary Search Tree
3.二叉搜索树中节点的父节点
In the following sections, we’ll describe the problem of finding a node’s parent in a BST and exercise a few approaches to solve it.
在下面的章节中,我们将描述在 BST 中查找节点父节点的问题,并练习几种解决方法。
3.1. Description of the Problem
3.1.问题描述
As we’ve seen throughout the article, a given node of a BST has pointers to its left and right children.
正如我们在整个文章中所看到的,BST 的一个给定节点有指向其左侧和右侧子节点的指针。
For instance, let’s picture a simple BST with three nodes:
例如,让我们想象一个有三个节点的简单 BST:
The node 8 contains two children, 5 and 12. Hence, node 8 is the parent of nodes 5 and 12.
节点 8 包含两个子节点 5 和 12。因此,节点 8 是节点 5 和 12 的父节点。
The problem consists of finding the parent of any given node value. In other words, we must find the node where any of its children equals the target value. For instance, in the BST of the image above, if we input 5 into our program, we expect 8 as output. If we input 12, we also expect 8.
问题包括找到任何给定节点值的父节点。换句话说,我们必须找到其任何一个子节点等于目标值的节点。例如,在上图的 BST 中,如果我们在程序中输入 5,则输出为 8。如果我们输入 12,我们也期望输出 8。
The edge cases for this problem are finding the parent for either the topmost root node or a node that doesn’t exist in the BST. In both cases, there’s no parent node.
这个问题的边缘情况是为最顶层的根节点或 BST 中不存在的节点寻找父节点。在这两种情况下,都没有父节点。
3.2. Test Structure
3.2.测试结构
Before diving into the various solutions, let’s first define a basic structure for our tests:
在深入探讨各种解决方案之前,我们首先要确定测试的基本结构:
class BinaryTreeParentNodeFinderUnitTest {
TreeNode subject;
@BeforeEach
void setUp() {
subject = new TreeNode(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);
}
}
The BinaryTreeParentNodeFinderUnitTest defines a setUp() method that creates the following BST:
BinaryTreeParentNodeFinderUnitTest 定义了一个 setUp() 方法,用于创建以下 BST:
4. Implementing a Recursive Solution
4.实施递归解决方案
The straightforward solution to the problem is using recursion to traverse the tree and early return the node where any of its children is equal to the target value.
解决问题的直接方法是使用递归遍历树,并尽早返回其任何子节点等于目标值的节点。
Let’s first define a public method in the TreeNode class:
让我们首先在 TreeNode 类中定义一个公共方法:
TreeNode parent(int target) throws NoSuchElementException {
return parent(this, new TreeNode(target));
}
Now, let’s define the recursive version of the parent() method in the TreeNode class:
现在,让我们在 TreeNode 类中定义 parent() 方法的递归版本:
TreeNode parent(TreeNode current, TreeNode target) throws NoSuchElementException {
if (target.equals(current) || current == null) {
throw new NoSuchElementException(format("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);
}
The algorithm first checks if the current node is the topmost root node or the node doesn’t exist in the tree. In both situations, the node doesn’t have a parent, so we throw a NoSuchElementException.
算法首先会检查当前节点是否是最顶层的根节点,或者该节点在树中不存在。在这两种情况下,节点都没有父节点,因此我们会抛出 NoSuchElementException 异常。
Then, the algorithm checks if any current node children equal the target. If so, the current node is the parent of the target node. Thus, we return current.
然后,算法会检查当前节点的任何子节点是否等于 目标节点。如果是,当前节点就是目标节点的父节点。因此,我们返回 当前。
Finally, we traverse the BST using recursive calls to left or right, depending on the target value.
最后,我们根据 target 值,使用向左或向右的递归调用遍历 BST。
Let’s test our recursive solution:
让我们来测试一下我们的递归解决方案:
@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);
// assertions for other nodes
}
In the worst-case scenario, the algorithm executes at most n recursive operations with O(1) cost each to find the parent node, where n is the number of nodes in the BST. Thus, it is O(n) in time complexity. That time falls to O(log n) in well-balanced BSTs since its height is always at most log n.
在最坏的情况下,该算法最多执行 n 次递归操作,每次操作的成本为 O(1) 以找到父节点,其中 n 是 BST 中的节点数。因此,在时间复杂度中是O(n)。在平衡良好的 BST 中,时间降至O(log n) ,因为其高度总是最多log n。
Additionally, the algorithm uses heap space for the recursive calls. Hence, in the worst-case scenario, the recursive calls stop when we find a leaf node. Therefore, the algorithm stacks at most h recursive calls, which makes it O(h) in space complexity, where h is the BST’s height.
此外,该算法使用 堆空间进行递归调用。因此,在最坏的情况下,当我们找到叶子节点时,递归调用就会停止。因此,该算法最多会堆叠 h 次递归调用,这使得 O(h) 的空间复杂度为O(h),其中 h 是 BST 的高度。
5. Implementing an Iterative Solution
5.实施迭代解决方案
Pretty much any recursive solution has an iterative version. Particularly, we can also find the parent of a BST using a stack and while loops instead of recursion.
几乎所有递归解决方案都有一个迭代版本。特别是,我们还可以使用堆栈和while循环来代替递归,从而找到 BST 的父级。
For that, let’s add the iterativeParent() method to the TreeNode class:
为此,让我们在 TreeNode 类中添加 iterativeParent() 方法:
TreeNode iterativeParent(int target) {
return iterativeParent(this, new TreeNode(target));
}
The method above is simply an interface to the helper method below:
上面的方法只是下面辅助方法的一个接口:
TreeNode iterativeParent(TreeNode current, TreeNode target) {
Deque <TreeNode> parentCandidates = new LinkedList<>();
String notFoundMessage = format("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);
}
The algorithm first initializes a stack to store parent candidates. Then it mostly depends on four main parts:
该算法首先初始化一个堆栈,用于存储父候选者。然后,它主要依赖于四个主要部分:
- The outer while loop checks if we are visiting a non-leaf node or if the stack of parent candidates is not empty. In both cases, we should continue traversing the BST until we find the target parent.
- The inner while loop checks again if we are visiting a non-leaf node. At that point, visiting a non-leaf node means we should traverse left first since we use an in-order traversal. Thus, we add the parent candidate to the stack and continue traversing left.
- After visiting the left nodes, we poll a node from the Deque, check if that node is the target’s parent, and return it if so. We keep traversing to the right if we don’t find a parent.
- Finally, if the main loop completes without returning any node, we can assume that the node doesn’t exist or it’s the topmost root node.
Now, let’s test the iterative approach:
现在,让我们来测试一下迭代法:
@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);
// assertion for other nodes
}
In the worst case, we need to traverse the entire o tree to find the parent, which makes the iterative solution O(n) in space complexity. Again, if the BST is well-balanced, we can do the same in O(log n).
在最坏的情况下,我们需要遍历整个 o 树才能找到父节点,这使得迭代求解的空间复杂度为 O(n) 。同样,如果 BST 平衡良好,我们可以在 O(log n) 内完成同样的工作。
When we reach a leaf node, we start polling elements from the parentCandidates stack. Hence, that additional stack to store the parent candidates contains, at most, h elements, where h is the height of the BST. Therefore, it also has O(h) space complexity.
当我们到达叶节点时,我们将开始轮询 parentCandidates 堆栈中的元素。因此,用于存储父节点候选元素的额外堆栈最多包含 h 个元素,其中 h 是 BST 的高度。因此,它的空间复杂度也是O(h)。
6. Creating a BST With Parent Pointers
6.使用父指针创建 BST
Another solution to the problem is to modify the existing BST data structure to store each node’s parent.
另一种解决方案是修改现有的 BST 数据结构,以存储每个节点的父节点。
For that, let’s create another class named ParentKeeperTreeNode with a new field called parent:
为此,让我们创建另一个名为 ParentKeeperTreeNode 的类,并添加一个名为 parent 的新字段:
class ParentKeeperTreeNode {
int value;
ParentKeeperTreeNode parent;
ParentKeeperTreeNode left;
ParentKeeperTreeNode right;
// value field arg constructor
// equals and hashcode
}
Now, we need to create a custom insert() method to also save the parent node:
现在,我们需要创建一个自定义 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);
}
}
The insert() method also saves the parent when creating a new left or right child for the current node. In that case, since we are creating a new child, the parent is always the current node we are visiting.
当为当前节点创建新的左子节点或右子节点时,insert()方法也会保存父节点。在这种情况下,由于我们正在创建一个新的子节点,因此父节点始终是我们正在访问的当前节点。
Finally, we can test the BST version that stores parent pointers:
最后,我们可以测试存储父指针的 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);
// tests for other nodes
}
In that type of BST, we calculate parents during node insertion. Thus, to verify the results, we can simply check the parent reference in each node.
在这种 BST 中,我们在插入节点时计算父节点。因此,要验证结果,我们只需检查每个节点中的父节点引用即可。
Therefore, instead of calculating the parent() of each given node in O(h), we can get it immediately by reference in O(1) time. Additionally, each node’s parent is just a reference to another existing object in memory. Thus, the space complexity is also O(1).
因此,我们不需要在 O(h) 内计算每个给定节点的 parent() ,而是可以在 O(1) 内立即通过引用获得。此外,每个节点的父节点只是对内存中另一个现有对象的引用。因此,空间复杂度也是O(1)。
That version of BST is helpful when we often need to retrieve the parent of a node since the parent() operation is well-optimized.
由于parent()操作经过了优化,因此当我们经常需要检索节点的父节点时,该版本的 BST 非常有用。
7. Conclusion
7.结论
In that article, we saw the problem of finding the parent of any given node of a BST.
在那篇文章中,我们看到了查找 BST 中任意给定节点的父节点的问题。
We’ve exercised three solutions to the problem with code examples. One uses recursion to traverse the BST. The other uses a stack to store parent candidates and traverse the BST. The last one keeps a parent reference in each node to get it in constant time.
我们用代码示例演练了解决问题的三种方法。其中一种使用递归遍历 BST。另一种使用堆栈来存储父节点候选,并遍历 BST。最后一种则在每个节点中保留父节点引用,以便在恒定时间内获取父节点。
As always, the source code is available over on GitHub.
与往常一样,源代码可在 GitHub 上获取。