web-dev-qa-db-ja.com

ビッグワードシーケンスで上位Kの頻出ワードを見つける最も効率的な方法

入力:正の整数Kと大きなテキスト。テキストは実際にはWordシーケンスとして表示できます。したがって、Wordシーケンスに分解する方法について心配する必要はありません。
出力:テキスト内で最も頻繁に使用されるK語。

私の考えはこんな感じです。

  1. ハッシュテーブルを使用して、Wordシーケンス全体を走査しながら、すべてのワードの頻度を記録します。このフェーズでは、キーは「Word」で、値は「Word-frequency」です。これにはO(n)時間かかります。

  2. (Word、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)を改善する方法を見つけていません。

77
Morgan Cheng

これはO(n) timeで実行できます

解決策1:

手順:

  1. 単語を数えてハッシュ化すると、このような構造になります

    var hash = {
      "I" : 13,
      "like" : 3,
      "meow" : 3,
      "geek" : 3,
      "burger" : 2,
      "cat" : 1,
      "foo" : 100,
      ...
      ...
    
  2. ハッシュを走査して、最も頻繁に使用されるWord(この場合は「foo」100)を見つけ、そのサイズの配列を作成します

  3. 次に、ハッシュを再度トラバースし、単語の出現回数を配列インデックスとして使用できます。インデックスに何もない場合は、配列を作成し、配列に追加します。次に、次のような配列になります。

      0   1      2            3                100
    [[ ],[ ],[burger],[like, meow, geek],[]...[foo]]
    
  4. 次に、配列を最後から走査して、k個の単語を収集します。

解決策2:

手順:

  1. 同上
  2. 最小ヒープを使用し、最小ヒープのサイズをkに維持し、ハッシュ内の各単語について、単語の出現を最小と比較します。1)最小値よりも大きい場合は、最小値を削除します(最小サイズの場合ヒープはk)に等しく、最小ヒープに数値を挿入します。 2)単純な条件を休ませる。
  3. 配列を走査した後、最小ヒープを配列に変換し、配列を返します。
61
Chihung Yu

一般的に、説明したソリューションよりも優れたランタイムは得られません。少なくともO(n)すべての単語を評価する作業を行ってから、O(k)上位k個の用語を見つけるための追加作業を行う必要があります。

問題セットがreally bigの場合、map/reduceなどの分散ソリューションを使用できます。 n個のマップワーカーがそれぞれテキストの1/nの頻度をカウントし、Wordごとに、Wordのハッシュに基づいて計算されたm個のリデューサーワーカーの1つに送信します。次に、リデューサーはカウントを合計します。レデューサーの出力を並べ替えると、最も人気のある単語が人気順に表示されます。

22
Nick Johnson

ソリューションのわずかな変動により、上位Kのランク付けを気にしない場合、O(n)アルゴリズムが生成され、O(n + k * lg(k))解決する場合。これらの境界は両方とも一定の要因内で最適であると思います。

ここでの最適化は、リストを実行し、ハッシュテーブルに挿入した後に再び行われます。 medianの中央値 アルゴリズムを使用して、リスト内でK番目に大きい要素を選択できます。このアルゴリズムは、おそらくO(n)です。

K番目に小さい要素を選択した後、クイックソートの場合と同様に、その要素の周りにリストを分割します。これも明らかにO(n)です。ピボットの「左側」にあるものはすべてK要素のグループに含まれているので、完了です(他のすべてを単純に破棄できます)。

したがって、この戦略は次のとおりです。

  1. 各Wordを調べて、ハッシュテーブルに挿入します:O(n)
  2. K番目に小さい要素を選択します:O(n)
  3. その要素の周りのパーティション:O(n)

K要素をランク付けする場合は、O(k * lg(k))時間で効率的な比較並べ替えを使用して単純に並べ替え、O(n + k * lg(k))の合計実行時間を生成します。

O(n)時間制限は、各単語を少なくとも1回検査する必要があるため、定数因子内で最適です。

O(n + k * lg(k))時間制限も最適です。k* lg(k)時間未満でk要素をソートする比較ベースの方法がないためです。

13
Andrew

「大きな単語リスト」が十分に大きい場合は、単純にサンプリングして推定値を取得できます。そうでなければ、ハッシュ集計が好きです。

編集

サンプルでは、​​ページのサブセットを選択し、それらのページで最も頻繁に使用されるWordを計算することを意味します。妥当な方法でページを選択し、統計的に有意なサンプルを選択した場合、最も頻繁に使用される単語の推定値は妥当なものになります。

この方法は、処理するデータがあまりにも多くてばかげている場合にのみ、本当に合理的です。数メガしか持っていない場合、推定値を計算するのではなく、データを切り抜けて、汗をかくことなく正確な答えを計算できるはずです。

8
Aaron Maenpaa
  1. メモリ効率の良いデータ構造を利用して単語を保存する
  2. MaxHeapを使用して、上位Kの頻出単語を見つけます。

ここにコードがあります

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

詳細については、 このテストケース を参照してください。

2
craftsmannadeem

あなたの問題はこれと同じです http://www.geeksforgeeks.org/find-the-k-most-frequent-words-from-a-file/

Trieと最小ヒープを使用して、効率的に解決します。

2
Jitendra Rathor

説明にバグがあります:カウントにはO(n)時間かかりますが、ソートにはO(m * lg(m))がかかります。mはuniqueワードこれは通常、ワードの合計数よりもはるかに少ないため、おそらくハッシュの作成方法を最適化する必要があります。

2
martinus

