前缀树Trie及其应用(上) ---Leetcode208、Leetcode720

前缀树是一种很重要的数据结构,本篇文章将通过Leetcode中的两道与前缀树相关的题目,以及在实际开发中的应用来探讨这个问题。
本篇文章,主要介绍前缀树的概念以及两道经典的前缀树算法题。

1.前缀树概念

Trie(发音为"try")即前缀树是一种数据结构,用于检索字符串数据集中的键。这一高效的数据结构有多种应用:

  1. 自动补全
  2. 拼写检查
  3. IP路由(最长前缀匹配)
  4. T9(九宫格)打字预测
  5. 单词游戏

当然了,还有其他的数据结构如平衡术和哈希表,同样可以使我们能够在字符串数据集中搜索单词。为什么我们还需要使用Trie树呢?
尽管哈希表可以在O(1)时间内寻找键值,却无法高效的完成以下操作:

  • 找到具有同一前缀的全部键值
  • 按词典序枚举字符串的数据集

Trie树优于哈希表的另一个理由是,随着哈希表大小的增加,会出现大量冲突,时间复杂度可能增加到O(n),其中n是插入的键的数量。与哈希表相比,Trie树在存储多个具有相同前缀的键时可以使用较少的空间,此时Trie树只需要O(m)的时间复杂度,其中m为键长。而平衡树中查找键值需要O(mlogn)时间复杂度。

Trie树的结点结构
Trie树是一个有根的树,其结点具有以下字段:

  • 最多R个指向子节点的链接,其中每个链接对应字母表数据集中的一个字母。
  • 布尔字段,以指定结点是对应键的结尾还是只是键的前缀。
    在这里插入图片描述

Trie数中最常见的是插入与查找操作。

2.实现Trie(Leetcode208)

题目描述

实现一个 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 构成的。
  • 保证所有输入均为非空字符串。
解题思路

本题可以直接根据Trie的定义来做,对其进行依次实现。
首先需要定义TrieNode。

  • 该节点需要具有子节点children = new TrieNode[26]
  • 该节点需要具有标识当前字母结尾是否是一个单词的功能
  • 由于本题只有26个小写的英文字母,因此,每个节点存储的具体是哪个字母可以通过其上层对应的index确认。

实现Trie的insert功能

  • 首先获取当前Trie的root,因为每一次insert的时候,都应当从root的位置开始算起。
  • 遍历0-25的index的children节点。如果为null则new个新的出来,并且将节点指向下一个要遍历的结点。
  • 当全部遍历完成时,将最后一个节点的isWord属性置为true。标识着这个字符串完成插入。

searchstartsWith函数的代码逻辑有些重复,因此两者都可以抽象出一个find的逻辑,即遍历这个Trie找到指定的word即可,并返回这个单词结尾字幕的TrieNode。

search逻辑实现

  • 调用find方法,如果返回的TrieNode!=null且TrieNode.isWord==true则说明在这个Trie中找到了这个单词。

startWith

  • 调用find方法,如果返回的TrieNode!=null则说明已经找到了,直接返回运算结果就好了。
解答代码
class Trie {

    /** Initialize your data structure here. */
    class TrieNode {
    	private boolean isWord;
    	private TrieNode[] children;
    	public TrieNode(){
			this.isWord = false;
			this.children = new TrieNode[26];
		}
    }
    private TrieNode root;
    public Trie() {
		root = new TrieNode();
    }
 
    /** Inserts a word into the trie. */
    public void insert(String word) {
		TrieNode p = root;
		for(int i=0; i<word.length(); i++) {
			int index = (int)(word.charAt(i)-'a');
			if(p.children[index]==null) {
				p.children[index] = new TrieNode();
			}
			p = p.children[index];
		}
		p.isWord = true;
    }
    
