二叉搜索树的定义中是不是允许重复键?

Posted

技术标签:

【中文标题】二叉搜索树的定义中是不是允许重复键?【英文标题】:Are duplicate keys allowed in the definition of binary search trees?二叉搜索树的定义中是否允许重复键? 【发布时间】:2010-09-22 23:46:16 【问题描述】:

我正在尝试查找二叉搜索树的定义,并且到处都在寻找不同的定义。

有人说对于任何给定的子树,左子键小于或等于根。

有人说对于任何给定的子树,右子键都大于或等于根。

我的旧大学数据结构书说“每个元素都有一个键,没有两个元素有相同的键。”

bst 有一个通用的定义吗?特别是关于如何处理具有相同键的多个实例的树。

编辑:也许我不清楚,我看到的定义是

1) 左

2) 左

3) left

【问题讨论】:

【参考方案1】:

许多算法会指定排除重复项。例如,麻省理工学院算法书中的示例算法通常提供没有重复的示例。实现重复是相当简单的(作为节点上的列表,或在一个特定方向。)

大多数(我见过的)将左孩子指定为。实际上,允许右子节点或左子节点等于根节点的 BST 将需要额外的计算步骤来完成允许重复节点的搜索。

最好在节点上使用列表来存储重复项,因为在节点的一侧插入'='值需要重写该侧的树以将节点放置为子节点,或者放置节点作为一个孙子,在下面的某个点上,这会降低一些搜索效率。

您必须记住,大多数课堂示例都经过简化来描述和传达概念。在许多现实世界的情况下,它们不值得深蹲。但是,“每个元素都有一个键,没有两个元素具有相同的键”这句话并没有因为在元素节点上使用列表而被违反。

那么就按照你的数据结构书所说的去做吧!

编辑:

二叉搜索树的通用定义涉及基于在两个方向之一上遍历数据结构来存储和搜索键。从实用的意义上讲,这意味着如果值为 ,则您将在两个“方向”之一中遍历数据结构。所以,从这个意义上说,重复值根本没有任何意义。

这与 BSP 或二分搜索分区不同,但并非完全不同。搜索算法具有“旅行”的两个方向之一,或者已经完成(成功与否)。所以我很抱歉我的原始答案没有解决“通用定义”的概念,因为重复确实是一个不同的主题(您在成功搜索后处理的内容,而不是作为二分搜索的一部分。)

【讨论】:

在节点使用列表有什么缺点? @Pacerier 我认为我们可以在每个节点上维护一个引用计数,并在发生重复时更新计数,而不是维护一个列表。这样的算法在搜索和存储方面会更容易和有效。此外,它需要对不支持重复的现有算法进行最小的更改。 @SimpleGuy 如果您的意思是参考 list,我可以同意。如果你真的是指一个参考 count (即有多个节点但一个存储计数),我认为那是行不通的。哪个节点将维护计数?如果树需要将该节点重新平衡到较低级别等怎么办?【参考方案2】:

如果你的二叉搜索树是一棵红黑树,或者你打算进行任何类型的“树旋转”操作,重复节点会导致问题。想象一下你的树规则是这样的:

现在想象一个简单的树,它的根为 5,左孩子为 nil,右孩子为 5。如果你对根进行左旋转,你最终会在左孩子中得到 5,在根中得到 5右孩子为零。现在左树中的某些东西等于根,但上面的规则假设左

我花了几个小时试图弄清楚为什么我的红/黑树偶尔会乱序遍历,问题就是我上面描述的。希望有人阅读此内容并在将来节省数小时的调试时间!

【讨论】:

当你有相等的节点时不要旋转!向下遍历到下一个级别并旋转它。 其他解决方案是将树规则更改为left <= node <= right,或者仅在第一次出现值之前插入。 这在实践中会引起什么问题?在我看来,如果你对 left @BjörnLindqvist 正如另一个答案中提到的那样,允许<= <= 的问题在于,您基本上破坏了拥有 BST 的主要目的之一:快速操作您的排序的集合。除非您按照 Rich 所说的去做,或者您只是将所有重复项放到同一个节点中,否则您将不得不向下遍历树的最底部以检查是否存在任何重复项。 @Rich 我对您的解决方案的一个问题是它基本上假设不会有很多很多相同密钥的重复项。如果有,那么你的树最终会非常不平衡。因此,只有在您确定重复不会永远非常普遍的情况下,才应该考虑这种情况。似乎对于通用 BST,使用相同节点的重复是要走的路。【参考方案3】:

