【问题标题】:For a given binary tree find maximum binary search sub-tree对于给定的二叉树,找到最大二叉搜索子树
【发布时间】:2011-03-10 22:52:54
【问题描述】:

对于给定的二叉树,找到最大的子树也是二叉搜索树?

例子:

输入:

                   10
               /         \
             50           150
            /  \         /   \
          25    75     200    20
         / \   / \    /  \    / \
        15 35 65 30  120 135 155 250 

输出:

                   50
                  /   \
                 25   75
                / \   /
               15 35  65

【问题讨论】:

  • 您是否正在尝试找到满足二叉搜索树要求的最大子树?
  • 是的,要求是找到满足二叉搜索树要求的最大子树。
  • 附注:我认为计算最大尺寸的脚本类似于:var c=0;var a=binaryTree.length;while(a=Math.floor(a/2)){++c};

标签: algorithm binary-tree binary-search-tree


【解决方案1】:

此答案之前包含基于链接/切割树的 O(n log n) 算法。这是一个更简单的 O(n) 解决方案。

核心是一个过程,它接受一个节点,唯一的最大 BSST 植根于它的左孩子,唯一的最大 BSST 植根于它的右孩子,以及指向这些 BSST 最左边和最右边元素的指针。它破坏其输入(可通过持久数据结构避免)并构造以给定节点为根的唯一最大 BSST 及其最小和最大元素。所有 BSST 节点都标注有后代的数量。和以前一样,从后序遍历中重复调用此过程。要恢复子树,请记住最大 BSST 的根;重建它只需要简单的遍历。

我将只处理左侧的 BSST;右边是对称的。如果左侧 BSST 的根大于新根,则移除整个子树,新根现在位于最左侧。否则,旧的最左边节点仍然是最左边的。从左侧 BSST 的最右侧节点开始向上移动,找到小于或等于根的第一个节点。它的右孩子必须被移除;现在请注意,由于 BST 属性,没有其他节点需要去!继续到左侧 BSST 的根,更新计数以反映删除。

这是 O(n) 的原因是,尽管存在循环,但原始树中的每条边本质上只遍历一次。


编辑:总的来说,遍历的路径是 BST 中的最大直线路径,除了左脊柱和右脊柱。例如,在输入上

              H
             / \
            /   \
           /     \
          /       \
         /         \
        /           \
       /             \
      D               L
     / \             / \
    /   \           /   \
   /     \         /     \
  B       F       J       N
 / \     / \     / \     / \
A   C   E   G   I   K   M   O

这里是遍历每条边的递归调用:

              H
             / \
            /   \
           /     \
          /       \
         /         \
        /           \
       /             \
      D               L
     / h             h \
    /   h           h   \
   /     h         h     \
  B       F       J       N
 / d     d h     h l     l \
A   C   E   G   I   K   M   O

【讨论】:

  • 链接/砍树上的论文链接:cs.cmu.edu/~sleator/papers/dynamic-trees.pdf
  • “进行根更新计数”是否意味着我们不止一次遍历边缘?
  • +1。我刚刚意识到,您不必沿着通往根的路径更新中间节点的计数(基本上我误解了您写的内容)。我觉得这很合适。
  • 对我来说也是正确的@throwawayacct - 我希望你能继续分享你的知识,而不是真的扔掉你的帐户:)
【解决方案2】:

之前的算法(参见修订版)O(n^2) - 我们可以通过注意以下事实将其概括为O(n log n)

  1. 如果 b 是最大 BST 的根且 b.left.value < b.value,则 b.left 也在 BST 中(b.right.value ≥ b.value 也是如此)
  2. 如果 b 是最大 BST 的根,并且 a 也在 BST 中,则 a 和 b 之间的每个节点都在 BST 中。

