前缀树

本文最后更新于 2024年6月25日 晚上

前缀树

前缀树(Trie)是一种用于字符串检索的树形结构,利用字符串的共同前缀达到节省空间的目的。它的查找效率优于二分查找,查找时间和字符串的字符数有关。

前缀树用边来代表字符,用节点来标记字符串的结束。基本操作有插入、查找、寻找前缀。

用树节点实现前缀树

Node 节点

private static class Node {
	// isEnd 用作字符串结束标记
    boolean isEnd;
	// 节点持有一个容量为 26 的数组,代表 26 个英文字符
    Node[] next = new Node[26];
}

如果不使用 isEnd 作为结束标记,当同时出现 app 和 apple 两个单词,我们可以通过没有后续节点判断 apple 存在,但因为 app 单词有后续节点,所以无法判断 app 单词是不是存在。使用 isEnd 可以在 p 这个边所指向的节点处标记 isEnd = true 来作为存在 app 的依据。

插入字符串。

public void insert(String word) {
    Node node = head;
	// 遍历字符
    for (int i = 0; i < word.length(); i++) {
        int idx = word.charAt(i) - 'a';
		// 当代表字符的边不存在时,新建节点,并形成对应的边
        if (node.next[idx] == null) {
            node.next[idx] = new Node();
        } 
        node = node.next[idx];
    }
	// 在最后一个节点标记字符串结束
    node.isEnd = true;
}

查找字符串。遍历字符串,按照字符映射的边沿着树向下查找,如果找不到对应的边,说明树中没有这个字符串。遍历完毕后,如果最后一个节点有结束标记,可以判断字符串是在树内。

public boolean search(String word) {
    Node node = head;
    for (int i = 0; i < word.length(); i++) {
        int idx = word.charAt(i) - 'a';
		// 如果字符对应的边不存在,说明树中没有这个字符串
        if (node.next[idx] == null) {
            return false;
        } else {
            node = node.next[idx];
        }
    }
    return node.isEnd;
}

查找前缀。大体和查找完整字符串一致,区别在遍历完给定的前缀后,无须判断是不是存在结束标记,直接返回存在前缀。

public boolean startsWith(String prefix) {
    Node node = head;
    for (int i = 0; i < prefix.length(); i++) {
        int idx = prefix.charAt(i) - 'a';
		// 如果字符对应的边不存在,说明树中没有这个字符串
        if (node.next[idx] == null) {
            return false;
        } else {
            node = node.next[idx];
        }
    }
	// 因为是查找前缀,所以不用判断结束标记
    return true;
}

完整代码


class Trie {

    private static class Node {
        boolean isEnd;
        Node[] next = new Node[26];
    }

    private Node head;

    public Trie() {
        head = new Node();
    }
    

    
    public boolean search(String word) {
        Node node = head;
        for (int i = 0; i < word.length(); i++) {
            int idx = word.charAt(i) - 'a';
            if (node.next[idx] == null) {
                return false;
            } else {
                node = node.next[idx];
            }
        }
        return node.isEnd;
    }
    
    public boolean startsWith(String prefix) {
        Node node = head;
        for (int i = 0; i < prefix.length(); i++) {
            int idx = prefix.charAt(i) - 'a';
            if (node.next[idx] == null) {
                return false;
            } else {
                node = node.next[idx];
            }
        }
        return true;
    }
}

用二维数组实现前缀树

通过节点实现的前缀树中,每个节点都持有一个容量 26 的数组,每次需要新节点时会创建新的对象。

可以把这些数组全都提前用二维数组创建好,然后使用行号作为指向下一个节点的指针。

二维数组实现前缀树相对于树节点来说并没有任何优势,主要是解题的时候写起来飞快。

class Trie {
	// 新节点的指针
    private int spaceIndex = 0;
	// 提前开好二维数组,每一行都是一个前缀树节点
    private int[][] tr = new int[200100][26];
	// 每一行的结束标记
    private int[] end = new int[200100];

    public Trie() {
        
    }
    
    public void insert(String word) {
        int node = 0;
        for (int i = 0; i < word.length(); i++) {
            int idx = word.charAt(i) - 'a';
			// 如果为 0 表示指针是空,指向新的节点
            if (tr[node][idx] == 0) {
                tr[node][idx] = ++spaceIndex;
            }
			// 指针移动到新的节点上
            node = tr[node][idx];
        }
        end[node]++;
    }
    
    public boolean search(String word) {
        int node = 0;
        for (int i = 0; i < word.length(); i++) {
            int idx = word.charAt(i) - 'a';
            node = tr[node][idx];
            if (node == 0) {
                return false;
            } 
        }
        return end[node] > 0;
    }
    
    public boolean startsWith(String prefix) {
        int node = 0;
        for (int i = 0; i < prefix.length(); i++) {
            int idx = prefix.charAt(i) - 'a';
            node = tr[node][idx];
            if (node == 0) {
                return false;
            } 
        }
        return true;
    }
}

使用数组构建前缀树唯一要注意的是,需要预估判题时加入树中的字符串数量来设定二维数组的行数,即预设的节点数量。


前缀树
https://blog.avezah.com/tech/20230815-trie/
作者
avezah
发布于
2023年8月15日
许可协议