必要なものがkのリストである場合、実用的なkそして自然言語については、アルゴリズムの複雑さは関係ありません。

ちょうどsample、たとえばテキストから数百万語、数秒で任意のアルゴリズムで処理します、そして最も頻繁なカウントは非常に正確です。

副次的な注意事項として、ダミーアルゴリズムの複雑さ(1.すべてをカウント2.カウントを並べ替え3.最良の結果を得る)はO(n + m * log(m))です。ここで、mはユーザーのさまざまな単語の数テキスト。 log(m)は(n/m)よりもはるかに小さいため、O(n)のままです。

実際には、長いステップがカウントされます。

2

単語の最初の文字を使用して分割し、次の文字を使用して最大の複数単語セットを分割して、k個の単一単語セットになるまでさらに時間を短縮できます。葉に部分的/完全な単語のリストを含む256種類のツリーを使用します。どこでも文字列のコピーが発生しないように、非常に注意する必要があります。

このアルゴリズムはO(m)です。ここで、mは文字数です。 kへの依存を回避します。これは、大きなkに対して非常にいいです[投稿された実行時間が間違っているため、O(n * lg(k))である必要があります。 m]。

両方のアルゴリズムを並べて実行すると、漸近的に最適なO(min(m、n * lg(k)))アルゴリズムであると確信できるものが得られますが、それは含まれていないため、平均で高速になるはずですハッシュまたはソート。

2
Strilanc
  1. ハッシュテーブルを使用して、Wordシーケンス全体を走査しながら、すべてのワードの頻度を記録します。このフェーズでは、キーは「Word」で、値は「Word-frequency」です。これにはO(n)時間かかります。これは上記で説明したものと同じです。

  2. Hashmapに自分自身を挿入する間、上位10個の頻出語を保持するために、サイズ10(k = 10)のTreeset(Java固有、すべての言語の実装があります)を保持します。サイズが10未満になるまで、追加し続けます。サイズが10に等しい場合、挿入された要素が最小要素、つまり最初の要素より大きい場合。はいの場合、削除して新しい要素を挿入します

ツリーセットのサイズを制限するには、 このリンク を参照してください

1
M Sach

この問題の他の解決策を見つけました。しかし、それが正しいかどうかはわかりません。溶液:

  1. ハッシュテーブルを使用して、すべての単語の頻度を記録するT(n) = O(n)
  2. ハッシュテーブルの最初のk個の要素を選択し、それらを1つのバッファー(スペース= k)に復元します。 T(n) = O(k)
  3. 毎回、最初にバッファの現在の最小要素を見つけ、バッファの最小要素とハッシュテーブルの(n-k)要素を1つずつ比較する必要があります。ハッシュテーブルの要素がバッファのこの最小要素より大きい場合、現在のバッファの最小値を削除し、ハッシュテーブルの要素を追加します。したがって、バッファに最小値が必要になるたびに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
zproject89

私もこれに苦労し、@ alyに触発されました。後でソートする代わりに、事前にソートされた単語のリスト(List<Set<String>>)そして、WordはXの現在のカウントである位置Xのセットになります。一般に、次のように機能します。

  1. 各Wordについて、その発生のマップの一部として保存します: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);
    }

}
0
Shawn

これは検索する興味深いアイデアであり、Top-Kに関連するこの論文を見つけることができました https://icmi.cs.ucsb.edu/research/tech_reports/reports/2005-23.pd f

また、その実装もあります here

0
Anayag

この種の問題に取り組むための特別なデータ構造を考えてみてください。この場合、特定の方法で文字列を保存するトライのような特別な種類のツリーは、非常に効率的です。または、単語を数えるような独自のソリューションを構築する2番目の方法。私はこのTBのデータは英語であると推測し、一般的に約600,000語があるので、それらの単語のみを保存し、どの文字列が繰り返されるかを数えることができます+このソリューションはいくつかの特殊文字を削除するために正規表現が必要です。最初の解決策はより高速になります。

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

0
blueberry0xff

最も頻繁に使用されるWordの出現を取得する最も単純なコード。

 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
ngLover
**

上記の考え方の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
asad_nitp

これらの状況では、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 をご覧ください。役に立てば幸いです。

0
Vahid

この問題は、O(n)アルゴリズムで解決できると思います。その場でソートを行うことができます。つまり、その場合のソートは、従来のサブ問題です。ハッシュテーブルにアクセスするたびに1つだけ増分されるため、ソートの問題が発生します。最初は、すべてのカウンターがゼロであるため、リストがソートされます。カウンターをインクリメントするたびに、ランク付けされた配列のインデックスをチェックし、そのカウントがリスト内の先行するものを超えているかどうかを確認し、そうであれば、これらの2つの要素を交換します。 O(n)ここで、nは元のテキストの単語数です。

0
Aly Farahat

Wordシーケンス "ad" "ad" "boy" "big" "bad" "com" "come" "cold"があるとします。そして、K = 2。 「単語の最初の文字を使用したパーティション分割」に言及したように、( "ad"、 "ad")( "boy"、 "big"、 "bad")( "com" "come" "cold") "then k個の単一単語セットになるまで、次の文字を使用して最大の複数単語セットをパーティション分割します。」パーティション(「boy」、「big」、「bad」)(「com」「come」「cold」)、最初のパーティション(「ad」、「ad」)は見逃されますが、「ad」は実際には最も頻繁なWord。

おそらく私はあなたの主張を誤解しています。パーティションに関するプロセスを詳しく説明していただけますか?

0
Morgan Cheng