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
#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;
}
};
并查集(Union Find),又称不相交集合(Disjiont Set),它应用于N个元素的集合求并与查 询问题,在该应用场景中,我们通常是在开始时让每个元素构成一个单元素的集合, 然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在 哪个集合中。虽然该问题并不复杂,但面对极大的数据量时,普通的数据结构往往无 法解决,并查集就是解决该种问题最为优秀的算法。
使用森林存储集合之间的关系,属于同一集合的不同元素,都有一个相同的根节点 ,代表着这个集合。
当进行查找某元素属于哪个集合时,即遍历该元素到根节点,返回根节点所代表的集 合;在遍历过程中使用路径压缩的优化算法,使整体树的形状更加扁平,从而优化查 询的时间复杂度。 当进行合并时,即将两颗子树合为一颗树,将一颗子树的根节点指向另一颗子树的根 节点;在合并时可按子树的大小,将规模较小的子树合并到规模较大的子树上,从而 使树规模更加平衡,从而优化未来查询的时间复杂度。
并查集的find和union操作:
#include
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
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_();
}
};