算法十 高级数据结构(Java实现)

1,083 阅读3分钟

刷题时间:2021.10.4-2021.10.10

Trie树(字典树)

trie树,又称字典树或前缀树,是一种有序的、用于统计、排序和存储字符串的数据结构,它与二叉查找树不同,关键字不是直接保存在节点中,而是由节点在树中的位置决定。

一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

trie树的最大优点就是利用字符串的公共前缀来减少存储空间与查询时间,从而最大限度地减少无谓的字符串比较,是非常高效的字符串查找数据结构。

并查集

并查集(Union Find),又称不相交集合(Disjiont Set), 它应用于N个元素的集合求并与查询问题,在该应用场景中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。虽然该问题并不复杂,但面对极大的数据量时,普通的数据结构往往无法解决,并查集就是解决该种问题最为优秀的算法。

森林实现

使用森林存储集合之间的关系,属于同一集合的不同元素,都有一个相同的根节点代表着这个集合。

当进行查找某元素属于哪个集合时,即遍历该元素到根节点,返回根节点所代表的集合;在遍历过程中使用路径压缩的优化算法,使整体树的形状更加扁平,从而优化查询的时间复杂度。

当进行合并时,即将两颗子树合为一颗树,将一颗子树的根节点指向另一颗子树的根节点;在合并时可按子树的大小,将规模较小的子树合并到规模较大的子树上,从而使树规模更加平衡,从而优化未来查询的时间复杂度。

线段树

线段树是一种平衡二叉搜索树(完全二叉树),它将一个线段区间划分成一些单元区间。对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b],最后的叶子节点数目为N,与数组下标对应。线段树的一般包括建立、查询、插入、更新等操作,建立规模为N的时间复杂度是O(NlogN),其他操作时间复杂度为O(logN)。

1、LeetCode208实现Trie (前缀树)

LeetCode官方题解

public class Trie {
    public class TrieNode{
    public int path;
    public int end;
    public HashMap<Character, TrieNode> next;

    public TrieNode(){
        path = 0;
        end = 0;
        next = new HashMap<>();
    }
}

    private TrieNode root;
    public Trie(){
        root = new TrieNode();
    }

    public void insert(String word){
        if(word == null || word.equals(""))  return ;
        TrieNode node = root;
        for(int i = 0; i<word.length(); i++){
            char ch = word.charAt(i);
            if(!node.next.containsKey(ch)) {
                node.next.put(ch,new TrieNode());
            }
            node = node.next.get(ch);
            node.path++;
        }
        node.end++;
    }

    public boolean search(String word){
        if(word == null || word.equals("")) return false;
        TrieNode node = root;
        for(int i = 0; i<word.length(); i++){
            char ch = word.charAt(i);
            if(!node.next.containsKey(ch)) return false;
            node = node.next.get(ch);
        }
        if(node.end == 0) return false;
        return true;
    }
    
    public boolean startsWith(String word){
        if(word == null || word.equals("")) return false;
        TrieNode node = root;
        for(int i = 0; i<word.length(); i++){
            char ch = word.charAt(i);
            if(!node.next.containsKey(ch)) return false;
            node = node.next.get(ch);
        }
        return true;
    }
}

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * boolean param_2 = obj.search(word);
 * boolean param_3 = obj.startsWith(prefix);
 */

2、LeetCode211添加与搜索单词 - 数据结构设计

LeetCode题解:

 // 前缀树Trie + DFS递归
class WordDictionary {

    // 前缀树节点
    private class TrieNode {
        TrieNode[] path;
        boolean end; // 是否存在以当前字符结尾的word
        public TrieNode() {
            path = new TrieNode[26]; // word中只含26个小写字母,准备26条路
        }
    }

    private TrieNode root;

    public WordDictionary() {
        root = new TrieNode();
    }

    public void addWord(String word) {
        if (word == null || word.length() == 0) return;
        // 将word挂到前缀树上:
        TrieNode cur = root;
        for (char c : word.toCharArray()) {
            // 无路建路,有路上路:
            if (cur.path[c - 'a'] == null) cur.path[c - 'a'] = new TrieNode();
            cur = cur.path[c - 'a'];
        }
        cur.end = true;
    }

    public boolean search(String word) {
        if (word == null || word.length() == 0) return false;
        return search(word.toCharArray(), 0, root);
    }

