91

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

私の考えはこうです。

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

  2. (単語、単語-頻度) ペアを並べ替えます。鍵は「単語頻度」です。これには、通常のソート アルゴリズムでは O(n*lg(n)) の時間がかかります。

  3. ソート後、最初の K 語だけを取得します。これには O(K) 時間がかかります。

まとめると、合計時間は O(n+n lg(n)+K) です。K は N よりも小さいので、実際には O(n lg(n)) です。

これを改善できます。実際には、上位 K 個の単語が必要なだけです。他の言葉の頻度は私たちには関係ありません。したがって、「部分ヒープソート」を使用できます。ステップ 2) と 3) では、単に並べ替えを行うだけではありません。代わりに、次のように変更します。

2') 「単語頻度」をキーとして (単語、単語頻度) ペアのヒープを構築します。ヒープを構築するには O(n) 時間かかります。

3') ヒープから上位 K 個の単語を抽出します。各抽出は O(lg(n)) です。したがって、合計時間は O(k*lg(n)) です。

要約すると、このソリューションには O(n+k*lg(n)) の時間がかかります。

これは私の考えです。ステップ1)を改善する方法がわかりません。
情報検索の専門家がこの質問にもっと光を当ててくれることを願っています。

4

19 に答える 19

72

これは 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
    [[ ],[cat],[burger],[like, meow, geek],[]...[foo]]
    
  4. 次に、配列を最後からトラバースし、k 個の単語を収集します。

解決策 2:

手順:

  1. 同上
  2. 最小ヒープを使用し、最小ヒープのサイズを k に維持し、ハッシュ内の各単語について、単語の出現を最小値と比較します。1) 最小値より大きい場合は、最小値を削除します (最小値のサイズが最小値の場合) heap は k) に等しく、その数値を最小ヒープに挿入します。2) 残りの単純な条件。
  3. 配列をトラバースした後、最小ヒープを配列に変換して配列を返します。
于 2014-03-12T03:51:20.437 に答える
22

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

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

于 2008-10-09T07:55:50.017 に答える
14

上位 K のランク付けを気にしない場合は、ソリューションの小さなバリエーションでO(n)アルゴリズムが生成され、そうする場合はO(n+k*lg(k))ソリューションが生成されます。これらの境界は両方とも、一定の係数内で最適であると思います。

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

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

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

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

K 個の要素をランク付けする場合は、O(k * lg(k)) 時間で効率的な比較ソートを使用してそれらをソートするだけで、総実行時間は O(n+k * lg(k)) になります。

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

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

于 2008-12-26T00:54:53.343 に答える
9

If your "big word list" is big enough, you can simply sample and get estimates. Otherwise, I like hash aggregation.

Edit:

By sample I mean choose some subset of pages and calculate the most frequent word in those pages. Provided you select the pages in a reasonable way and select a statistically significant sample, your estimates of the most frequent words should be reasonable.

This approach is really only reasonable if you have so much data that processing it all is just kind of silly. If you only have a few megs, you should be able to tear through the data and calculate an exact answer without breaking a sweat rather than bothering to calculate an estimate.

于 2008-10-09T02:26:35.267 に答える
2

求めているのが、実用的なkおよび自然言語のテキストで最も頻繁に使用されるk 個の単語のリストである場合、アルゴリズムの複雑さは関係ありません。

たとえば、テキストから数百万の単語をサンプリングし、それを任意のアルゴリズムで数秒で処理すると、最も頻繁なカウントが非常に正確になります。

ちなみに、ダミー アルゴリズムの複雑さ (1. すべてをカウントする 2. カウントを並べ替える 3. 最適なものを選択する) は O(n+m*log(m)) です。ここで、m は、文章。log(m) は (n/m) よりもはるかに小さいため、O(n) のままです。

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

于 2015-05-05T22:49:19.987 に答える
2

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

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

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

于 2008-10-09T03:22:34.827 に答える
2

説明にバグがあります。カウントには O(n) 時間がかかりますが、並べ替えには O(m*lg(m)) かかります。ここで、m は一意の単語の数です。これは通常、単語の総数よりもはるかに小さいため、ハッシュの構築方法を最適化する必要があります。

于 2008-12-26T00:33:36.193 に答える
0

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

また、ここに実装があります

于 2015-03-04T13:22:42.367 に答える
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をご覧ください。お役に立てば幸いです。

于 2016-09-25T19:33:26.230 に答える
0

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

  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((nk) * 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 が非常に小さい場合、これは線形時間です。そうですか?本当によくわかりません。
于 2014-02-09T21:28:21.443 に答える
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])
}
于 2015-10-08T12:35:12.490 に答える
0

私もこれに苦労していて、@aly に触発されました。後で並べ替える代わりに、事前に並べ替えられた単語のリスト ( ) を維持するだけでList<Set<String>>、その単語はセット内の位置 X に配置されます。ここで、X は単語の現在のカウントです。一般的には、次のように機能します。

  1. 単語ごとに、その出現のマップの一部として保存します: Map<String, Integer>
  2. 次に、カウントに基づいて、前のカウント セットから削除し、新しいカウント セットに追加します。

これの欠点は、リストが大きくなる可能性があることです.aを使用して最適化できます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);
    }

}
于 2013-10-19T00:26:34.617 に答える
0

「ad」「ad」「boy」「big」「bad」「com」「come」「cold」という単語列があるとします。そしてK=2です。「単語の最初の文字を使用したパーティション分割」とおっしゃったように、("ad", "ad") ("boy", "big", "bad") ("com" "come" "cold") "then k個の単一単語セットになるまで、次の文字を使用して最大の複数単語セットを分割します。」それは ("boy", "big", "bad") ("com" "com" "cold") を分割し、最初のパーティション ("ad", "ad") は失われますが、"ad" は実際には最も多い言葉。

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

于 2008-10-09T11:44:43.427 に答える