单词的压缩编码
给定一个单词列表,我们将这个列表编码成一个索引字符串 S 与一个索引列表 A。
例如,如果这个列表是 [“time”, “me”, “bell”],我们就可以将其表示为 S = “time#bell#” 和 indexes = [0, 2, 5]。
对于每一个索引,我们可以通过从字符串 S 中索引的位置开始读取字符串,直到 “#” 结束,来恢复我们之前的单词列表。
那么成功对给定单词列表进行编码的最小字符串长度是多少呢?
示例:
输入: words = ["time", "me", "bell"]
输出: 10
说明: S = "time#bell#" , indexes = [0, 2, 5] 。
提示:
1 <= words.length <= 2000
1 <= words[i].length <= 7
每个单词都是小写字母 。
思路
1.题目有点绕,简单说就是索引中的0,2,5分别为字符串的起始位置,#为终止位置分割S中的字符串。创建一个26位的字典树,遍历字符串数组。题中如果一个长字符串的后缀可以装下短字符串时,不统计短字符串长度,所以,倒序遍历字符串元素,如果其中每一位字符都存在则可以不统计它的长度,如果不存在,则存储该字符且统计该字符对应词的长度+1。
class Trie {
int val;
Trie[] tries = new Trie[26];
public Trie() {
}
}
public int minimumLengthEncoding(String[] words) {
//将字符串数组以字段长度倒序
Arrays.sort(words, (o1, o2) -> o2.length() - o1.length());
Trie trie = new Trie();
int length = 0;
for (String word : words) {
boolean isNew = false;
//不能操作头节点,因为每次都要从头节点重新遍历
Trie cur = trie;
//从后往前遍历,方便查找单词的后缀与后面的单词是否重合
for (int i = word.length() - 1; i >= 0; i--) {
//该字符在26位数组中的索引
int c = word.charAt(i) - 'a';
//如果当前的字符是新的,那么他会重新创建一个分支节点并延续。
if (cur.tries[c] == null) {
isNew = true;
cur.tries[c] = new Trie();
}
//字典树向下遍历
cur = cur.tries[c];
}
//如果是新单词,必须计算整个单词长度
if (isNew) {
length += word.length() + 1;
}
}
return length;
}
2.将字符串数组放入Set集合中,然后遍历字符串数组,每次都对字符串从后面截取部分,然后去Set集合中删除元素,最终Set集合留下来的肯定是后缀不包含元素。
public int minimumLengthEncoding(String[] words) {
//将数组放入set集合
Set<String> good = new HashSet(Arrays.asList(words));
//遍历删除后缀
for (String word: words) {
for (int k = 1; k < word.length(); ++k) {
good.remove(word.substring(k));
}
}
int ans = 0;
//统计剩余的字符串长度
for (String word: good) {
ans += word.length() + 1;
}
return ans;
}
拓展
实现 Trie (前缀树)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。所有的输入都是由小写字母 a-z 构成的。示例:
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
1.初始化一个每一个节点有26个子节点的字典树。insert时,字典树不存在则创建,然后向下遍历字典树。使用search时,前缀查询返回false,所以需要一个isEnd变量,保存单词的结束位置。
public class Trie {
boolean isEnd;
Trie[] tries;
public Trie() {
//创建一个26位的数组
tries = new Trie[26];
}
public void insert(String word) {
if (word.length() == 0) {
return;
}
//开始时在Trie中创建并实例化了一个head节点,会一致自己创建自己导致栈溢出。。。使用this代表当前的类
Trie aTry = this;
for (int i = 0; i < word.length(); i++) {
int c = word.charAt(i) - 'a';
//如果字符不存在,则创建
if (aTry.tries[c] == null) {
aTry.tries[c] = new Trie();
}
//向下遍历字典树
aTry = aTry.tries[c];
}
//单词插入完成后,在最后一个字符的位置标记上isEnd
aTry.isEnd = true;
}
public boolean search(String word) {
Trie aTry = this;
for (int i = 0; i < word.length(); i++) {
int c = word.charAt(i) - 'a';
if (aTry.tries[c] == null) {
return false;
}
aTry = aTry.tries[c];
}
//如果isEnd不为true,说明该词只是一个前缀,也返回false
if (aTry.isEnd) {
return true;
}
return false;
}
public boolean startsWith(String prefix) {
Trie aTry = this;
for (int i = 0; i < prefix.length(); i++) {
int c = prefix.charAt(i) - 'a';
//如果不包含字符,则返回false
if (aTry.tries[c] == null) {
return false;
}
aTry = aTry.tries[c];
}
return true;
}
}