维护二叉树中的列表顺序

Posted

技术标签:

【中文标题】维护二叉树中的列表顺序【英文标题】:Maintaing list order in binary tree 【发布时间】:2011-02-16 23:16:59 【问题描述】:

给定一个数字序列,我想将这些数字插入到平衡二叉树中,这样当我在树上进行中序遍历时,它会返回序列。

如何构造对应这个需求的插入方法?

请记住,树必须是平衡的,因此没有完全简单的解决方案。我试图用 AVL 树的修改版本来做到这一点,但我不确定这是否可行。

我也希望能够实现删除操作。 Delete 应该删除列表中第 i 个位置的项目。

编辑:澄清:

我想要: Insert(i, e),在序列中的第 i 个元素之前插入单个元素 e。 Delete(i),删除序列的第 i 个元素。

如果我执行 insert(0, 5), insert(0, 4), insert(0, 7),那么我存储的序列现在是 7, 4, 5,二叉树上的中序遍历应该给我 7, 4、5。

如果我执行 delete(1),那么二叉树上的中序遍历应该给我 7、5。如您所见,在这种情况下,删除 i = 1 的第 i 个序列元素后,序列顺序保持不变(如我从 0) 开始索引我的序列。

【问题讨论】:

是否应该在没有多余空间的情况下这样做? Kakira,没有多余的空间就无法完成。您至少需要 n*sizeof(reference) 额外空间。 看起来应该可以在不使用额外空间的情况下实现,不是吗? 不,不可能。您需要引用来保存它们插入顺序的信息。您不需要按数据大小顺序排列的空间。添加列表之前的树的大小将是,假设一个父,左和右,Size = N * (3*ref + size(data)) 其中 ref 是引用的大小,N 是节点的数量。添加它会导致它成为Size = N * (4*ref + size(data))Size = N * (5*ref + size(data)),具体取决于您的列表是否是双重链接的。所需空间的~ORDER~没有改变,但是常量改变了。 哦,我误会了,我认为“额外”空间的含义不仅仅是节点数的恒定倍数。我现在看到您的评论排除了这种可能性:P 【参考方案1】:

在树中保留一个链表。多亏了树,您已经有了对节点的指针/引用。当你插入到树中时,也在链表的末尾插入。从树中删除时,从链表中删除。因为你有引用,所以它们都是 O(1) 操作,如果你愿意,你可以按插入顺序迭代。

编辑: 为了澄清 Bart 的担忧,这里有一个 Java 实现

class LinkedTreeNode<E> 
   E data;
   LinkedTreeNode<E> parent;
   LinkedTreeNode<E> left;
   LinkedTreeNode<E> right;
   LinkedTreeNode<E> insertNext;
   LinkedTreeNode<E> insertPrev;


class LinkedTree<E> 
    LinkedTreeNode<E> root;
    LinkedTreeNode<E> insertHead;
    LinkedTreeNode<E> insertTail;

    // skip some stuff
    void add(E e) 
        LinkedTreeNode<E> node = new LinkedTreeNode<E>(e);

        // perform the tree insert using whatever method you like

        // update the list
        node.insertNext = insertTail;
        node.insertPrev = insertTail.insertPrev.insertPrev;
        node.insertPrev.insertNext = node;
        insertTail.insertPrev = node;
    

    E remove(E e) 
        LinkedTreeNode<E> rem = // whatever method to remove the node
        rem.insertNext.insertPrev = rem.insertPrev;
        rem.insertPrev.insertNext = rem.insertNext;
        return rem.data;
    
 

【讨论】:

从链表中删除是 O(n),即使你知道引用(或索引)。我猜一个链接的哈希集就可以了(add(...)remove(...) 在 O(1) 中,顺序正确)。 不,如果您拥有对节点指针的引用和访问权限,则从链表中删除是 O(1)。使用方法 list.remove(obj) 可能是 O(n),但我假设他有能力直接使用节点。 那你说的不是java.util.LinkedList。请参阅:this Q&A:remove(...) 是 O(n)。 没错,我不是在谈论 java.util.LinkedList - OP 正在寻找算法,而不是 Java 集合。 没关系,我以为我在某处看到了标签java。但是许多链表都有 O(n) 的删除方法,所以简单地说这样的操作是 O(1) 有点误导(当然是恕我直言!)。【参考方案2】:

如果您可以随机访问序列的内容(例如,您有一个数组),那么您可以直接构建树,以便前序遍历提供您想要的序列。它实际上可以在没有随机访问的情况下完成,但效率较低,因为可能需要线性扫描才能找到序列的中点。

这里的关键观察是序列中的第一个值进入树的根,然后序列的其余部分可以分成两半。前半部分将进入以左孩子为根的子树,后半部分将进入以右孩子为根的子树。因为您在每一步都将剩余的列表分成两半,所以生成的树将有 log_2 n 个级别并且将是平衡的。

这里有一些 C++ 代码。请注意,“end”指的是刚刚超过当前数组段末尾的元素的索引。

struct Node 
    Node(int v) : value(v), l(0), r(0) 

    int value;
    Node* l;
    Node* r;
;

Node* vector_to_tree(int begin, int end, int array[]) 
    if (begin == end)
        return NULL;

    Node* root = new Node(array[begin++]);
    int mid = (begin + end) / 2;
    root->l = vector_to_tree(begin, mid, array);
    root->r = vector_to_tree(mid,   end, array);

    return root;


void preorder_traverse(Node* root) 
    if (!root)
        return;

    std::cout << root->value << std::endl;
    traverse(root->l);
    traverse(root->r);

【讨论】:

这更符合我正在寻找的内容,但是如果要在给出了初始序列。 我现在没有时间考虑它,但是对于插入来说,应该可以根据新元素在序列中的索引来计算新元素在树中的位置最多 O(lg n) 时间——它实际上可能在恒定时间内是可行的。然后插入需要将树步行到适当的位置加上一些旋转以保持平衡。删除应该类似。这就是我的直觉所说的;我需要仔细考虑。 log n次可以找到要删除的item,但是如果删除了需要对vector重新排序,也就是O(n)。【参考方案3】:

这可以通过 AVL 树来完成。

通过添加到树的最右侧节点来将项目添加到 AVL 树。

由于旋转的性质,AVL 平衡不会影响中序遍历属性。

在每个节点存储树的大小,并在插入/删除/平衡期间为每个受影响的节点维护这些值可以在 O(log n) 时间内完成。在每个节点存储树的大小允许在序列中按排名搜索项目,因为树中项目的排名由节点的左孩子的大小 + 1 知道。

可以通过用节点右子树的最左边节点替换已删除的节点来完成从 AVL 树中的删除。

然后插入和删除操作在 O(log n) 时间内完成,因为树总是平衡的,并且节点上的大小更新是沿着从插入/删除节点到根的路径完成的。

由于除了二叉树之外没有后备数据结构,因此可以完成其他受益于树的操作,例如在 O(log n ) 时间。

【讨论】:

以上是关于维护二叉树中的列表顺序的主要内容,如果未能解决你的问题,请参考以下文章

JS中的二叉树遍历

二叉树中的数字总和

1367. 二叉树中的列表 dfs or bfs

1367. 二叉树中的列表

使用顺序遍历搜索二叉树中的元素

Leetcode 1367 二叉树中的列表 DFS