数据结构与算法实战(十二)线段树,Trie和并查集

线段树,Trie和并查集

线段树(区间树)Segment Tree

为什么要使用线段树

问题引出:区间染色问题

颜色可被覆盖

在这里插入图片描述

操作划分:

  • 染色操作(更新区间)
  • 查询操作(查询区间)

使用数组实现,染色和查询都是遍历数组的操作,时间复杂度都是O(n),而线段树可以更高效。

另一经典问题:区间查询

在这里插入图片描述
在这里插入图片描述

对于给定区间:

  • 更新:更新区间中一个元素或者一个区间的值
  • 查询:查询一个区间[i,j]的最大值,最小值,或者区间数字和

什么是线段树

在这里插入图片描述

以求和为例,每一个节点存储的是线段和。

线段树不是完全二叉树,但线段树是平衡二叉树。

平衡二叉树:对于整棵树,最大深度和最小深度之间的差最多为1

在这里插入图片描述

可以将它看作是满二叉树,将那些无叶子节点的根的叶子节点看作是空,故可以用数组表示。

数组表示的节点个数

在这里插入图片描述
在这里插入图片描述

我们的线段树不考虑添加元素,即区间固定,使用4n的静态空间即可。

线段树的创建

/**
 * @Auther Ycj
 * @Date 2021-01-21 9:33
 */

public class SegmentTree<E> {

    private E[] data;
    private E[] tree;
    private Merger<E> merger;

    public SegmentTree(E[] arr, Merger<E> merger){

        this.merger = merger;
        data = (E[])new Object[arr.length];
        for (int i = 0; i < data.length ; i++) {
            data[i] = arr[i];
        }
        tree = (E[])new Object[4 * arr.length];
        buildSegmentTree(0, 0, data.length - 1);
    }

    //在treeIndex的位置创建表示区间[l... r]的线段树
    private void buildSegmentTree(int treeIndex, int l, int r){

        if(l == r){
            tree[treeIndex] = data[l];
            return;
        }

        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);

        int mid = l + (r - l) / 2;
        buildSegmentTree(leftTreeIndex, l, mid);
        buildSegmentTree(rightTreeIndex, mid + 1, r);

        //业务逻辑部分
        tree[treeIndex] = merger.merge(tree[leftTreeIndex] , tree[rightTreeIndex]);
    }

    public E get(int index){

        if(index < 0 || index >= data.length)
            throw new IllegalArgumentException("Index is illegal");
        return data[index];
    }

    public int getSize(){
        return data.length;
    }

    //返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){

        return index * 2 + 1;
    }

    //返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){

        return index * 2 + 2;
    }

    @Override
    public String toString() {

        StringBuilder res = new StringBuilder();
        res.append('[');
        for (int i = 0; i < tree.length; i++) {
            if(tree[i] != null)
                res.append(tree[i]);
            else
                res.append("null");
            if(i != tree.length - 1)
                res.append(",  ");
        }
        res.append(']');
        return res.toString();
    }
}

public interface Merger<E> {
    E merge(E a, E b);
}
/**
 * @Auther Ycj
 * @Date 2021-01-21 11:06
 */

public class Main {

    public static void main(String[] args) {

        Integer[] nums = {-2, 0, 3, -5, 2, -1};
        SegmentTree<Integer> segmentTree = new SegmentTree<>(nums, (a,b) -> a + b );

        System.out.println(segmentTree);
    }
}

在这里插入图片描述

线段树的查询

//返回区间[querL, querR]的值
public E query(int queryL, int queryR){

    if(queryL < 0 || queryL >= data.length
            || queryR < 0 || queryR >= data.length || queryL > queryR)
        throw new IllegalArgumentException("Index is illegal");

    return query(0, 0, data.length - 1, queryL, queryR);
}
//在以treeIndex为根的线段树中[l....r]的范围里,搜索区间[queryL ... queryR]的值
private E query(int treeIndex, int l, int r, int queryL, int queryR){

    if(l == queryL && r == queryR){
        return tree[treeIndex];
    }
    int mid = l + (r - l) / 2;
    int leftTreeIndex = leftChild(treeIndex);
    int rightTreeIndex = rightChild(treeIndex);

    if(queryL >= mid + 1)
        return query(rightTreeIndex,mid + 1, r, queryL,queryR);
    else if(queryR <= mid)
        return query(leftTreeIndex, l, mid, queryL, queryR);

    //区间有一部分在左节点,一部分在右节点
    E leftRes = query(leftTreeIndex, l, mid, queryL, mid);
    E rightRes = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);

    return merger.merge(leftRes,rightRes);
}

