复杂度为O(n)的词频


11

在接受Java开发人员职位面试时,有人问我以下问题:

编写一个具有两个参数的函数:

  1. 代表文本文档的字符串,以及
  2. 提供要返回的项目数的整数。

实现函数,使其返回按单词频率排序的字符串列表,最频繁出现的单词在前。您的解决方案应在时间运行,其中是文档中的字符数。nO(n)n

以下是我的回答(用伪代码),由于排序,它不是,而是时间。我不知道该怎么做时间。 O n log n O n O(n)O(nlogn)O(n)

wordFrequencyMap = new HashMap<String, Integer>();
words = inputString.split(' ');

for (String word : words) {
  count = wordFrequencyMap.get(word);
  count = (count == null) ? 1 : ++count;
  wordFrequencyMap.put(word, count);
}

return wordFrequencyMap.sortByValue.keys

有人知道或有人可以给我一些提示吗?


1
使用哈希表。
Yuval Filmus 2014年

使用哈希表不能解决问题。此外,哈希表是旧版Java。
user2712937 2014年

哈希表通常是将复杂度从到的技巧。即使它们是旧版Java,也意味着什么。我尚未检查此特殊情况,因此您可能是正确的。O n O(nlogn)O(n)
Yuval Filmus 2014年

@YuvalFilmus。谢谢,但是哈希表与我已经在使用的哈希图几乎相同(两个数据结构之间的主要区别是同步,此处不适用)。我的log(n)来自对哈希图中的值进行排序。
user2712937 2014年

3
顺便说一句,该站点专注于概念和算法,而不是代码。因此,通常情况下,我们会要求您删除Java代码并给出您的方法的概念性描述(必要时可以使用简洁的高级伪代码)。同样,在该站点上,相关的问题是要使用什么数据结构和算法;特定的Java API对于该站点而言是不合适的(但您可以在StackOverflow上询问它),并且类似地,Hashtable对于该站点的目的而言,是否遗留旧版Java确实无关紧要。
DW

Answers:


10

我建议分配计数的一种变化:

  1. 阅读文本,然后将遇到的所有单词插入到trie中,并在每个节点中维护该节点代表的单词出现的频率。此外,还要跟踪说出的最高字数maxWordCound。-O(n)
  2. 初始化一个size数组maxWordCount。条目类型是字符串列表。-,因为计数不能更高。O(n)
  3. 遍历trie,并为每个节点将相应的字符串添加到由count指示的数组条目中。-,由于串的总长度为界。nO(n)n
  4. 以降序遍历数组并输出所需的字符串数。-,因为这是一个结合在两者的大小和数据的阵列中的量。O(n)

在第一阶段中,您可能会用其他数据结构替换trie。


+1,尽管对此我不确定。因为要返回的单词数以n(字符数)为界,所以它是O(n),但这是问题要问的吗?还是结果与返回的单词数无关?
Nikos M.

@NikosM。它 ; 是返回的单词数的一般最坏情况上限,不是必要的假设。n
拉斐尔

@Raphael,叶氏纠正我想到这一点,因为它被要求在接受记者采访时,可能技巧的问题..
尼科斯·M.

我想知道是否有节省空间的线性时间算法。
saadtaame 2014年

3
@saadtaame,是的,这是一个有趣的问题。可能值得单独发布作为一个单独的问题。不仅仅是空间效率;trie解决方案也是指针密集型的,这可能使其在实践中变慢(考虑到内存层次结构在实际计算机中的工作方式)。“效率”不同于最坏情况下的运行时间。一个干净的时间算法击败一个指针密集型时间算法并不少见,因此这个问题似乎已经排除了一些可能在实践中更好的选择。O n O(nlgn)O(n)
DW

3

发生次数的收集为O(n),因此诀窍实际上只是找到前k个发生次数。

堆是汇总前k个值的常用方法,尽管可以使用其他方法(请参阅https://en.wikipedia.org/wiki/Partial_sorting)。

假设k是上面的第二个参数,并且在问题陈述中它是一个常数(它似乎是):

  1. 在每个节点上建立一个带有出现次数的单词树。
  2. 初始化大小为k的堆。
  3. 遍历trie和min-probe /将每对(叶子,出现次数)对插入top-k堆中。
  4. 输出前k个叶子并计数(实际上这很麻烦,因为您需要父指针将每个叶子映射回一个单词)。

由于堆大小为常数,因此堆操作为O(1),因此步骤3为O(n)。

构建特里树时,还可以动态维护堆。


2

您的算法甚至没有在时间;在哈希表中插入东西已经花费了(最坏的情况)。Θ n Ω n 2O(nlogn)Θ(n)Ω(n2)


接下来是错误的 ; 我暂时将其留在此处仅供说明。

以下算法在最坏情况下的时间(假定字母大小恒定)中运行,即文本中的字符数。Σ ÑO(n)Σn

  1. 构造文本的后缀树,例如使用Ukkonen算法

    如果构造尚未执行此操作,则将可到达的叶子数添加到每个(内部)节点。

  2. 从根开始遍历树,并在第一个(空白)处切断所有分支。

  3. 遍历树,并根据其叶数对每个节点的子级列表进行排序。

  4. 现在,树的产量(从左到右的叶子)是所有单词的列表,按频率排序。

关于运行时:

  1. Ukkonen的算法(以增强形式)在时间;保持叶子数不会增加算法的“成本。ΘO(n)Θ
  2. 我们必须遍历文本中出现的每个单词的每个字符一个节点。由于最多有不同的字-字符对,因此我们最多访问节点。ñnn
  3. 我们最多访问节点(参见2),并花费时间每个节点。ø |&Sigma; |&CenterDot;&日志|&Sigma; |= Ô 1 nO(|Σ|log|Σ|)=O(1)
  4. 我们可以通过时间的简单遍历来获得收益(当然,收益的大小为(参见2)。O n O(n)O(n)

通过使用不同单词数对运行时进行参数化,可以获得更精确的界限。如果很少,则树在2之后变小。


该算法不正确(无法排序)。我不再确定线性时间是否可行。
拉斐尔

1

HashMap1..nO(n)O(n)

O(n)O(n)O(n)

O(n)O(n)


Θ(n)Ω(n2)

我不能代表面试官说话,但我犹豫要用他们的粗鲁来作为借口。另外,此站点是关于科学的(如您自己在上面所述),而不是挥舞着“我将如何尽快获得报酬”的编程技巧。
拉斐尔

只要这种理解是明确的,我就可以了。我在这里看到了太多被混淆的问题,因为一些隐含的“理解”助长了错误的想法。
拉斐尔

0

基于哈希表的解决方案

Ω(n2)n

nΩ(n)

O(1)O(n)O(n2)n

假设哈希算法在时间上相对于字符数是线性的。

基数排序解决方案

O(kN)kNnkO(n)

2nnO(n)

英文中最长的几个单词长得很荒谬,但随后可以将单词的长度限制为一个合理的数字(例如30个或更小),并截断接受可能附带的误差范围的单词。


Θ(n)Θ(n)

O(n+n)O(n2)

(3)无论您选择哪种哈希函数,我都可以提供一个输入,该特定函数会降级。在知道输入之后选择哈希函数通常不是一个选择。(请记住,您可能要说的评论是关于最坏的情况,而不是典型的情况。)
FrankW

O(n2)

O(n2)O(1)Ω(1)O(1)O(1)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.