acwing算法基础课-数据结构部分

  • Post author:
  • Post category:其他


第二章:数据结构

2-1-1 单链表

(因为传统链式存储需要大量使用New函数,会带来大量时间上的开销,所以我们用数组模拟)

(通常情况下,数据规模1e6)

#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(){
    
    
}

/*双链表*/
#include<bits/stdc++.h>
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
//如果是在下标为k的点的左边插入X,直接调用add(k-1,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];
}
int main(){
    
    
}

const int N=100010;
//定义栈
int stk[N],tt;
//插入
stk[++tt] = x;
//弹出
tt--;
//判断栈是否为空
if (tt >0)  not empty
else empty
//取栈顶元素
stk[tt];

//定义队列
int q[N],hh,tt=-1;
//在队尾插入元素,在队头弹出元素
//插入
q[++tt]=x;
//弹出
hh++;
//判断队列是否为空
if(hh<=tt) not empty
else empty
//取出队头元素
q[hh]

2-2-5 单调栈

常见题型:给出一个序列,找出每个数左边(或右边)满足某个性质最近的数

暴力做法:

for(int i=0;i<n;i++){
	for(int j=i-1;j>=0;j--){
        if(a[j]<a[i]){
            cout<<j<<endl;
            break;
        }
    }
}
#include<iostream>
using namespace std;
const int N=100010;
int n;
int stk[N],tt;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
	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;
}

2-2-6 单调队列

求滑动窗口的最大值最小值

#include<bits/stdc++.h>
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--;//把队列里所有比a[i]大的数都踢掉,它们将永无出头之日
		q[++tt]=i;
		if(i>=k-1) printf("%d ",a[q[hh]]); 
	}
	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

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=1000010;
int n,m;//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++){
        while(j && p[i]!=p[j+1]) j=ne[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];
        if(s[i]==p[j+1]) j++;
        if(j==n){
            printf("%d ",i-n);//不加1是因为下标要从0开始
            //j已经走到头了,为了下一次匹配j走到ne[j]
            j=ne[j];//为下一次匹配做准备
        }
    }
    return 0;
}

2-2-8 Tire树

用来高效的存储和查找字符串集合的数据结构

题目一定会限制字符的种类

存储方法:如图所示。

最后需要在每一个单词的结尾加上一个标记

查询方法:

①在上图查询单词”abcf”

由根节点a–>b—>c,发现下面没有f,故不存在单词”abcf”

②在上图查询单词”abcd”

由根节点a—>b—>c—>d,虽然存在,但是d字母上没有标记,所以”abcd”也不存在

模板题:

维护一个字符串集合,支持两种操作:

“I x”向集合中插入一个字符串x;

“Q x”询问一个字符串在集合中出现了多少次。

共有N个操作,输入的字符串总长度不超过 1 e 5 1e51e5,字符串仅包含小写英文字母。

#include<iostream>
using namespace std;
const int N=100010;
int son[N][26],cnt[N],idx;//son存储每个点的子结点,cnt存储当前点为结尾的单词个数,idx指示当前用到的下标
char str[N];
//下标是0的点,既是根结点,又是空结点
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点不存在u这个儿子,创建它
        p=son[p][u];
    }
    //最后p点为该单词的结尾,对其加上标记,次数+1
    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;
}

应用:最大异或对

在给定的N个整数A1,A2……AN中选出两个进行xor(异或)运算,得到的结果最大是多少?

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=32*N+5;
int son[N][2],idx;
int a[N];
void insert(int x){
	int p=0;
	for(int i=30;i>=0;i--){
		int u=x>>i &1;//取出整数的第i位 
		if(!son[p][u]) son[p][u]=++idx;
		p=son[p][u];
	}
}
int query(int x){
	int p=0;
	int t=0;
	for(int i=30;i>=0;i--){
		int u=x>>i &1;
		if(son[p][!u]){
			p=son[p][!u];
			t=t*2+!u;
		}else{
			p=son[p][u];
			t=t*2+u;
		}
	}
	return t;
}
int main(){
	int n;
	cin>>n;
	for(int i=0;i<n;i++) cin>>a[i];
	int res=0;
	for(int i=0;i<n;i++){
		insert(a[i]);
		int t=query(a[i]);
		res=max(res,a[i]^t);
	}
	cout<<res<<endl;
	return 0;
}

2-2-9 并查集

快速地处理以下问题:

1、将两个集合合并

2、询问两个元素是否在一个集合当中

对于暴力解法:

belong[x]=a;//x元素属于集合a
//查询是否在同一个集合操作
if(belong[x]==belong[y]) //O(1)
//合并两个集合
//需要一个一个修改每个元素的编号,时间复杂度太高

2-2-10 堆

如何手写一个堆?

手写堆主要解决以下问题:

1、插入一个数

heap[++size]=x;up(size);

1

2、求集合当中的最小值

heap[1];

1

3、删除最小值