所有三个定义都是可以接受和正确的。它们定义了 BST 的不同变体。

您的大学数据结构的书未能阐明它的定义不是唯一可能的。

当然,允许重复会增加复杂性。如果你使用“left

      3
    /   \
  2       4

然后将“3”重复键添加到此树将导致:

      3
    /   \
  2       4
    \
     3

请注意,重复项不在连续级别中。

在上述 BST 表示中允许重复时,这是一个大问题:重复可以由任意数量的级别分隔,因此检查重复的存在并不像检查节点的直接子节点那么简单。

避免此问题的一个选项是不要在结构上表示重复项(作为单独的节点),而是使用一个计数器来计算键的出现次数。前面的例子会有这样的树:

      3(1)
    /     \
  2(1)     4(1)

在插入重复的“3”键后,它将变为:

      3(2)
    /     \
  2(1)     4(1)

这简化了查找、删除和插入操作,但代价是一些额外的字节和计数器操作。

【讨论】:

我很惊讶我正在使用的教科书中从未提及这一点。教授也没有提到它,也没有提到重复键甚至是一个问题...... @OloffBiermann 可能是因为大多数人只是认为,“我们已经介绍了二叉搜索树,./”而没有考虑允许重复的重大影响。也许他们决定如果您了解 BST,那么您可以根据需要进行自己的修改。在他们的辩护中,仅可能的树结构数量就非常庞大。关于它们有很多不同的实现细节。作为初学者,请看这里:en.wikipedia.org/wiki/List_of_data_structures#Trees 真的很喜欢这里的计数器方法,似乎是一个可靠的例子。【参考方案4】:

在 BST 中,从节点左侧下降的所有值都小于(或等于,见后文)节点本身。类似地,在节点右侧下降的所有值都大于(或等于)该节点值(a)

一些 BST 可能选择允许重复值,因此上面的“或等于”限定词。下面的例子可以说明:

     14
    /  \
  13    22
 /     /  \
1    16    29
          /  \
        28    29

这显示了一个允许重复的 BST(b) - 你可以看到要找到一个值,你从根节点开始,根据你的搜索值是否沿着左子树或右子树小于或大于节点值。

这可以通过以下方式递归完成:

def hasVal (node, srchval):
    if node == NULL:
         return false
    if node.val == srchval:
        return true
    if node.val > srchval:
        return hasVal (node.left, srchval)
    return hasVal (node.right, srchval)

并调用它:

foundIt = hasVal (rootNode, valToLookFor)

重复项会增加一点复杂性,因为一旦找到值,您可能需要继续搜索具有相同值的其他节点。显然,这对hasVal 无关紧要,因为有多少并不重要,只要至少存在一个即可。然而,这对于countVal 之类的东西很重要,因为它需要知道有多少。


(a) 如果您希望调整搜索特定键的方式,您可以实际上对它们进行排序。 BST 只需要维护某种排序顺序,无论是升序还是降序(甚至是一些奇怪的多层排序方法,例如所有奇数升序,然后所有偶数降序)都是无关紧要的。


(b) 有趣的是,如果您的排序键使用存储在节点上的 整个 值(因此包含相同键的节点具有 no 其他额外信息来区分它们),可以通过向每个节点添加计数而不是允许重复节点来提高性能。

主要好处是添加或删除重复项只会修改计数,而不是插入或删除新节点(可能需要重新平衡树的操作)。

所以,要添加一个项目,你首先要检查它是否已经存在。如果是这样,只需增加计数并退出。如果没有,则需要插入一个计数为 1 的新节点,然后重新平衡。

