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.

原文地址:https://www.cnblogs.com/jelly123/p/10390085.html