因此,如果 c 介于 a 和 b 之间,并且 c 不在以 b 为根的 BST 中,则 a 也不是(由于 (2.))。使用这个事实,我们可以很容易地确定一个节点是否在以任何给定祖先为根的 BST 中。我们将通过将一个节点连同它的祖先列表以及如果该祖先确实是最大 BST 的根(我们'我会将此列表称为ancestorList)。我们会将整个潜在根集合存储在overallRootsList

让我们定义一个名为 potentialRoot 的结构如下:

每个 potentialRoot 都包含以下值:
* node:我们正在考虑的 BST 根节点
* minValue 和 maxValue:另一个节点必须介于之间的范围才能成为以节点为根的 BST 的一部分(每个节点都不同)
* subNodes:以节点为根的最大 BST 中其余节点的列表

伪代码如下所示(注意所有提到的列表都是潜在根的列表)

FindLargestBST(node, ancestorList):
    leftList, rightList = empty lists
    for each potentialRoot in ancestorList:
        if potentialRoot.minValue < node.Value ≤ potentialRoot.maxValue:
            add node to potentialRoot.subNodes (due to (1.))
            (note that the following copies contain references, not copies, of subNodes)
            add copy of potentialRoot to leftList, setting maxValue = node.Value
            add copy of potentialRoot to rightList, setting minValue = node.Value

    add the potentialRoot (node, -∞, +∞) to leftList, rightList, and overallRootsList
    FindLargestBST(node.left, leftList)
    FindLargestBST(node.right, rightList)

最后overallRootsList 将是n potentialRoots 的列表,每个都有一个子节点列表。子节点列表最大的是你的 BST。

由于ancestorList中有O(n log n)中运行

【讨论】:

  • 这不是O(n^2)吗?它有效,但这(对我来说,无论如何,我承认它可能对 OP 有帮助)非常微不足道,O(n) 中的东西会更好:)。 +1,因为它确实回答了这个问题。
  • @IVlad:是的,这是O(n^2),但这是迄今为止发布的唯一有效的方法。如果您发现更好的东西,请发布!
  • 这个算法的渐近复杂度太离谱了!但是无论如何要为 正确 +1。 :-) 我想不出有什么方法可以解决这个问题(使用可能的子树∝ 2^N)。
  • @IVlad, @BlueRaja - 我认为这比 n^2 差得多。我认为这个问题是指数级的。每个额外的叶节点都会使问题规模加倍(因为它是递归调用中递归调用的目标)。细节很复杂(而且我不知道如何计算确切的值),但是明确的最小复杂度是基于每个叶节点都在解决方案中或在解决方案之外的事实,因此 m 个叶节点有 2^m 种可能性。
  • @Jeffrey - 我不这么认为。请注意,LargestBST 函数中有 nLargestBSTIncludingNode 的递归调用(每个节点调用一次)。然后在LargestBSTIncludingNode 中,每个节点只被访问一次。这在我看来 O(n^2)
【解决方案3】:

有趣的问题!

我之前的尝试是愚蠢的错误!

这是另一个尝试(希望这次更正)。

我假设树是连接的。

假设对于树的每个节点 n,您有一组 n 的后代,Sn,其属性为

  • 对于 Sn 的每个成员 x,从 n 到 x 的唯一路径是二叉搜索树(它只是一条路径,但您仍然可以将其视为一棵树)。

  • 对于x的每个后代y,使得从n到y的路径是一个BST,y在S中n

节点集 Sn,给你以 n 为根的最大 BST。

我们可以为每个节点构造Sn,方法是在树上进行深度优先搜索,并传入路径信息(从根到当前节点的路径)并更新节点的集合路径中的节点通过沿路径回溯。

当我们访问一个节点时,我们沿着路径走,并检查到目前为止走的那段路径是否满足 BST 属性。如果是这样,我们将当前节点添加到我们刚刚走过的路径的相应节点集合中。在违反 BST 属性的那一刻,我们停止行走。检查到目前为止我们走过的路径段是否是 BST 可以在 O(1) 时间内完成,对于 O(path_length) 时间的总处理时间,对于每个节点。

最后,每个节点都会填充其对应的 Sn。我们现在可以遍历树并选择 Sn 值最大的节点。