heap[1]=heap[size];size–;down(1);

1

4、删除任意一个元素

heap[k]=heap[size];size–;down(k);up(k);//down和up只会执行一次

1

5、修改任意一个元素

heap[k]=x;down(k);up(k);

1

注:下标从1开始比较方便

核心函数:

down操作

int h[N],Size;
void down(int u){
    int t=u;
    if(u*2<=Size && h[u*2]<h[t]) t=u*2;
    if(u*2+1<=Size && h[u*2+1] < h[t]) t=u*2+1;
    if(u!=t){
        swap(h[u],h[t]);
        down(t);
    }
}

up操作:

void up(int u){
    while(u/2 &&h[u/2]>h[u]){
        swap(h[u/2],h[u]);
        u/=2;
    }
}

例题:838. 堆排序

输入一个长度为n的整数数列,从小到大输出前m小的数。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m;
int h[N],Size;
void down(int u){
    int t=u;
    if(u*2<=Size && h[u*2]<h[t]) t=u*2;
    if(u*2+1<=Size && h[u*2+1] < h[t]) t=u*2+1;
    if(u!=t){
        swap(h[u],h[t]);
        down(t);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&h[i]);
    Size=n;
    for(int i=n/2;i;i--) down(i);
    while(m--){
        printf("%d ",h[1]);
        h[1]=h[Size];
        Size--;
        down(1);
    }
    return 0;
}

模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:

“I x”,插入一个数x;

“PM”,输出当前集合中的最小值;

“DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);

“D k”,删除第k个插入的数;

“C k x”,修改第k个插入的数,将其变为x;

现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。

#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N=100010;
int n;
int h[N],ph[N],hp[N],Size;
/*ph[i]表示第i个插入的数的下标   hp[i]表示下标为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<=Size && h[u*2]<h[t]) t=u*2;
    if(u*2+1<=Size && 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(){
   	scanf("%d",&n);
    int m=0;
    while(n--){
        char op[10];
        int k,x;
        scanf("%s",op);
        if(!strcmp(op,"I")){
            scanf("%d",&x);
            Size++;
            m++;
            ph[m]=Size;
            hp[Size]=m;
            h[Size]=x;
            up(Size);
        }else if(!strcmp(op,"PM")){
			printf("%d\n",h[1]);
        }else if(!strcmp(op,"DM")){
			heap_swap(1,Size);
            Size--;
            down(1);
        }else if(!strcmp(op,"D")){
            scanf("%d",&k);
            k=ph[k];
            heap_swap(k,Size);
            Size--;
            down(k);up(k);
        }else{
            scanf("%d%d",&k,&x);
            k=ph[k];
            h[k]=x;
            down(k),up(k);
        }
    }
    return 0;
}

2-2-11 哈希表

哈希表存储结构:①开放寻址法 ②拉链法

字符串哈希方式

例如有哈希函数 h(x),将区间[-1e9,1e9]的数字映射到[0,1e5]中

方法:直接将 x mod 1e5,但是这样会存在哈希冲突**(取模的数尽可能是质数)**

解决哈希冲突的方法:①开放寻址法 ②拉链法

模拟散列表

维护一个集合,支持如下几种操作:

“I x”,插入一个数x;

“Q x”,询问数x是否在集合中出现过;

现在要进行N次操作,对于每个询问操作输出对应的结果。

拉链法:

#include<iostream>
#include<cstring>
using namespace std;
const int N=100003;//尽可能取成素数
int h[N],e[N],ne[N],idx;
void insert(int x){
	int t=(x%N+N)%N;//防止出现负数
	e[idx]=x;ne[idx]=h[t];h[t]=idx++;
}
bool find(int x){
	int t=(x%N+N)%N;
	for(int i=h[t];i!=-1;i=ne[i]){
		int u=e[i];
		if(u==x) return 1;
	}
	return 0;
}
int main(){
	int n;
	scanf("%d",&n);
	memset(h,-1,sizeof h);
	while(n--){
		char op[2];
		int x;
		scanf("%s%d",op,&x);
		if(op[0]=='I'){
			insert(x);
		}else{
			if(find(x)){
				puts("Yes");
			}else{
				puts("No");
			}
		}
	}

}

开放寻址法:

#include<iostream>
#include<cstring>
using namespace std;
const int N=100003,null=0x3f3f3f3f;
int h[3*N];
int find(int x){
	int t=(x%N+N)%N;
	while(h[t]!=null&&h[t]!=x){
		t++;
		if(t==N) t=0;
	}
	return t;
}
int main(){
	int n;
	scanf("%d",&n);
	memset(h,0x3f,sizeof h);
	while(n--){
		char op[2];
		int x;
		scanf("%s%d",op,&x);
		int k=find(x);
		if(op[0]=='I'){
			h[k]=x;
		}else{
			if(h[k]==null) puts("No");
			else puts("Yes");
		}
	}
}



版权声明:本文为a13352912632原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。