删除一个项目,你找到它然后减少计数 - 只有当结果计数为零时,你才会从树中删除实际节点并重新平衡。

由于节点较少,搜索也更快,但这可能影响不大。

例如,以下两棵树(左侧不计数,右侧计数)将等价(在计数树中,i.c 表示 ci 的副本):

     __14__                    ___22.2___
    /      \                  /          \
  14        22             7.1            29.1
 /  \      /  \           /   \          /    \
1    14  22    29      1.1     14.3  28.1      30.1
 \            /  \
  7         28    30

从左树中删除叶节点22 将涉及重新平衡(因为它现在有两个高度差)生成的22-29-28-30 子树如下所示(这是一个选项,还有其他也满足“高差必须为零或一”的规则):

\                      \
 22                     29
   \                   /  \
    29      -->      28    30
   /  \             /
 28    30         22

在右树上执行相同的操作只是将根节点从22.2 修改为22.1(无需重新平衡)。

【讨论】:

对于重复的情况,能否在node.val == srchval: 子句中只检查右子节点是否与当前节点相同,如果是则正确? @bneil 晚了,但是:不,你不能,因为,由于自平衡 BST 重新平衡到中位数以保持子树的合理高度/权重的方式(你不想要一个双向链表),您很容易遇到左孩子、当前节点和右孩子都是相同值的情况,除非您要明确确保例如通过>= 比较,只有一组重复项中最左边的节点可以是父节点(从而确保所有节点都是右子节点);但是,如果有许多相同的重复项,这可能会导致一棵灾难性的树。 @bneil Lazy Ren 的answer 很好地解释了这一点:“......即使 search() 找到了带有 key 的节点,它也必须向下遍历到带有 [the] 的节点的叶子节点重复密钥。”举个例子,在这里取 paxdiablo 的答案中的树并将 28 换成另一个 29。您可以想象他们的孩子中可能还有更多的 29。 duilio 的answer 有另一个很好的例子。【参考方案5】:

在 Cormen、Leiserson、Rivest 和 Stein 所著的第三版“算法简介”一书中,二叉搜索树 (BST) 被明确定义为允许重复。这可以在图 12.1 和以下(第 287 页)中看到:

“二叉搜索树中的键总是以满足二叉搜索树属性的方式存储:让x是二叉搜索树中的一个节点。如果y是一个节点x的左子树,然后y:key <= x:key。如果yx的右子树中的一个节点,那么y:key >= x:key。"

此外,red-black 树在第 308 页上被定义为:

“红黑树是一棵二叉搜索树,每个节点有一个额外的存储位:它的颜色”

因此,本书中定义的红黑树支持重复。

【讨论】:

二叉搜索树没有允许重复,这只是一个选项。它还可能禁止奇数、素数、元音过多的字符串或任何其他类型的数据。唯一真正的要求是以某种方式排序,并且最好自平衡。【参考方案6】:

任何定义都是有效的。只要您在实现中保持一致(始终将相等的节点放在右侧,始终将它们放在左侧,或者永远不允许它们),那么您就可以了。我认为不允许它们是最常见的,但如果它们被允许并且放置在左侧或右侧,它仍然是 BST。

【讨论】:

如果您有一组包含重复键的数据,那么这些项目都应该通过不同的方法(链表等)存储在树上的 1 个节点中。树应该只包含唯一的键。 另外请注意,wiki 中的右子树包含“大于或等于”根的值。因此 wiki 的定义是自相矛盾的。 +1:不同的人使用不同的定义。如果您实施新的 BST,您需要确保明确说明您对重复条目所做的假设。 当允许重复时,似乎共识是(左 不正确!它是左 根 >= 右或左 >= 根 > 右【参考方案7】:

我只是想为@Robert Paulson 的回答添加更多信息。

假设节点包含键和数据。因此,具有相同键的节点可能包含不同的数据。 (所以搜索必须找到所有具有相同键的节点)

    left 兄弟节点。
    left

1 & 2. 如果树没有任何与旋转相关的功能来防止偏斜,则可以正常工作。 但是这种形式不适用于AVL 树红黑树,因为旋转会破坏主体。 并且即使 search() 找到具有键的节点,它也必须向下遍历到具有重复键的节点的叶节点。 使搜索的时间复杂度 = theta(logN) 3. 适用于任何形式的具有旋转相关功能的 BST。 但是搜索会花费 O(n),从而破坏了使用 BST 的目的。 假设我们有如下树,有 3) 主体。

         12
       /    \
     10     20
    /  \    /
   9   11  12 
      /      \
    10       12