    // DFS递归
    // 当前字符:chars[i],当前来到的前缀树节点:cur
    // 返回:[i, ...]能否匹配上
    private boolean search(char[] chars, int i, TrieNode cur) {
        if (cur == null) return false;

        char curChar = chars[i];
        if (i == chars.length-1) { // 当前已是最后一个字符
            if (curChar != '.') { // 最后一个字符不是点,需要在前缀树上严格匹配(有对应的路,并且这条路上有end)
                return cur.path[curChar - 'a'] != null && cur.path[curChar - 'a'].end;
            } else { // 最后一个字符是点,只要存在一条有end的路即可匹配成功
                for (TrieNode path : cur.path) {
                    if (path != null && path.end) return true;
                }
                return false;
            }
        }
        // 当前不是最后一个字符,还有不止一个字符需要匹配:
        if (curChar != '.') { // 不是点,需要在前缀树上严格匹配(有对应的路,并且后续也能匹配上)
            return cur.path[curChar - 'a'] != null && search(chars, i+1, cur.path[curChar - 'a']);
        }
        // curChar == '.',只要存在一条路,使得后续可以匹配上即可
        for (TrieNode path : cur.path) {
            if (search(chars, i+1, path)) return true;
        }
        return false;
    }
}

3、LeetCode547省份数量

方法一:DFS(LeetCode官方题解)

遍历所有城市,对于每个城市,如果该城市尚未被访问过,则从该城市开始深度优先搜索,通过矩阵isConnected 得到与该城市直接相连的城市有哪些,这些城市和该城市属于同一个连通分量,然后对这些城市继续深度优先搜索,直到同一个连通分量的所有城市都被访问到,即可得到一个省份。遍历完全部城市以后,即可得到连通分量的总数,即省份的总数。

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int provinces = isConnected.length;
        boolean[] visited = new boolean[provinces];
        int circles = 0;
        for (int i = 0; i < provinces; i++) {
            if (!visited[i]) {
                dfs(isConnected, visited, provinces, i);
                circles++;
            }
        }
        return circles;
    }

    public void dfs(int[][] isConnected, boolean[] visited, int provinces, int i) {
        for (int j = 0; j < provinces; j++) {
            if (isConnected[i][j] == 1 && !visited[j]) {
                visited[j] = true;
                dfs(isConnected, visited, provinces, j);
            }
        }
    }
}

方法二:并查集(LeetCode官方题解)

class UnionFind {
    // 记录父节点
    private Map<Integer,Integer> father;
    // 记录集合的数量
    private int numOfSets = 0;
    
    public UnionFind() {
        father = new HashMap<Integer,Integer>();
        numOfSets = 0;
    }
    
    public void add(int x) {
        if (!father.containsKey(x)) {
            father.put(x, null);
            numOfSets++;
        }
    }
    
    public void merge(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX != rootY){
            father.put(rootX,rootY);
            numOfSets--;
        }
    }
    
    public int find(int x) {
        int root = x;
        
        while(father.get(root) != null){
            root = father.get(root);
        }
        
        while(x != root){
            int original_father = father.get(x);
            father.put(x,root);
            x = original_father;
        }
        
        return root;
    }
    
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
    
    public int getNumOfSets() {
        return numOfSets;
    }
}

class Solution {
    public int findCircleNum(int[][] isConnected) {
        UnionFind uf = new UnionFind();
        for(int i = 0;i < isConnected.length;i++){
            uf.add(i);
            for(int j = 0;j < i;j++){
                if(isConnected[i][j] == 1){
                    uf.merge(i,j);
                }
            }
        }
        
        return uf.getNumOfSets();
    }
}

4、LeetCode307区域和检索 - 数组可修改

LeetCode官方题解

class NumArray {
    int[] tree;
    int n;
    public NumArray(int[] nums) {
        if (nums.length > 0) {
            n = nums.length;
            tree = new int[n * 2];
            buildTree(nums);
        }
    }

    private void buildTree(int[] nums) {
        for (int i = n, j = 0;  i < 2 * n; i++,  j++)
            tree[i] = nums[j];
        for (int i = n - 1; i > 0; --i)
            tree[i] = tree[i * 2] + tree[i * 2 + 1];
    }
 
    void update(int pos, int val) {
        pos += n;
        tree[pos] = val;
        while (pos > 0) {
            int left = pos;
            int right = pos;
            if (pos % 2 == 0) {
                right = pos + 1;
            } else {
                left = pos - 1;
            }
            tree[pos / 2] = tree[left] + tree[right];
            pos /= 2;
        }
    }
    
    public int sumRange(int l, int r) {
        l += n;
        r += n;
        int sum = 0;
        while (l <= r) {
            if ((l % 2) == 1) {
                sum += tree[l];
                l++;
            }
            if ((r % 2) == 0) {
                sum += tree[r];
                r--;
            }
            l /= 2;
            r /= 2;
        }
        return sum;
    }
}
/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(index,val);
 * int param_2 = obj.sumRange(left,right);
 */