    /** Returns if the word is in the trie. */
    public boolean search(String word) {
		TrieNode w = find(word);
		return w!=null && w.isWord == true;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
		return find(prefix)!=null;
    }
	private TrieNode find(String word) {
		TrieNode p = root;
		for(int i=0; i<word.length(); i++) {
			int index = (int)(word.charAt(i)-'a');
			if(p.children[index]==null) {
				return null;
			}
			p = p.children[index];
		}
		return p;
	}
}

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * boolean param_2 = obj.search(word);
 * boolean param_3 = obj.startsWith(prefix);
 */

3.词典中最长的单词(Leetcode720)

题目描述

给出一个字符串数组words组成的一本英语词典。从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中字典序最小的单词。

若无答案,则返回空字符串。

示例 1:

输入: 
words = ["w","wo","wor","worl", "world"]
输出: "world"
解释: 
单词"world"可由"w", "wo", "wor", 和 "worl"添加一个字母组成。

示例 2:

输入: 
words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
输出: "apple"
解释: 
"apply"和"apple"都能由词典中的单词组成。但是"apple"得字典序小于"apply"。

注意:

所有输入的字符串都只包含小写字母。
words数组长度范围为[1,1000]。
words[i]的长度范围为[1,30]。

解题思路

一看到词典,就很自然的想到了Trie但是这个前缀树的结构与上一题208的不可以定义成一样的。因为上一题的定义有个很严重的问题,我们只能通过前一个节点去找到下一个节点对应的是什么值。这中间缺少了很多数据。
因此这里需要改进前缀树TrieNode以记录更多的信息。
本题所用的前缀树与208的相比进行了以下改进

  • 增加了char c用于存储该TrieNode所具体对应的字母
  • end标识由boolean类型改成了int类型。并且end存储的数值为数组中以该字母结尾的字符串的index值加1。这么做的目的是为了后续找这个字符串的时候,可以根据这个字母快速的找到这个字母对应的字符串。
    这个地方的处理非常的巧妙啊,为什么要加1,因为默认都是0。感觉这个处理很trick!
  • 最后通过BFS进行遍历整个Trie从而找到对应的值。
    遍历的这个过程也用了一些手法,因为我们需要找的是字典顺序靠前的最长字符串。HashMap的遍历是无序的,因此就要比较,当前的String与ans直接的关系:
String word = words[node.end-1];
	if(word.length()>ans.length() 
		|| word.length()==ans.length() && word.compareTo(ans)<0) {
		ans = word;	
}

在长度一样的时候,巧妙的运用了java自带的字符串比较的compareTo函数。
最后返回ans


当然本题还有其他解法,但是使用的就不是Trie。因此本文不再赘述。

解答代码
class Solution {
    public String longestWord(String[] words) {
		Trie trie = new Trie();
		int index = 0;
		for(String word: words) {
			trie.insert(word, ++index);
		}
		trie.words = words;
		return trie.dfs();
    }
}
class TrieNode {
	char c;
	int end;
	HashMap<Character, TrieNode> children = new HashMap<>();
	public TrieNode(char c) {
		this.c = c;
	}
}
class Trie {
	TrieNode root;
	String[] words;
	public Trie() {
		root = new TrieNode('0');
	}
	public void insert(String word, int index) {
		TrieNode p = root;
		for(char c : word.toCharArray()) {
			p.children.putIfAbsent(c, new TrieNode(c));
			p = p.children.get(c);
		}
		p.end = index;
	}
	public String dfs() {
		String ans = "";
		Stack<TrieNode> stack = new Stack();
		stack.push(root);
		while(!stack.isEmpty()) {
			TrieNode node = stack.pop();
			if(node.end>0 || node == root) {
				if(node!=root) {
					String word = words[node.end-1];
					if(word.length()>ans.length() 
						|| word.length()==ans.length() && word.compareTo(ans)<0) {
						ans = word;	
					}
				}
				for (TrieNode n : node.children.values()) {
					stack.push(n);
				}
			}
		}
		return ans;
	}
}

参考资料

  1. Leetcode官方题解:实现前缀树
  2. Leetcode208.实现Trie(前缀树)
  3. Leetcode720.词典中最长的单词
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值