文章目录
一, 线性数据结构
栈:(stack)
栈的应用非常广,如递归调用的就是系统栈。栈具有后进先出的特点,即对于栈的操作只有对于栈顶元素的操作,所以我们只需用一个top指针指向栈顶就够了。
应用:
例:
中缀表达式->后缀表达式
我们平时见到的表达式都是中缀表达式,而计算机是无法直接处理这种表达式的,因为运算顺序不是严格的从左到右,还有括号,优先级等限制,而后缀表达式则没有这种问题,对于一个后缀表达式,计算机只需不断的读取元素,然后根据元素出现的顺序进行运算,下面举个例子:
中缀表达式:(4+35)(5-2) 后缀表达式:4 3 5 * + 5 2 - *
后缀表达式的运算:对于后缀表达式,我们采取以下策略:若遇到数,则直接压栈,遇到运算符,则将栈顶的两个数取出来,通过这个运算符进行运算,然后将运算结果压栈,最后栈中剩下的元素就是结果。
下面说一下如何将后缀表达式转为中缀表达式:
首先,我们要将运算符之间的优先级确定出来,如+和-是平级的,*和/是平级的且优先级大于加和减,(优先级高于所有的,)优先级低于所有的,我们将数和运算符分开放置,遇到运算符就进行压栈,如果该运算符优先级高于栈顶的,说明该运算符要先算,所以直接压,而如果该运算符优先级低于栈顶的运算符,则要先算栈顶的运算符,就将栈顶元素不断退栈直到栈顶元素的运算符优先级小于等于当前要进栈的运算符,左右括号直接退栈但不加入后缀表达式。
例
产品排序
(product.pas/c/cpp)
【问题描述】
你是一个公司的员工,你会按时间顺序受到一些产品的订单,你需要用一个栈来改变这
些订单的顺序(每个产品都必须入栈和出栈一次)。
按初始顺序,每次可以将一个产品入栈,或将栈顶产品弹至现在的序列末尾。每个产品
有一个制作时间??和单位时间惩罚值??,总的惩罚值为∑ (?? ×
?
?=1 ??),其中??为第?个产品的
完成时间,你需要最小化总的惩罚值。
【输入】
输入文件 product.in。
第一行一个数?,表示产品个数。
接下来?行,每行两个数表示??
,??。
【输出】
输出文件 product.out。
一行一个数表示最小的总惩罚值。
【输入输出样例】
product.in product.out
4
1 4
3 2
5 2
2 1
40
【样例解释】
操作步骤 排序后的序列(数字表示产品编号)
1.将第一个产品入栈
2.弹出栈顶元素 1
3.将第二个产品入栈 1
4.弹出栈顶元素 1 2
5.将第三个产品入栈 1 2
6.将第四个产品入栈 1 2
7.弹出栈顶元素 1 2 4
8.弹出栈顶元素 1 2 4 3
总惩罚值为 1*4+(1+3)*2+(1+3+2)*1+(1+3+2+5)*2=40
【数据说明】
30%: ? ≤ 15
50%: ? ≤ 100
100%: ? ≤ 200,??
,?? ≤ 1000
这道题是个DP题,但是可以直接用栈来模拟,进行搜索,用两个指针变量Top和top分别指向两个栈顶,进行出栈和入栈的操作
stl
#include <stack>
#include <vector>
#include <list>
#include <cstdio>
//可以使用list或vector作为栈的容器,默认是使用deque的。
stack<int, list<int>> s;
stack<int, vector<int>> s;
s.top();
s.push(x);
s.pop();
s.size();
w.empty();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
队列: (queue)
队列的应用也很广泛,BFS的状态记录就是一个队列。队列满足先进先出的原则,也就是队头支持调用和删除,队尾支持插入,这个性质满足了对头一定在队尾之前加入,这是一个很重要的性质,如拓扑排序,BFS找最优解都应用到了这个性质。另外,由于这个性质,队列中元素的坐标就具有了单调性,这个性质与二份查找结合起来,可以将很多算法的复杂度由O(n2)降为O(nlogn)如LIS的O(nlogn做法)。
并查集:
并查集是一种很强大的数据结构,由于语言中的集合功能不够强大,但集合的性质在很多地方都有用处,所以并查集是一种很重要的数据结构。并查集有两个功能:
查询两个点是否在一个集合2.将两个点合并到一个集合。
基本操作:
初始化:最开始每一个元素独立,代表元为自己,即自己是自己的爸爸
for(int i=1;i<=n;i++) f[i]=i;
- 1
找到一个元素所在集合的代表元:不断找父亲,直到父亲等于自己,这个元素就是代表元。
int find(int x) {return f[x]==x?x:f[x]=find(f[x]);}
- 1
查询:两个元素是否在一个集合只需直接判断两个元素所在集合的代表元是否相同。
if(find(x)==find(y))
- 1
合并:将两个集合合并。此时一定要找到代表元,将其中一个代表元连到另一个代表元上,两个集合就合并到一起了。
并查集在信息学竞赛中应用很广,如在维护图的连通性,最小生成树中都有重要应用。
f[find(x)]=find(y);
- 1
来看一个非常扯得并查集的运用的题目
愚人节的游戏
【题目描述】某年愚人节,螃蟹被公鸡坑蒙拐骗去玩一个不可能过关的游戏这个游戏在开始的时候有 n 个孤立的小岛,然后游戏会接连进行下列某种操作
1.add i j,表示在小岛 i,j 间建一座桥
2.del i j,表示拆除小岛 i,j 间的桥(当然这座桥肯定会存在)
3.ask i j,表示询问小岛 i,j 是否连通(直接或间接连通皆可)特别的是,对于任意 i,j,游戏只会在小岛 i,j 间建至多一座桥,并拆除至多一次,也就是说,如果小岛 i,j 之间建了一座桥,又拆除了,那么 i,j 之间以后就不会再建桥了显然,如果要拆除某座桥,那座桥肯定已经存在眼看螃蟹就要被游戏给虐翻了,不如写个程序帮下它吧
【输入数据】第一行两个数 n,m,表示小岛数和操作数以下 m 行每行格式为上述 3 种操作中的一种,所有操作均无 i=j
【输出数据】对于每个 ask 操作输出一行连通则输出 1,否则输出 0
【输入样例】3 6
add 1 2
add 2 3
ask 1 3
del 2 1
ask 3 1
ask 3 2
【输出样例】101
【数据约定】30%数据满足 n≤40,m≤100100%数据满足 n≤400,m≤1000
这道题有很多很多很多高端的方法(那时的我都不会),于是就想。。查询,链接。。。不就是并查集吗。。。于是。。就A了
#include<bits/stdc++.h>
#define FOR(i,n,m) for(int i=n;i<=m;++i)
using namespace std;
int n,m,f[401];
inline int find(int x) {
return f[x]==x?x:f[x]=find(f[x]);
}
int main() {
scanf("%d%d",&n,&m);
FOR(i,1,n) f[i]=i;
int p,q;
string ss;
FOR(i,1,m) {
cin>>ss>>p>>q;
if (ss[1]=='d') { //add
if (q>p) f[find(p)]=find(q);
else f[find(q)]=find(p); //假定成一个线性结构
}
if (ss[1]=='s') { //ask
if (find(p)==find(q)) cout<<"1"<<endl;
else cout<<"0"<<endl;
}
if (ss[0]=='d') {
if (p>q) f[q]=q;
else f[p]=p;
}
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
Hash:
哈希表用于判重,通常可以和map同用。
哈希表的精髓在于哈希二字上面,也就是数学里面常用到的映射关系,它是通过哈希函数将关键字映射到表中的某个位置上进行存放,以实现快速插入和查询的。
为什么需要哈希函数?简单来说,解决存储空间的考虑。试想,将100个关键字存入大小为100的数组里,此时肯定是不需要哈希函数的,一对一的放,肯定是可以实现的。但是当数据量增大,将1000个关键字,存入大小为100的数组里呢?此时一个一个的放,那剩下的怎么办呢,所以,我们需要某种计算方法,既能把这1000个关键字存进去,而且最主要是还能取出来。这就是哈希函数要做的事情,给每一个关键字找一个合适的位置,让你既能存进去,又能取出来。
哈希的力量博大精深,各种取法,各种哈希,足够学习很久了。
二、树形数据结构
树状数组
差不多长这样的
也长这个样子
c1=a1;
c2=a1+a2;
c3=a3;
c4=a1+a2+a3+a4;
- 1
- 2
- 3
- 4
然后悄咪咪的把它变成二进制
c0001=a0001
c0010=a0001+a0010
c0011=a0011
c0100=a0001+a0010+a0011+a0100
- 1
- 2
- 3
- 4
顿时发现了规律有没有
先来简单的介绍一下lowbit函数,它是指这个数最后一个一所在的位表示的二进制的数的值,他是用来链接树状数组关系的函数,说的通俗点,它每个节点存储的是他的子子孙孙的和
int lowbit {returnx&(-x);}
- 1
那么这个lowbit是什么意思呢
这个算出来的是他的最后一位非零数的位所对应的值
比如
1,3,5,7这些奇数,他们的lowbit值就是1,同样,对应到图上,我们会发现它的位置都在第一排,也就是说,他们是没有儿子的,它存储的和就是他自己,
2,6,10这些2的倍数,lowbit是2
4 8 12这些lowbit是4
这些x加上他的lowbit值就变成了他的爸爸,而减去就变成了他的弟弟(结合图观察)
lowbit (1)+1=2
lowbit (2)+2=4
lowbit (3)+3=4
lowbit (4)+4=8
lowbit (5)+5=6
.......
- 1
- 2
- 3
- 4
- 5
- 6
修改
这是我们对树状数组进行修改时的重要依据,每次先自我修改,然后向上找爸爸,对爸爸进行修改一直向上
那么就顺便附一波代码
void add(int x,int k) { //给编号为x的点加上k
while(x<=n){ //n为数组的一个上限,通常因题而异
tree[x]+=k;
x=x+lowbit(x); //找爸爸
}
}
- 1
- 2
- 3
- 4
- 5
- 6
建树时,树状数组比较简单,不像线段树那么麻烦
for(int i=1;i<=n;i++) {scanf("%d",&a);add(i,a)} //在i的位置附上a就相当于加a
- 1
顺便就来说一下修改
单点修改可以直接使用
scanf("%d%d",&a,&k);add(a,k);
- 1
区间修改时需要用一点差分的思想
如果将x到y区间加上一个k,那就是从x到n都加上一个k,再从y+1到n加上一个-k
那么是这么用的
scanf("%d%d%d",&x,&y,&k);
add(x,k),add(y+1,-k);
- 1
- 2
整个的修改就是这样,大概就是一直找爸爸的过程
求值
修改实在一直找爸爸,而求值是一直找弟弟,如果没有弟弟,它爸爸的弟弟也是他的弟弟(这个关系好复杂 )
首先先来看一下每个数减去他的lowbit值
1-lowbit (1)=0
2-lowbit (2)=0
3-lowbit (3)=2
4-lowbit (4)=0
5-lowbit (5)=4
6-lowbit (6)=4
7-lowbit (7)=6
8 ......
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
为了让这个关系看的清楚点,再把图片放一遍(hiahiahia)
经过对比,思考一下这个找弟弟的过程
那么他是怎么查询呢
就拿7来模拟一下
tree[7]=a[7]; 7-lowbit (7)=6;
tree[6]=a[5]+a[6]; 6-lowbit (6)=4;
tree[4]=a[1]+a[2]+a[3]+a[4] 4-lowbit (4)=0;
- 1
- 2
- 3
根据这个,我们可以推出他的求值的代码
int query(int x) { //x及以前所有的和,复杂度logn
int sum=0;
while(x!=0){
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
所以单点求值就是简单的直接printf(“%d”,query(x));
就行了
区间求值后面的减去前面的,还是比较好理解的
printf("%d",query(r)-query(l-1));
- 1
所以树状数组就是这个样子,他代码非常好打,就是在理解上有一定的难度,精髓是他的lowbit函数
附luoguAC代码
p3374
#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR(i,n,m) for(int i=n;i<=m;i++)
#define FR(j,n,m) for(int i=n;i>=m;i--)
#define MAX 100
#define N 500020
#define mo 1000
#define ll long long
#define ull unsigned long long
using namespace std;
int n,m,tree[N];
int lowbit(int x) {return x&(-x);}
void add(int x,int y) {
while(x<=n){
tree[x]+=y;
x=x+lowbit(x);
}
}
int query(int x){
int ans=0;
while(x!=0){
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
int main(){
int a;
scanf("%d%d",&n,&m);
FOR(i,1,n) scanf("%d",&a), add(i,a);
FOR(i,1,m){
int k,x,y;
cin>>k>>x>>y;
if(k1) add(x,y);
if(k2) cout<<query(y)-query(x-1)<<endl;
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
p3368
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
int n,m,tree[N];
int lowbit(int x) {return x&(-x);}
void add(int x,int y) {
while(x<=n){
tree[x]+=y;
x=x+lowbit(x);
}
}
int query(int x){
int ans=0;
while(x!=0){
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) {
scanf("%d",&t);
add(i,t),add(i+1,-t);
}
for(int i=1; i<=m; i++) {
scanf("%d",&t);
if(t1) {
scanf("%d%d%d",&x,&y,&z);
add(x,z),add(y+1,-z);
} else if(t2) {
scanf("%d",&x);
printf("%d\n",query(x));
}
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
树状数组还可以用来求逆序对,个人认为这个比用归并排序求好用(主要是打的比较短)。
int lowbit(int x) {return x&(-x);}
void add(int x,int y) {
while(x<=n){
tree[x]+=y;
x=x+lowbit(x);
}
}
int query(int x){
int ans=0;
while(x!=0){
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
void BIT(){ //是树状数组英文名(Binary Indexed Trees)的缩写
FOR(i,1,n){
add(f[i],1);
ans=ans+f[i]-query(f[i]);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
线段树
线段树好难
线段树一直以来学了很久,一直掌握的不太好。
建树
线段树和树状数组的不同点之一是建树,树状数组是不需要单独建树的,线段树却不同
先来看一下他的建树思路
void pushup(int id) {sumv[id]=sumv[ls]+sumv[rs];} //标记上传
- 1
void create (int l,int r,int id) {
if(l==r) {
sumv[id]=a[l];
return ;
}
int mid=(l+r)>>1;
create(l,mid,ls); //向下建树
create(mid+1,r,rs);
pushup(id);//值上传,给他的爸爸
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
#define ls id<<1
#define rs id<<1|1
- 1
- 2
线段树的建树思路整体还是比较好理解的,就是不断地找儿子,然后给父亲赋值,也就是pushup上传值
单点修改
单点修改就是对一个点修改,然后向上传,对父亲节点的值进行修改,代码如下
void aadd(int k,int id){
addv[k]=id;
pushup(id);
}
- 1
- 2
- 3
- 4
区间修改
区间修改时,我们引入一个新的玩意,叫做lazy标记,先来说一下lazy标记的下传
void pushdown(int l,int r,int id)
{
if(addv[id])
{
int mid=(l+r)>>1;
addv[ls]+=addv[id];
addv[rs]+=addv[id];
sumv[ls]+=(mid-l+1)*addv[id];
sumv[rs]+=(r-mid)*addv[id];
addv[id]=0;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
void add(int l,int r,int x,int y,int k,int id)
{
if(l>=x&&r<=y)
{
sumv[id]+=(r-l+1)*k;
addv[id]+=k;
return;
}
pushdown(l,r,id);
int mid=(l+r)>>1;
if(x<=mid) add(l,mid,x,y,k,ls);
if(y>mid) add(mid+1,r,x,y,k,rs);
pushup(id);
}
// 区间修改
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
void sum(int l,int r,int x,int y,int id)
{
if(l>=x&&r<=y)
{
_sum+=sumv[id];
return;
}
pushdown(l,r,id);
int mid=(l+r)>>1;
if(x<=mid) sum(l,mid,x,y,ls);
if(y>mid) sum(mid+1,r,x,y,rs);
}
//区间求和
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
二叉搜索树
存储结构
二叉树的存储结构如下:
struct node {
int left; //左儿子
int right; //右儿子
int fa; //父节点
int num; //其他信息
}a[N];
- 1
- 2
- 3
- 4
- 5
- 6
左儿子,右儿子,和父节点相当于指针,存储的是他们的’地址’,如果会用指针的话,可以直接使用指针。
二叉树的遍历和普通的遍历差不多,通常情况下,中序遍历用的最多,这就形成了一个二叉搜索树。
二叉搜索树:
二叉搜索树的存储有点像树的中序遍历。
遍历:
void print (int p) {
if(!p) return;
print(a[p].left);
cout<<a[p].num<<endl;
print(a[p].right);
}
查找
int find(int x,int p) {
if(!p) return 0;
if(x==a[p].num) return p;
else if(x<a[p].num) return find(x,a[p].right);
else return find(x,a[p].right);
}
- 1
- 2
- 3
- 4
- 5
- 6
查找最值
int find(int p) {
if(!p) return ;
else if(!a[p].left) return a[p].num;
else return find(a[p].left);
}
- 1
- 2
- 3
- 4
- 5
递归形式
int find(int p) {
if(p) while(a[p].left) p=a[p].left;
return a[p].num;
}
- 1
- 2
- 3
- 4
非递归形式
</div>
<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-7b4cdcb592.css" rel="stylesheet">
</div>