打败算法 —— 单词的压缩编码
本文参考
出自LeetCode上的题库 —— 单词的压缩编码,这道题原本是通过字典树或是字典序的正序和逆序排序来求解,不过我在评论区看到有大大提出用"桶排序"求解,因此本篇文章讲一下下这个问题的桶排序解法
https://leetcode-cn.com/problems/short-encoding-of-words/
单词的压缩编码问题
给定一个单词列表,我们将这个列表编码成一个索引字符串 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]
桶排序简介
百度百科上对桶排序有一个"浓缩"的介绍 —— 桶排序 (Bucket sort)或所谓的箱排序,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
那么,最开始是如何将数组中的各个元素放到对应的桶里呢?这里放桶的方式有很多种,例如数组中元素大小介于1~ 100之间,可以定义5个桶,每个桶对应的区间大小分别为1 ~ 20(第一个桶),21 ~ 40,41 ~ 60,61 ~ 80,81 ~ 100(最后一个桶),我们在遍历数组的元素时,将该元素放到对应区间的桶内,同时保证每个桶内的元素也是有序的。所有元素放入桶后,我们遍历每个桶和每个桶内的元素,即可获得排序好的数组元素
当然每个桶对应的也可以不是一个区间,而是一个具体的数值或字符,在上例中我们也可以定义101个桶(下标0 ~ 100),此时只要通过每个桶的计数值加1就能完成排序
更加详细的介绍可以参考下面两篇文章:
https://www.cnblogs.com/bqwzx/p/11029264.html
https://zhuanlan.zhihu.com/p/54998028
解题思路
如果单词A是另一个单词B的后缀,就可以把单词A合并到单词B中。在字典树算法中,每一个节点都代表一个字符(除根节点,根节点为"空"),我们在遍历每一个单词时进行深度优先搜索,若可以从根节点出发找到一条路径,路径上的节点的字符能够构成这个单词,不需要新建新的节点,那么这个单词是另外一个单词的后缀,具体参考LeetCode上@Sweetiee的题解
与字典树算法截然不同的是,在桶排序算法中,我们根据单词最后一个字符放入对应的桶内,再对桶内的单词进行字典序逆序排序,感谢@LinSen的解惑
字典树解法
def minimumLengthEncoding(words: Array[String]): Int = {
/*
* 先按长度排序,再将每个单词进行字符反转
*/
val sortedWords = words.sortBy(_.length)(Ordering[Int].reverse).map(word => word.reverse)
/*
* TrieNode的数据成员为:var nodeValue: Char 和 var children = new Array[TrieNode](26)
*/
val root = new TrieNode(' ')
var sum = 0
for (word <- sortedWords) {
/*
* 判断是否是新单词后缀
*/
var flag = false
var node = root
for (char <- word) {
if (node.children(char - 'a') == null) {
flag = true
node.children(char - 'a') = new TrieNode(char)
}
node = node.children(char - 'a')
}
if (flag) sum += word.length + 1
}
sum
}
桶排序解法
def minimumLengthEncoding(words: Array[String]): Int = {
val bucket = new Array[List[String]](26)
/*
* 将每个元素逆序后放入桶中(未排序)
*/
for (word <- words) {
val i = word(word.length - 1) - 'a'
if (bucket(i) == null) bucket(i) = Nil
bucket(i) = List(word.reverse) ::: bucket(i)
}
/*
* 对每个桶进行排序
*/
for (i <- bucket.indices) {
if (bucket(i) != null)
bucket(i) = bucket(i).sorted
}
var sum = 0
for (i <- bucket.indices) {
if (bucket(i) != null) {
for (j <- bucket(i).indices) {
/*
* 若当前元素是桶中的最后一个元素,直接加上它的长度
* 否则判断是否是下一个元素的前缀
*/
if (j == bucket(i).length - 1 || !bucket(i)(j + 1).startsWith(bucket(i)(j))) {
sum += bucket(i)(j).length + 1
}
}
}
}
sum
}
测试用例(100个字符串):
"gtgwzg","bgmwmrk","nqslwdi","nwsfvi","ixfez","muovikm","cfxptlx","nffdyw",
"zrmtvv","odmhe","btupmf","sjfmx","pytwab","kznqxp","jngry","ppivkj","bwwmqpq","lxbnu",
"altks","motdd","jimgy","lppjek","kbanc","lxtgvb","uqvvek","ntpxnyn","qlrdcx","xcmgzwt",
"gtcapjg","sntqu","tkfwow","xqbja","fyqbiw","ruawk","frjdyp","txknwrh","kzyjg","bttxz",
"lgntv","ewfxgz","lchzsg","yqfoa","zhsbm","htxcg","qjqkxou","gkcxv","lhsjs","igrtnjv",
"ifuecww","slzcs","yceue","retyxs","klybm","jbxjv","erhosw","bjhjpjr","nvwkcq","mezursm",
"ykbvin","xzlij","uiopt","zyuxddz","rmfhp","xfltr","csluqps","gzuvj","oyqyjy","lgjuw",
"hytegp","gkoxj","boirzbg","dsqre","gxrgabo","jdlab","kchijrb","kuozwmp","vrjqov","hfmehfl",
"xkonfn","yfhkp","ocota","akfao","qllffp","etrpndt","nrnmeh","kaemhl","diqeja","wxclkjl","ypprher"
"bggfny","krvmmx","wofbj","dliqwvn","fcihtkt","fonqx","irawity","kkmlx","gjmshvq","llcov","vyqbaz"
输出:
688