为此花费的时间是节点深度的总和(在最坏的情况下),在平均情况下是 O(nlogn)(参见 http://www.toves.org/books/data/ch05-trees/index.html 的第 5.2.4 节),但是 O(n ^2) 在最坏的情况下。

也许更新集合的更聪明的方法可以保证减少最坏情况的时间。

伪代码可能是这样的:

static Tree void LargestBST(Tree t)
{
    LargestBST(t, new List<Pair>());
    // Walk the tree and return the largest subtree with max |S_n|.
}

static Tree LargestBST(Tree t, List<Pair> path)
{
    if (t == null) return;

    t.Set.Add(t.Value);

    int value = t.Value;
    int maxVal = value;
    int minVal = value;

    foreach (Pair p in path)
    {
        if (p.isRight)
        {
            if (minVal < p.node.Value)
            {
                break;
            }
        }

        if (!p.isRight)
        {
            if (maxVal > p.node.Value)
            {
                break;
            }
        }

        p.node.Set.Add(t.Value);

        if (p.node.Value <= minVal)
        {
            minVal = p.node.Value;
        }

        if (p.node.Value >= maxVal)
        {
            maxVal = p.node.Value;
        }
    }

    Pair pl = new Pair();
    pl.node = t;
    pl.isRight = false;

    path.Insert(0, pl);
    LargestBST(t.Left, path);

    path.RemoveAt(0);

    Pair pr = new Pair();
    pr.node = t;
    pr.isRight = true;

    path.Insert(0, pr);

    LargestBST(t.Right, path);

    path.RemoveAt(0);

}

【讨论】:

  • 我们不知道左子树的所有元素都小于根(右子树更大也是如此)。例如,在 OP 的示例中,如果我们将 35 替换为 60,则包含 25 的最大 BST 有 3 个元素,75 有 2 个元素,但 50 没有 3+2+1= 6个元素。但是,如果我们为每个子树调用引入上限/下限,则类似于此的递归解决方案将起作用。
  • @BlueRaja:你是对的。也许我应该尝试证明这一点! :-)
  • @BlueRaja:我已经用不同的方法更新了答案。
  • 不错。它或多或少等同于我刚刚发布的算法(我在阅读本文之前写的,我发誓!)。或者至少,它们都是平衡树的O(n log n) 和非平衡树的最坏情况O(n^2),并且都依赖(相反)相同的事实。
  • @BlueRaja:不仅仅是平衡树。如果你取一棵随机树,深度的预期总和是
【解决方案4】:

二叉树中最大的二叉搜索树:

我们有两种方法可以解决这个问题,

i)最大的 BST 未诱导(从一个节点,它的所有子节点不需要满足 BST 条件)

ii)最大 BST 诱导(从一个节点,它的所有子节点都将满足 BST 条件)

我们将在这里讨论最大的 BST(Not Induced)。我们将采用自下而上的方法(后序遍历)来解决这个问题。

a)到达叶子节点

b)一个树节点(来自叶子)将返回一个 TreeNodeHelper 对象,其中包含以下字段。

public static class TreeNodeHelper {
        TreeNode node;
        int nodes;
        Integer maxValue;
        Integer minValue;
        boolean isBST;


        public TreeNodeHelper() {}

        public TreeNodeHelper(TreeNode node, int nodes, Integer maxValue, Integer minValue, boolean isBST) {
            this.node = node;
            this.nodes = nodes;
            this.maxValue = maxValue;
            this.minValue = minValue;
            this.isBST = isBST;
        }      
    }

c)从叶节点开始,nodes=1,isBST=true,minValue=maxValue=node.data。此外,如果满足 BST 条件,则节点数将增加。

d)在此的帮助下,我们将检查当前节点的 BST 条件。我们将重复相同的操作直到 root。

e)从每个节点将返回两个对象。一个用于最后的最大 BST,另一个用于当前 BST 满足节点。因此,从每个节点(叶子上方)(2+2)=4(左子树为 2,右子树为 2)对象将进行比较,并返回两个。

f) 来自根的最终最大节点对象将是最大的 BST

问题:

这种方法存在问题。在遵循这种方法时,如果子树不满足当前节点的 BST 条件,我们不能简单地忽略子树(即使它的节点数较少)。例如

 55
  \
   75
  /  \
 27  89
    /  \
   26  95
      /  \
     23  105
         /  \
        20  110

从叶子节点(20,110)开始,对象将使用节点(105)进行测试,它满足条件。但是当它到达节点(95)时,叶节点(20)不满足 BST 条件。由于此解决方案适用于 BST(未诱导),因此我们不应忽略满足条件的节点(105)和节点(110)。因此,我们必须从节点(95)开始再次回溯测试 BST 条件并捕获那些节点(105、110)。

此实现的完整代码可在此链接中获得

