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.

最新文章

  1. 编译 wxWidgets-3.0.2 on Mac OS X Yosemite 出错?!的解决方法
  2. jQuery.buildFragment源码分析以及在构造jQuery对象的作用
  3. IIS 伪静态配置(安装ISAPI_Rewrite配置)
  4. 【wikioi】1041 Car的旅行路线
  5. cookie sessionStorage localStorage 区别
  6. 烂泥:KVM安装Windows Server 2008 R2使用virtio硬盘
  7. 软件工程 speedsnail 冲刺1
  8. Hibernate笔记——Hibernate介绍和初次环境配置
  9. BIND9配置文件详解模板[转载]
  10. python-列表、字典、元组的员工信息处理接口(第二篇(五):基于列表、字典和元组的员工信息处理接口)
  11. JQuery弹出层,点击按钮后弹出遮罩层,有关闭按钮
  12. [转载]php 处理上百万条的数据库如何提高处理查询速度
  13. HDU 1222(数论,最大公约数)
  14. Microsoft office PPT 2007 保存时速度慢(整理自网上)
  15. Node.js开发工具、开发包、框架等总结
  16. Hibernate 的原生 SQL 查询
  17. int类型被强制转换成较低精度的byte类型
  18. MySQL学习笔记(五)并发时经典常见的死锁原因及解决方法
  19. MQ选型之RabbitMQ
  20. &lt;构建之法&gt;第三10、11、12章

热门文章

  1. RHCSA-EXAM 模拟题目
  2. 【LG3229】[HNOI2013]旅行
  3. Ceph学习之路(二)之Ceph的工作原理及流程
  4. Mac OS 上 VIM 8.0 安装体验
  5. Spring学习(十二)-----Spring @PostConstruct和@PreDestroy实例
  6. sqlite两表更新update
  7. python-前方高能-面向对象-进阶3
  8. Java 分割、合并byte数组
  9. Angular7运行机制--根据腾讯课堂米斯特吴 《Angular4从入门到实战》学习笔记分析完成
  10. HIVE中的数据怎么导出到hdfs或本地呢