测试:

在这里插入图片描述

线段树的更新

//将index位置的值,更新为e
public void set(int index, E e){

    if(index < 0 || index >= data.length)
        throw new IllegalArgumentException("Index is illegal");
    data[index] = e;
    set(0, 0, data.length - 1, index, e);
}
//在以treeIndex为根的线段树中更新index位置的值为e
private void set(int treeIndex, int l, int r, int index, E e){

    if(l == r){
        tree[treeIndex] = e;
        return;
    }
    int mid = l + (r - l) / 2;
    int leftTreeIndex = leftChild(treeIndex);
    int rightTreeIndex = rightChild(treeIndex);
    if(index >= mid + 1)
        set(rightTreeIndex, mid + 1, r, index, e);
    else
        set(leftTreeIndex, l, mid, index, e);

    tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}

字典树(前缀树)Trie

什么是Trie

在这里插入图片描述

常用于对单词(字符串)进行操作

例如查询单词:cat,dog,deer,panda

在这里插入图片描述

  • 每个节点有若干个指向下个节点的指针

  • 考虑不同的语言,不同的情景,比如对于区分大小写的情况,要包含52个指针

  • class Node{
    	char c;//可以省略
        boolean isWord;//当前节点是否代表为一个单词的结尾
    	Map<char, Node> next;
      
    }
    

代码实现

import java.util.TreeMap;

/**
 * @Auther Ycj
 * @Date 2021-01-23 9:20
 */

public class Trie {

    private class Node{

        public boolean isWord;
        public TreeMap<Character, Node> next;

        public Node(boolean isWord){
            this.isWord = isWord;
            next = new TreeMap<>();
        }

        public Node(){
            this(false);
        }
    }

    private Node root;
    private int size;

    public Trie(){
        root = new Node();
        size = 0;
    }

    //获取Trie中存储的单词数量
    public int getSize() {
        return size;
    }

    //向Trie中新添加一个单词word
    public void add(String word){

        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if(cur.next.get(c) == null)
                cur.next.put(c, new Node());
            cur = cur.next.get(c);
        }
        if(!cur.isWord){
            cur.isWord = true;
            size ++;
        }
    }

    //查询单词word是否在Trie中
    public boolean contains(String word){

        Node cur = root;
        for (int i = 0; i < word.length(); i++) {

            char c = word.charAt(i);
            if(cur.next.get(c) == null)
                return false;
            cur = cur.next.get(c);
        }
        return cur.isWord;
    }

    //在Trie中查找是否有以单词prefix为前缀
    public boolean isPrefix(String prefix){

        Node cur = root;
        for (int i = 0; i < prefix.length(); i++) {
            char c = prefix.charAt(i);
            if(cur.next.get(c) == null)
                return false;
            cur = cur.next.get(c);
        }
        return true;
    }
}



并查集

可以高效解决节点之间的连接问题

连接问题

在这里插入图片描述

并查集Union Find

对于一组数据,主要支持两个动作:

  • union(p , q):在并查集内部将两个数据以及它们所在的集合进行合并
  • isConnected(p , q):对于给定的两个数据是否属于同一个集合
public interface UF {
    
    //不考虑添加和删除元素
    
    boolean isConnected(int p, int q);
    
    void unionElements(int p, int q);
    
    int getSize();
}

Quick Find

在这里插入图片描述

isConnected(p, q) ----> find§ == find(q)

Quick Find 时间复杂度为O(1)

Quick Find 下的Union

例如union(1, 4)

会将两个集合的所有元素都相连,意味着属于同一个集合

在这里插入图片描述

/**
 * @Auther Ycj
 * @Date 2021-01-24 9:02
 */
//Quick Find
public class UnionFind1 implements UF {

    private int[] id;

    public UnionFind1(int size){
        id = new int[size];

        for (int i = 0; i < id.length; i++) {
            id[i] = i;
        }
    }

    //查找元素p所对应的集合编号
    private int find(int p){

        if(p < 0 && p >= id.length)
            throw new IllegalArgumentException("p is out of bound");
        return id[p];
    }

    //查看p和q是否属于同一个集合
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    //合并p和q所属集合
    @Override
    public void unionElements(int p, int q) {
    
        int pId = find(p);
        int qId = find(q);
        if(pId == qId)
            return;
        for (int i = 0; i < id.length; i++) {
            if(id[i] == pId)
                id[i] = qId;
        }
    }

