在n log n时间内根据排列构造二叉树

时间:2017-03-19 10:54:48

标签: algorithm time-complexity divide-and-conquer

数字1到n以指定顺序p_1,p_2,...,p_n插入二叉搜索树中。描述一个O(nlog n)时间算法来构造最终的二进制搜索树。

请注意: -

  1. 我不需要平均时间n log n,但是最差的时间。
  2. 我需要使用通常规则进行插入时产生的确切树。不允许使用AVL或红黑树。
  3. 这是一个作业问题。这非常非常重要。事实上,乍一看似乎是不可能的。我已经考虑过了很多。我的观察: -

    1. 我们用来证明排序至少需要n log n时间的论点并没有消除这种算法的存在。
    2. 如果总是可以在O(n)时间内找到一个小树在树的两个分数之间的子树,那么这个问题就很容易解决了。
    3. 选择root的中位数或左子项作为子树的根不起作用。

6 个答案:

答案 0 :(得分:4)

诀窍是不要使用构造的BST进行查找。相反,请为查找保留额外的平衡BST。链接叶子。

例如,我们可能有

Constructed    Balanced

       3           2
      / \         / \
     2   D       1   3
    / \         / | | \
   1   C       a  b c  d
  / \
 A   B

其中a, b, c, d分别是A, B, C, D的指针,而A, B, C, D通常是空指针。

要插入,首先插入平衡BST(O(log n)),按照指向构造树(O(1))的指针,执行构造插入(O(1)),然后重新链接新叶子(O(1))。

答案 1 :(得分:2)

由于David Eisenstat没有时间扩展他的答案,我会尝试将更多细节纳入类似的算法。

<强>直觉

算法背后的主要直觉基于以下陈述:

声明#1 :如果BST包含值aba < b)并且它们之间没有值,那么{{1 (A的节点)是a的父级(可能是间接的)(值B的节点)或b是{的{父级} {父} {1}}。

这句话显然是正确的,因为如果它们的最低共同祖先B是除AC之外的其他节点,则其值A必须介于{{1}之间}和B。请注意,对于任何BST(平衡或不平衡),语句#1都是正确的。

声明#2 :如果简单(非平衡)BST包含值cab)并且它们之间没有值我们正在尝试添加值a,以便b,然后a < b(值x的节点)将是a < x < b的直接右(更大)子项或直接左(较少)子节点X中的哪个节点在树中较低。

