在大词序列中找到前K个频繁词的最有效方法

时间:2008-10-09 02:24:12

标签: algorithm word-frequency

输入:正整数K和大文本。实际上,文本可以被视为单词序列。因此,我们不必担心如何将其分解为单词序列 输出:文本中最常见的K字。

我的想法是这样的。

  1. 使用哈希表在遍历整个单词序列时记录所有单词的频率。在此阶段,键是“字”,值是“字频”。这需要O(n)时间。

  2. 对(word,word-frequency)对进行排序;关键是“字频”。这需要使用正常排序算法的O(n * lg(n))时间。

  3. 排序后,我们只取第一个K字。这需要O(K)时间。

  4. 总而言之,总时间是O(n + n lg(n)+ K),因为K肯定小于N,所以它实际上是O(n lg(n))

    我们可以改善这一点。实际上,我们只想要前K个词。换句话说,频率对我们来说并不重要。因此,我们可以使用“部分堆排序”。对于步骤2)和3),我们不仅仅进行排序。相反,我们将其更改为

    2')构建一堆(word,word-frequency)对,以“word-frequency”为关键。构建堆需要花费O(n)时间;

    3')从堆中提取前K个单词。每次提取为O(lg(n))。所以,总时间是O(k * lg(n))。

    总而言之,该解决方案花费时间O(n + k * lg(n))。

    这只是我的想法。我还没有找到改进步骤1)的方法 我希望一些信息检索专家能够更好地阐述这个问题。

19 个答案:

答案 0 :(得分:59)

这可以在O(n)时间内完成

解决方案1:

步骤:

  1. 计算单词并对其进行哈希处理,这将最终出现在像

    这样的结构中
    var hash = {
      "I" : 13,
      "like" : 3,
      "meow" : 3,
      "geek" : 3,
      "burger" : 2,
      "cat" : 1,
      "foo" : 100,
      ...
      ...
    
  2. 遍历散列并找到最常用的单词(在本例中为“foo”100),然后创建该大小的数组

  3. 然后我们可以再次遍历哈希并使用单词出现次数作为数组索引,如果索引中没有任何内容,则创建一个数组,否则将其附加到数组中。然后我们得到一个像:

    这样的数组
      0   1      2            3                100
    [[ ],[ ],[burger],[like, meow, geek],[]...[foo]]
    
  4. 然后从最后遍历数组,并收集k个单词。

  5. 解决方案2:

    步骤:

    1. 与上述相同
    2. 使用min heap并将min heap的大小保持为k,并且对于散列中的每个单词,我们将单词的出现与min进行比较,1)如果它大于min值,则删除min(如果大小)最小堆的等于k)并在最小堆中插入数字。 2)休息简单的条件。
    3. 遍历数组后,我们只需将最小堆转换为数组并返回数组。

答案 1 :(得分:21)

你不会比你描述的解决方案获得更好的运行时间。你必须至少做O(n)工作来评估所有的单词,然后O(k)额外的工作来找到前k个术语。

如果您的问题集真的很大,您可以使用分布式解决方案,例如map / reduce。 n个映射工作者在每个文本的1 / n处计算频率,并且对于每个单词,将其发送给基于单词的散列计算的m个reducer工作者中的一个。然后减速器将计数相加。对减速器输出的合并排序将为您提供最流行的单词,以便受欢迎。

答案 2 :(得分:13)

如果我们不关心排名最高的K,并且 O(n + k * lg(k)),解决方案的一个小变化会产生 O(n)算法)解决方案,如果我们这样做。我相信这两个界限都是恒定因子内的最佳界限。

在我们遍历列表并插入哈希表之后,这里再次进行优化。我们可以使用median of medians算法来选择列表中的第K个最大元素。该算法可证明是O(n)。

选择第K个最小元素后,我们就像在quicksort中一样对该元素进行分区。这显然也是O(n)。枢轴“左”侧的任何东西都在我们的K元素组中,所以我们已经完成了(我们可以随便扔掉其他所有东西)。

