給定一個file, 查找出裡面出現頻率最高的10個單詞
之前已經總結了給定一組數字, 如何線上性時間內找到第k小的數字。
這兩個問題看似有十分subtle的關係。 很顯然這裡是找最大的前K個單詞。 單詞相當於衛星資料, 直接對單詞的鍵值, 即頻率排序啦。
現在我們對這個求top K frequent words做一個小小的總結。
方法一: minheap + external sort(即小頂堆 + 外部排序)
之所以使用外部排序, 是因為考慮到檔案很大, 無法一次裝入記憶體中。
首先第一步就是統計單詞的頻率。 例如 “the”, "word"等等單詞。 例如 [1233, "the"], 表示單詞“the”出現了1233次。統計完成之後, 我們開始對單詞的頻率進行排序。 可以使用一個大小為K的minHeap。 一旦有一個單詞的頻率大於minHeap的root(對應著小頂堆中, 關鍵字(即頻率最小(的單詞))), 我們就用這個新加進來的單詞替換掉root, 然後再調整heap以得到滿足heap性質的新的heap。
方法二: min heap + hashmap(C++11中, 對應著unordered_map)
使用雜湊。 首先把所有的單詞一個一個的對映到一個hash table(雜湊表)中。 如果一個單詞已經出現在表中, 就對count加1操作。 最終, 對映完成之後, 我們得到一個具有檔案中所有的單詞的個數的統計資訊。 我們只需要遍歷雜湊表, 返回具有最大的Count的k個單詞即可。
方法三: Trie+ MinHeap(字典樹 + 最小堆)
我還不知道Trie是幹嘛的。 先惡補一下, 其實很簡單。
Trie又被稱為字典樹, 或者稱為字首樹。
為了理解Trie, 首先看看我們可以用Trie幹嘛的呀。
(1)Trie的典型應用是進行詞頻統計的。 通常被搜尋引擎系統用來進行文字詞頻統計的。 優點是利用字串的公共字首來減少查詢時間, 從而減少無謂的字串比較。 查詢效率很高。
不難看出如下的性質:
(1) 根節點不包含任何字元, 或者我們認為是空字元。
(2) 從根節點沿著某一路徑到達某個節點, 路徑上經過的字元連結起來, 就是該節點對應的字串。
(3)每個單詞的公共字首作為一個字元節點儲存。
(4)詞頻統計的時候, 葉子節點的鍵值為這個單詞, 而對應的衛星資料是該word的頻率統計。
使用Trie進行詞頻統計的好處就是非常的節省記憶體,這也是Trie較之於hash或者一個heap進行詞頻統計的優點所在。 因為公共字首都是儲存在Trie的一個節點中的。
(2)字串串排序。 給定N個互不相同的僅由一個單詞構成的英文名。 讓你將他們按照字典序從小到達輸出。
我們可以利用字典樹進行排序, 採用陣列的方式建立字典樹。 這個樹的每個節點的所有兒子要按照其字母從小到達的順序進行排序。 然後我們對這個樹進行前序遍歷即可。
(3)最長公共字首。 對所有的字串建立字典樹。
對所有串建立字典樹,對於任意兩個串的最長公共字首的長度即他們所在的結點的公共祖先個數,於是,問題就轉化為當時公共祖先問題。 我們只需要求出所有字串在字典樹的公共祖先即可。
下面編寫Trie:
一個Trie節點:
首先, 一個Trie節點應該包含:
(1)指向record的指標, record 對應著這個節點的(單詞, 頻率)對。
(3)需要一個資訊去標記到這個Trie的節點了, 是否可以結束分支了。 因為給定的是字串, 我們必須知道一個單詞的結束的位置, 例如空格等。
(2) Trie 是多叉樹。
參考如下程式:
#include <iostream>
#include <vector>
using namespace std;
class Node { // 節點類
public:
// 建構子, 該節點預設的建構子內容為空字元, 不是單詞的結束
Node() { mContent = ' '; mMarker = false; }
// 解構函式
~Node() {}
// 返回這個節點的內容
char content() { return mContent; }
// 設定這個節點的內容
void setContent(char c) { mContent = c; }
// 這個節點是否是這個單詞的結束標誌
bool wordMarker() { return mMarker; }
// 設定當前的節點為單詞結束標誌
void setWordMarker() { mMarker = true; }
// 給定字元c, 找到這個字元對應當前節點的的孩子節點
Node* findChild(char c);
// 將一個節點作為當前節點的孩子節點, append上
void appendChild(Node* child) { mChildren.push_back(child); }
// 放回當前節點的所有孩子節點
vector<Node*> children() { return mChildren; }
private:
char mContent; // 節點的字元
bool mMarker; // 該節點是否為單詞的結束位置
vector<Node*> mChildren; // 該節點的孩子, 是vector of nodes(為這個節點的孩子)
};
// 字典樹的類
class Trie {
public:
Trie();
~Trie();
// 新增一個單詞到孩子節點
void addWord(string s);
//給定字元s, 查詢當前的子點數中是否有這個單詞
bool searchWord(string s);
// 給定單詞, 刪除當前字典樹中的這個單詞
void deleteWord(string s);
private:
// 字典樹的根節點
Node* root;
};
Node* Node::findChild(char c)
{
// 檢查當前節點的孩子節點是否有字元c, 若有, 則返回這個節點
for ( int i = 0; i < mChildren.size(); i++ )
{
Node* tmp = mChildren.at(i);
if ( tmp->content() == c )
{
return tmp;
}
}
// 在當前節點的孩子孩子中沒找到, 返回NULL
return NULL;
}
Trie::Trie()
{
root = new Node();
}
Trie::~Trie()
{
// Free memory
}
void Trie::addWord(string s)
{
Node* current = root;
//插入的字元為空字元, 直接把當前的(根節點)設定為
if ( s.length() == 0 )
{
current->setWordMarker(); // an empty word
return;
}
for ( int i = 0; i < s.length(); i++ )
{
Node* child = current->findChild(s[i]);
if ( child != NULL )
{
// 找到了這個節點的位置
current = child;
}
else
{
// 沒有找到到, 則建立
Node* tmp = new Node();
tmp->setContent(s[i]);
// 將這個節點設定為孩子節點
current->appendChild(tmp);
current = tmp;
}
// 最後一個字元設定這裡的單詞結束標誌
if ( i == s.length() - 1 )
current->setWordMarker();
}
}
bool Trie::searchWord(string s)
{
Node* current = root;
while ( current != NULL )
{
for ( int i = 0; i < s.length(); i++ )
{
Node* tmp = current->findChild(s[i]);
if ( tmp == NULL )
return false;
current = tmp;
}
if ( current->wordMarker() )
return true;
else
return false;
}
return false;
}
// Test program
int main()
{
Trie* trie = new Trie();
trie->addWord("Hello");
trie->addWord("Balloon");
trie->addWord("Ball");
if ( trie->searchWord("Hell") )
cout << "Found Hell" << endl;
if ( trie->searchWord("Hello") )
cout << "Found Hello" << endl;
if ( trie->searchWord("Helloo") )
cout << "Found Helloo" << endl;
if ( trie->searchWord("Ball") )
cout << "Found Ball" << endl;
if ( trie->searchWord("Balloon") )
cout << "Found Balloon" << endl;
delete trie;
}
執行如下: