1032. 字符流

按下述要求实现 StreamChecker 类:

  • StreamChecker(words):构造函数,用给定的字词初始化数据结构。
  • query(letter):如果存在某些 k >= 1,可以用查询的最后 k个字符(按从旧到新顺序,包括刚刚查询的字母)拼写出给定字词表中的某一字词时,返回 true。否则,返回 false。
treamChecker streamChecker = new StreamChecker(["cd","f","kl"]); // 初始化字典
streamChecker.query('a');          // 返回 false
streamChecker.query('b');          // 返回 false
streamChecker.query('c');          // 返回 false
streamChecker.query('d');          // 返回 true,因为 'cd' 在字词表中
streamChecker.query('e');          // 返回 false
streamChecker.query('f');          // 返回 true,因为 'f' 在字词表中
streamChecker.query('g');          // 返回 false
streamChecker.query('h');          // 返回 false
streamChecker.query('i');          // 返回 false
streamChecker.query('j');          // 返回 false
streamChecker.query('k');          // 返回 false
streamChecker.query('l');          // 返回 true,因为 'kl' 在字词表中。

 

提示:

1 <= words.length <= 20001 <= words[i].length <= 2000


字典树Trie

字典树是一种查找树,说到查找我们很容易想起来map,接下来考虑一些情况,比如统计字符串,这些字符串有大量重复的前缀:“aaaaaapple","aaaapple","aaaamelon"..... 在数据量不大的时候我们能轻易的将它们存到map中,很方便。这时候突然要求统计前面包括4个a的字符串有多少,我们又需要将map中每个string拿出来,截前4个和aaaa比较是否相同,很不方便。

字典树,能够处理大量字符串,优点在于利用字符串的公共前缀,在存储时节约存储空间,并在查询时最大限度减少无谓的字符串比较。

性质:

根节点不包含字符,除根节点外每个子节点都包含一个字符

从根节点到某叶子节点,路径经过字符串联,就对应一个字符串

举个栗子:

下图展示了一个小字典树,包括字符串:”to","tea","ted","ten","a","inn"

应用场景:

最先想到的就是词频统计,给出一段字符,问某集合中有多少个字符串能够前缀匹配。

还有一个很经典的应用,在许多搜索框中,比如某些编辑器的控件搜索栏,我们输入前几个字符,它就会把包括这些字符的选项在下面显示出来。比如下面是vscode扩展中输入go,会自动显示go开头的选项。

字典树的节点结构不是一成不变的,字典树只是一个思想,根据不用的需求有不同的节点结构,比如我们只有查找的需求,就可以将树节点设置为下面这样:

// golang
type TrieNode struct {
    flg bool
    children map[byte]*TrieNode
}
// c++
struct TrieNode {
    bool flg;
    trieNode* children[26];
}

这时候又突然要求我们能够统计以某字符串为前缀的字符串有多少个,那么我们就得在struct中添加一个计数单位

type TrieNode struct {
    flg bool
    sum int
    children map[byte]*TrieNode
}

回到这道题。

第一步,很明显将words中的字符串保存为字典树。

第二步,要保存query的字符组成的字符串。

第三步,考虑到最后比较的时候不要做无谓的比较,记录一下书中最长的字符串长度,比较的时候从letter中最多取该长度的字符来比较。

第四步,考虑好上面三点,就能做出StreamCheker结构。

第五步,实现Query方法,查询逻辑很简单了。

之前用C++写过字典树,现在转golang了也就试着用go写,过程磕磕绊绊也算写完了,测试通过。

type TrieNode struct {
    flg bool
    child map[byte]*TrieNode
}
func newTrieNode() *TrieNode {
    return &TrieNode{flg:false, child:make(map[byte]*TrieNode)}
}

type StreamChecker struct {
    root *TrieNode
    letter []byte
    maxLen int
}

func NewChecker() StreamChecker {
    return StreamChecker{root:newTrieNode(),letter:make([]byte,0),maxLen:0}
}

func reverse(c []byte) []byte {
    for i,j:=0,len(c)-1;i<j;{
        c[i],c[j] = c[j],c[i]
        i++
        j--
    }
    return c
}

func (sc *StreamChecker)insert(s string) {
    cs := []byte(s)
	cs = reverse(cs)
    node := sc.root
    for i:=0;i<len(s);i++ {
        _,ok := node.child[cs[i]]
        if !ok {
            node.child[cs[i]] = newTrieNode()
        }
        node = node.child[cs[i]]
    }
    node.flg = true
}

func Constructor(words []string) StreamChecker {
    sc := NewChecker()
    for _,v := range words {
        sc.insert(v)
        if len(v) > sc.maxLen {
            sc.maxLen = len(v)
        }
    }
    return sc
}


func (this *StreamChecker) Query(letter byte) bool {
    this.letter = append(this.letter, letter)
    node := this.root
    for i,ml:= len(this.letter)-1,1;i>=0 && ml<=this.maxLen;i-- {
		_,ok := node.child[this.letter[i]]
        if !ok {
            return false
		}
        node = node.child[this.letter[i]]
		if node.flg {
			return true
		}
	}
    return node.flg
}

 

参考资料:

 


记录每天解决的小问题,积累起来去解决大问题