所以这个策略是:

  1. 浏览每个单词并将其插入哈希表:O(n)
  2. 选择第K个最小元素:O(n)
  3. 围绕该元素的分区:O(n)
  4. 如果要对K个元素进行排名,只需在O(k * lg(k))时间内使用任何有效的比较排序对它们进行排序,得到总运行时间为O(n + k * lg(k))。

    O(n)时间限制在常数因子内是最佳的,因为我们必须至少检查一次每个单词。

    O(n + k * lg(k))时间界限也是最佳的,因为没有基于比较的方式在小于k * lg(k)时间内对k个元素进行排序。

答案 3 :(得分:8)

如果您的“大单词列表”足够大,您可以简单地抽样并获得估算值。否则,我喜欢散列聚合。

修改

通过样本,我的意思是选择一些页面子集并计算这些页面中最常用的单词。如果您以合理的方式选择页面并选择具有统计意义的样本,则您对最常用单词的估计应该是合理的。

如果您拥有如此多的数据来处理所有数据只是一种愚蠢的做法,这种方法真的是合理的。如果你只有几个megs,你应该能够撕掉数据并计算出准确的答案,而不必费心而不是费心去计算估计值。

答案 4 :(得分:2)

您可以使用单词的第一个字母进行分区,然后使用下一个字符对最大的多字集进行分区,直到您有k个单字集,从而进一步缩短时间。您将使用256路树的排序,其中包含叶子上的部分/完整单词列表。您需要非常小心,不要在任何地方造成字符串副本。

该算法为O(m),其中m是字符数。它避免了对k的依赖,这对于大k来说是非常好的[通过你的发布运行时间错误的方式,它应该是O(n * lg(k)),并且我不确定那是什么意思米]。

如果你并排运行两种算法,你会得到我非常确定的渐近最优O(min(m,n * lg(k)))算法,但我的平均速度应该更快,因为它不会不涉及散列或排序。

答案 5 :(得分:2)

  1. 利用内存高效的数据结构来存储单词
  2. 使用MaxHeap,找到最常见的K个词。
  3. 这是代码

    import java.util.ArrayList;
    import java.util.Comparator;
    import java.util.List;
    import java.util.PriorityQueue;
    
    import com.nadeem.app.dsa.adt.Trie;
    import com.nadeem.app.dsa.adt.Trie.TrieEntry;
    import com.nadeem.app.dsa.adt.impl.TrieImpl;
    
    public class TopKFrequentItems {
    
    private int maxSize;
    
    private Trie trie = new TrieImpl();
    private PriorityQueue<TrieEntry> maxHeap;
    
    public TopKFrequentItems(int k) {
        this.maxSize = k;
        this.maxHeap = new PriorityQueue<TrieEntry>(k, maxHeapComparator());
    }
    
    private Comparator<TrieEntry> maxHeapComparator() {
        return new Comparator<TrieEntry>() {
            @Override
            public int compare(TrieEntry o1, TrieEntry o2) {
                return o1.frequency - o2.frequency;
            }           
        };
    }
    
    public void add(String word) {
        this.trie.insert(word);
    }
    
    public List<TopK> getItems() {
    
        for (TrieEntry trieEntry : this.trie.getAll()) {
            if (this.maxHeap.size() < this.maxSize) {
                this.maxHeap.add(trieEntry);
            } else if (this.maxHeap.peek().frequency < trieEntry.frequency) {
                this.maxHeap.remove();
                this.maxHeap.add(trieEntry);
            }
        }
        List<TopK> result = new ArrayList<TopK>();
        for (TrieEntry entry : this.maxHeap) {
            result.add(new TopK(entry));
        }       
        return result;
    }
    
    public static class TopK {
        public String item;
        public int frequency;
    
        public TopK(String item, int frequency) {
            this.item = item;
            this.frequency = frequency;
        }
        public TopK(TrieEntry entry) {
            this(entry.word, entry.frequency);
        }
        @Override
        public String toString() {
            return String.format("TopK [item=%s, frequency=%s]", item, frequency);
        }
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + frequency;
            result = prime * result + ((item == null) ? 0 : item.hashCode());
            return result;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            TopK other = (TopK) obj;
            if (frequency != other.frequency)
                return false;
            if (item == null) {
                if (other.item != null)
                    return false;
            } else if (!item.equals(other.item))
                return false;
            return true;
        }
    
    }   
    

    }

    这是单元测试

    @Test
    public void test() {
        TopKFrequentItems stream = new TopKFrequentItems(2);
    
        stream.add("hell");
        stream.add("hello");
        stream.add("hello");
        stream.add("hello");
        stream.add("hello");
        stream.add("hello");
        stream.add("hero");
        stream.add("hero");
        stream.add("hero");
        stream.add("hello");
        stream.add("hello");
        stream.add("hello");
        stream.add("home");
        stream.add("go");
        stream.add("go");
        assertThat(stream.getItems()).hasSize(2).contains(new TopK("hero", 3), new TopK("hello", 8));
    }
    

    有关详细信息,请参阅this test case

