第二章 数据结构
笔试中动态链表的方式如上述new的时候很耗时,
数组模拟链表
一般都是把新节点插到头结点的位置
#include <iostream>
using namespace std;
const int N = 100010;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x,ne[idx] = head, head = idx, idx ++ ;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx ++ ;
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m;
cin >> m;
init();
while (m -- )
{
int k, x;
char op;
cin >> op;
if(op =='H')
{
cin >> x;
add_to_head(x);
}
else if (op =='D')
{
cin >> k;
if(!k)head = ne[head];//如果k=0,要删除头结点
remove(k-1);
}
else
{
cin >> k >> x;
add(k-1,x);//第一个插入的数下标是0,
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
双链表
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N],r[N], idx;
// 初始化
void init()
{
// 0表示左端点,1表示右端点
r[0] = 1,l[1] = 0;
idx = 2;
}
// 在下标是k的点的右边,插入x
void add(int k, int x)
{
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx;
}
// 删除第k个点
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
tt表示栈顶的下标,
#include <iostream>
using namespace std;
const int N = 100010;
//*********************栈
int stk[N], tt;
// 插入
stk[ ++ tt] = x;
// 弹出
tt --;
//判断栈是否为空
if (tt > 0) not empty
else empty
// 栈顶
stk[tt];
//*********************队列
//栈的tt从0开始,队列的tt队尾从-1开始
// 在队尾插入元素,在队头弹出元素
int q[N], hh, tt = -1;
// 插入
q[ ++ tt] = x;
// 弹出
hh ++ ;
// 判断队列是否为空
if (hh <= tt) not empty
else empty
// 取出队头元素
q[hh]
单调栈:给定一个序列,求在这个序列中每一个数左边离它最近的数在什么地方,或者右边离得最近比他大的数,
如果在栈里有x<y,且ax>=ay,则ax可以被删掉,即只要有这样的逆序的关系,前面的数就会被删掉,剩下的序列就是一个单调上升的序列,
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;
int main()
{
ios::sync_with_stdio(false);
cin >> n;
for (int i = 0; i < n; i ++ )
{
int x;
cin >> x;
while (tt && stk[tt] >= x) tt --;
if (tt) cout << stk[tt] << ' ';
else cout << -1 <<' ';
stk[ ++ tt] = x;
}
return 0;
}
单调队列:求滑动窗口里的最大值或最小值
队列里面存的是下标
当前终点是i,起点就是i-k+1,i-k+1>q【hh】说明q【hh】出了窗口,hh要加加即往右移动,队列里面每次最多只有一个数是不在窗口内的,窗口每次只会移动一位,所以是if不是while
新插入的数如果比队尾小,队尾就没用了,就可以把队尾删掉,即t–就可以删掉队尾元素,
#include <iostream>
using namespace std;
const int N = 1000010;
int n, k;
int a[N], q[N];
int main()
{
scanf("%d%d",&n,&k);
for (int i = 0; i< n; i ++ ) scanf("%d",&a[i]);
int hh = 0,tt = -1;
for (int 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) printf("%d ",a[q[hh]]);
}//只有够k个数才开始输出
puts("");
hh = 0,tt = -1;
for (int 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) printf("%d ",a[q[hh]]);
}
puts("");
return 0;
}
KMP下标习惯从1开始,
next【i】表示的就是模板串中以i为终点的后缀和从1开始的前缀相等,且后缀的长度最长,长度为j,即p从1到j这一段与p以i为终点的长度为j的这段相等,假设主串i-1及之前都匹配,从i开始不匹配了,对应的模板串的j及之前都匹配,从j+1开始不匹配,next【j】就是以j为终点的后缀和从1开始的前缀相等,移动后的下标为next【j】,当换成next【j】后,再看下一个点是否和s【i】匹配了,如果匹配就继续往后,如果不匹配就递归的进行next过程,
#include<iostream>
using namespace std;
const int N = 10010,M = 100010;
int n, m;
char p[N],s[M];
int ne[N];
int main()
{
cin >> n >> p+1>>m >> s + 1;
// 求next的过程
for (int i = 2,j=0;i <= n;i ++ )//next【1】==0,因为第一个字母失败了就会从第一个开始匹配,
{
while (j && p[i] != p[j + 1]) j = ne[j];//j退后,
if (p[i] == p[j+1])j++ ;//退后之后可以匹配时,
ne[i] = j;
}
// kmp 配过程
for (int i = 1,j= 0;i <= m; i ++ )
{
while (j && s[i] != p[j + 1]) j= ne[j];//j没有退回起点,并且当前的s【i】不能和p【j+1】匹配,此时找的是把模板串往后最少移动多少可以使得前后缀相等,相当于j是往前退了,退完后再看能不能继续往下和主串匹配,移动到next【j】可以保证模板串j以及前面这个最大的段和主串是相等的,此时可以看新的next【j】后面的点和s【i】是否匹配,不匹配就把j移到next【j】的位置,直到退无可退和已经匹配了,
if (s[i] == p[j + 1]) j ++ ;//如果移动next后新的已经匹配了,那么j就可以移到下一位了,
if(j== n)//如果j等于n就说明匹配成功,
{
printf("%d ",i - n);
j= ne[j];//匹配成功后j已经到最后不能再往后了,下次要匹配的话要让j最少往后移动继续可以匹配,可以使得最大后缀和前缀相等,
}
}
return 0;
}
时间复杂度是O(n),因为kmp匹配过程中在i循环的时候每一次j最多会加上1,因此i总共会循环m次,j总共也会加m次,而ne【j】一定是小于j的,因此j每次至少会减一,因此while循环每执行一次,j起码都会减一,j最多加m次,j大于0 ,所以j最多减m次,因此whle循环最多会执行m次,所以总共的时间复杂度是2m,(表面上看是两重循环)
Trie树是可以高效地存储和查找字符串集合的数据结构
#include <iostream>
using namespace std;
const int N = 100010;
//idx表示当前到了哪一个下标 cnt【x】存的是以x为结尾的点的个数
int son[N][26],cnt[N],idx;// 下标是e的点,既是根节点,又是空节点
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;
scanf("%d",&n);
while (n -- )
{
char op[2];
scanf("%s%s",op,str);
if (op[0] =='I') insert(str);
else printf("%d\n",query(str));
}
return 0;
}
并查集
并查集:
1.将两个集合合并
2询问两个元素是否在一个集合当中
并查集可以在近乎O(1)的时间内完成上述两个操作,
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N];
int find(int x) // 返回x的祖宗节点 +路径压缩
{
if (p[x] != x) p[x] = find(p[x]);//如果当前结点不是根节点的话,就返回父亲结点的祖宗结点,并且将路径上的每个结点都指向祖宗结点即根节点起到路径压缩
return p[x];
}
int main()
{
scanf("%d%d",&n,&m);
for (int i = 1; i<= n; i ++ ) p[i]=i;
while (m -- )
{
char op[2];//用scanf读一个字符的时候,也最好用读入字符串的形式,%s可以忽略空格回车
int a,b;
scanf("%s%d%d",op,&a, &b);
if (op[0] =='M') p[find(a)] = find(b);
else
{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
用一个集合来维护一个连通块,一个连通块的点就在一个集合当中,当在两个集合直接连线的时候,其实就是把两个集合合并,
在做并查集的操作的时候,可以同时维护一下每个集合当作元素的数量,只要在初始化的时候把每个集合里面的元素个数赋值为1,然后在合并的时候在新的根节点上加上当前这根树的里面的点的个数就可以了,
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N];
int sizep[N];//size表示的就是每个集合里点的数量,最开始只有一个点,
int find(int x) // 返回x的祖宗节点 +路径压缩
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d",&n,&m);
for (int i = 1; i<= n; i ++ )
{
p[i]=i;
sizep[i]=1;
}
while (m -- )
{
char op[5];
int a,b;
scanf("%s",op);
if (op[0] =='C')
{
scanf("%d%d",&a,&b);
if(find(a)==find(b)) continue;//a和b已经在一个集合了就不用size相加了,
sizep[find(b)] += sizep[find(a)];
p[find(a)] = find(b);
}
else if(op[1]=='1')
{
scanf("%d%d",&a,&b);
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
else
{
scanf("%d",&a);
printf("%d\n",sizep[find(a)]);
}
}
return 0;
}
堆
堆是一棵完全二叉树,
小根堆:每一个点都是小于等于左右儿子的,递归定义的,根节点就是整个数据结构里面的最小值,堆用一维数组存储,一号结点是根节点,数组下标从1开始,方便左右孩子下标确定
插入元素是在堆的最后一个位置插入x,
up操作和down操作与树的高度成正比,时间复杂度是logn,建堆的时候如果每次插入,每次是logn,n个元素是nlogn,但是有一个O(n)的建堆操作,
t存的是三个结点里面最小的结点的编号,
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n,m;
int h[N], sizeh;
void down(int u)
{
int t = u;
if (u*2 <= sizeh && h[u *2]< h[t]) t =u*2;
if (u*2+1<= sizeh && h[u*2+1]<h[t])t=u*2+1;
if (u != t)
{
swap(h[u],h[t]);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u / 2] > h[u])
{
swap(h[u / 2],h[u]);
u/= 2;
}
}
int main()
{
scanf("%d%d",&n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d",&h[i]);
sizeh = n;
for (int i= n / 2; i; i -- ) down(i);//建堆,倒数第二层最多只会降一层,倒数第二层最多只会降两层,总共的时间复杂度小于O(n)
while (m -- )
{
printf("%d ",h[1]);
h[1] = h[sizeh];
sizeh -- ;
down(1);
}
return 0;
}
ph[k]存的是第k个插入的点是哪一个点,在堆里面下标是什么,hp[k]存的是堆里面的某一个点是第几个插入的点,互为反函数,既要从第几个插入的点找到堆里面的元素,又要从堆里面的元素找回来,两者一一对应,
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
int h[N],ph[N],hp[N], sizeh;//sizeh是元素在数组中的下标
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 <= sizeh && h[u *2]< h[t]) t =u*2;
if (u*2+1<= sizeh && 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/2,u);
u/= 2;
}
}
int main()
{
int n, m = 0;
scanf("%d",&n);
while (n -- )
{
char op[10];
int k, x;
scanf("%s",op);
if(!strcmp(op,"I"))
{
scanf("%d",&x);
sizeh ++ ;//第m个插入的元素一开始在size这个位置,堆里面size这个位置的数是第m个插入的数,
m ++ ;
ph[m] = sizeh,hp[sizeh] = m;
h[sizeh] = x;
up(sizeh);
}
else if(!strcmp(op,"PM"))printf("%d\n",h[1]);
else if (!strcmp(op,"DM"))
{
heap_swap(1,sizeh);
sizeh--;
down(1);
}
else if (!strcmp(op,"D"))
{
scanf("%d",&k);
k = ph[k];
heap_swap(k,sizeh);
sizeh -- ;
down(k),up(k);//两个操作最多只会执行一个
}
else
{
scanf("%d%d",&k, &x);
k = ph[k];
h[k] = x;
down(k),up(k);
}
}
return 0;
}
哈希表是一种期望算法,拉链法在平均情况下来看,每一条链的长度可以看成是常数,一般情况下,哈希表的时间复杂度可以看成O(1),在算法题里一般情况下不用从哈希表里删除元素,一般只有添加和查找两个操作,如果要实现删除,一般也不是真的把这个点删掉,而是直接在这个点上打一个标记,比如开一个布尔变量,标记被删除了,
取模的要是一个质数,并且此质数要尽可能的离2的整次幂远,在数学上可以证明,这么取产生冲突的概率是最小的,大于十万的第一个质数是100003,所以N最好取100003
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003;//是大于100000的最小的质数
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 ++ ;//h【k】存的是当前这个链上第一个结点的下标,
}
bool find(int x)
{
int k=(x%N+N)%N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
}
int main()
{
int n;
scanf("%d",&n);
memset(h,-1,sizeof h);//空指针下标是-1这个值,
while (n -- )
{
char op[2];
int x;
scanf("%s%d",op,&x);
if (*op =='I') insert(x);
else
{
if (find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
开放寻址法只开了一个数组,没有开链表,但是数组的大小一般是题目数据数量的2-3倍,此时冲突的概率就低了,删除操作也是找到后打一个标记,表示被删了,实际上没有删,
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003,null = 0x3f3f3f3f;//先找到大于20w的最小的质数f//用这个数表示这个位置上是空的,0x3f3f3f3f是一个大于10的九次方的数
int h[N];
int find(int x)
{
int k=(x%N+N)%N;//用一个哈希函数把x映射到数组下标范围内,
while (h[k] != null && h[k] != x)//h[k]不为空但是不是要找的x,就会往后找
{
k ++ ;
if (k== N) k=0;//看完最后一个坑位了,就循环再看第一个坑位,
}
return k;//如果x在哈希表中,k就是x的下标,如果x不在哈希表中,k就是x应该存储的位置
}
int main()
{
int n;
scanf("%d",&n);
memset(h,0x3f,sizeof h);//空指针用-1这个值表示,
//memset是按字节来赋值,h是int型数组,一共有四个字节,让每一个字节都是0x3f,因此每个int数就是四个0x3f,
while (n -- )
{
char op[2];
int x;
scanf("%s%d",op,&x);
int k = find(x);
if (*op =='I') h[k] = x;
else
{
if (h[k] != null) puts("Yes");
else puts("No");
}
}
return 0;
}
字符串前缀哈希法
把字符串看成一个p进制的数,字符串有几个字母就是有几位数,然后把p进制的数转化为10进制的数,最后对整个数模一个q,就可以把任意一个字符串映射到0-Q-1的数
可以用unsigned long long来存储,因为溢出就相当于mod2的64次方,
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
int n, m;
char str[N];
ULL h[N],p[N];//h数组表示的是某个前缀这一段的哈希值,p[i]是每一位的权值,P表示P进制为131
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
scanf("%d%d%s", &n, &m, 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;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if (get(l1,r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
想要快速判断两个字符串是否相等的时候就可以用字符串哈希,可以达到O(1)的时间而不用一个字母一个字母比较了,
STL
优先队列就是堆,
并不是所有的容器都有clear清空这个函数,
系统为某一程序分配空间的时候,所需时间,与空间大小无关,与申请次数有关,长度为n的数组,申请开辟空间的次数是logn的,均摊下来是O(1)
/*vector变长数组,倍增的思想,
size()返回元素个数
empty()返回是否为空
clear()清空
front()/back()
push back()/pop back()
begin()/end()
[]随机访问
支持比较运算,两个vector比较按照字典序比较,
pair<int,int>构造一个pair的时候可以用make_pair,或者直接写
first,第一个元素
second,第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
pair也可存储有三个属性的,把第二个属性弄成pair就可以,
string,字符串,substr(),c_str()
size()/length()返回字符串长度
empty()
clear()
substr(下标从主串的哪个位置开始,长度是多少)返回一个子串,第二个参数长度超过整个字符串长度后,就会输出到最后一个字母为止,也可把第二个参数省略,就会返回从第一个参数下标位置开始到整个字符串结束,
如果想用printf输出一个string的话,其实要输出的是字符数组的起始地址,用c_str()可以返回字符串的起始地址,printf("%s\n",a.c_str());
queue ,priority_queue, stack是没有clear()函数的,如果想要去清空一个queue,重新构建一个queue就可以了,queue<int> q;q=queue<int>();
queue,队列
size()
empty()
push()向队尾插入一个元素
front()返回队头元素
back()返回队尾元素
pop()弹出队头元素
priority_queue,优先队列,默认是大根堆
push()插入一个元系
top()返回堆顶元素
pop()弹出堆顶元素
priority_queue<int, vector<int>,greater<int>> heap;定义的时候多加两个参数就是小根堆了,
stack,栈
size()
empty()
push()向栈顶插入一个元素
top()返回栈顶元素
pop()弹出栈顶元素
deque,双端队列效率比较慢,
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end()
[]
set里面是不能有重复元素的,如果往里面插入一个重复元素,那么这个操作就会被忽略掉,multiset里面是可以有重复元素的,set所有操作时间复杂度都是O(logn)
set,map,multiset,multimap,基于平衡二叉树 (红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end() ++ ,--返回前驱和后继 时间复杂度O(logn)
set/multiset
insert()插入一个数
find()查找一个数,不存在的话返回的是end迭代器,
count()返回某一个数的个数
erase()
(1)输入是一个数x,删除所有x,k是x的个数O(k + logn)
(2)输入一个迭代器,删除这个迭代器
lower_bound()/upper bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器,不存在的话返回的是end迭代器,
upper_bound(x) 返回大于x的最小的数的选代器,不存在的话返回的是end迭代器,
map/multimap
insert()插入的数是一个pair
erase()输入的参数是pair或者迭代器
find()
[] 时间复杂度是 o(logn)
lower bound()/upper bound()
unordered_set,unordered_map,unordered_multiset, unordered_multimap,哈希表
和上面类似,增删改查的时间复杂度是 0(1)
不支持 lower_bound()/upper_bound(),迭代器的++,--也不支持,
bitset<个数> s;
bitset,压位
bitset<10000> s;
~,&,|,^
>>,<<
==,!=
[]
count()返回有多少个1
any()判断是否至少有一个1
none()判断是否全为0
set()把所有位置成1
set(k,v) 将第k位变成v
reset()把所有位变成0
flip()等价于~
flip(k)把第k位取反
*/
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
vector<int> a;
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 (vector<int>::iterator i = a.begin(); i != a.end(); i ++ ) cout << *i <<' ';
cout << endl;//a.begin()就是a[0],a.end()就是a[size()],vector<int>::iterator可以写成auto
for (auto x :a) cout << x <<' ';
cout << endl;
return 0;
}