数据结构与算法实战(十二)线段树,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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
易语言官方支持库以及网上流传的很多模块中的很多命令或数据结构仅仅是实现了功能,但是并不怎么高效,再有就是有人将本来时间复杂度就高的算法使用汇编进行优化,然并卵,O(n^2)的复杂度和O(nlogn)的复杂度根本不是一个数量级的【注:这里的log以2为底数】。 举个例子:对超大数组进行排序,如果你用传统的选择排序(O(n^2))对一个超大数组进行排序,你恐怕得等几分钟,而采用二分思想的快速排序(最坏情况O(nlogn),最好情况O(n)),最好情况只需要很短时间。量化一下,假如数组内有1024个元素,传统的选择排序本来需要1024*1024=1048576次循环,而使用快速排序,最坏情况只需要1024*log1024=10240次,最好情况只需要1024次,节省了大量时间! 那么,作为一位OIer(Olympic in Informatics),决定将易库中没有的算法以及数据结构以此模块作为补充,将时间复杂度优化到最优,使你的程序运行效率更高! 当前已支持算法:快速排序、插入排序、堆排序、归并排序、取最大、取最小、向下取整、向上取整、反转字节集、反转文本、反转数组、扩展欧几里得、中国剩余定理、快速幂、线性回归相关、线性规划 当前已支持数据结构:大根堆、小根堆、并查集Trie、表达式、高精度整数(部分)、线段树、树状数组、栈 当前计划后续版本更新内容:KMP、AC自动机、红黑树、AVL、SBT、Treap、块状数组、Splay、普通二叉查找树、深度搜索框架、广度搜索框架、图论相关(方向:有向图、无向图;类型:邻接表式、邻接矩阵式;包含算法:dijkstra、floyd、SPFA、kruskal、prim、tarjan强连通分量、遍历、求哈密顿环、匈牙利算法求二分图最大匹配、连通性判断) 另外给喜爱算法的人推荐一本书:刘汝佳的《算法竞赛入门经典》,pan.baidu.com/s/1boNjXYv 密码: pr96,不过有一点,这本书不但会从0给你教算法,还会从0给你教C++。与之配套的是跟这个封皮差不多的蓝色封皮的书,里面有更加高深的算法,是这本书的延伸(这个蓝皮的我在网上找不到电子版,网上有的都是旧版,不是新版)。
12篇学通csharp网络编程——第四篇 TCP应用编程 12篇学通csharp网络编程——第三篇 HTTP应用编程(下) 12篇学通csharp网络编程——第二篇 HTTP应用编程(上) 12篇学通csharp网络编程——第一篇 基础之进程线程 Lucene(1)lucene,你也会(7篇)——第一篇 快速入门 MongoDB(8)8天学通MongoDB——第八天 驱动实践 8天学通MongoDB——第七天 运维技术 8天学通MongoDB——第六天 分片技术 8天学通MongoDB——第五天 主从复制 8天学通MongoDB——第四天 索引操作 8天学通MongoDB——第三天 细说高级操作 8天学通MongoDB——第二天 细说增删查改 8天学通MongoDB——第一天 基础入门 UML系列(4)团队沟通利器之UML——类图 团队沟通利器之UML—— 序列图 团队沟通利器之UML——用例图 团队沟通利器之UML——活动图 wcf系列(5)wcf系列学习5天速成——第五天 服务托管 wcf系列学习5天速成——第四天 wcf之分布式架构 wcf系列学习5天速成——第三天 事务的使用 wcf系列5天速成——第二天 binding的使用(2) wcf系列5天速成——第一天 binding的使用(1) wpf系列(8)8天入门wpf—— 第八天 最后的补充 8天入门wpf—— 第七天 画刷 8天入门wpf—— 第六天 细说控件 8天入门wpf—— 第五天 数据绑定 8天入门wpf—— 第四天 模板 8天入门wpf—— 第三天 样式 8天入门wpf—— 第二天 xaml详解 8天入门wpf—— 第一天 基础概念介绍 并行开发(8)8天玩转并行开发——第八天 用VS性能向导解剖你的程序 8天玩转并行开发——第七天 简要分析任务与线程池 8天玩转并行开发——第六天 异步编程模型 8天玩转并行开发——第五天 同步机制(下) 8天玩转并行开发——第四天 同步机制(上) 8天玩转并行开发——第三天 plinq的使用 8天玩转并行开发——第二天 Task的使用 8天玩转并行开发——第一天 Parallel的使用 多线程系列(5)5天不再惧怕多线程——第五天 线程池 5天不再惧怕多线程——第四天 信号量 5天不再惧怕多线程——第三天 互斥体 5天不再惧怕多线程——第二天 锁机制 5天不再惧怕多线程——第一天 尝试Thread 经典算法专题(21)经典算法题每日演练——第二十一题 十字链表 经典算法题每日演练——第二十题 三元组 经典算法题每日演练——第十九题 双端队列 经典算法题每日演练——第十八题 外排序 经典算法题每日演练——第十七题 Dijkstra算法 经典算法题每日演练——第十六题 Kruskal算法 经典算法题每日演练——第十五题 并查集 经典算法题每日演练——第十四题 Prim算法 经典算法题每日演练——第十三题 赫夫曼树 经典算法题每日演练——第十二线段树 经典算法题每日演练——第十一题 Bitmap算法 经典算法题每日演练——第十题 树状数组 经典算法题每日演练——第九题 优先队列 经典算法题每日演练——第八题 AC自动机 经典算法题每日演练——第七题 KMP算法 经典算法题每日演练——第六题 协同推荐SlopeOne 算法 经典算法题每日演练——第五题 字符串相似度 经典算法题每日演练——第四题 最长公共子序列 经典算法题每日演练——第三题 猴子吃桃 经典算法题每日演练——第二题 五家共井 经典算法题每日演练——第一题 百钱买百鸡 开发利器系列(1)介绍一个小工具 Linqer 那点所谓的分布式(2)那点所谓的分布式——memcache 那点所谓的分布式——redis 树结构专题(5)6天通吃树结构—— 第五天 Trie树 6天通吃树结构—— 第四天 伸展树 6天通吃树结构—— 第三天 Treap树 6天通吃树结构—— 第二天 平衡二叉树 6天通吃树结构—— 第一天 二叉查找树 算法速成系列(15)算法系列15天速成——第十五天 图【下】(大结局) 算法系列15天速成——第十四天 图【上】 算法系列15天速成——第十三天 树操作【下】 算法系列15天速成——第十二天 树操作【中】 算法系列15天速成——第十一天 树操作(上) 算法系列15天速成——第十天 栈 算法系列15天速成——第九天 队列 算法系列15天速成——第八天 线性表【下】 算法系列15天速成——第七天 线性表【上】 算法系列15天速成——第六天 五大经典查找【下】 算法系列15天速成——第五天 五大经典查找【中】 算法系列15天速成——第四天 五大经典查找【上】 算法系列15天速成——第三天 七大经典排序【下】 算法系列15天速成——第二天 七大经典排序【中】 算法系列15天速成——第一天 七大经典排序【上】 算法洗脑系列(8)算法洗脑系列(8篇)——第八篇 概率思想 算法洗脑系列(8篇)——第七篇 动态规划 算法洗脑系列(8篇)——第六篇 回溯思想 算法洗脑系列(8篇)——第五篇 分治思想 算法洗脑系列(8篇)——第四篇 枚举思想 算法洗脑系列(8篇)——第三篇 贪心思想 算法洗脑系列(8篇)——第二篇 递归思想 算法洗脑系列(8篇)——第一篇 递推思想 天籁数学(3)天籁数学——数列篇(3) 天籁数学——数列篇(2) 天籁数学——数列篇(1) 图形图像(1)玩玩图形图像——第一篇:图片灰度化 小爬虫系列(4)玩玩小爬虫——抓取时的几个小细节 玩玩小爬虫——抓取动态页面 玩玩小爬虫——试搭小架构 玩玩小爬虫——入门

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值