微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用特定方法拆分二叉树

如何解决使用特定方法拆分二叉树

给定一棵二叉树,我必须返回一棵包含所有小于 k、大于 k 的元素的树和一棵仅包含一个元素 - k 的树。 允许使用的方法删除节点 - O(n) 插入 - O(n) 查找 - O(n) 求最小值 - O(n) 我假设这些方法很复杂,因为在练习中并没有写出树是平衡的。 所需复杂度 - O(n) 原始树必须保持其结构。 我完全被困住了。非常感谢任何帮助!

给定的树是二叉搜索树,输出应该是二叉搜索树。

解决方法

我认为没有办法用给定的黑盒函数及其时间复杂度设计 O(n) 算法,因为它们只能被称为(最大)恒定次数(例如 3次)以保持在 O(n) 约束内。

但是如果允许使用基本的标准节点操作来访问和创建 BST(通过左或右孩子遍历,将左或右孩子设置为给定的子树),那么您可以执行以下操作:

创建三个将被填充和返回的新的空 BST。将它们命名为 leftmidright,其中第一个将具有小于 k 的所有值,第二个将具有至多一个节点(值 k),最后一个将拥有其余所有内容。 在填充 leftright 时,维护对最接近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 tstr 构建一棵新树,使得 ts 中的每个键都小于 {{1} 中的每个键}.

如果您有两个独占树 trT<

Two exclusive trees

那么T>可以看成如下:

join_ex(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

Tree to split

然后可以看到分裂的图形表示:

split_key_rec(T,k,T<,T>)

T 根据键 split_key_rec() 将树分成两棵树 tstg。在操作结束时,k 包含键小于 ts 的 BST,k 是键大于或等于 tg 的 BST。

现在,为了完成您的要求,您调用 k 并进入 split_key_rec(t,tg) 一个 BST,所有密钥都小于 ts。几乎对称地,你得到一个 BST,其中所有的键都大于或等于 k。因此,最后一件事是验证 tg 的根是否为 k,如果是这种情况,则取消链接,并在 tg、{{1} } 和 kts 是没有 k 的树)。

如果 tg' 在原始树中,那么 tg' 的根将是 k,并且 k 不会有左子树。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。