字典树(Trie树)

Posted 文大侠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字典树(Trie树)相关的知识,希望对你有一定的参考价值。

字典树主要是为了解决前缀匹配问题,比如下图的搜索输入前缀后匹配

比如有6个字符串how,hi,her,hello,so,see,如果现在输入字符判断是否要前缀匹配指定字符串,必须一个字符串比较,如果有100万就要比对100万次,如何优化这个问题呢?

其实我们可以理解,每输入一个字符,应该整个搜索空间就会缩小,具体来说就是构造一颗如下的树,就是所谓的字典树(Trie树),一旦构建完成,搜索过程中,即可完成动态剪裁,最大搜索次数就是最长字符串长度。

具体实现来说就是一颗多叉树,可以使用hashmap或者数组保存子节点指针。

如下是最常见的实现,对于确定的26个字母构成单词的trie树,可通过下标转换快速定位

public class Trie {
    public class TrieNode {
        public char data;
        public TrieNode[] link = new TrieNode[26];// 多叉树存储所有下级子节点(26个字母)
        public boolean isEndingChar = false; // 标记当前点是否为字符串结束点,判断是否完全匹配

        public TrieNode(char c) {
            this.data = c;
        }
    }

    TrieNode root;

    /**
     * Initialize your data structure here.
     */
    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 linki = word.charAt(i) - 'a';
            if (p.link[linki] == null) {// 不存在
                p.link[linki] = new TrieNode(word.charAt(i));
            }

            p = p.link[linki];
        }

        p.isEndingChar = true;
    }

    /**
     * Returns if the word is in the trie.
     */
    public boolean search(String word) {
        TrieNode p = root;

        for (int i = 0; i < word.length(); i++) {
            int linki = word.charAt(i) - 'a';
            if (p.link[linki] == null) {// 不存在
                return false;
            }

            p = p.link[linki];
        }

        return p.isEndingChar; // true 完全匹配/ false 前缀匹配,不是完全匹配
    }

    /**
     * Returns if there is any word in the trie that starts with the given prefix.
     */
    public boolean startsWith(String prefix) {
        TrieNode p = root;

        for (int i = 0; i < prefix.length(); i++) {
            int linki = prefix.charAt(i) - 'a';
            if (p.link[linki] == null) {// 不存在
                return false;
            }

            p = p.link[linki];
        }

        return true;
    }

    /**
     * 获取指定前缀匹配的列表
     *
     * @param prefix
     * @return
     */
    public String[] getPrefixList(String prefix) {
        ArrayList<String> ret = new ArrayList<>();

        TrieNode p = root;

        for (int i = 0; i < prefix.length(); i++) {
            int linki = prefix.charAt(i) - 'a';
            if (p.link[linki] == null) {// 不存在
                return ret.toArray(new String[]{});
            }

            p = p.link[linki];
        }

        getFullStringFromNode(ret, prefix, p);
        return ret.toArray(new String[]{});
    }

    public void getFullStringFromNode(ArrayList<String> ret, String prefix, TrieNode p) {
        if (p.isEndingChar) {
            ret.add(prefix);
        }

        for (int i = 0; i < p.link.length; i++) {
            if (p.link[i] != null) {
                getFullStringFromNode(ret, prefix + p.link[i].data, p.link[i]);
            }
        }
    }
}

这里有很多优化空间,比如不是26个字母时如何处理,通常采用hashmap/treemap/skiplist都可以来优化空点带来的额外开销,比如使用hashmap优化这个过程,思想都差不多

type TrieNode struct {
	data         byte
	link         map[byte]*TrieNode // 多叉树存储所有下级子节点(26个字母)
	isEndingChar bool               // 标记当前点是否为字符串结束点,判断是否完全匹配
}

func getTrieNode(c byte) *TrieNode {
	return &TrieNode{c, make(map[byte]*TrieNode), false}
}

type Trie struct {
	root *TrieNode
}

/** Initialize your data structure here. */
func Constructor() Trie {
	return Trie{getTrieNode('/')}
}

/** Inserts a word into the trie. */
func (this *Trie) Insert(word string) {
	p := this.root

	for _, v := range []byte(word) {
		if _, ok := p.link[v]; !ok { // 不存在
			p.link[v] = getTrieNode(v)
		}

		p = p.link[v]
	}

	p.isEndingChar = true
}

/** Returns if the word is in the trie. */
func (this *Trie) Search(word string) bool {
	p := this.root

	for _, v := range []byte(word) { // 不存在
		if _, ok := p.link[v]; !ok {
			return false
		}

		p = p.link[v]
	}

	return p.isEndingChar
}

/** Returns if there is any word in the trie that starts with the given prefix. */
func (this *Trie) StartsWith(prefix string) bool {
	p := this.root

	for _, v := range []byte(prefix) {
		if _, ok := p.link[v]; !ok { // 不存在
			return false
		}

		p = p.link[v]
	}

	return true
}

func (this *Trie) GetPrefixList(prefix string) []string {
	ret := make([]string, 0)

	p := this.root

	for _, v := range []byte(prefix) {
		if _, ok := p.link[v]; !ok { // 不存在
			return ret
		}

		p = p.link[v]
	}

	// 匹配前缀后遍历输出全部匹配
	var getFullStringFromNode func(prefix string, p *TrieNode)
	getFullStringFromNode = func(prefix string, p *TrieNode) {
		if p.isEndingChar {
			ret = append(ret, prefix)
		}

		for _, v := range p.link {
			getFullStringFromNode(fmt.Sprintf("%s%c", prefix, v.data), v)
		}
	}

	getFullStringFromNode(prefix, p)
	return ret
}
  • 源代码位置 https://github.com/JimWen/go-algo/tree/main/trie

原创,转载请注明来自

以上是关于字典树(Trie树)的主要内容,如果未能解决你的问题,请参考以下文章

trie树(字典树)

前缀树(字典树/Trie) -----Java实现

数据结构——trie树(字典树)

Trie树(字典树)

LeetCode 208. 实现 Trie (前缀树)

01字典树