答案 6 :(得分:2)

如果你所追求的是 k 文本中最常见的单词列表,对于任何实际 k 以及任何自然语言,那么你算法的复杂性不相关。

只需示例,例如,您的文字中的几百万字,使用任何算法在几秒钟内处理,并且最常用的计数将非常准确

作为旁注,虚拟算法的复杂性(1.计数全部2.排序计数3.取最佳值)是O(n + m * log(m)),其中m是不同的数量你的文字中的单词。 log(m)远小于(n / m),因此它保持为O(n)。

实际上,漫长的步骤正在计算中。

答案 7 :(得分:2)

你的问题与此相同 -  http://www.geeksforgeeks.org/find-the-k-most-frequent-words-from-a-file/

使用Trie和min heap来有效地解决它。

答案 8 :(得分:2)

您的描述中有一个错误:计数需要O(n)时间,但排序需要O(m * lg(m)),其中m是唯一字数。这通常比单词总数小得多,因此可能应该优化散列的构建方式。

答案 9 :(得分:1)

  1. 使用哈希表在遍历整个单词序列时记录所有单词的频率。在此阶段,键是“字”,值是“字频”。这需要O(n)时间。这与上面解释的每一个相同

  2. 在将自己插入hashmap时,保留Treeset(特定于java,每种语言都有实现),大小为10(k = 10),以保留前10个常用词。直到小于10,继续添加。如果大小等于10,则插入元素大于最小元素,即第一元素。如果是,请删除它并插入新元素

  3. 要限制树集的大小,请参阅this link

答案 10 :(得分:0)

我只是找到了解决这个问题的另一个解决方案。但我不确定这是对的。 解决方案:

  1. 使用哈希表记录所有单词的频率T(n)= O(n)
  2. 选择哈希表的前k个元素,并将它们恢复到一个缓冲区(其空间= k)。 T(n)= O(k)
  3. 每次,首先我们需要找到缓冲区的当前min元素,然后逐个比较缓冲区的min元素和hash表的(n-k)元素。如果哈希表的元素大于缓冲区的这个min元素,则删除当前缓冲区的min,并添加哈希表的元素。因此,每次我们发现缓冲区中的最小值需要T(n)= O(k),并且遍历整个哈希表需要T(n)= O(n-k)。因此,该过程的整个时间复杂度为T(n)= O((n-k)* k)。
  4. 遍历整个哈希表后,结果就在此缓冲区中。
  5. 整个时间复杂度:T(n)= O(n)+ O(k)+ O(kn-k ^ 2)= O(kn + n-k ^ 2 + k)。因为,k一般比n小。因此对于该解决方案,时间复杂度是 T(n)= O(kn)。那是线性时间,当k非常小时。这样对吗?我真的不确定。

答案 11 :(得分:0)