https://github.com/dineshappavoo/Implementation/tree/master/LARGEST_BST_IN_BT_NOT_INDUCED_VER1.0

【讨论】:

    【解决方案5】:

    如果您执行按顺序遍历,二叉搜索树将为您提供排序结果。因此,对整个二叉树进行中序遍历。最长的排序序列是你最大的二叉搜索子树。

    • 对元素进行中序遍历(访问左、访问根、访问右)
    • 同时获取节点数据,比较上一个节点数据是否小于下一个数据。如果是,则将 counter 加 1。存储起始节点。
    • 当比较失败时,存储结束节点并将计数器重置为0
    • 将此信息(计数器、开始、结束)节点存储在一个数组结构中,以便以后查找具有最大值的节点,这将为您提供最长的二叉搜索子树

    【讨论】:

    • 我可能遗漏了一些东西,但我认为这行不通。考虑 OP 发布的示例,特别是以50 为根的子树。考虑用完全破坏 BST 属性的值替换除 3550 之外的每个值。中序遍历将按排序顺序找到35 50,这根本没有任何意义。您能详细说明如何避免这种情况吗?
    • @IVlad,我不明白.. 如果您破坏 BST 属性,35 50 将如何按排序顺序给出结果?我猜你是想让我指出我的逻辑中的一个大漏洞,请解释一下。
    • 在 OP 的示例中以 50 为根的子树中将 75 替换为 20。你的算法输出什么?您的算法只有在不考虑65 时才是正确的,否则它将输出断开连接的树。那你怎么知道忽略65
    【解决方案6】:
    GetLargestSortedBinarySubtree(thisNode, ref OverallBestTree)
        if thisNode == null
            Return null
        LeftLargest = GetLargestSortedBinarySubtree(thisNode.LeftNode, ref OverallBestTree)
        RightLargest = GetLargestSortedBinarySubtree(thisNode.RightNode, ref OverallBestTree)
        if LeftLargest.Max < thisNode.Value & RightLargest.Min > thisNode.Value
            currentBestTree = new BinaryTree(LeftLargest, thisNode.Value, RightLargest)
        else if LeftLargest.Max < thisNode.Value
            currentBestTree = new BinaryTree(LeftLargest, thisNode.Value, null)
        else if RightLargest.Min > thisNode.Value
            currentBestTree = new BinaryTree(null, thisNode.Value, RightLargest)
        else
            currentBestTree = new BinaryTree(null, thisNode.Value, null)
        if (currentBestTree.Size > OverallBestTree.Size)
            OverallBestTree = currentBestTree
        return currentBestTree
    

    正如 BlueRaja 指出的,这个算法是不正确的。

    真的应该叫GetLargestSortedBinarySubtreeThatCanBeRecursivelyConstructedFromMaximalSortedSubtrees

    【讨论】:

    • 在与我对 M 解决方案的评论相同的情况下失败(将 35 替换为 60),但出于不同的原因 - 包含根的最佳 BST 可能包含 part 最好的左 BST,但不一定是全部。在这种情况下,该算法将给出大小为 3(以 25 为根)而不是大小为 5(以 50 为根)的树。
    【解决方案7】:
    root(Tree L A R) = A
    
    MaxBST(NULL) = (true, 0, NULL)
    MaxBST(Tree L A R as T) = 
      let
        # Look at both children
        (L_is_BST, L_size, L_sub) = MaxBST(L)
        (R_is_BST, R_size, R_sub) = MaxBST(R)
      in
      # If they're both good, then this node might be good too
      if L_is_BST and R_is_BST and (L == NULL or root(L) < A) and (R == NULL or A < root(R))
      then (true, 1 + L_size + R_size, T)
      else
           # This node is no good, so give back the best our children had to offer
           (false, max(L_size, R_size), if L_size > R_size then L_sub else R_sub)
    

    只查看每个树节点一次,因此在 O(N) 中运行。

    编辑:克鲁德,这并不认为它可以遗漏子树的某些部分。当我阅读子树时,我假设“整个树都植根于某个节点”。我以后可能会回来解决这个问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-08
      • 2017-09-28
      • 1970-01-01
      • 1970-01-01
      • 2023-03-08
      • 1970-01-01
      相关资源
      最近更新 更多