如何解决使用特定方法拆分二叉树
给定一棵二叉树,我必须返回一棵包含所有小于 k、大于 k 的元素的树和一棵仅包含一个元素 - k 的树。 允许使用的方法: 删除节点 - O(n) 插入 - O(n) 查找 - O(n) 求最小值 - O(n) 我假设这些方法很复杂,因为在练习中并没有写出树是平衡的。 所需复杂度 - O(n) 原始树必须保持其结构。 我完全被困住了。非常感谢任何帮助!
解决方法
我认为没有办法用给定的黑盒函数及其时间复杂度设计 O(n) 算法,因为它们只能被称为(最大)恒定次数(例如 3次)以保持在 O(n) 约束内。
但是如果允许使用基本的标准节点操作来访问和创建 BST(通过左或右孩子遍历,将左或右孩子设置为给定的子树),那么您可以执行以下操作:
创建三个将被填充和返回的新的空 BST。将它们命名为 left
、mid
和 right
,其中第一个将具有小于 k 的所有值,第二个将具有至多一个节点(值 k),最后一个将拥有其余所有内容。
在填充 left
和 right
时,维护对最接近值 k 的节点的引用:在 left
中,这将是具有最大值的节点,在 right
中具有最小值的节点。
请按照以下步骤操作:
- 应用通常的二分搜索从根向具有值 k 的节点走去
- 这样做时:无论何时选择节点的左子节点,节点本身及其右子树都属于
right
。但是,此时不应包含左子节点,因此创建一个复制当前节点的新节点,但不包含其左子节点。维护对right
中具有最小值的节点的引用,因为当此步骤发生不止一次时,该节点可能会获得左新子树。 - 在选择节点的右子节点时执行类似的操作。
- 当找到具有k的节点时,算法可以将其左子树添加到
left
,将右子树添加到right
,并创建单节点树值k
。
时间复杂度
在最坏的情况下,搜索具有值 k 的节点可能需要 O(n),因为 BST 不是平衡的。所有其他操作(向其中一个新 BST 中的特定节点添加子树)都在恒定时间内运行,因此在最坏的情况下它们总共执行 O(n) 次。
如果给定的 BST 是平衡的(不一定是完美的,但就像 AVL 规则一样),那么算法会在 O(logn) 时间内运行。但是,输出 BST 可能不那么平衡,并且可能违反 AVL 规则,因此需要旋转。
示例实现
这是一个 JavaScript 实现。当您运行此代码段时,一个测试用例将运行一个 BST,其节点值为 0..19(以随机顺序插入)和 k=10。输出将按顺序迭代创建的三个 BST,以验证它们分别输出 0..9、10 和 11..19:
class Node {
constructor(value,left=null,right=null) {
this.value = value;
this.left = left;
this.right = right;
}
insert(value) { // Insert as a leaf,maintaining the BST property
if (value < this.value) {
if (this.left !== null) {
return this.left.insert(value);
}
this.left = new Node(value);
return this.left;
} else {
if (this.right !== null) {
return this.right.insert(value);
}
this.right = new Node(value);
return this.right;
}
}
// Utility function to iterate the BST values in in-order sequence
* [Symbol.iterator]() {
if (this.left !== null) yield * this.left;
yield this.value;
if (this.right !== null) yield * this.right;
}
}
// The main algorithm
function splitInThree(root,k) {
let node = root;
// Variables for the roots of the trees to return:
let left = null;
let mid = null;
let right = null;
// Reference to the nodes that are lexically closest to k:
let next = null;
let prev = null;
while (node !== null) {
// Create a copy of the current node
newNode = new Node(node.value);
if (k < node.value) {
// All nodes at the right go with it,but it gets no left child at this stage
newNode.right = node.right;
// Merge this with the tree we are creating for nodes with value > k
if (right === null) {
right = newNode;
} else {
next.left = newNode;
}
next = newNode;
node = node.left;
} else if (k > node.value) {
// All nodes at the left go with it,but it gets no right child at this stage
newNode.left = node.left;
// Merge this with the tree we are creating for nodes with value < k
if (left === null) {
left = newNode;
} else {
prev.right = newNode;
}
prev = newNode;
node = node.right;
} else {
// Create the root-only tree for k
mid = newNode;
// The left subtree belongs in the left tree
if (left === null) {
left = node.left;
} else {
prev.right = node.left;
}
// ...and the right subtree in the right tree
if (right === null) {
right = node.right;
} else {
next.left = node.right;
}
// All nodes have been allocated to a target tree
break;
}
}
// return the three new trees:
return [left,mid,right];
}
// === Test code for the algorithm ===
// Utility function
function shuffled(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i],a[j]] = [a[j],a[i]];
}
return a;
}
// Create a shuffled array of the integers 0...19
let arr = shuffled([...Array(20).keys()]);
// Insert these values into a new BST:
let root = new Node(arr.pop());
for (let val of arr) root.insert(val);
// Apply the algorithm with k=10
let [left,right] = splitInThree(root,10);
// Print out the values from the three BSTs:
console.log(...left); // 0..9
console.log(...mid); // 10
console.log(...right); // 11..19
本质上,您的目标是创建一个有效的 BST,其中 k 是根节点;在这种情况下,左子树是一个包含所有小于 k 元素的 BST,右子树是一个包含所有大于 k 元素的 BST。
这可以通过一系列tree rotations来实现:
- 首先,对值为 k 的节点进行 O(n) 搜索,构建其祖先的堆栈,直到根节点。
- 当还有任何剩余的祖先时,从堆栈中弹出一个,并执行树旋转,使 k 成为该祖先的父级。
每次旋转需要 O(1) 时间,因此该算法在 O(n) 时间内终止,因为最多有 O(n) 个祖先。在平衡树中,该算法花费 O(log n) 时间,尽管结果不是平衡树。
在您的问题中,您写道“插入”和“删除”操作需要 O(n) 时间,但这是您的假设,即问题中没有说明这些操作需要准时。如果你只操作你已经有指针指向的节点,那么基本操作需要 O(1) 时间。
如果要求不破坏原始树,那么您可以先在 O(n) 时间内复制它。
,我真的没有看到一种简单有效的方法来拆分您提到的操作。但我认为实现非常高效的拆分相对容易。
如果树是平衡的,那么您可以在 O(log n) 中执行拆分,前提是您定义了一个称为 join exclusive 的特殊操作。让我首先将 join_ex()
定义为有问题的操作:
Node * join_exclusive(Node *& ts,Node *& tg)
{
if (ts == NULL)
return tg;
if (tg == NULL)
return ts;
tg=.llink) = join_exclusive(ts->rlink,tg->llink);
ts->rlink = tg;
Node * ret_val = ts;
ts = tg = NULL; // empty the trees
return ret_val;
}
join_ex()
假设您想从两个 BST ts
和 tr
构建一棵新树,使得 ts
中的每个键都小于 {{1} 中的每个键}.
如果您有两个独占树 tr
和 T<
:
那么T>
可以看成如下:
注意,如果你为任何BST取任何节点,那么它的子树满足这个条件;左子树中的每个键都小于右子树中的每个键。你可以基于join_ex()
设计一个不错的删除算法。
现在我们准备好进行拆分操作了:
join_ex()
如果将图中的void split_key_rec(Node * root,const key_type & key,Node *& ts,Node *& tg)
{
if (root == NULL)
{
ts = tg = NULL;
return;
}
if (key < root->key)
{
split_key_rec(root->llink,key,ts,root->llink);
tg = root;
}
else
{
split_key_rec(root->rlink,root->rlink,tg)
ts = root;
}
}
设为root
然后可以看到分裂的图形表示:
T
根据键 split_key_rec()
将树分成两棵树 ts
和 tg
。在操作结束时,k
包含键小于 ts
的 BST,k
是键大于或等于 tg
的 BST。
现在,为了完成您的要求,您调用 k
并进入 split_key_rec(t,tg)
一个 BST,所有密钥都小于 ts
。几乎对称地,你得到一个 BST,其中所有的键都大于或等于 k
。因此,最后一件事是验证 tg
的根是否为 k
,如果是这种情况,则取消链接,并在 tg
、{{1} } 和 k
(ts
是没有 k
的树)。
如果 tg'
在原始树中,那么 tg'
的根将是 k
,并且 k
不会有左子树。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。