这是S. Skiena的“算法。设计手册”一书中的一个问题,问题陈述是:
给出一个查找有序单词对的算法(例如“纽约”) 在给定网页中以最高频率发生。 你会使用哪种数据结构?优化时间和空间。
一个明显的解决方案是在哈希映射中插入每个有序对,然后迭代所有这些,以找到最常见的一个,但是,肯定应该有更好的方法,任何人都可以提出任何建议吗?
答案 0 :(得分:1)
我认为要注意的第一点是找到最频繁的有序单词对比找到最常用单词更难(或更少)。唯一的区别是,不是由字母a..z + AZ组成的单词由标点符号或空格分隔,而是在寻找由字母a..z + A..Z + exact_one_space组成的单词对,类似用标点符号或空格分隔。
如果您的网页有n个单词,那么只有n-1个单词对。因此,对每个字对进行散列然后在哈希表上进行迭代将在时间和内存中都为O(n)。即使n是~10 ^ 6(即平均小说的长度),这应该很快就能做到。除非n相当小,否则我无法想象任何更有效的东西,在这种情况下,构造有序的字对列表(而不是哈希表)所带来的内存节省可能会超过将时间复杂度增加到O(nlogn)的成本。 )
答案 1 :(得分:0)
为什么不将具有10个元素阵列的所有有序对保留在AVL树中以跟踪前10个有序对。在AVL中,我们将保留所有订单对及其出现的计数,前10位将保留在数组中。这种方式搜索任何有序对将是O(log N)并且遍历将是O(N)。
答案 2 :(得分:0)
在包含n
个单词的文本中,我们有n - 1
个有序的单词对(当然不是唯一的)。一种解决方案是使用最大优先级队列。我们只需将每个对插入频率为1的最大PQ中(如果尚未存在)。如果存在,我们增加密钥。但是,如果使用Trie,则无需分别表示所有n - 1
对。例如以下文本:
纽约的一只新小狗对纽约的生活感到满意。
生成的Trie如下所示:
如果我们在叶节点中存储一对出现的次数,我们可以轻松地计算线性时间中的最大出现次数。由于我们需要查看每个单词,所以这是我们可以做的最好的选择。
下面的工作Scala代码。官方网站上有solution in Python。
class TrieNode(val parent: Option[TrieNode] = None,
val children: MutableMap[Char, TrieNode] = MutableMap.empty,
var n: Int = 0) {
def add(c: Char): TrieNode = {
val child = children.getOrElseUpdate(c, new TrieNode(parent = Some(this)))
child.n += 1
child
}
def letter(node: TrieNode): Char = {
node.parent
.flatMap(_.children.find(_._2 eq node))
.map(_._1)
.getOrElse('\u0000')
}
override def toString: String = {
Iterator
.iterate((ListBuffer.empty[Char], Option(this))) {
case (buffer, node) =>
node
.filter(_.parent.isDefined)
.map(letter)
.foreach(buffer.prepend(_))
(buffer, node.flatMap(_.parent))
}
.dropWhile(_._2.isDefined)
.take(1)
.map(_._1.mkString)
.next()
}
}
def mostCommonPair(text: String): (String, Int) = {
val root = new TrieNode()
@tailrec
def loop(s: String,
mostCommon: TrieNode,
count: Int,
parent: TrieNode): (String, Int) = {
s.split("\\s+", 2) match {
case Array(head, tail @ _*) if head.nonEmpty =>
val word = head.foldLeft(parent)((tn, c) => tn.add(c))
val (common, n, p) =
if (parent eq root) (mostCommon, count, word.add(' '))
else if (word.n > count) (word, word.n, root)
else (mostCommon, count, root)
loop(tail.headOption.getOrElse(""), common, n, p)
case _ => (mostCommon.toString, count)
}
}
loop(text, new TrieNode(), -1, root)
}
受到问题here的启发。
答案 3 :(得分:-1)
我认为我们在时间方面做得不比O(n)好,因为人们必须至少看到每个元素一次。因此,时间复杂性无法进一步优化。
但我们可以使用 trie 来优化使用的空间。在页面中,通常会有重复的单词,因此这可能会导致空间使用量显着减少。 trie cold中的叶节点存储有序对的频率,并使用两个指针迭代文本,其中一个指向当前单词,第二个指向前一个单词。