    @Override
    public int getSize() {
        return id.length;
    }
}

在这里插入图片描述

Quick Union

将每一个元素,看做是一个节点

树的结构为:孩子指向父亲

在这里插入图片描述

/**
 * @Auther Ycj
 * @Date 2021-01-24 9:43
 */

//Quick Union
public class UnionFind2 implements UF{
   
   private int[] parent;
   
   public UnionFind2(int size){
       
       parent = new int[size];
       //初始化时,每个节点都指向自己
       for (int i = 0; i < size; i++) {
           parent[i] = i;
       }
   }
   
   //查找过程,查找元素p所对应的集合编号
    //O(h)复杂度,h为树的高度
   private int find(int p){

       if(p < 0 && p >= parent.length)
           throw new IllegalArgumentException("p is out of bound");
       while (p != parent[p])
           p = parent[p];
       return p;
   }

    //O(h)复杂度,h为树的高度
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    //O(h)复杂度,h为树的高度
    @Override
    public void unionElements(int p, int q) {

       int pRoot = find(p);
       int qRoot = find(q);
       if(pRoot == qRoot)
           return;
       parent[pRoot] = qRoot;
    }

    @Override
    public int getSize() {
        return parent.length;
    }
}

基于size的优化

/**
 * @Auther Ycj
 * @Date 2021-01-24 10:02
 */

//基于size优化
public class UnionFind3 implements UF{

    private int[] parent;
    private int[] sz; //sz[i]表示以i为根的集合中元素的个数

    public UnionFind3(int size){

        parent = new int[size];
        //初始化时,每个节点都指向自己
        for (int i = 0; i < size; i++) {
            parent[i] = i;
            sz[i] = 1;
        }
    }

    //查找过程,查找元素p所对应的集合编号
    //O(h)复杂度,h为树的高度
    private int find(int p){

        if(p < 0 && p >= parent.length)
            throw new IllegalArgumentException("p is out of bound");
        while (p != parent[p])
            p = parent[p];
        return p;
    }

    //O(h)复杂度,h为树的高度
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    //O(h)复杂度,h为树的高度
    @Override
    public void unionElements(int p, int q) {

        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot)
            return;
        //根据两个元素所在树的元素个数不同判断合并方向
        //将元素个数少的集合合并到元素个数多的集合上
        if(sz[pRoot] < sz[qRoot]){
            parent[pRoot] = qRoot;
            sz[qRoot] += sz[qRoot];
        }else{
            parent[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }

    @Override
    public int getSize() {
        return parent.length;
    }
}

基于rank的优化

rank[i] 表示根节点为i的树的高度

/**
 * @Auther Ycj
 * @Date 2021-01-24 10:28
 */

//基于rank优化
public class UnionFind4 implements UF{

    private int[] parent;
    private int[] rank; //rank[i] 表示根节点为i的集合所表示的树的层数

    public UnionFind4(int size){

        parent = new int[size];
        //初始化时,每个节点都指向自己
        for (int i = 0; i < size; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

    //查找过程,查找元素p所对应的集合编号
    //O(h)复杂度,h为树的高度
    private int find(int p){

        if(p < 0 && p >= parent.length)
            throw new IllegalArgumentException("p is out of bound");
        while (p != parent[p])
            p = parent[p];
        return p;
    }

    //O(h)复杂度,h为树的高度
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    //O(h)复杂度,h为树的高度
    @Override
    public void unionElements(int p, int q) {

        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot)
            return;
        //根据两个元素所在树的rank不同判断合并方向
        //将rank低的集合合并到rank高的集合上
        if(rank[pRoot] < rank[qRoot]){
            parent[pRoot] = qRoot;
        }else if(rank[pRoot] > rank[qRoot])
            parent[qRoot] = pRoot;
        else{
            parent[qRoot] = pRoot;
            rank[pRoot] += 1;
        }
    }

    @Override
    public int getSize() {
        return parent.length;
    }
}

路径压缩

在这里插入图片描述

修改find方法

//查找过程,查找元素p所对应的集合编号
//O(h)复杂度,h为树的高度
private int find(int p){

    if(p < 0 && p >= parent.length)
        throw new IllegalArgumentException("p is out of bound");
    while (p != parent[p]){
        
        parent[p] = parent[parent[p]];
        p = parent[p];
    }
    return p;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值