我也在努力解决这个问题,并受到@aly的启发。我们可以只维护一个预先排序的单词列表(List<Set<String>>)而不是后面的排序,单词将位于位置X的集合中,其中X是单词的当前计数。通常,这是它的工作原理:

  1. 对于每个单词,将其存储为其出现地图的一部分:Map<String, Integer>
  2. 然后,根据计数,将其从先前的计数集中删除,并将其添加到新的计数集中。
  3. 这样做的缺点是列表可能很大 - 可以使用TreeMap<Integer, Set<String>>进行优化 - 但这会增加一些开销。最终,我们可以混合使用HashMap或我们自己的数据结构。

    代码

    public class WordFrequencyCounter {
        private static final int WORD_SEPARATOR_MAX = 32; // UNICODE 0000-001F: control chars
        Map<String, MutableCounter> counters = new HashMap<String, MutableCounter>();
        List<Set<String>> reverseCounters = new ArrayList<Set<String>>();
    
        private static class MutableCounter {
            int i = 1;
        }
    
        public List<String> countMostFrequentWords(String text, int max) {
            int lastPosition = 0;
            int length = text.length();
            for (int i = 0; i < length; i++) {
                char c = text.charAt(i);
                if (c <= WORD_SEPARATOR_MAX) {
                    if (i != lastPosition) {
                        String word = text.substring(lastPosition, i);
                        MutableCounter counter = counters.get(word);
                        if (counter == null) {
                            counter = new MutableCounter();
                            counters.put(word, counter);
                        } else {
                            Set<String> strings = reverseCounters.get(counter.i);
                            strings.remove(word);
                            counter.i ++;
                        }
                        addToReverseLookup(counter.i, word);
                    }
                    lastPosition = i + 1;
                }
            }
    
            List<String> ret = new ArrayList<String>();
            int count = 0;
            for (int i = reverseCounters.size() - 1; i >= 0; i--) {
                Set<String> strings = reverseCounters.get(i);
                for (String s : strings) {
                    ret.add(s);
                    System.out.print(s + ":" + i);
                    count++;
                    if (count == max) break;
                }
                if (count == max) break;
            }
            return ret;
        }
    
        private void addToReverseLookup(int count, String word) {
            while (count >= reverseCounters.size()) {
                reverseCounters.add(new HashSet<String>());
            }
            Set<String> strings = reverseCounters.get(count);
            strings.add(word);
        }
    
    }
    

答案 12 :(得分:0)

尝试考虑特殊的数据结构来解决这类问题。在这种情况下,特殊类型的树像trie以特定方式存储字符串,非常有效。或者第二种方法来构建自己的解决方案,比如计算单词我想这个数据TB将是英文的,那么我们一般有大约600,000个单词所以只能存储那些单词并计算哪些字符串会被重复+这个解决方案需要正则表达式来消除一些特殊字符。第一种解决方案会更快,我很确定。

http://en.wikipedia.org/wiki/Trie

答案 13 :(得分:0)

我相信这个问题可以通过O(n)算法来解决。我们可以在运行中进行排序。换句话说,在这种情况下的排序是传统排序问题的子问题,因为每次访问哈希表时只有一个计数器增加1。最初,列表已排序,因为所有计数器都为零。当我们继续在哈希表中递增计数器时,我们预订另一个按频率排序的哈希值数组,如下所示。每次我们递增计数器时,我们都会检查排序数组中的索引,并检查其计数是否超过列表中的前一个。如果是这样,我们交换这两个元素。因此,我们获得的解决方案最多为O(n),其中n是原始文本中的单词数。

答案 14 :(得分:0)

这是一个有趣的搜索想法,我可以找到与Top-K相关的论文https://icmi.cs.ucsb.edu/research/tech_reports/reports/2005-23.pd f

还有一个here的实现。

答案 15 :(得分:0)

最简单的代码,用于获取最常用单词的出现。

 function strOccurence(str){
    var arr = str.split(" ");
    var length = arr.length,temp = {},max; 
    while(length--){
    if(temp[arr[length]] == undefined && arr[length].trim().length > 0)
    {
        temp[arr[length]] = 1;
    }
    else if(arr[length].trim().length > 0)
    {
        temp[arr[length]] = temp[arr[length]] + 1;

    }
}
    console.log(temp);
    var max = [];
    for(i in temp)
    {
        max[temp[i]] = i;
    }
    console.log(max[max.length])
   //if you want second highest
   console.log(max[max.length - 2])
}

