三叉树 vs 哈希表

14
我需要知道三叉树是否比哈希表更好。
我在另一个问题的回复中看到了这个问题,有人说三叉树通常比哈希表快。我觉得很难相信,所以我决定进行一些研究。 普林斯顿大学的这个网站似乎是这种信仰的来源。我看了一下算法,它被描述为O(log n + k),其中n是存储的单词数,k是键的长度。
在我看来,唯一可能更快的方法是如果你经常搜索尚未存储的元素。还有一件事让我感到困扰,那就是trie的非连续爬行会导致您命中已被交换的页面,但这是否是主要影响只能通过基准测试来确定。
现在我知道两者可能都有优缺点,如果是这样,我想知道它们是什么。基准测试也很有用。

鉴于上下文,你几乎可以肯定要比较具有“纯函数特性”的树和哈希表。命令式平衡树可以用数组的术语来表达,因此它们比纯函数对应物(也就是Haskell中所提供的)更具有优势。 - J D
5个回答

7

从普林斯顿提供的链接中可以看到Dr. Dobbs Article,以下是我的总结:

  1. 在某些搜索问题上,三叉查找树比哈希表快10%。它们有时会更慢 - 这取决于使用的机器。
  2. TRTs是一种自定义数据结构,由计算机科学界最杰出的两个人物Jon Bentley和Robert Sedgewick调整,他们都写了好的教材,并且在实际编程中做出了自己的贡献。哈希表被认为是平庸的。
  3. 所涉及的常数很重要,正如Hao Wooi Lin所说。
  4. 总体而言,这取决于您正在解决的问题。在许多编程语言中,更快的开发时间和几乎无处不在的哈希表支持通常比运行时间提高10%更重要。

4

回答这个问题的唯一方法是经验主义。答案取决于您的实现细节,您进行的搜索类型,您拥有的硬件以及您使用的编译器。您可以从普林斯顿大学复制C代码。如果您想尝试一种函数式语言,标准ML具有哈希表(请查看SML/NJ),这里是一些用于三叉搜索树的ML代码:

type key = Key.ord_key
type item = Key.ord_key list

datatype set = NODE of { key : key, lt : set, eq : set, gt : set }
             | LEAF

val empty = LEAF

fun member (_, LEAF) = false
  | member (h::t, NODE {key, lt, eq, gt}) =
      (case Key.compare (h, key)
         of EQUAL   => member(t, eq)
          | LESS    => member(h::t, lt)
          | GREATER => member(h::t, gt))
  | member ([], NODE {key, lt, eq, gt}) =
      (case Key.compare (Key.sentinel, key)
         of EQUAL   => true
          | LESS    => member([], lt)
          | GREATER => member([], gt))

exception AlreadyPresent

fun insert(h::t, LEAF) =
      NODE { key = h, eq = insert(t, LEAF), lt = LEAF, gt = LEAF }
  | insert([], LEAF) =
      NODE { key = Key.sentinel, eq = LEAF, lt = LEAF, gt = LEAF }
  | insert(h::t, NODE {key, lt, eq, gt}) =
      (case Key.compare (h, key)
         of EQUAL   => NODE {key = key, lt = lt, gt = gt, eq = insert(t, eq)}
          | LESS    => NODE {key = key, lt = insert(h::t, lt), gt = gt, eq = eq}
          | GREATER => NODE {key = key, lt = lt, gt = insert(h::t, gt), eq = eq})
  | insert([], NODE {key, lt, eq, gt}) =
      (case Key.compare (Key.sentinel, key)
         of EQUAL   => raise AlreadyPresent
          | LESS    => NODE {key = key, lt = insert([], lt), gt = gt, eq = eq}
          | GREATER => NODE {key = key, lt = lt, gt = insert([], gt), eq = eq})

fun add(l, n) = insert(l, n) handle AlreadyPresent => n

开放地址哈希表通常比闭合地址哈希表快3-18倍,并且作为结果是.NET上默认的哈希表实现。然而,据我所知,今天的OCaml、Standard ML或Haskell的任何实现都无法表达这种数据结构。 - J D
1
@Jon:如果你所说的“开放寻址”是指维基百科中的含义(每个桶保存一个值而不是指向单独链表的指针),那么这在任何ML方言中都很容易实现。有点繁琐且无法用ML表达的是,你可能希望数组元素是未装箱的——这是效率考虑的重要因素。未装箱的数组不能用标准Haskell表示,但可以使用GHC扩展表示。然而,我不是如何使用此功能与可变性(使用IO单子)的专家。 - Norman Ramsey
在未装箱的数组中,可变性并不是问题,而是 GHC 垃圾收集器中的性能缺陷。他们最近为一个长期存在的 bug 添加了一种解决方法,但写入单个元素仍然是 O(n),因此填充哈希表仍然是 O(n^2) 在 Haskell 中。昔日的 FPL 实现对此真的很糟糕。如果你想客观地比较哈希表,那么就必须避免使用它们。 - J D
2
@Jon:多么令人沮丧啊。我应该使用哪个库模块来暴露这个 bug?如果填充哈希表的复杂度是 O(n^2),那么语言实现者应该受到公开谴责。具有讽刺意味的是,由于表动态增长时存在一个 bug,Objective Caml 哈希表在 多年 内的时间复杂度都是 O(n^2)。你不会相信需要多少抱怨声才能解决这个问题——我想 Xavier 不喜欢我的补丁。简单的数据结构难以正确处理,真是太神奇了。 - Norman Ramsey
哇,这真是令人沮丧。听起来Haskell现在也正在经历同样的成长烦恼。 GHC 6.10的Data.Hashtable出现了这个错误。查看shootout中基于malloc的k-nucleotide解决方案以获取解决方法。我真正喜欢F#的一件事是微软接受了我所有的建议,并采取行动或给出了一个很好的理由不这样做。在HLVM中,我没有遇到任何困难来正确使用哈希表,所以这些PL专家没有借口!顺便说一下,是否有可能推出新的开源FPL实现,像F#一样解决这些问题? - J D

1

log(n) 增长速度缓慢,因此在考虑常数因子时,通常需要大量数据才能比 O(1) 算法慢。


2
这并不是__那么__巨大。如果你将O(1)视为O(k),其中k是键的长度,那么如果你有k = 10,则仅需要1025个元素才能使log(n)二叉树变慢。对于三叉分支树,大约需要60000个元素,这很大,但不足以阻止它发生。 - Unknown
@Unknown:你假设常数因子相等,但实际上它们并不相等。在实践中,哈希表比树甚至在比这个小得多的大小时都更快。例如,在 .NET 4 上使用 F#,这里一个纯函数的 Set 只有在集合元素小于3个时才比 .NET 的 HashSet 更快。 - J D

0

这对我来说也很有趣。但是从我读的维基百科上看,三叉树在大多数搜索问题上都更快。这并不奇怪,因为即使哈希表具有O(1)的查找,你仍然需要时间进行哈希。所以它并不是真正的O(1),而更像是O(k),其中k不取决于数据结构中的N(项数)。这可能会给人留下哈希表更快的印象。然而,如果你处理的是大型结构,k很快就会累加,到了某个点,哈希表的搜索速度会变得比三叉树慢。


但问题是三叉树也依赖于k,特别是如果元素在树中。很多时候,我使用哈希表,其中N(项目数量)比k(键的长度)要大得多。 - Unknown

0
你可以看一下 tstdb:http://code.google.com/p/tstdb/ 这个 kv-store 基于三叉搜索树,与 Memcached 兼容。此外,tstdb 还支持前缀搜索和范围查询,这些功能都是由三叉搜索树实现的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接