后端面试常见数据结构1——前缀树(Prefix Tree)

前缀树(Prefix Tree)

    • 1、背景
    • 2、定义
    • 3、特点
    • 4、构造
      • 4.1、实现 Trie (前缀树)——力扣 208
      • 4.2、Trie ——文本词频统计
  • 参考

1、背景

  • 节点所有的后代都与该节点相关的字符串有着共同的前缀。这就是前缀树名称的由来。
  • 对于一个字符串数据,我们要从查找某个字符串是否出现过,或者其中以“hell”开头 ,或者以"ive"结尾的字符是否出现以及出现的个数等等操作。我们只需要在定义前缀树的时候加上相应得数据项就可以了。

后端面试常见数据结构1——前缀树(Prefix Tree)_第1张图片

2、定义

前缀树是N叉树的一种特殊形式。通常来说,一个前缀树是用来存储字符串的。前缀树的每一个节点代表一个字符串(前缀)。每一个节点会有多个子节点,通往不同子节点的路径上有着不同的字符。子节点代表的字符串是由节点本身的原始字符串,以及通往该子节点路径上所有的字符组成的。

  • 在上图示例中,我们在节点中标记的值是该节点对应表示的字符串。例如,我们从根节点开始,选择第二条路径 ‘b’,然后选择它的第一个子节点
    ‘a’,接下来继续选择子节点 ‘d’,我们最终会到达叶节点 “bad”。节点的值是由从根节点开始,与其经过的路径中的字符按顺序形成的。
  • 以节点 “b” 为根的子树中的节点表示的字符串,都具有共同的前缀 “b”。反之亦然,具有公共前缀 “b” 的字符串,全部位于以 "b"为根的子树中,并且具有不同前缀的字符串来自不同的分支。

3、特点

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。(字母用边表示,不要塞到节点里)
  • 每个节点的所有子节点包含的字符都不相同。
  • 根节点表示空字符串

4、构造

数组存储子节点:

  • 如果我们只存储含有字母 a 到 z 的字符串,我们可以在每个节点中声明一个大小为26的数组来存储其子节点。对于特定字符 c,我们可以使用c - ‘a’ 作为索引来查找数组中相应的子节点。

4.1、实现 Trie (前缀树)——力扣 208

208. 实现 Trie (前缀树)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。

Trie trie = new Trie();

trie.insert("apple");
trie.search("apple");   // 返回 true
trie.search("app");     // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");   
trie.search("app");     // 返回 true

你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。

包含三个单词"sea",“sells”,"she"的 Trie 会长啥样呢?
后端面试常见数据结构1——前缀树(Prefix Tree)_第2张图片
步骤如下:

  • 1、定义类 Trie
class Trie {
private:
    bool isEnd;//表示它是一个单词的末尾。
    //字母映射表,每个位置对应一个Trie指针,不是字母
    //因为只有26个小写字母,所以构造26个,
    //索引利用ASC码辨识
    Trie* next[26];
public:
    //方法将在下文实现...
};

  • 2、插入

描述:向 Trie 中插入一个单词 word

实现:这个操作和构建链表很像。首先从根结点的子结点开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。

void insert(string word) {
    Trie* node = this;
    for (char c : word) {
        if (node->next[c-'a'] == NULL) {
            node->next[c-'a'] = new Trie();
        }
        node = node->next[c-'a'];
    }
    node->isEnd = true;
}

  • 3、查找
    描述:查找 Trie 中是否存在单词 word

实现:从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回false,如果匹配到了最后一个字符,那我们只需判断node->isEnd即可。

bool search(string word) {
    Trie* node = this;
    for (char c : word) {
        node = node->next[c - 'a'];
        if (node == NULL) {
            return false;
        }
    }
    return node->isEnd;
}

  • 4、前缀匹配
    描述:判断 Trie 中是或有以 prefix 为前缀的单词

实现:和 search 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的

bool startsWith(string prefix) {
    Trie* node = this;
    for (char c : prefix) {
        node = node->next[c-'a'];
        if (node == NULL) {
            return false;
        }
    }
    return true;
}

综合

class Trie {
private:
    bool isEnd;//表示它是一个单词的末尾。
    //字母映射表,每个位置对应一个Trie指针,不是字母
    //因为只有26个小写字母,所以构造26个,
    //索引利用ASC码辨识
    Trie* TrieNode[26];
public:
    /** Initialize your data structure here. */
    Trie() {
        isEnd = false;
        memset(TrieNode,0,sizeof(TrieNode));//指针赋0就相当于将该指针置为NULL。
    }
    
