第二讲 数据结构
1.数组模拟单链表、双链表、栈、队列,只需要用数组和指针来存储即可,略。
单调栈和单调队列:
应用范围非常少,一只手数得过来的那种,单调栈用于解决求一个序列中每一个数它的左边第一个小于等于它的数,我们通过暴力算法看下里面数的规律,得出如果存在a3>a5等情况,那么在a5右边的数都不可能选择a3,这意味着a3没有任何意义了,所以我们将数据读入栈,维护一个单调递增的栈,每当读入的数比栈顶小,说明该数的优先级比栈顶更高,我们通过while弹出栈顶元素,找到第一个小于等于新读入的数小的数,为该数的答案,被删掉的元素就再也没有用了。
代码:
for(int i=0;i<n;i++)
{
int x;
cin>>x;
while(tt!=0 && stack[tt]>=x) tt--;
if(!tt) cout<<"-1 ";
else cout<<stack[tt]<<" ";
stack[++tt]=x;
}
单调队列也是与单调栈内部一样的原理,找窗口内最小的数,我们就维护一个单调递增的队列,队列尾比新读入的数大,就不断排出队列尾,因为他们不仅大,还 属于前面的元素,比不过新元素。找最大也是一样的,判断条件相反,维护一个单调递减的队列就好了。
cin>>n>>k;
int i,j;
for(i=0;i<n;i++) cin>>a[i];
int hh=0;
int tt=-1;
for(i=0;i<n;i++)//找窗口内最小的数
{
if(hh<=tt&& i-k+1>q[hh]) hh++;
while(hh<=tt && a[q[tt]]>=a[i]) tt--;
q[++tt]=i;
if(i>=k-1) cout<<a[q[hh]]<<' ';
}
cout<<endl;
hh=0,tt=-1;
for(i=0;i<n;i++)//找窗口内最大的数
{
if(hh<=tt&& i-k+1>q[hh]) hh++;
while(hh<=tt && a[q[tt]]<=a[i]) tt--;
q[++tt]=i;
if(i>=k-1) cout<<a[q[hh]]<<' ';
}
return 0;
KMP
核心思想·:
用暴力算法得知,当s串和p串不匹配时,如果p串内部有存在相等部分,我们是可以直接拿来使用的,而不是又从0开始,如果我们预处理了p中每一个数下标i的最长匹配子串长度j(从i开始往前j步得出的子串与从头开始的长度为j的子串相等且j为最大的一个子串)存在数组ne[i]中。
当母串的i与子串的j+1(用j+1的话不匹配使用ne[j],比较方便)不匹配时,将j的位置移至ne[j]的位置,相当于子串右移,直到开始匹配,j++;如果j==n,则表示匹配成功,输出开始位置;
ne数组初始化也与上面匹配相似,自己和自己匹配,不匹配的话,j=ne[j],匹配j++,ne[i]=j;
代码:
#include<iostream>
using namespace std;
const int N=1e5+10,M=1e6+10;
char p[N],s[M];
int ne[N];
int n,m;
int main()
{
int i,j;
cin>> n >> p+1 >> m >> s+1;
for(i=2,j=0;i<=n;i++)
{
while(j!=0 && p[i]!=p[j+1]) j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
for(i=1,j=0;i<=m;i++)
{
while(j!=0 && s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n)
{
cout<<i-n<<" ";
j=ne[j];
}
}
return 0;
}
思路图解:
trie树:
一种高效的存储和查找字符串集合的方式
我们从头结点出发,如果头结点没有值为当前字母的子节点,我们就创建一个,如果有就继续往下,直到当前字符串输入完成,在字符串的末尾做一个标记,表示当前位置有字符串;
思路图解:
代码:
#include<iostream>
using namespace std;
const int N=1e5+10;
int son[N][26],cnt[N],idx;
char str[N];
void insert(char str[])
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
cnt[p]++;
}
int query(char str[])
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(!son[p][u]) return 0;
p=son[p][u];
}
return cnt[p];
}
int main()
{
int n;
char op;
cin>>n;
while(n--)
{
scanf("%c",&op);
scanf("%s",str);
if(op=='I') insert(str);
if(op=='Q') cout<<query(str)<<endl;
}
return 0;
}
并查集:
高效进行合并与查找集合元素操作
并查集上面两个操作时间复杂度o1,因为有路径压缩优化,再查找过一条路径上的根节点后,我们将这条路上所有点都指向根节点,其实还有按秩合并优化,但是一般不会用
代码:
#include<iostream>
using namespace std;
const int N=1e5+10;
int n,m;
int p[N];
int get(int x)
{
if(p[x]!=x) p[x]=get(p[x]);
return p[x];
}
int main()
{
cin>>n>>m;
for(int i=0;i<=n;i++)
{
p[i]=i;
}
while(m--)
{
char op[2];
int a,b;
scanf("%s%d%d",op,&a,&b);
if(op[0]=='M')
{
p[get(a)]=get(b);
}
else
{
if(get(a)==get(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
堆(一维数组实现)
删除并不实际删除,而是和最后一个交换位置,然后在调整变化元素该去哪 ,从1开始找左右儿子容易。
代码:
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N=1e5+10;
int h[N],ph[N],hp[N],hsize;//ph表示第i个插入的数下标是多少,hp表示第i个数是第几个插入的
void heap_swap(int a,int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a],hp[b]);
swap(h[a],h[b]);
}
void down(int u)
{
int t=u;
if(u*2 <=hsize && h[u*2]<h[t]) t=u*2;
if(u*2+1 <=hsize && h[u*2+1]<h[t]) t=u*2+1;
if(u!=t)
{
heap_swap(u,t);
down(t);
}
}
void up(int u)
{
while(u/2 && h[u/2]> h[u])
{
heap_swap(u,u/2);
u/=2;
}
}
int main()
{
int n,m=0;
cin>>n;
while(n--)
{
char op[10];
int k,x;
scanf("%s",op);
if(!strcmp(op,"I"))
{
cin>>x;
m++;
hsize++;
ph[m]=hsize,hp[hsize]=m;
h[hsize]=x;
up(hsize);
}
else if(!strcmp(op,"PM")) cout<<h[1]<<endl;
else if(!strcmp(op,"DM"))
{
heap_swap(1,hsize);
hsize--;
down(1);
}
else if(!strcmp(op,"D"))
{
cin>>k;
k=ph[k];
heap_swap(k,hsize);
hsize--;
down(k),up(k);
}
else
{
cin>>k>>x;
k=ph[k];
h[k]=x;
down(k),up(k);
}
}
return 0;
}
哈希表:
将数据通过通过关键字与值一一对应的关系将数据进行储存,用哈希来维护散列表时,我们一般是将数据模上一个略大于数据量的2倍或者3倍的素数,这样可以减少冲突的数量;我们有两种存数方式;1是拉链法,即将哈希值相同的数通过链表的方式存储在同一个位置上(使用头插法),2是开放寻址法,如果哈希值的位置上已经有数但不是目标数,则往下一位查看,若到达最尾端,则继续从头开始,直到找到空位。哈希表题目一般考察插入和查找,删除操作考的少,删除时,我们并不直接删去该数,而是打上一个标记即可
拉链法代码:
const int N=100003;
int h[N],e[N],ne[N],idx;
void insert(int x)
{
int k=(x%N+N)%N;
e[idx]=x;
ne[idx]=h[k];
h[k]=idx;
idx++;
}
int find(int x)
{
int k=(x%N+N)%N;
for(int i=h[k];i!=-1;i=ne[i])
{
if(e[i]==x) return 1;
}
return 0;
}
开放寻址法代码:
#include<iostream>
#include<cstring>
using namespace std;
const int N=200003,null=0x3f3f3f3f;
int h[N];
int find(int x)
{
int k=(x%N+N)%N;
while(h[k]!=null && h[k]!=x)
{
k++;
if(k==N) k=0;
}
return k;
}
int main()
{
int n;
cin>>n;
memset(h,0x3f,sizeof h);
for(int i=0;i<n;i++)
{
char op[2];
int x;
scanf("%s%d",op,&x);
if(*op=='I')
{
int k=find(x);
h[k]=x;
}
else
{
int k=find(x);
if(h[k]==x) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
return 0;
}
字符串哈希:通过字符串前缀哈希法将字符串哈希成一个对应的数字,如“ABCDEFG”,h[0]=0;h[1]=“A”对应的值,h[2]=“AB”对应的值.... ,将字符串定义为阿斯克码的p进制数,通过进制转换成十进制的数,然后再取Q的模,这样就可以将字符串映射成1~Q-1的数,需要注意的是不能映射成0,不然A和AA,AAA都是同一个值,是不允许的。经验得出将p取为131或13331时,Q=2的64次方,基本不会出现冲突。我们通过计算l到r之间字符串的哈希值就可用来查询等等,l到r的哈希值等于h[r]-h[l-1]*p的(r-l+1)次方(相当于10023和100,要计算23的哈希值就要将100变成10000,剩下的就是23的哈希值)。对于Q=2的64次方,我们可以将h变成unsigned long long 来存,这样如果溢出就相当于取模。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+5,P = 131;//131 13331
ULL h[N],p[N];
char str[N];
// h[i]前i个字符的hash值
// 字符串变成一个p进制数字,体现了字符+顺序,需要确保不同的字符串对应不同的数字
// P = 131 或 13331 Q=2^64,在99%的情况下不会出现冲突
// 使用场景: 两个字符串的子串是否相同
ULL query(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
int main(){
int n,m;
cin>>n>>m;
scanf("%s",str+1);
p[0]=1;
for(int i=1;i<=n;i++)
{
p[i]=p[i-1]*P;
h[i]=h[i-1]*P+str[i];
}
while(m--){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(query(l1,r1) == query(l2,r2)) printf("Yes\n");
else printf("No\n");
}
return 0;
}
STL容器(现用现查)
1.vector:变长数组,倍增的思想;c++系统为程序分配空间的时间与空间大小无关,与请求次数有关的,所以变长数组,每次申请都申请两倍空间,空间换时间。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
int main()
{
vector<int> a;
vector<int> a(n);
vector<int> a(n,x);//n个数每个数都是x;
vector<int> a[10];//10个vector数组
a.size();
a.empty();
a.clear();
a.begin();
a.end();
a.front();
a.back();
a.push_back();
a.pop_back();
for(int i=0;i<10;i++) a.push_back(i);
for(int i=0;i<a.size();i++) cout<<a[i]<<" ";
cout<<endl;
for(auto i=a.begin();i!=a.end();i++) cout<<*i<<" ";//相当于指针
cout<<endl;
for(auto x:a) cout<<x<<" ";
cout<<endl;
//a支持比较,通过字典序比较
}
2.pair存储2元(n元)组,很像结构体,自带比较的结构体
#include<vector>
int main()
{
pair<int,int> a;
pair<int,string> b;
//first表示pair第一元素,second为第二元素
//pair排序也是字典序排序一first为第一排序,second为第二排序
p=make_pair(1,2);
p={2,"a"};
//三元组
pair<int,pair<int,int>> c;
}
3.string字符串
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
/*
string字符串:substr(),c_str();
size()
empty()
clear()
lentgth()
*/
using namespace std;
int main()
{
string a="yxc";
a+="def";
a+='c';
cout<<a.substr(1,2)<<endl;//从下标1开始放回长度为2的字符串
cout<<a.substr(1,10)<<endl; //超过返回到尾部,若省略第二个参数则返回从1到尾
printf("%s\n",a.c_str());//返回字符数组的起始地址
return 0;
}
4.队列queue
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
/*
queue队列
push()队尾插入
pop()
front()
back()
size()
empty()
没有clear()
*/
using namespace std;
int main()
{
queue<int> q;
q=queue<int>();//清空即为重新申请一个
return 0;
}
5.priority_queue优先队列(用堆实现)(需要queue)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
/*
priority_queue优先队列(用堆实现)
push()堆中插入元素
top()返回对堆顶
pop()弹出堆顶
*/
using namespace std;
int main()
{
priority_queue<int> heap; //默认为大根堆(即最大堆),想要实现小根堆,只需要在插入元素时,直接插入负数
//或者定义为小根堆
priority_queue<int ,vector<int>, greater<int>> heap2;
return 0;
}
6.stack栈
/*
stack 栈
push()
top()
pop()
empty()
size()
*/
7.deque
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<deque>
/*
deque双端队列
size()
empty()
clear()
front()
back()
push_back()/pop_back
push_front/pop_front
begin()/end()
[]
但是速度很慢
*/
using namespace std;
int main()
{
deque<int> a;
a.clear();
return 0;
}
8.set集合,不可以有重复元素,multiset可以有
map(内部维护红黑树)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
/*
set/multiset o(logn)
insert()
find() 不存在返回end()
count()
begin()/end() ++,--返回前驱和后继
erase()
(1) 输入一个数x,删除所有x o(k + logn)
(2) 输入是一个迭代器,删除这个迭代器
lower_bound(x) 找到第一个大于等于x的数(最小) 的迭代器
upper_bound(x) 找到大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 参数可以为pair或者迭代器
find()
[]
lower_bound()/upper_lower()
*/
using namespace std;
int main()
{
map<string,int> a;
a["yxc"]=0;
cout<<a["yxc"]<<endl;//时间复杂度为o(logn)
return 0;
}
9.unorder_set,unorder_map,unorder_multiset,unorder_multimap
/*
unorder_set,unorder_map,unorder_multiset,unorder_multimap
和上面类似,增删改查复杂度为o(1)
不支持 lower_bound/upper_bound(内部无序),不支持迭代器++,--;
*/
10,bitset压位
bool每个字节存8位
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<deque>
#include<map>
/*
bitset
bitset<10000>长度为10000
支持位运算:&,|,~,^
<<,>>
==,!=
[]
count()返回有多少个1
any()判断是否至少有一个1/none()判断是否全为0
set() 把有位置成1
set(k,v)将第k位变成v
reset() 把所有位变成0
flip() 相当于~
flip(k) 将第k位取反
*/
using namespace std;
int main()
{
return 0;
}