zoukankan      html  css  js  c++  java
  • qbxt Day 2 笔记 数据结构

    Day 2 笔记 数据结构

    1.栈、队列、链表等数据结构都是线性数据结构
    2.树状数据结构:二叉堆,线段树,树状数组,并查集,st表...

    优先队列其实与二叉堆的存储方式并不相同。


    一、二叉堆

    1.二叉堆的基本功能:

    1.插入元素:O(logn)
    2.查找元素:O(logn)
    3.删除元素:O(logn)
    4.输出堆:O(n)

    2.完全二叉树(堆)定义:

    所有的叶子节点比非叶子结点的编号小(放左不放右)

    3.二叉树存储:

    向量存储:根节点是x,左儿子是2x,右儿子是2x+1.
    为什么?请看下面的图:

    做个数组就好了嘛。

    4.二叉堆具有堆序性质:

    常见性质:
    大根堆:父亲节点的权值永远要比儿子节点的权值要大。
    所以:儿子结点作为子树来说是最大的。+(顶部最大)
    小根堆:父亲节点的权值永远要比儿子节点的权值要小。
    所以:儿子结点作为子树来说是最小的。+(顶部最小)
    如果要找最大、最小的值,只需要返回heap[1]的值。

    5.维护方式:

    不停地上浮(上滤)、下沉(下滤)(swap+向量存储实现)(O(logn))
    写一个代码实现方式吧。

    //例子就用小根堆了
    void update(int n)
    {//上浮
    	while (n!=1)
        {
    		if (heap[n>>1].num<=heap[n].num)break;
            //已经满足堆序性质
            swap(heap[n>>1].num,heap[n].num);
            n<<=1;//继续检查上一层的节点是否满足堆序性质
        }
    }
    

    下沉的时候找一个最满足堆序性质的儿子。

    void downdate(int m)
    {//下沉
        while ((m<<1)<=n)//还不是叶子节点
        {
        	int y=(heap[m<<1].num<=heap[(m<<1)+1])?(m<<1):(m<<1)+1;
            //让y成为小儿子的下标
            if (heap[x].num<=heap[y].num)break;
            //满足堆序性质,就退出while
            swap(heap[x].num,heap[y].num);
            x=y;
            //继续向下检查堆序性质,直到成为叶节点或者break出来
        }
    }
    

    6.建堆、删除元素方法:

    再放入(删除)元素的同时维护一遍即可。
    删除的时候是把最后一个元素放在堆顶并维护。

    7.删除任意元素(假的):

    查找到元素,删掉他,换成它作为堆顶的子堆的最后一个元素。
    但是,这种情况会使二叉堆不满足二叉树的性质。
    整体移动子树? 不存在的(时间伤不起)
    放个表示不存在的bool变量表示它不存在就好了...
    代码(结构体方式维护。)

    二、二叉搜索树

    1.定义:

    它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
    ——摘自百度百科。

    问题:这玩意有什么用?

    2.作用:

    返回这玩意查找的数据的位置。(废话)
    比如第几小的整数,只需要搜一条链就好了。(o(logn))

    3.拓展:

    1.首先他的思想非常重要,OI考试基本必考
    2.有一些基于二叉搜索树的数据结构

    三、线段树

    1.定义:

    中文名 线段树
    外文名 Segment Tree
    功 能 单点、区间的修改、查询
    时间复杂度 log(n)(建树为nlogn)
    学 科 数据结构
    领 域 数据结构

    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
    对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
    ——来自百度百科。

    这一大坨什么玩意?

    举个例子。

    二分18,得到14,再次分割,直到只剩点(区间端点重合、叶节点)为止。
    也就是说线段树里面的元素都是一些线段,叶子节点都是点(长度为0的线段)!

    所以到底有什么用处?

    没有其他数据的线段树就是一条咸鱼。
    其实可以想象一下让你求一个线段的最大值,朴素O(n),因为线段树树高不超过logn,所以求最大值也变成了O(logn)! 优秀啊...
    其他的复杂度可以类比,就是建树过程变成O(nlogn)。
    关键还在于强制在线的时候,支持在线修改,真是方便。
    但是,缺点也很明显,没有优化的线段树需要四倍空间存储,MLE预警。

    食用方法如下:

    1. 在每一个节点处设置一个sum或者min或max或bool的recover,用来求对应的:线段和、最小值、最大值、覆盖情况(求线段长度)。在插入、删除的时候维护上述的值,可以得到O(logn)的算法。
    2. 但是,如果sum被全部遍历,算法会退化到O(n),解决方法是打标记法(Lazy思想),对于整个节点修改不是真正执行,但是保存操作的+/-的值,在查询(或者进行其他影响到节点结果的操作的时候再往下分割执行。

    具体问题具体分析,还是要回归他支持的操作上(全部在线)
    1.插入元素n
    2.删除元素n
    3.输出区间l,r的最大值min
    4.输出区间l,r的最小值max
    5.其他用途(例如:输出某一段区间的覆盖情况等)

    还是给一个代码样例,防止考试忘记线段树板子...

    1.建树如何实现?

    首先我们会得到一段区间,这一段区间可能有初始值,
    假设线段点初始值存在a[ ]数组里的话。

    int a[]={.........};//原先的线段初始化
    void build_tree(int rt,int l,int r)//是根、左节点、右节点
    {//建树操作
    	segment[rt].l=l,segment[rt].r=r;
        //先把这个节点的左右端点放进去
    	if (l==r)
        {
    		segment[rt]=a[l];return;
        }
        //按照预想,在叶子节点即点上,会有初始化的数据,拿过来用就行
    	int m=(l+r)>>1;//得到线段中点
    	build_tree(rt<<1,l,m);//建左子树,其实就是二分思想
    	build_tree((rt<<1)+1,m+1,r);//建右子树,二分思想*2
    	segment[rt].min=min(segment[rt<<1].min,segment[(rt<<1)+1].min);
        //维护的最小值,按照需求选择是否加上
    	segment[rt].max=max(segment[rt<<1].max,segment[(rt<<1)+1].max);
        //维护的最大值,选择同上
    	segment[rt].sum=segment[rt<<1].sum+segment[(rt<<1)+1].sum;
        //维护的和,选择同上
    	segment[rt].state_=true;
        //维护的存在性,选择同上
        //以上四个维护任选其一,按图索骥,要啥就维护啥。
    }
    

    (原先的代码因为猝不及防的if虚悬问题RE了...)
    代码也很好理解,就是先递归分解,直到得到线段的值,然后回溯回来,就得到维护的值,然后就...然后就...然后就...最后建好了。(因为树高是logn,节点遍历次数是n,维护操作是O(1),所以整个建树操作就是O((n×logn×1))=O((nlogn)))。

    2.单点查询和修改

    既然我们要避免以上所说的维护O(n)的复杂度,我们引入一个新的变量tag表示标记,如果是最大最小值或者存在性问题,可以设置成bool变量,如果是求和问题,可以设置成整形变量。
    标记的好处就在于你不用非得全部遍历,就按照需要打标记就可以,甚至可以反复打标记,大大优化了原先的算法。
    所以我们还要有一个在操作过程中下传标记的算法

    • 1.操作到了就不用标记了,所以清空标记
    • 2.向下传递,传递的过程如果并不需要继续操作,就又懒上了

    然后也这就是优化的进一步推论。
    代码实现甚至不需要进一步递归!(Lazy真香啊)Code:

    void pushdown(int p)
    {//实行对p节点的下传标记
    	if (segment[p].l==segment[p].r)
        {//叶子节点往哪里传?就自己吃了标记算了
        	if (segment[p].tag==0)return;//没标记传个啥?直接杀掉
        	segment[p].sum+=tag;//比如求和
            segment[p].tag=0;//清除这个标记,用完了,卸磨杀驴
        }
        if (segment[p].tag==0)return;//没标记传个啥?直接杀掉
    	segment[p].sum+=(segment[p].l+segment[p].r)*tag;//比如维护和的操作
    	segment[p<<1].tag=segment[p].tag;//传输左儿子
    	if ((p<<1)+1<=n)segment[(p<<1)+1].tag=segment[p].tag;
        //存在右儿子也传过去
        segment[p].tag=0;//干掉这个标记,用完了,卸磨杀驴
    }
    

    所以单点查询的时候就杀一批驴,然后找找区间,杀掉的驴的个数是logn只,所以将查询的复杂度优化到了O(logn)
    综上所述(查询的铺垫),查询操作Code:

    int ask_(int p)//2
    {//单点查询状态,查询sum
    	if (segment[p].l==segment.r)return segment[p].sum;
        //如果是叶子节点,就是单点,就返回sum的值
    	pushdown_(p);//先杀驴,再访问
    	int m=(segment[p].l+segment[p].r)>>1;//中间节点
    	if (p<=m)return ask_(p<<1);//驴在左孩子家里
    	else return ans_((p<<1)+1);//驴在右孩子家里
    } 
    

    单点修改也差不多,基本思想就是找孩子(单点) ,然后一路杀驴,就是在向上的时候不需要返回值,只是再次更新一下维护的值
    杀掉的驴还是logn只,维护的值与其同时进行(同一层栈),所以时间复杂度还是O(logn)
    综上所述(修改的铺垫),单改Code:

    void change_p(int p,int x,int num)//单点修改找孩子 
    {//
        if(segment[p].l==segment[p].r)//找到x了(叶子节点)
        {
            segment[p].num+=num;//修改操作在这里
            return;//可以停下回去更新了
        }
        if(segment[p].f!=segment[p].r) pushdown_(p);//不是x节点,就杀驴去(更新)
        int m=(segment[p].l+segment[p].r)>>1;//取个中点找孩子去
        if(x<=m) change_p(p<<1,x,num);//左孩子家里
        else change_p((p<<1)+1,x,num);//右孩子家里
        segment[p].w=segment[p<<1].w+segment[(p<<1)+1].w;//更新线段和
    }
    

    3.操作一个区间的值,询问一个区间

    方式大同小异,就是改变区间的时候打上标记,回来的时候维护改变的值,
    操作区间的时候就再次维护拆开的两半,拆之前杀驴即可。
    改变区间的值Code:

    void change_seg(int p,int l,int r,int num)
    {
    	if (segment[p].l==l,segment[p].r==r)
    	{//恰好完全覆盖这条p线段
    		segment[p].tag+=num;//打上一个tag标记,养驴
    		segment[p].sum+=num*(r-l+1);
    		//加上更改的数值*节点个数,仅仅更改这一层数据,更新上一层维护数据
    		return;
    	}
    	//下面均为不完全覆盖p线段
    	int m=(segment[p].l+segment[p].r)>>1;
        if (r<=m)
    	{//覆盖的线段在左儿子家里
    		change_seg(p<<1,l,r,num);//改左儿子
    	}
    	else if(l>m)
    	{//覆盖的线段在右儿子家里
    		change_seg((p<<1)+1,l,r,num);//改右儿子
    	}
    	else
    	{//两个儿子全覆盖
    		change_seg(p<<1,l,m,num);//给左儿子覆盖
    		change_seg((p<<1)+1,m+1,r,num);//给右儿子覆盖
    	}
    	segment[p].sum=segment[p<<1].sum+segment[(p<<1)+1].sum;//更新维护数据
    }
    

    查询的话...

    int ask_seg(int p,int l,int r)
    {
    	if (segment[p].l==l&&r==segment[p].r)return segment[p].sum;
    	int m=(segment[p].l+segment[p].r)>>1;
    	if (r<=m)return ask_seg(p<<1,l,r);
    	else if (l>=m+1) return ask_seg((p<<1)+1,l,r);
    	else return ask_seg(p<<1,l,m)+ask_seg((p<<1)+1,m+1,r);
    }
    

    最后放一个整个的线段树板子吧

    点击传送剪贴板

    四、树状数组

    1.定义

    树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
    ——来自百度百科

    2.算法实现

    令这棵树的结点编号为C1,C2...Cn。令每个结点的值为这棵树的值的总和,那么容易发现:
    C1 = A1
    C2 = A1 + A2
    C3 = A3
    C4 = A1 + A2 + A3 + A4
    C5 = A5
    C6 = A5 + A6
    C7 = A7
    C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
    ...
    C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16
    这里有一个有趣的性质:
    设节点编号为x,那么这个节点管辖的区间为2^k个元素
    (其中k为x二进制末尾0的个数)。(不信你就模拟一下看看)
    因为这个区间最后一个元素必然为Ax,
    所以很明显:Cn = A(n – 2^k + 1) + ... + An
    相对于维护,这样的计算省下多少力气...
    求k的值也很简单,已知lowbit(n)=:

    • 1.找到n在二进制下的第一个1
    • 2.返回这个1和它右边的0组成的二进制数的十进制。

    利用补码的性质,我们可以得出:

    int lowbit(int n){return n & -n;}
    

    所以,只需要反复进行某些步骤就好了:

    • 1.初始化sum=0;
    • 2.如果n<=0,停止;否则就sum+=c[n];
    • 3.n-=lowbit(n);然后是第二步。

    如果是修改呢?

    while(i<=n)//0.在数组内部
    {
    	c[i]=c[i]+x;//1.修改
    	i+=lowbit(i);//2.跳转到下一个与其有关的值
    }
    

    五、ST表

    1.定义:

    定义一个二维数组st_list[ i ][ j ],表示闭区间[ i,i + 2^j-1 ]的最小值/最大值。

    2.推论和建表方法:

    根据这个原理,当j=0的时候,i+2^j-1 = i+1-1 = i,
    也就是说st表的第一列是原数组!
    将数组二分成两部分,即[i,i+2(j-1)]和[2(j-1)+1,j-1],
    分啊分,直到分到点的值(原数组),倒着推回去,就可以得到ST表了。
    所以这种建表方式本质上是一种dp , 递推。

    3.性质(好处):

    1.建表过程O(nlogn),访问过程O(1)

    2.离线(静态)操作。不支持在线(动态)。

    问题又来了:如何O(1)访问最大最小值呢?

    1.先寻找刚好使得满足2^j <= i < 2^(j+1)的i值(建议预处理)
    2.因为2^log(a)>a/2,所以我们要查找区间[i,j]的最大/小值就是
    st(l,i)和st(r-2i+1,k)的max/min。(从l向后推2i,从r向前推2^i)
    还是画个图好理解。

    一次操作,仅需要O(1)的优秀复杂度。

    六、最近公共祖先和欧拉序遍历

    1.欧拉序遍历是什么?

    我们知道一个树的先序遍历是头、左、中间...、右,而欧拉序遍历是在返回的时候再次遍历根节点,变成了头、左、头、中间1、头、...、头、右、头。(也可以打破从左到右遍历的顺序。)

    上面的树的欧拉序遍历就是:
    1 2 1.5 2 7 2 3 2 1 4 2.5 11 9 11 8 11 2.5 4 1
    自己观察,会发现元素a,b的最近公共祖先就在上述遍历中,
    第一次出现a和最后一次出现b的这样一个线段内。


    尾声。祝大家考个好成绩,早日AK IOI,多多RP++。

    各种板子传送门:

    二叉堆:

    1.合并果子
    2.陶陶摘苹果(升级版)
    3.纯板子

    二叉搜索树:

    没找着...

    线段树:

    1.校门外的树
    2.[USACO1.2]挤牛奶Milking Cows
    3.统计和

    树状数组:

    1.同上3.
    2.两个板子:板子一 板子二

    ST 表:

    继续板子(数据及其加强预警)

    最近公共祖先和欧拉序遍历:

    打板子不多说


    Thanks a lot!

    Especially my teacher guided me.

  • 相关阅读:
    位图索引,数据库索引浅浅的学习
    Java 锁的学习
    Redis学习
    如何通俗理解相机标定公式推导与Python编程实践教程
    编译orb slam遇到的一些bug和错误解决方案
    ubuntu cmake Fatal error: iostream: No such file or directory
    cv2 drawKeypoints 使用自定义数组来绘制关键点
    报错信息ax = axes if axes else plt.gca() ValueError: The truth value of an array with more than one ele
    如何理解二值图连通域快速标记算法快速连通物体检测Fast-Connected-Component教程附带python代码实现
    如何理解图像处理中的Otsu's 二值化算法(大津算法)Python编程实践
  • 原文地址:https://www.cnblogs.com/jelly123/p/10390085.html
Copyright © 2011-2022 走看看