答案 16 :(得分:0)

假设我们有一个单词序列“ad”“ad”“boy”“big”“bad”“com”“come”“cold”。并且K = 2。 正如你提到的“使用单词的第一个字母进行分区”,我们得到了 (“ad”,“ad”)(“男孩”,“大”,“坏”)(“com”“来”“冷”) “然后使用下一个字符对最大的多字集进行分区,直到你有k个单字集。” 它将分区(“男孩”,“大”,“坏”)(“com”“来”“冷”),第一个分区(“广告”,“广告”)被遗漏,而“广告”实际上是最常见的词。

也许我误解了你的观点。您能否详细说明您的分区流程?

答案 17 :(得分:0)

在这些情况下,我建议使用Java内置功能。因为,它们已经过良好的测试和稳定。在这个问题中,我通过使用HashMap数据结构找到了单词的重复。然后,我将结果推送到一个对象数组。我通过Arrays.sort()对对象进行排序,并打印前k个单词及其重复。

import java.io.*;
import java.lang.reflect.Array;
import java.util.*;

public class TopKWordsTextFile {

    static class SortObject implements Comparable<SortObject>{

        private String key;
        private int value;

        public SortObject(String key, int value) {
            super();
            this.key = key;
            this.value = value;
        }

        @Override
        public int compareTo(SortObject o) {
            //descending order
            return o.value - this.value;
        }
    }


    public static void main(String[] args) {
        HashMap<String,Integer> hm = new HashMap<>();
        int k = 1;
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("words.in")));

            String line;
            while ((line = br.readLine()) != null) {
                // process the line.
                //System.out.println(line);
                String[] tokens = line.split(" ");
                for(int i=0; i<tokens.length; i++){
                    if(hm.containsKey(tokens[i])){
                        //If the key already exists
                        Integer prev = hm.get(tokens[i]);
                        hm.put(tokens[i],prev+1);
                    }else{
                        //If the key doesn't exist
                        hm.put(tokens[i],1);
                    }
                }
            }
            //Close the input
            br.close();
            //Print all words with their repetitions. You can use 3 for printing top 3 words.
            k = hm.size();
            // Get a set of the entries
            Set set = hm.entrySet();
            // Get an iterator
            Iterator i = set.iterator();
            int index = 0;
            // Display elements
            SortObject[] objects = new SortObject[hm.size()];
            while(i.hasNext()) {
                Map.Entry e = (Map.Entry)i.next();
                //System.out.print("Key: "+e.getKey() + ": ");
                //System.out.println(" Value: "+e.getValue());
                String tempS = (String) e.getKey();
                int tempI = (int) e.getValue();
                objects[index] = new SortObject(tempS,tempI);
                index++;
            }
            System.out.println();
            //Sort the array
            Arrays.sort(objects);
            //Print top k
            for(int j=0; j<k; j++){
                System.out.println(objects[j].key+":"+objects[j].value);
            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

有关详细信息,请访问JSBin。我希望它有所帮助。

答案 18 :(得分:0)

**
  

C ++ 11上述思想的实现

**

class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {

    unordered_map<int,int> map;
    for(int num : nums){
        map[num]++;
    }

    vector<int> res;
    // we use the priority queue, like the max-heap , we will keep (size-k) smallest elements in the queue
    // pair<first, second>: first is frequency,  second is number 
    priority_queue<pair<int,int>> pq; 
    for(auto it = map.begin(); it != map.end(); it++){
        pq.push(make_pair(it->second, it->first));

        // onece the size bigger than size-k, we will pop the value, which is the top k frequent element value 

        if(pq.size() > (int)map.size() - k){
            res.push_back(pq.top().second);
            pq.pop();
        }
    }
    return res;

}

};