Trie字典树理论
我们在浏览器上输入一个关键字时,浏览器都支持:当你正在输入关键字的时候,一边输入,它的下边的输入栏里就会给你自动的提醒,以你现在输入的串的前缀的所有可能出现的关键字都给你列出来了,你可以进行快速的选择,那现在有1千万个串,甚至1亿个串,如果现在输入个”abc“,怎么能够快速的把这个以abc开头的所有的关键字,串显示出来呢???
如果说是每次输个abc,再去搜这1亿个串,哪怕是用一个哈希表,都没有那么快速的,因为你要搜的是前缀,而哈希表是无序的,除非把哈希表整个搜索一遍,否则你也不知道以指定的前缀开头的单词都有哪些。而且用哈希表组织字符串也没有办法进行串排序。
我们来看看字典树,字典树和哈希表都可以进行串的快速检索,但是字典树比哈希表省空间。
因为字典树:
红色的点相当于一个字符串的结尾
这里面相当于出现了:abc,abcd,abd,bcd,efg,hi
为什么字典树进行串的快速检索比哈希表省空间呢?
因为哈希表每个串是单独存储的,而在字典树中,如果多个串的前缀是相同的,前缀只存储1遍,也就是说,所有前缀相同的串,前缀只存储1遍,省空间。
在字典树上进行快速搜索,假如说,我们去搜索“abcd",我们就从字典树的根节点开始,先找到a,然后找b,然后找c,然后找d。
很容易看出来,字典树的搜索的时间复杂度是O(m),m表示串的长度。
也是属于一种空间换时间的数据结构。
串的快速检索:KMP算法搜索字符,如果是多个串,串是单独的,创建哈希表查找,把字符串分割好在字典树查也可以
串排序:放到字典树,对字典树进行前序遍历,就可以得到排序好的所有的单词串。
前缀搜索:所有以ab开头的串打印出来,从b往下走的所有路径,就是了。其他3个根下的路径就不用去找了,这是哈希表做不了的。
Java版本代码实现
package com.fixbug;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* 字典树代码实现
*/
class TrieTree{
//字典树的根节点类型
private TrieNode root;
public TrieTree() {//构造函数
//创建Trie树的根节点
this.root = new TrieNode('\u0000', 0, new HashMap<Character, TrieNode>());
}
/**
* Trie树的增加元素
* @param word
*/
public void add(String word){
TrieNode cur = root;//从根节点开始遍历判断
for (int i = 0; i < word.length(); i++) {//遍历这个word
char ch = word.charAt(i);//获取word的第i个位置的字符
TrieNode child = cur.nodeMap.get(ch);//获取当前指向的节点的下一层中找,有没有ch字符
if(child == null){//表示cur节点的下一级节点,没有ch字符节点
child = new TrieNode(ch, 0, new HashMap<Character, TrieNode>());//创建节点
cur.nodeMap.put(ch, child);//添加到map表里
}
cur = child;//继续处理下一个节点了
}
//cur就指向了word串的最后一个字符节点了
cur.freqs++;
}
/**
* Trie树的删除操作
* @param word
*/
public void remove(String word){
TrieNode cur = root;//指向删除串的末尾位置
TrieNode del = root;//指向的cur上面的最近的freqs不为0的节点
char delch = word.charAt(0);
for (int i = 0; i < word.length(); i++) {
TrieNode child = cur.nodeMap.get(word.charAt(i));
if(child == null){//串不存在
return;
}
//更新一下del和delch,del记录的是离cur最近的freqs不为0的字符节点
//或者del更新到最后一个下一级字符节点不为1的节点处
if(cur.freqs > 0 || cur.nodeMap.size() > 1){
del = cur;
delch = word.charAt(i);
}
cur = child;
}
if(cur.nodeMap.size() == 0){//后面没有其它字符节点了
del.nodeMap.remove(delch);
} else {
cur.freqs = 0;//后面还有其它字符节点,此处不删除任何节点,只把次数写成0,代表删除操作
}
}
/**
* Trie树的查询操作
* @param word
* @return 找到word,返回串的次数,否则返回0
*/
public int query(String word){
TrieNode cur = root;//从根节点开始
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);//获取word的第i个位置的字符
TrieNode child = cur.nodeMap.get(ch);
if(child == null){
return 0;//没找到
}
cur = child;//更新,往后走
}
return cur.freqs;
}
/**
* 前缀查找
* @param prefix
*/
public void queryPrefix(String prefix){
TrieNode cur = root;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
TrieNode child = cur.nodeMap.get(ch);
if(child == null){
return;
}
cur = child;
}
//cur就指向了前缀的最后一个字符了
String word = new String(prefix.substring(0, prefix.length()-1));
preOrder(cur, word);
}
/**
* 打印Trie树,相当于是进行了串排序
*/
public void showTrieTree(){
String word = new String();
preOrder(root, word);
}
/**
* Trie树的前序遍历代码
* @param root
* @param word
*/
private void preOrder(TrieNode root, String word) {
if(root == null){
return;
}
if(root != this.root){
word += root.ch;
if(root.freqs > 0){
System.out.println(word);
}
}
for(Map.Entry<Character, TrieNode> entry : root.nodeMap.entrySet()){
if(entry.getValue() != null){
preOrder(entry.getValue(), word);
}
}
}
/**
* 字典树的节点类型
*/
static class TrieNode{
char ch;//节点存储的字符
int freqs;//出现的次数,在每一个串的末尾字符+1,记录这个串出现的次数
Map<Character, TrieNode> nodeMap;//存储当前节点下一层节点的信息
public TrieNode(char ch, int freqs, Map<Character, TrieNode> nodeMap) {
this.ch = ch;
this.freqs = freqs;
this.nodeMap = nodeMap;
}
}
}
/**
* 字典树代码测试
*
*/
public class TrieTestCase
{
public static void main( String[] args )
{
TrieTree trie = new TrieTree();
trie.add("hello");
trie.add("hello");
trie.add("hel");
trie.add("hel");
trie.add("hel");
trie.add("china");
trie.add("ch");
trie.add("ch");
trie.add("heword");
trie.add("hellw");
System.out.println(trie.query("hello"));
System.out.println(trie.query("hel"));
System.out.println(trie.query("china"));
System.out.println(trie.query("ch"));
trie.showTrieTree();
System.out.println("搜索前缀字符串:");
trie.queryPrefix("he");
trie.queryPrefix("ch");
System.out.println("-------------------------");
trie.remove("hello");
System.out.println(trie.query("hello"));
System.out.println(trie.query("hel"));
System.out.println("-------------------------");
trie.showTrieTree();
ArrayList<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 9; i++) {
list.add(i);
}
}
}
C++实现版本
#include<iostream>
#include<unordered_map>
using namespace std;
/**
* 字典树代码实现
*/
struct TrieNode//节点类型
{
TrieNode(char chg, int freqsh, unordered_map<char, TrieNode*> nodeMape)
{
ch = chg;
freqs = freqsh;
nodeMap = nodeMape;
}
char ch;//节点存储的字符
int freqs;//出现的次数,在每一个串的末尾字符+1,记录这个串出现的次数
unordered_map<char, TrieNode*>nodeMap;//存储当前节点下一层节点的信息
};
class TrieTree
{
public:
TrieTree()//构造函数
{
root = new TrieNode('\u0000', 0, unordered_map<char, TrieNode*>());
}
void add(string word)//增加字符串
{
TrieNode* cur = root;//从根节点开始遍历判断
for (int i = 0; i < word.size(); i++)//遍历这个word
{
char ch = word.at(i);//获取word的第i个位置的字符
auto it = cur->nodeMap.find(ch);
TrieNode* child = nullptr;
if (it != cur->nodeMap.end())
{
child = it->second;//获取当前指向的节点的下一层中找,有没有ch字符
}
else
{ //表示cur节点的下一级节点,没有ch字符节点,所以要创建了
child = new TrieNode(ch, 0, unordered_map<char, TrieNode*>());//创建节点
cur->nodeMap.insert({ ch, child });//添加到map表里
}
cur = child;//继续处理下一个节点了
}
//cur就指向了word串的最后一个字符节点了
cur->freqs++;
}
void remove(string word)//删除字符串
{
TrieNode* cur = root;//指向删除串的末尾位置
TrieNode* del = root;//指向的cur上面的最近的freqs不为0的节点
char delch = word.at(0);
for (int i = 0; i < word.length(); i++)
{
TrieNode* child = nullptr;
auto it= cur->nodeMap.find(word.at(i));
if (it==cur->nodeMap.end())//串不存在
{
return;
}
child = it->second;
//更新一下del和delch,del记录的是离cur最近的freqs不为0的字符节点
//或者del更新到最后一个下一级字符节点不为1的节点处
if (cur->freqs > 0 || cur->nodeMap.size() > 1)
{
del = cur;
delch = word.at(i);
}
cur = child;
}
if (cur->nodeMap.size() == 0)
{ //后面没有其它字符节点了
del->nodeMap.erase(delch);
}
else
{
cur->freqs = 0;//后面还有其它字符节点,此处不删除任何节点,只把次数写成0,代表删除操作
}
}
int query(string word)//查找字符串
{
TrieNode* cur = root;//从根节点开始
for (int i = 0; i < word.size(); i++)
{
char ch = word.at(i);//获取word的第i个位置的字符
TrieNode* child = nullptr;
auto it= cur->nodeMap.find(ch);
if (it==cur->nodeMap.end())
{
return 0;//没找到
}
cur = child;//更新,往后走
}
return cur->freqs;
}
void queryPrefix(string prefix)//前缀查找
{
TrieNode* cur = root;
for (int i = 0; i < prefix.size(); i++)
{
char ch = prefix.at(i);
TrieNode* child = nullptr;
auto it = cur->nodeMap.find(ch);
if (it == cur->nodeMap.end())
{
return;//没找到
}
cur = child;
}
//cur就指向了前缀的最后一个字符了
string word = string(prefix.substr(0, prefix.size() - 1));
preOrder(cur, word);
}
void showTrieTree()//打印Trie树,相当于是进行了串排序
{
string word = string();
preOrder(root, word);
}
void preOrder(TrieNode* root, string word)//Trie树的前序遍历代码
{
if (root == nullptr)
{
return;
}
if (root != this->root)
{
word += root->ch;
if (root->freqs > 0)
{
cout << word;
}
}
for (auto &entry : root->nodeMap)
{
if (entry.second!=nullptr)
{
preOrder(entry.second, word);
}
}
}
private:
TrieNode* root;
};