    /** Inserts a word into the trie. */
    void insert(string word) {
        if(word.size() < 1)
             return ;
        Trie* Node = this;
        for(char c:word)
        {
            if(Node->TrieNode[c-'a'] == NULL)//c-'a'得到索引,对应TrieNode表对应的位置被记录
                Node->TrieNode[c-'a'] = new Trie;
            Node = Node->TrieNode[c-'a'];//c字母对应的位置被标记以后,需要进行c之后的字母
        }
        Node->isEnd = true;
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
         if(word.size() < 1)
            return true;
        Trie* Node = this;
        for(char c:word)
        {
            if(Node->TrieNode[c-'a'] == NULL)
                return false;
            Node = Node->TrieNode[c-'a'];
        }
        return Node->isEnd;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        if(prefix.size() < 1)
            return true;
        Trie* Node = this;
        for(char c:prefix)
        {
            if(Node->TrieNode[c-'a'] == NULL)
                return false;
            Node = Node->TrieNode[c-'a'];
        }
        return true;
    }
};

4.2、Trie ——文本词频统计

相比较力扣 208,这里需要增加新的功能,

  • 1、统计前缀字符串出现的次数
  • 2、节点删除
    前缀树如下4个操作:
  • 1.插入字符串: void insert(string str)
    遍历字符串,沿途经过的pass++,如果出现某个字符从未出现时则新建一个。遍历到最后一个字符时,其结点的end++;
  • 2.删除字符串: void delete(string str)
    • 遍历字符串,每个字符串的pass–,如果遍历到最后一个,end也–;
    • 如果沿途发现某个结点的pass(自减之前)值为1,则直接删除该结点。
  • 3.在前缀树中查询字符串出现的次数: int search(string str)
    遍历字符串,返回最后一个字符对应结点的end值。
  • 4.在前缀树中查询以str字符串为前缀的个数: int prefixNumber(string str)
    遍历字符串,返回最后一个字符对应的结点的pass值。
    后端面试常见数据结构1——前缀树(Prefix Tree)_第3张图片
//前缀树结点
class Trienode{
public:
	int pass;//为经过该结点的次数
	int end;//以该结点结尾的次数
	Trienode* nexts[26];//同上
};
 
class Trie{
public:
	Trienode* root;
	Trie(){//构造函数
	root=new Trienode;//头部空节点
}	
	//插入操作
	void insert(string word)
	{
		if(word.size()==0)
			return;	 
		Trienode* cur=root;
		for(char c:word)
		{
			int index = c-'a';
			//节点从没有插入过,需要新建
			if(cur->nexts[index]==NULL)
				cur->nexts[index]=new Trienode;
			//指向新建节点,或者原来已建立的位置
			cur=cur->nexts[index];
			cur->pass++;// 划过当前节点的字符串数+1				
		}
		cur->end++;// 遍历结束了,记录下以该字母结束的字符串数+1
	}
  // 在trie树中查找word字符串出现的次数
	int search(string word)
	{
		if(word.size()==0)
			return 0;	
		Trienode* cur=root;
		for(char c:word)
		{
			int index = c-'a';
			if(cur->nexts[index]==NULL)
					return 0;//说明此单词不存在
			// 到达了该字母记录的节点路径,继续往下走
			cur=cur->nexts[index];					
		}
		return cur->end;//返回word出现的次数
	}
     // 删除一个字符串
	void deletenode(string word)
	{
		// 删除之前,先判断有没有
        if(search(word) == 0){
            return;
        }
		Trienode* cur=root;
		for(char c:word)
		{
			int index = c-'a';
			// 如果遍历到某个节点时,将其index处passNum减1后等于0,则说明没有其他字符串经过它了
			if(--cur->nexts[index]->pass == 0)
			{
				delete cur->nexts[index];
				return;
			}
			cur=cur->nexts[index];
		}
			// 遍历完了,删除了整个单词,则将以该单词最后一个字符结尾的字符串的数目减1
			cur->end--;		
	}
 // 返回有多少单词以pre为前缀的
	int  prefixnum(string word)
	{
		if(word.size()==0)
			return 0;	
		Trienode* cur=root;
		for(char c:word)
		{
			int index = c-'a';
			if(cur->nexts[index]==NULL)
					return 0;	// 不存在	
			cur=cur->nexts[index];							
		}
		return cur->pass;// 找到word最后一个字符的pass值
	} 	
};

参考

1、https://www.cnblogs.com/vincent1997/p/11237389.html
2、https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/trie-tree-de-shi-xian-gua-he-chu-xue-zhe-by-huwt/
3、https://blog.csdn.net/y1054765649/article/details/88700590

你可能感兴趣的:(数据结构,c++,数据结构)