查找BST中的路径上是否存在给定的总和

时间:2012-10-27 22:22:55

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

问题是找出BST中任何路径上是否存在给定的总和。如果路径意味着根到叶子,那么这个问题很容易,或者如果路径意味着从根到叶子的路径的一部分可能不包括根或叶子,则该问题很容易。但这里变得困难,因为路径可能跨越节点的左右子节点。例如,在给定的图中,在圆圈路径上存在132的总和。我怎样才能找到这样一条路径的存在?使用散列来存储节点下的所有可能总和是不受欢迎的!

enter image description here

4 个答案:

答案 0 :(得分:2)

我将按顺序遍历左子树,并以相反的顺序遍历右子树同时合并排序的工作方式。每次移动使aum更接近的迭代器。就像几乎合并排序一样。它的顺序n

答案 1 :(得分:2)

你当然可以生成所有可能的路径,随着时间的推移逐步求和。树是BST的事实可能会让你通过限制某些总和来节省时间,尽管我不确定它会给渐近的速度增加。问题是使用给定节点的左子节点形成的总和不一定小于使用右子节点形成的总和,因为前一和的路径可能包含更多节点。以下算法适用于所有树,而不仅仅是BST。

要生成所有可能的路径,请注意路径的最高点是特殊的:它是路径中唯一允许(尽管不是必需)使路径中包含两个子路径的点。每条路径都包含一个独特的最高点。因此,递归的外层应该是访问每个树节点,并生成将该节点作为最顶点的所有路径。

// Report whether any path whose topmost node is t sums to target.
// Recurses to examine every node under t.
EnumerateTopmost(Tree t, int target) {
    // Get a list of sums for paths containing the left child.
    // Include a 0 at the start to account for a "zero-length path" that
    // does not contain any children.  This will be in increasing order.
    a = append(0, EnumerateSums(t.left))
    // Do the same for paths containing the right child.  This needs to
    // be sorted in decreasing order.
    b = reverse(append(0, EnumerateSums(t.right)))

    // "List match" to detect any pair of sums that works.
    // This is a linear-time algorithm that takes two sorted lists --
    // one increasing, the other decreasing -- and detects whether there is
    // any pair of elements (one from the first list, the other from the
    // second) that sum to a given value.  Starting at the beginning of
    // each list, we compute the current sum, and proceed to strike out any
    // elements that we know cannot be part of a satisfying pair.
    // If the sum of a[i] and b[j] is too small, then we know that a[i]
    // cannot be part of any satisfying pair, since all remaining elements
    // from b that it could be added to are at least as small as b[j], so we
    // can strike it out (which we do by advancing i by 1).  Similarly if
    // the sum of a[i] and b[j] is too big, then we know that b[j] cannot
    // be part of any satisfying pair, since all remaining elements from a
    // that b[j] could be added to are at least as big as a[i], so we can
    // strike it out (which we do by advancing j by 1).  If we get to the
    // end of either list without finding the right sum, there can be
    // no satisfying pair.
    i = 0
    j = 0
    while (i < length(a) and j < length(b)) {
        if (a[i] + b[j] + t.value < target) {
            i = i + 1
        } else if (a[i] + b[j] + t.value > target) {
            j = j + 1
        } else {
            print "Found!  Topmost node=", t
            return
        }
    }

    // Recurse to examine the rest of the tree.
    EnumerateTopmost(t.left)
    EnumerateTopmost(t.right)
}

// Return a list of all sums that contain t and at most one of its children,
// in increasing order.
EnumerateSums(Tree t) {
    If (t == NULL) {
        // We have been called with the "child" of a leaf node.
        return []     // Empty list
    } else {
        // Include a 0 in one of the child sum lists to stand for
        // "just node t" (arbitrarily picking left here).
        // Note that even if t is a leaf node, we still call ourselves on
        // its "children" here -- in C/C++, a special "NULL" value represents
        // these nonexistent children.
        a = append(0, EnumerateSums(t.left))
        b = EnumerateSums(t.right)
        Add t.value to each element in a
        Add t.value to each element in b
        // "Ordinary" list merge that simply combines two sorted lists
        // to produce a new sorted list, in linear time.
        c = ListMerge(a, b)
        return c
    }
}

上述伪代码仅报告路径中的最顶层节点。通过让EnumerateSums()返回对(sum, goesLeft)的列表而不是简单的和列表,可以重建整个路径,其中goesLeft是一个布尔值,指示用于生成该总和的路径最初从父节点向左走。

上述伪代码为每个节点多次计算总和列表:EnumerateSums(t)将为树中t以上的每个节点调用一次,此外还要为t本身调用。EnumerateSums()。有可能使EnumerateSums()记忆每个节点的总和列表,这样它就不会在后续调用中重新计算,但实际上这并没有改进渐近:只需要O(n)工作使用普通递归生成n个和的列表,并将其更改为O(1)不会改变总体时间复杂度,因为任何对EnumerateSums()的调用产生的总和列表通常必须由无论如何,调用者需要O(n)时间。 编辑:正如Evgeny Kluev所指出的,EnumerateSums()实际上表现得像合并排序,是O(nlog n)当树完全平衡时,当它是单一路径时O(n ^ 2)。因此,备忘录实际上会提供渐近的性能提升。

