在一个大的单词序列中查找前K个高频词的最有效方法

93

输入: 一个正整数K和一段大文本。该文本实际上可以看作是单词序列。因此我们不必担心如何将其分解为单词序列。
输出: 文本中出现频率最高的K个单词。

我的思路如下:

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

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

  3. 排序后,我们只需提取前K个单词。这需要O(K)时间。

总结一下,总时间复杂度为O(n+n*lg(n)+K),由于K肯定小于N,因此实际上是O(n*lg(n))。

我们可以改进这个算法。实际上,我们只想要前K个单词。其他单词的频率与我们无关。因此,我们可以使用“部分堆排序”。对于步骤2)和3),我们不仅要进行排序,而是将它们改变为:

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

3')从堆中提取前K个单词。每次提取的复杂度为O(lg(n))。因此,总时间复杂度为O(k*lg(n))。

总结一下,这个解决方案的时间复杂度为O(n+k*lg(n))。

这只是我的想法。我还没有找到改进步骤1)的方法。
希望一些信息检索专家能够对这个问题进行更深入的探讨。


你会使用归并排序还是快速排序来进行O(n*logn)的排序? - committedandroider
1
对于实际应用而言,Aaron Maenpaa的回答中的样本计数方法是最好的选择。这不像“最常见”的单词会从你的样本中隐藏起来。对于复杂度极客而言,它的时间复杂度为O(1),因为样本的大小是固定的。你得不到精确的计数,但你也没有要求它们。 - Nikana Reklawyks
如果您想要对复杂性分析进行审查,那么我最好提一下:如果n是文本中单词的数量,而m是不同单词(我们称之为类型)的数量,则步骤1的时间复杂度为O(n),但步骤2的时间复杂度为O(m.lg(m)),且m << n(您可能有数十亿个单词,但不到一百万个类型,请尝试一下)。因此,即使使用虚拟算法,它仍然是O(n + m lg(m)) = O(n)。 - Nikana Reklawyks
1
请在问题中添加一个假设,即我们有足够的主内存来容纳大文本中的所有单词。从10GB文件中查找k = 100个单词的方法将非常有趣(即所有单词都无法适合4GB RAM)! - KGhatak
我投票关闭此问题,因为它没有提出一个问题。 - TylerH
显示剩余2条评论
19个回答

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])
}

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;

}

};


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();
        }
    }

}

更多信息,请访问https://github.com/m-vahidalizadeh/foundations/blob/master/src/algorithms/TopKWordsTextFile.java。希望能对您有所帮助。


这种方法在哪些方面改进了问题中概述的方法?(请勿忽略SE上所呈现的代码注释。)(我建议使用Java内置功能,例如foreach循环流处理?) - greybeard
正如您所知,设计高效算法的最重要因素之一是选择正确的数据结构。然后,解决问题的方法也很重要。例如,您需要通过分治法来解决一个问题,需要通过贪心算法来解决另一个问题。正如您所知,Oracle公司正在开发Java。他们是世界上最好的技术公司之一。有一些最杰出的工程师在那里开发Java内置功能。因此,这些功能经过了充分测试和验证。如果我们能够利用它们,在我看来最好使用它们。 - Mohammad

0
尝试思考特殊的数据结构来处理这种问题。在这种情况下,使用像trie这样的特殊树来以特定的方式存储字符串非常高效。或者第二种方法是建立自己的解决方案,比如计数单词。我猜这个TB的数据会是英文,那么一般有大约60万个单词,所以只需要存储那些重复出现的字符串并计数,同时这个解决方案需要使用正则表达式来消除一些特殊字符。第一个解决方案会更快,我很确定。

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


0

我刚刚找到了另一种解决这个问题的方法。但我不确定它是否正确。 解决方案:

  1. 使用哈希表记录所有单词的频率 T(n) = O(n)
  2. 选择哈希表的前k个元素,并将它们存储在一个缓冲区中(其空间为k)。T(n) = O(k)
  3. 每次,首先需要找到当前缓冲区的最小元素,然后将缓冲区的最小元素与哈希表的(n-k)个元素逐一比较。如果哈希表的元素大于缓冲区的这个最小元素,则删除当前缓冲区的最小元素,并添加哈希表的元素。因此,每次在缓冲区中找到最小元素需要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非常小时,这是线性时间。这样说对吗?我真的不确定。

0

您的链接返回404。 - mbdev

0
假设我们有一个单词序列 "ad" "ad" "boy" "big" "bad" "com" "come" "cold",并且 K=2。根据您提到的“使用单词的第一个字母进行分区”,我们得到了 ("ad", "ad") ("boy", "big", "bad") ("com" "come" "cold")。"然后使用下一个字符对最大的多个单词集进行分区,直到你有k个单个单词集。"这将会将("boy", "big", "bad") ("com" "come" "cold") 进行分区,第一个分区("ad", "ad") 被忽略了,而 "ad" 实际上是最常见的单词。
也许我误解了您的意思。您能详细说明一下您分区的过程吗?

0

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


这通常是一个好的方向,但它有一个缺陷。当计数增加时,我们不仅需要检查“它的前任”,而且还需要检查“前任们”。例如,数组很可能是[4,3,1,1,1,1,1,1,1,1,1] - 1的数量可能很多 - 这将使它不那么高效,因为我们必须回顾所有的前任才能找到适当的交换对象。 - Shawn
这难道不比O(n)更糟糕吗?更像是O(n^2),因为它本质上是一种相当低效的排序方法? - dcarr622
嗨,肖恩。是的,我同意你的观点。但我怀疑你提到的问题是这个问题的根本所在。实际上,如果我们不仅保留一个排序后的值数组,而是保留一个(值,索引)对数组,其中索引指向重复元素的第一个出现位置,那么这个问题应该可以在O(n)时间内解决。例如,[4,3,1,1,1,1,1,1,1,1,1]将变成[(4,0),(3,1),(1,2),(1,2),(1,2),...,(1,2)];索引从0开始。 - Aly Farahat

0

我也曾经为此苦恼,受到了@aly的启发。我们可以维护一个预排序的单词列表(List<Set<String>>),并且单词将在集合中的位置X处,其中X是当前单词的计数。一般来说,它的工作原理如下:

  1. 对于每个单词,将其存储为其出现次数的映射的一部分:Map<String, Integer>
  2. 然后,根据计数,从先前的计数集中删除它,并将其添加到新的计数集中。

这种方法的缺点是列表可能很大 - 可以通过使用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);
    }

}

是的,我们可以使用TreeMap而不是列表,它将具有键(数字)列表和值ArrayList的列表。我们可以使用首选项为顶部的降序来提取值。 - Sam Berchmans

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