使用特定方法拆分二叉树

Posted

技术标签:

【中文标题】使用特定方法拆分二叉树【英文标题】:Split a Binary Tree using specific methods 【发布时间】:2021-01-26 20:35:42 【问题描述】:

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

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

【问题讨论】:

是二叉搜索树吗? 信息不足。例如,如果输出树可以是退化树(没有一个节点有左孩子),所以更像链表,那么插入是恒定的。因此,整个过程是 O(n) 并且非常简单。如果树是二叉搜索树,并且输出也必须是二叉搜索树,那么我们可能会遇到一个更有趣的问题。 @trincot 感谢您的更正。是的,所有讨论的树都是 BTS。更新了问题。 是否允许在树中导航,例如逐步执行二分查找,而不使用黑盒find 函数? 你能回到我们身边吗?同时有几个答案,你没有任何反应...... 【参考方案1】:

我认为没有办法设计具有给定黑盒函数及其时间复杂度的 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 实现。当你运行这个 sn-p 时,一个测试用例将运行一个 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, mid, 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

【讨论】:

【参考方案2】:

本质上,您的目标是创建一个有效的 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) 时间内复制它。

【讨论】:

【参考方案3】:

我真的没有看到一种简单有效的方法来拆分您提到的操作。但我认为实现非常有效的拆分相对容易。

如果树是平衡的,那么如果您定义了一个称为独占连接的特殊操作,那么您可以在 O(log n) 内执行拆分。让我先将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 中的每个键都小于 tr 中的每个键。

如果你有两棵专属树T&lt;T&gt;

那么join_ex()可以看到如下:

请注意,如果您为任何 BST 取任何节点,则其子树满足此条件;左子树中的每个键都小于右子树中的每个键。你可以根据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, key, root->rlink, tg)
         ts = root;
       
  

如果在本图中将root设置为T

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

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

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

如果k 在原始树中,那么tg 的根将是k,而tg 将没有左子树。

【讨论】:

以上是关于使用特定方法拆分二叉树的主要内容,如果未能解决你的问题,请参考以下文章

树二叉树完全/满/平衡二叉树的理解与对比

算法漫游指北(第十三篇):二叉树的基本概念满二叉树完全二叉树二叉树性质二叉搜索树二叉树定义二叉树的广度优先遍历

树二叉树存储结构二叉数遍历& 数据结构基本概念和术语

树二叉树存储结构二叉数遍历& 数据结构基本概念和术语

二叉树及特殊二叉树(满二叉树完全二叉树二叉排序树平衡二叉树)的定义和性质(附详细推理过程)

二叉树二叉树的镜像