本章闡述尋找最小的k個數的反面,即尋找最大的k個數,儘管尋找最大的k個樹和尋找最小的k個數,本質上是同樣的。但這個尋找最大的k個數的問題的實用範圍更廣,由於它牽扯到了一個Top K算法問題,以及有關搜索引擎,海量數據處理等普遍的問題,因此本文特地對這個Top K算法問題,進行闡述以及實現。node
一:尋找最大的k個數面試
把以前第三章的問題,改幾個字,即成爲尋找最大的k個數的問題了,以下所述:算法
題目描述:api
輸入n個整數,輸出其中最大的k個。數組
例如輸入1,2,3,4,5,6,7和8這8個數字,則最大的4個數字爲8,7,6和5。數據結構
分析:併發
因爲尋找最大的k個數的問題與以前的尋找最小的k個數的問題,本質是同樣的,因此,這裏就簡單闡述下一個思路:app
維護k個元素的最小堆,即用容量爲k的最小堆存儲最早遍歷到的k個數,建堆費時O(k),並調整堆(費時O(logk))。繼續遍歷數列,每次遍歷一個元素x,與堆頂元素比較,若x>kmin,則更新堆(用時logk),不然不更新堆。這樣下來,總費時O(k*logk+(n- k)*logk)=O(n*logk)。函數
本文以後的例子主要採用這種思路,剩下的思路不在贅述。性能
二:搜索引擎熱門查詢統計
題目描述:
搜索引擎會經過日誌文件把用戶每次檢索使用的全部檢索串都記錄下來,每一個查詢串的長度爲1-255字節。
假設目前有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但若是除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就是越熱門),請統計最熱門的10個查詢串,要求使用的內存不能超過1G。
分析:
第一步、先對這批海量數據預處理,在O(N)的時間內用Hash表完成統計;
第二步、藉助堆這個數據結構,找出Top K,時間複雜度爲N*logK。或者:採用trie樹,關鍵字域存該查詢串出現的次數,沒有出現爲0。最後用10個元素的最小推來對出現頻率進行排序。
爲了下降實現上的難度,假設這些記錄所有是一些英文單詞, ok,複雜問題簡單化了以後,編寫代碼實現也相對輕鬆多了,下面爲部分代碼:
// 結點指針
typedef struct node_no_space *ptr_no_space; //for hashtable
typedef struct node_has_space *ptr_has_space; //for heap
ptr_no_space head[HASHLEN]; //hash表
struct node_no_space
{
char *word;
int count;
ptr_no_space next;
};
struct node_has_space
{
char word[WORDLEN];
int count;
ptr_has_space next;
};
// 最簡單hash函數
int hash_function(char const *p)
{
int value = 0;
while (*p !='/0')
{
value = value* 31 + *p++;
if (value > HASHLEN)
value = value % HASHLEN;
}
return value;
}
// 添加單詞到hash表
void append_word(char const *str)
{
int index = hash_function(str);
ptr_no_space p = head[index];
while (p != NULL)
{
if (strcmp(str, p->word) == 0)
{
(p->count)++;
return;
}
p = p->next;
}
// 新建一個結點
ptr_no_space q = new node_no_space;
q->count = 1;
q->word = new char [strlen(str)+1];
strcpy(q->word, str);
q->next = head[index];
head[index] = q;
}
// 將哈希表結果寫入文件
void write_to_file()
// 從上往下篩選,維持最小堆性質
void shift_down(node_has_space heap[], int i, int len)
// 創建小根堆
void build_min_heap(node_has_space heap[], int len)
// 去除字符串先後符號
void handle_symbol(char *str, int n)
int main(int argc, char **argv)
{
if(argc != 2)
{
printf("argu error\n");
return -1;
}
//初始化哈希表
char str[WORDLEN];
for (int i = 0; i< HASHLEN; i++)
head[i] = NULL;
// 讀取文件,創建哈希表
FILE *fp_passage = fopen(argv[1], "r");
assert(fp_passage);
while (fscanf(fp_passage, "%s", str) != EOF)
{
int n = strlen(str) - 1;
if (n > 0)
handle_symbol(str, n);
append_word(str);
}
fclose(fp_passage);
// 將統計結果輸入文件
write_to_file();
int n= 10;
ptr_has_space heap = new node_has_space [n+1];
int c;
FILE *fp_word = fopen("result.txt", "r");
assert(fp_word);
for (int j = 1; j <= n; j++)
{
fscanf(fp_word, "%s%d", &str, &c);
heap[j].count = c;
strcpy(heap[j].word, str);
}
// 創建最小堆
build_min_heap(heap, n);
// 查找出現頻率最大的10個單詞
while (fscanf(fp_word, "%s %d",&str, &c) != EOF)
{
if (c > heap[1].count)
{
heap[1].count = c;
strcpy(heap[1].word, str);
sift_down(heap, 1, n);
}
}
fclose(fp_word);
// 輸出出現頻率最大的單詞
for (int k = 1; k <= n; k++)
cout << heap[k].count <<" " << heap[k].word << endl;
return 0;
}
三:統計出現次數最多的數據
題目描述:
給你上千萬或上億數據(有重複),統計其中出現次數最多的前N個數據。
分析:
上千萬或上億的數據,如今的機器的內存應該能存下(也許能夠,也許不能夠)。因此考慮採用hash_map/搜索二叉樹/紅黑樹等來進行統計次數。而後就是取出前N個出現次數最多的數據了。固然,也能夠堆實現。
此題與上題相似,最好的方法是用hash_map統計出現的次數,而後再借用堆找出出現次數最多的N個數據。不過,上一題統計搜索引擎最熱門的查詢已經採用過hash表統計單詞出現的次數,特此, 本題改用紅黑樹取代以前的用hash表,來完成最初的統計,而後用堆更新,找出出現次數最多的前N個數據。下面爲部分代碼:
typedef enum rb_color{ RED, BLACK } RB_COLOR;
typedef struct rb_node
{
int key;
int data;
RB_COLOR color;
struct rb_node * left;
struct rb_node * right;
struct rb_node * parent;
}RB_NODE;
RB_NODE * RB_CreatNode(int key, int data)
/*左旋*/
RB_NODE * RB_RotateLeft(RB_NODE * node, RB_NODE * root)
/* 右旋 */
RB_NODE * RB_RotateRight(RB_NODE * node, RB_NODE * root)
/*紅黑樹查找結點*/
RB_NODE *RB_SearchAuxiliary(int key, RB_NODE* root, RB_NODE** save)
/* 返回上述rb_search_auxiliary查找結果 */
RB_NODE *RB_Search(int key, RB_NODE* root)
/* 紅黑樹的插入*/
RB_NODE *RB_Insert(int key, int data, RB_NODE* root)
typedef struct rb_heap
{
int key; //key表示數值自己
int data; //data表示該數值出現次數
}RB_HEAP;
const int heapSize = 10;
RB_HEAP heap[heapSize+1];
/*MAX_HEAPIFY函數對堆進行更新,使以i爲根的子樹成最小堆 */
void MIN_HEAPIFY(RB_HEAP* A, const int& size,int i)
/*BUILD_MINHEAP函數對數組A中的數據創建最小堆*/
void BUILD_MINHEAP(RB_HEAP * A, const int & size)
//中序遍歷RBTree
void InOrderTraverse(RB_NODE * node)
{
if (node == NULL)
{
return;
}
else
{
InOrderTraverse(node->left);
if(node->data > heap[1].data) //當前節點data大於最小堆的最小元素,更新堆數據
{
heap[1].data = node->data;
heap[1].key= node->key;
MIN_HEAPIFY(heap, heapSize, 1);
}
InOrderTraverse(node->right);
}
}
void RB_Destroy(RB_NODE * node)
int main()
{
RB_NODE * root = NULL;
RB_NODE * node = NULL;
// 初始化最小堆
for (int i = 1; i <= 10; ++i)
{
heap[i].key = i;
heap[i].data = -i;
}
BUILD_MINHEAP(heap, heapSize);
FILE* fp = fopen("data.txt","r");
int num;
while (!feof(fp))
{
int res = -1;
res = fscanf(fp,"%d", &num);
if(res > 0)
{
root = RB_Insert(num, 1, root);
}
else
{
break;
}
}
fclose(fp);
InOrderTraverse(root); //遞歸遍歷紅黑樹
RB_Destroy(root);
for (i = 1; i <= 10; ++i)
{
printf("%d/t%d/n",heap[i].key, heap[i].data);
}
return 0;
}
因爲在遍歷紅黑樹採用的是遞歸方式比較耗內存,能夠採用一個非遞歸的遍歷的程序。
下面是用hash和堆解決此題,很明顯比採用上面的紅黑樹,整個實現簡潔了很多,部分源碼以下:
#define HASHTABLESIZE 2807303
#define HEAPSIZE 10
#define A 0.6180339887 // (A )
#define M 16384 //m=2^14
typedef struct hash_node
{
int data;
int count;
struct hash_node* next;
}HASH_NODE;
HASH_NODE * hash_table[HASHTABLESIZE];
HASH_NODE * creat_node(int & data)
{
HASH_NODE * node = (HASH_NODE*)malloc(sizeof(HASH_NODE));
if (NULL == node)
{
printf("malloc node failed!/n");
exit(EXIT_FAILURE);
}
node->data = data;
node->count = 1;
node->next = NULL;
return node;
}
/**
* hash函數採用乘法散列法
* h(k)=int(m*(A*k mod 1))
*/
int hash_function(int & key)
{
double result = A * key;
return (int)(M * (result - (int)result));
}
void insert(int & data)
{
int index = hash_function(data);
HASH_NODE * pnode = hash_table[index];
while (NULL != pnode)
{ // 以存在data,則count++
if (pnode->data == data)
{
pnode->count += 1;
return;
}
pnode = pnode->next;
}
// 創建一個新的節點,在表頭插入
pnode = creat_node(data);
pnode->next = hash_table[index];
hash_table[index] = pnode;
}
typedef struct min_heap
{
int count;
int data;
}MIN_HEAP;
MIN_HEAP heap[HEAPSIZE + 1];
/**
*traverse_hashtale函數遍歷整個hashtable,更新最小堆
*/
void traverse_hashtale()
{
HASH_NODE * p = NULL;
for (int i = 0; i< HASHTABLESIZE; ++i)
{
p = hash_table[i];
while (NULL != p)
{ // 若是當前節點的數量大於最小堆的最小值,則更新堆
if (p->count >heap[1].count)
{
heap[1].count = p->count;
heap[1].data = p->data;
min_heapify(heap, HEAPSIZE, 1);
}
p = p->next;
}
}
}
intmain()
{
// 初始化最小堆
for (int i = 1; i <= 10; ++i)
{
heap[i].count = -i;
heap[i].data = i;
}
build_min_heap(heap, HEAPSIZE);
FILE* fp = fopen("data.txt","r");
int num;
while (!feof(fp))
{
intres = -1;
res =fscanf(fp, "%d", &num);
if(res> 0)
{
insert(num);
}
else
{
break;
}
}
fclose(fp);
traverse_hashtale();
for (i = 1; i <= 10; ++i)
{
printf("%d\t%d\n",heap[i].data, heap[i].count);
}
return 0;
}
四:海量數據處理問題通常總結
關於海量數據處理的問題,通常有Bloom filter,Hashing,bit-map,堆,trie樹等方法來處理。更詳細的介紹,請查看此文:十道海量數據處理面試題與十個方法大總結。
首先TopK問題,確定須要有併發的,不然串行搞確定慢,IO和計算重疊度不高。其次在IO上須要一些技巧,固然可能只是驗證算法,在實踐中IO的提高會很是明顯。最後上文的代碼可讀性雖好,但機器的感受可能就會差,這樣會影響性能。(好比讀文件的函數使用fscanf)
同時,TopK能夠當作從地球上選拔k個跑的最快的,參加奧林匹克比賽,各個國家自行選拔,各個大洲選拔,層層選拔,最後找出最快的10個。發揮多機多核的優點。
http://blog.csdn.net/v_JULY_v/article/details/6403777