如果我们在这棵树上执行 search(12),即使我们在根处找到 12,我们也必须继续搜索左右子节点以查找重复键。 正如我所说,这需要 O(n) 时间。 4.是我个人的最爱。假设 sibling 表示具有相同键的节点。 我们可以把上面的树变成下面的。

         12 - 12 - 12
       /    \
10 - 10     20
    /  \
   9   11

现在任何搜索都将花费 O(logN),因为我们不必遍历子项来查找重复键。 而且这个主体也适用于 AVLRB 树

【讨论】:

这是一个很好的答案。如果可以的话,我会把它标记为答案。 #4 绝对是“正确”的方式。 (P.S. 这里没有提到第 6 种方式,但我在下面的评论中回复了它:***.com/a/339597/1599699)【参考方案8】:

在处理红黑树实现时,我在使用多个键验证树时遇到问题,直到我意识到使用红黑插入旋转时,您必须放松对

的约束

left <= root <= right

由于我查看的所有文档都不允许重复键,并且我不想重写旋转方法来解决它,我只是决定修改我的节点以允许节点内有多个值,并且没有树中的重复键。

【讨论】:

【参考方案9】:

你说的这三件事都是真的。

键是唯一的 左边是小于这个的键 右边是比这个大的键

我想你可以反转你的树并将较小的键放在右边,但实际上“左”和“右”的概念就是这样:一个帮助我们思考数据结构的视觉概念有左有右,所以没关系。

【讨论】:

【参考方案10】:

1.) 左

2.) 左

3.) left

我可能不得不去翻阅我的算法书籍,但我脑海中浮现的 (3) 是规范形式。

(1) 或 (2) 仅在您开始允许重复节点时出现并且您将重复节点放入树本身(而不是包含列表的节点)。

【讨论】:

你能解释一下为什么 left 查看@paxdiablo 接受的答案 - 您可以看到>= 可能存在重复值。 理想 取决于您的要求,但如果您确实有很多重复值,并且您允许重复值存在于结构中,那么您的 bst 最终可能是线性的 - 即 O(n)。 【参考方案11】:

重复键 • 如果有多个数据项 同一把钥匙? – 这在红黑树中提出了一个小问题。 – 重要的是具有相同密钥的节点分布在 其他节点的两侧具有相同的密钥。 – 也就是说,如果密钥按 50、50、50 的顺序到达, • 您希望第二个 50 位于第一个的右侧,并且 第三个 50 到第一个的左边。 • 否则,树会变得不平衡。 • 这可以通过某种随机化来处理 插入算法的过程。 – 然而,搜索过程会变得更加复杂,如果 必须找到具有相同密钥的所有项目。 • 使用相同密钥取缔物品更简单。 – 在本次讨论中,我们假设不允许重复

可以为树的每个节点创建一个包含重复键的链表并将数据存储在链表中。

【讨论】:

【参考方案12】:

元素排序关系total order,所以关系必须是自反的,但通常二叉搜索树(又名BST)是没有重复的树。

否则如果有重复则需要运行两次或多次相同的删除功能!

【讨论】:

以上是关于二叉搜索树的定义中是不是允许重复键?的主要内容,如果未能解决你的问题,请参考以下文章

❤️数据结构入门❤️(2 - 1)- 二叉搜索树

红黑二叉搜索树中的几何搜索

二叉搜索树和最优二叉搜索树的时间复杂度各是多少?

测试树是不是是二叉搜索树[重复]

检查二叉树是不是也是二叉搜索树的问题

PAT天梯赛练习题 L3-010. 是否完全二叉搜索树(完全二叉树的判断)