假设两个节点中较低的一个是x(另一种情况是对称的)。在插入阶段,值A在插入过程中将与B行进相同的路径,因为树不包含ax之间的任何值,即任何比较值{{ 1}}和a无法区分。这意味着值a将导航树直到节点x并且将在某个更早的步骤传递节点a(参见语句#1)。作为x,它应该成为x的正确孩子。此时A的直接右孩必须为空,因为B位于x > a的子树中,即该子树中的所有值都小于A,因为没有树中AA之间的值,没有值可以是节点B的右子节点。

请注意,在执行重新平衡之后,对于某些平衡的BST,语句#2可能不正确,尽管这应该是一个奇怪的情况。

声明#3 :对于尚未在树中的任何值b的平衡BST,您可以在a时间内找到距离最近且值最接近的值。< / p>

这直接来自语句#1和#2:您需要的只是在BST中找到值b的潜在插入点(取A),其中一个值将是插入点的直接父级,并找到另一个需要将树移回根目录(再次使用x)。

现在,算法背后的 想法 变得清晰:为了快速插入不平衡的BST,我们需要找到值越来越接近的节点。如果我们另外使用与目标(非平衡)BST相同的密钥以及来自该BST的相应节点作为值来维持平衡的BST,我们可以轻松地执行此操作。使用该附加数据结构,我们可以在O(log(N))时间内找到每个新值的插入点,并在x时间内使用新值更新此数据结构。

<强>算法

  1. 使用O(log(N))初始化“主要”O(log(N))O(log(N))
  2. 对于列表中的每个值O(log(N))执行:
  3. 如果这是第一个值,只需将其作为根节点添加到两个树中,然后转到#2
  4. {li}在root指定的树中查找与最接近的(balancedRoot,指向主BST中的节点null)相对应的节点,并且最接近的更大(x ,指向主BST中的节点balancedRoot
    • 如果没有最接近的较低值,即我们正在添加最小元素,请将其作为左子项添加到节点BalancedA
    • 如果没有最接近的更大值,即我们正在添加最大元素,请将其作为正确的子项添加到节点A
    • 查找树中较低的节点BalancedBB。您可以使用存储在节点中的显式B。如果下方节点为A(节点数较少),请将A添加为B的直接右侧孩子,否则添加level作为A的直接左子节点(更大的节点)。或者(更聪明地)你可能会注意到,从#1和#2语句开始,两个候选插入位置中只有一个(x是右子或A的左子)为空,您可以在此处插入值x
  5. 将值B添加到平衡树(可能会从步骤#4重复使用)。

  6. 转到步骤#2
  7. 由于循环的内部步骤不超过A,因此总复杂度为B

    Java实施

    我懒得自己实现平衡的BST所以我使用标准的Java TreeMap实现了Red-Black树,并且使用了对应于步骤#4的有用的xx方法算法(您可以查看source code以确保两者实际上都是O(log(N)))。

    O(N*log(N))

答案 2 :(得分:2)

这是线性时间算法。 (我说我不打算在这个问题上工作,所以如果你喜欢这个答案,请把赏金奖给SergGr。)

使用节点1..n创建双向链表并计算p的倒数。对于i从n到1,让q成为列表中p_i的左邻居,让r成为右邻居。如果p ^ -1(q)> p ^ -1(r),然后使p_i成为q的右子。如果p ^ -1(q)< p ^ -1(r),然后使p_i成为r的左子。从列表中删除p_i。

在Python中:

class Node(object):
    __slots__ = ('left', 'key', 'right')

    def __init__(self, key):
        self.left = None
        self.key = key
        self.right = None


def construct(p):
    # Validate the input.
    p = list(p)
    n = len(p)
    assert set(p) == set(range(n))  # 0 .. n-1

    # Compute p^-1.
    p_inv = [None] * n
    for i in range(n):
        p_inv[p[i]] = i

    # Set up the list.
    nodes = [Node(i) for i in range(n)]
    for i in range(n):
        if i >= 1:
            nodes[i].left = nodes[i - 1]
        if i < n - 1:
            nodes[i].right = nodes[i + 1]

    # Process p.
    for i in range(n - 1, 0, -1):  # n-1, n-2 .. 1
        q = nodes[p[i]].left
        r = nodes[p[i]].right
        if r is None or (q is not None and p_inv[q.key] > p_inv[r.key]):
            print(p[i], 'is the right child of', q.key)
        else:
            print(p[i], 'is the left child of', r.key)
        if q is not None:
            q.right = r
        if r is not None:
            r.left = q


construct([1, 3, 2, 0])

答案 3 :(得分:1)

这是我的O(n log ^ 2 n)尝试,不需要构建平衡树。

按照自然顺序(1到n)将节点放入数组中。还可以按插入顺序将它们链接到链接列表中。每个节点都存储其插入顺序以及密钥。

算法是这样的。

输入是链表中的节点,节点数组中索引的范围(low, high)

  1. 调用输入节点root,其密钥为rootkey。取消它与列表的链接。
  2. 确定输入节点的哪个子树较小。
  3. 遍历相应的数组范围,从链接列表中取消链接每个节点,然后将它们链接到单独的链接列表中,并按插入顺序再次对列表进行排序。
  4. 两个结果列表的头部是输入节点的子节点。
  5. 以递归方式对输入节点的子节点执行算法,将范围(low, rootkey-1)(rootkey+1, high)作为索引范围传递。
  6. 每个级别的排序操作为算法提供了额外的log n复杂度因子。

答案 4 :(得分:1)

这是一个O(n log n)算法,通过使用Y-fast trie而不是平衡二叉树,也可以适应O(n log log m)时间,其中m是范围。

在二叉搜索树中,较低的值保留较高的值。插入顺序与沿最终树行进时的右或左节点选择相对应。任何节点x的父节点是先前插入的最小数字或先前插入的最小数字,以后插入的为止。

我们可以使用上面O(n log n)最差时间的逻辑识别并连接列出的节点,并通过维护访问节点的平衡二叉树到目前为止我们遍历插入顺序。

说明:

让我们想象一下拟议的下级父母p。现在假设在l > p之前插入了一个x但仍然低于p的数字。 (1)p在插入过程中通过了l,在这种情况下,x必须通过l才能转到p,但这与x相矛盾1}}如果达到l,一定是正确的;或(2)p未通过l,在这种情况下,p位于l左侧的子树中,但这意味着插入的数字小于{{} 1}}但大于l,这是一个矛盾。

显然,x之后插入的l < x,大于p的数字也会pp的父母相矛盾1)x在插入期间传递了l,这意味着在插入p时已经分配了p的正确孩子;或(2)x位于l右侧的子树中,这又意味着插入的数字小于p但大于l,这是一个矛盾

因此,对于具有较低父级的任何节点x,该父级必须是低于x之前插入的最大数字。类似的逻辑涵盖了更高的拟议父母的情景。

现在让我们假设x的父x已插入p < x之前,最小数字大于h之前插入。{1}}。然后(1)x通过h,在这种情况下,p的右边节点在插入p时已被分配;或(2)x位于h的子树右侧,这意味着之前插入的数字低于p且大于h,但这与我们的断言相矛盾x是迄今为止插入的最小数字,大于h

答案 5 :(得分:0)

由于这是一项作业,我正在发布提示而不是答案。

对数字进行排序,同时保持插入顺序。假设您输入了[1,7,3,5,8,2,4]。排序后,您将获得[[1,0], [2,5], [3,2], [4, 6], [5,3], [7,1], [8,4]]。这实际上是生成树的有序遍历。在给定有序遍历和插入顺序(这部分将是线性时间)的情况下,仔细考虑如何重构树。

如果你确实需要它们,会有更多提示。