可以通过将EnumerateSumsDown()重新排列为类似迭代器的对象来执行列表合并,并且可以查询以按递增顺序检索下一个求和来删除临时的和列表。这还需要创建一个reverse(append(0, EnumerateSums(t.right)))来执行相同的操作,但是按递减顺序检索总和,并使用它来代替{{1}}。这样做会将算法的空间复杂度降低到O(n),其中n是树中节点的数量,因为每个迭代器对象需要恒定的空间(指向左右子迭代器对象的指针,以及记录最后一个总和)每个树节点最多可以有一个。

答案 2 :(得分:2)

不是最快,但简单的方法是使用两个嵌套的深度优先搜索。

使用普通深度优先搜索来获取起始节点。使用深度优先搜索的第二个修改版本,从该节点开始检查所有路径的总和。

enter image description here

第二次深度优先搜索与普通深度优先搜索的不同之处在于两个细节:

  1. 保持当前路径总和。每次将新节点添加到路径时,它会为总和增加值,并在删除某个节点时从总和中删除值。
  2. 它仅沿相反方向(图中的红色边缘)遍历从根到起始节点的路径边缘。像往常一样,所有其他边缘都在正确的方向上移动(图中的黑色边缘)。要沿相反方向遍历边缘,它要么使用原始BST的“父”指针(如果有的话),要么偷看第一次深度优先搜索的堆栈以获得这些“父”指针。
  3. 每个DFS在O(N)中的时间复杂度,因此总时间复杂度为O(N 2 )。空间要求是O(N)(两个DFS堆栈的空间)。如果原始BST包含“父”指针,则空间要求为O(1)(“父”指针允许在没有堆栈的任何方向上遍历树)。


    其他方法基于j_random_hacker和robert king的想法(维护总和列表,匹配它们,然后将它们合并在一起)。它以自下而上的方式处理树(从叶子开始)。

    使用DFS查找某个叶节点。然后返回并找到最后一个分支节点,即该叶节点的grand -...- grand-parent。这给出了分支节点和叶节点之间的链。处理这个链:

    match1(chain)
    sum_list = sum(chain)
    
    match1(chain):
      i = j = sum = 0
      loop:
        while (sum += chain[i]) < target:
          ++i
        while (sum -= chain[j]) > target:
          ++j
        if sum == target:
          success!
    
    sum(chain):
      result = [0]
      sum = 0
      i = chain.length - 1
      loop:
        sum += chain[i]
        --i
        result.append(sum)
      return result
    

    enter image description here

    继续DFS并搜索其他叶链。当发现来自同一节点的两个链时,可能在另一个链(图中的红色和绿色链,前面是蓝色链)之后,处理这些链:

    match2(parent, sum_list1, sum_list2)
    sum_list3 = merge1(parent, sum_list1, sum_list2)
    
    if !chain3.empty:
      match1(chain3)
      match3(sum_list3, chain3)
      sum_list4 = merge2(sum_list3, chain3)
    
    match2(parent, sum_list1, sum_list2):
      i = 0
      j = chain2.length - 1
      sum = target - parent.value
      loop:
        while sum < sum_list1[i] + sum_list2[j]:
          ++i
        while sum > sum_list1[i] + sum_list2[j]:
          --j
        if sum == sum_list1[i] + sum_list2[j]:
          success!
    
    merge1(parent, sum_list1, sum_list2):
      result = [0, parent.value]
      i = j = 1
      loop:
        if sum_list1[i] < sum_list2[j]:
          result.append(parent.value + sum_list1[i])
          ++i
        else:
          result.append(parent.value + sum_list2[j])
          ++j
      return result
    
    match3(sum_list3, chain3):
      i = sum = 0
      j = sum_list3.length - 1
      loop:
        sum += chain3[i++]
        while sum_list3[j] + sum > target:
          --j
        if sum_list3[j] + sum == target:
          success!
    
    merge2(sum_list3, chain3):
      result = [0]
      sum = 0
      i = chain3.length - 1
      loop:
        sum += chain3[i--]
        result.append(sum)
      result.append(sum_list3[1...] + sum)
    

    在任何两个sum列表或链和sum列表都是同一节点的后代的任何地方都这样做。可以继续该过程,直到属于根节点的单个和的列表保持不变。

答案 3 :(得分:1)

是否存在复杂性限制? 正如你所说的那样:“如果一条路径意味着根到叶子,那么很容易,或者如果路径意味着从根到叶子的路径的一部分可能不包括根或叶子”则很容易。 您可以通过每次将根设置为不同的节点并进行n次搜索来将问题减少到此语句。 这将是一种直截了当的方法,不确定是否最佳。

编辑:如果树是单向的,这种类似的东西可能有用(伪代码):

findSum(tree, sum)
    if(isLeaf(tree))
        return (sum == tree->data)
    for (i = 0 to sum)
        isfound |= findSum(leftSubStree, i) && findSum(rightSubTree, sum-i)
    return isfound;

这里可能有很多错误,但希望它澄清了这个想法。