1. Trie樹
Trie樹,又稱字典樹或前綴樹,是一種有序的、 用於統計、排序和存儲字符串的數據結構,它 與二叉查找樹不同,關鍵字不是直接保存在節點 中,而是由節點在樹中的位置決定,每個節點 代表了一個字符,從第一層孩子節點到中間的 某個標記的節點代表了存儲的字符串。 一個節點的所有子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應空字符串 。一般情況下,不是所有的節點都有對應的字符 串,只有葉子節點和部分內部節點進行標記, 才存儲了字符串。
trie樹的最大優點就是利用字符串的公共前綴來 減少存儲空間與查詢時間,從而最大限度地減少 無謂的字符串比較,是非常高效的字符串查找數 據結構,查找和插入字符串都可達到O(1)算法復 雜度。
trie中的鍵通常是字符串,但也可以是其它的結構。trie的算法可以很容易地修改爲處理其它結構的有序序列,比如一串數字或者形狀的排列。比如,bitwise trie中的鍵是一串位元,可以用於表示整數或者內存地址
基本性質
1,根節點不包含字符,除根節點意外每個節點只包含一個字符。
2,從根節點到某一個節點,路徑上經過的字符連接起來,爲該節點對應的字符串。
3,每個節點的所有子節點包含的字符串不相同。
優點:
可以最大限度地減少無謂的字符串比較,故可以用於詞頻統計和大量字符串排序。
跟哈希表比較:
1,最壞情況時間複雜度比hash表好
2,沒有衝突,除非一個key對應多個值(除key外的其他信息)
3,自帶排序功能(類似Radix Sort),中序遍歷trie可以得到排序。
缺點:
1,雖然不同單詞共享前綴,但其實trie是一個以空間換時間的算法。其每一個字符都可能包含至多字符集大小數目的指針(不包含衛星數據)。
每個結點的子樹的根節點的組織方式有幾種。1>如果默認包含所有字符集,則查找速度快但浪費空間(特別是靠近樹底部葉子)。2>如果用鏈接法(如左兒子右兄弟),則節省空間但查找需順序(部分)遍歷鏈表。3>alphabet reduction: 減少字符寬度以減少字母集個數。,4>對字符集使用bitmap,再配合鏈接法。
2,如果數據存儲在外部存儲器等較慢位置,Trie會較hash速度慢(hash訪問O(1)次外存,Trie訪問O(樹高))。
3,長的浮點數等會讓鏈變得很長。可用bitwise trie改進。
Trie樹的數據結構和遍歷代碼:
#include<stdio.h>
#define TIRE_MAX_NUM 26
struct TrieNode{
TrieNode *child[TIRE_MAX_NUM];
bool is_end;
TrieNode():is_end(false){
for(int i=0; i<TIRE_MAX_NUM; ++i){
child[i] = 0;
}
}
};
void preorder_trie(TrieNode* node, int layer){
for(int i=0; i<TIRE_MAX_NUM; ++i){
if(node->child[i]){
for(int j=0; j<layer; ++j){
printf("---");
}
printf("%c", i+'a');
if(node->is_end){
printf("(end)");
}
printf("\n");
preorder_trie(node->child[i], layer++);
}
}
}
Trie樹的insert,search,startwith操作:
LeetCode 208
https://leetcode-cn.com/problems/implement-trie-prefix-tree/
class Trie {
Trie *child[26];
bool is_end;
public:
/** Initialize your data structure here. */
Trie() {
is_end = false;
for(int i=0; i<26; ++i){
child[i] = nullptr;
}
}
/** Inserts a word into the trie. */
void insert(string word) {
Trie *t = this;
for(char c:word){
int pos = c-'a';
if(!t->child[pos]){
t->child[pos] = new Trie();
}
t = t->child[pos];
}
t->is_end = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
Trie *t = this;
for(char c:word){
int pos = c-'a';
if(!t->child[pos]){
return false;
}
t = t->child[pos];
}
return t->is_end;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
Trie *t = this;
for(char c:prefix){
int pos = c-'a';
if(!t->child[pos]){
return false;
}
t = t->child[pos];
}
return true;
}
};
2.並查集
並查集(Union Find),又稱不相交集合(Disjiont Set),它應用於N個元素的集合求並與查 詢問題,在該應用場景中,我們通常是在開始時讓每個元素構成一個單元素的集合, 然後按一定順序將屬於同一組的元素所在的集合合併,其間要反覆查找一個元素在 哪個集合中。雖然該問題並不複雜,但面對極大的數據量時,普通的數據結構往往無 法解決,並查集就是解決該種問題最爲優秀的算法。
使用森林存儲集合之間的關係,屬於同一集合的不同元素,都有一個相同的根節點 ,代表着這個集合。
當進行查找某元素屬於哪個集合時,即遍歷該元素到根節點,返回根節點所代表的集 合;在遍歷過程中使用路徑壓縮的優化算法,使整體樹的形狀更加扁平,從而優化查 詢的時間複雜度。 當進行合併時,即將兩顆子樹合爲一顆樹,將一顆子樹的根節點指向另一顆子樹的根 節點;在合併時可按子樹的大小,將規模較小的子樹合併到規模較大的子樹上,從而 使樹規模更加平衡,從而優化未來查詢的時間複雜度。
並查集的find和union操作:
#include<iostream>
using namespace std;
class DisJointSet{
private:
vector<int> id;
vector<int> size;
int count;
public:
DisJointSet(int n){
for(int i=0; i<n; ++i){
id[i] = i;
size[i] = 1;
}
count = n;
}
int find(int p){
while(p!=id[p]){
id[p] = id[id[p]];
p = id[p];
}
return p;
}
void union_(int p, int q){
int i = find(p);
int j = find(q);
if(i==j){
return;
}
if(size[i]<size[j]){
id[i] = j;
size[j] += size[i];
}
else{
id[j] = i;
size[i] += size[j];
}
count--;
}
int count_(){
return count;
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
DisJointSet djset(M.size());
for(int i=0; i<M.size(); ++i){
for(int j=i+1; j<M.size(); ++j){
if(M[i][j]){
djset.union_(i, j);
}
}
}
return djset.count_();
}
};
int main(){
Solution s;
s.findCircleNum(M);
}
Leetcode 547 朋友圈
https://leetcode-cn.com/problems/friend-circles/
方法1:DFS
用時96 ms
內存消耗83.4 MB
class Solution {
public:
void DFS(int i, vector<vector<int>> M, vector<int> &visited){
visited[i]=1;
for(int j=0; j<M.size(); ++j){
if(!visited[j] && M[i][j]==1){
DFS(j, M, visited);
}
}
}
int findCircleNum(vector<vector<int>>& M) {
int n = M.size();
vector<int> visited(n, 0);
int count = 0;
for(int i=0; i<n; ++i){
if(visited[i]==0){
DFS(i, M, visited);
count++;
}
}
return count;
}
};
方法2:並查集
用時16 ms
內存消耗11 MB,比DFS方法要好很多
#include<iostream>
using namespace std;
class DisJointSet{
public:
vector<int> id;
vector<int> size;
int count;
DisJointSet(int n){
for(int i=0; i<n; ++i){
id.push_back(i);
size.push_back(1);
}
count = n;
}
int find(int p){
while(p!=id[p]){
id[p] = id[id[p]];
p = id[p];
}
return p;
}
void union_(int p, int q){
int i = find(p);
int j = find(q);
if(i==j){
return;
}
if(size[i]<size[j]){
id[i] = j;
size[j] += size[i];
}
else{
id[j] = i;
size[i] += size[j];
}
count--;
}
int count_(){
return count;
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
DisJointSet djset(M.size());
for(int i=0; i<M.size(); ++i){
for(int j=i+1; j<M.size(); ++j){
if(M[i][j]==1){
djset.union_(i, j);
}
}
}
return djset.count_();
}
};