常用数据结构

数据结构

单链表

1.单链表

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 k 个插入的数后面的数;
  3. 在第 k 个插入的数后插入一个数。

现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 x。
  2. D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
  3. I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。

输出格式

共一行,将整个链表从头到尾输出。

数据范围

1≤M≤100000
所有操作保证合法。

输入样例:

10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6

输出样例:

6 4 6 5
//要点
//head为链表头的下标
//要使得终点指的是-1
#include<iostream>
using namespace std;
const int N=1e5+10;
int e[N],ne[N];
int idx,m,head;
void init(){
    head=-1;
    idx=0;
}
//向链表头插入一个数 x
void H(int x){
    e[idx]=x;
    ne[idx]=head;
    head=idx;
    idx++;
}
//在第k个插入的数后面插入一个数 x(此操作中 k 均大于 0)
void I(int k,int x){
    e[idx]=x;
    ne[idx]=ne[k];
    ne[k]=idx;
    idx++;
}
//删除下标为k的数的后面的数
void D(int k){
    ne[k]=ne[ne[k]];
}
int main(){
    int x,y;
    init();
    cin>>m;
    while(m--){
        string op;
        cin>>op;
        if(op=="H"){
            cin>>x;
            H(x);
        }
        //`D k`,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)
        else if(op=="D"){
            cin>>x;
            if(!x)  head=ne[head];
            D(x-1);
        }
        //`I k x`,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)
        else {
            cin>>x>>y;
            I(x-1,y);
        }
    }
    //遍历单链表
    for(int i=head;i!=-1;i=ne[i])
        cout<<e[i]<<" ";
    cout<<endl;
    return 0;
}

双链表

2.双链表

实现一个双链表,双链表初始为空,支持 5 种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k 个插入的数删除;
  4. 在第 k 个插入的数左侧插入一个数;
  5. 在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数 x。
  2. R x,表示在链表的最右端插入数 x。
  3. D k,表示将第 k 个插入的数删除。
  4. IL k x,表示在第 k 个插入的数左侧插入一个数。
  5. IR k x,表示在第 k 个插入的数右侧插入一个数。
输出格式

共一行,将整个链表从左到右输出。

数据范围

1≤M≤100000
所有操作保证合法。

输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
//每个点有两个指针,一个指向前,一个指向后
//l[N]存左,r[N]存右

//第一种解法思路
#include<iostream>
using namespace std;
const int N=1e5+10;
int e[N],idx,l[N],r[N];
//0表示左端点,1表示右端点,且0和1表示的是端点的值
void init(){
    idx=2;
    r[0]=1;
    l[1]=0;
}
//`L x`,表示在链表的最左端插入数 x
void L(int x){
    e[idx]=x;
    //两边伸展
    l[idx]=0; r[idx]=r[0];
    //先右后左
    l[r[0]]=idx;
    r[0]=idx;
	idx++;

}
//`R x`,表示在链表的最右端插入数 x。
void R(int x){
    e[idx]=x;
    //两边伸展
    l[idx]=l[1];
    r[idx]=1;
    //先左后右
    r[l[1]]=idx;
    l[1]=idx;
    idx++;

}
//`D k`,表示将第 k 个插入的数删除。
//[将下标为k的结点删除]
void D(int k){
    l[r[k]]=l[k];
    r[l[k]]=r[k];
}
//`IR k x`,表示在第 k 个插入的数右侧插入一个数
//[函数表示的k是下标为k]
void IR(int k,int x){
    e[idx]=x;
    r[idx]=r[k]; l[idx]=k; //两边伸展
    //先右后左
    l[r[k]]=idx;
    r[k]=idx;
    idx++;
}

int main(){
    int m;
    int k,x;
    init();
    cin>>m;
    while(m--){
        string op;
        cin>>op;
        //`L x`,表示在链表的最左端插入数 x。
        if(op=="L")  {
            int x;
            cin>>x;
            L(x);
        }
        //`R x`,表示在链表的最右端插入数 x
        else if(op=="R"){
            int x;
            cin>>x;
            R(x);
        }
        //`D k`,表示将第 k 个插入的数删除
        else if(op=="D")
        {   int k;
            cin>>k;
            D(k+1);
        }


        //`IR k x`,表示在第 k 个插入的数右侧插入一个数
        else if(op=="IR"){
            int k,x;
            cin>>k>>x;
            IR(k+1,x);
        }
        //`IR k x`,表示在第 k 个插入的数左侧插入一个数
        else{
            int k,x;
            cin>>k>>x;
            IR(l[k+1],x);;
        }
    }
    for(int i=r[0];i!=1;i=r[i]){
        cout<<e[i]<<" ";
    }
    return 0;
}
//第二种解法思路

#include<iostream>
using namespace std;
const int N=1e5+10;
int e[N],idx,l[N],r[N];
//0表示左端点,1表示右端点,且0和1表示的是端点的值
void init(){
    idx=2;
    r[0]=1;
    l[1]=0;
}
//[将下标为k的结点删除]
void D(int k){
    l[r[k]]=l[k];
    r[l[k]]=r[k];
}
//`IR k x`,表示在第 k 个插入的数右侧插入一个数
//[函数表示的k是下标为k]
void IR(int k,int x){
    e[idx]=x;
    r[idx]=r[k]; l[idx]=k; //两边伸展
    //先右后左
    l[r[k]]=idx;
    r[k]=idx;
    idx++;
}

int main(){
    int m;
    int k,x;
    init();
    cin>>m;
    while(m--){
        string op;
        cin>>op;
        //`L x`,表示在链表的最左端插入数 x。
        if(op=="L")  {
            int x;
            cin>>x;
            IR(0,x);
        }
        //`R x`,表示在链表的最右端插入数 x
        else if(op=="R"){
            int x;
            cin>>x;
            IR(l[1],x);
        }
        //`D k`,表示将第 k 个插入的数删除
        else if(op=="D")
        {   int k;
            cin>>k;
            D(k+1);
        }
        //`IR k x`,表示在第 k 个插入的数右侧插入一个数
        else if(op=="IR"){
            int k,x;
            cin>>k>>x;
            IR(k+1,x);
        }
        //`IR k x`,表示在第 k 个插入的数左侧插入一个数
        else{
            int k,x;
            cin>>k>>x;
            IR(l[k+1],x);;
        }
    }
    for(int i=r[0];i!=1;i=r[i]){
        cout<<e[i]<<" ";
    }
    return 0;
}

3.模拟栈

实现一个栈,栈初始为空,支持四种操作:

  1. push x – 向栈顶插入一个数 x;
  2. pop – 从栈顶弹出一个数;
  3. empty – 判断栈是否为空;
  4. query – 查询栈顶元素。

现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围

1≤M≤100000,
1≤x≤109
所有操作保证合法。

输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
//tt下标从1开始存东西
//tt就是本身,每次先给tt加一下再存东西
//query--查询栈顶元素一定是保证不空
#include<iostream>
using namespace std;
const int N=1e5+10;
int stk[N],tt;
int main(){
    int m;
    cin>>m;
    while(m--){
        string op;int x;
        cin>>op;
        //1. `push x` – 向栈顶插入一个数 x;
        //2. `pop` – 从栈顶弹出一个数;
        //3. `empty` – 判断栈是否为空;
        //4. `query` – 查询栈顶元素。
        if(op=="push"){
            cin>>x;
            stk[++tt]=x;
        }else if(op=="query"){
            if(tt>0) cout<<stk[tt]<<endl;

        }else if(op=="pop"){
            tt--;
        }else if(op=="empty"){
            if(!tt) cout<<"YES"<<endl;
            else  cout<<"NO"<<endl;
        }
    }
    return 0;
}

4.表达式求值

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

  • 数据保证给定的表达式合法。
  • 题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
  • 题目保证表达式中所有数字均为正整数。
  • 题目保证表达式在中间计算过程以及结果中,均不超过 231−1。
  • 题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。
  • C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。
输入格式

共一行,为给定表达式。

输出格式

共一行,为表达式的结果。

数据范围

表达式的长度不超过 105105。

输入样例:
(2+2)*(1+1)
输出样例:
8
//栈顶的优先级大于等于要进入栈的就出栈运算
//栈顶的优先级小于要进栈的就加进来进栈的
#include<iostream>
#include<stack>
#include<string>
#include<unordered_map>
using namespace std;

stack<int> num;
stack<char> op;

unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};

void eval(){
    int b=num.top(); num.pop();
    int a=num.top(); num.pop();
    int x=0;
    if(op.top()=='+') x=a+b;
    if(op.top()=='-') x=a-b;
    if(op.top()=='*') x=a*b;
    if(op.top()=='/') x=a/b;
    op.pop();
    num.push(x);
}
int main(){
    string str;
    cin>>str;
    for(int i=0;i<str.size();i++){
        auto c = str[i];
        if(isdigit(c))
        {
            int x=0,j=i;
            while(j<str.size()&&isdigit(str[j]))
            {  x=x*10+str[j]-'0';  j++; }
            num.push(x);
            i=j-1;
        }
        else if(c=='(')
            op.push(c);
        else if(c==')'){
            while(op.top()!='(')  eval();
            op.pop();//出左括号
        }
        else {
            //算的是栈里面的
            while(op.size()&&pr[op.top()]>=pr[str[i]]) eval();
            //然后新进栈的放后面
            op.push(str[i]);
        }
    }
    while(op.size()) eval();
    cout<<num.top()<<endl;
    return 0;
}

队列

5.模拟队列

实现一个队列,队列初始为空,支持四种操作:

  1. push x – 向队尾插入一个数 x;
  2. pop – 从队头弹出一个数;
  3. empty – 判断队列是否为空;
  4. query – 查询队头元素。

现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示队头元素的值。

数据范围

1≤M≤100000,
1≤x≤109,
所有操作保证合法。

输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
//1. `push x` – 向队尾插入一个数 x;
//2. `pop` – 从队头弹出一个数;
//3. `empty` – 判断队列是否为空;
//4. `query` – 查询队头元素。
//队头指向第一个,队尾指向下一个
#include<iostream>
using namespace std;
const int N=1e5+10;
int q[N],hh,tt;
int main(){
    int m,x;
    cin>>m;
    string op;
    while(m--){

        cin>>op;
        if(op=="push")
        {  cin>>x;
            q[tt++]=x;
        }
        else if(op=="pop"){
            hh++;
        }
        else if(op=="empty"){
            if(tt>hh) cout<<"NO"<<endl;
            else cout<<"YES"<<endl;
        }
        else if(op=="query"){
            cout<<q[hh]<<endl;
        }

    }
    return 0;
}

单调栈

6.单调栈

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围

1≤N≤105
1≤数列中元素≤109

输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2
//单调栈,就近原则
#include<iostream>
using namespace std;
const int N=1e5+10;
int stk[N],top;
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        int x;
        cin>>x;
        //比较
        while(top&&stk[top]>=x) top--;
        
        if(!top) cout<<-1<<" ";
        else cout<<stk[top]<<" ";
        stk[++top]=x;
    }
    return 0;
}

单调队列

7.滑动窗口

给定一个大小为 n≤106 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。

窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。

第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
样例执行过程
    1 3 -1 -3 5 3 6 7
    k=3
    i=0    队列中 1进队 
    i=1    1<3  3进队
    i=2    -1比1、3都小,队尾出队 3出队 1出队 -1进队     i+1>=3 输出-1
    i=3    -1的下标是2 满足3-3+1=1小于2          -1比-3大 -1出队 -3进队           输出队头-3
    i=4    此时队列中只有一个元素-3 且下标为3 满足4-3+1=2不大于3  5比-3大 5进队           输出队头-3
    i=5    此时队列中有两个元素 -3 5 下标分别为3、4 满足5-3+1=3 不大于3 5>3 将5出队 3进队  输出队头-3
    i=6    此时队列中有两个元素 -3 3 下标分别为3、5 满足6-3+1=4>3 队头后移 此时队列中有一个元素3  3小于6 6进队 输出队头3
    i=7    此时队列中有两个元素 3 6  下标分别为5、6 满足7-3+1=5 不大于5 6<7 将7入队,输出队头3
    程序结束
//单调队列
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int a[N],q[N],hh,tt=-1;
int main(){
    int n,k;
    cin>>n>>k;
    for(int i=0;i<n;i++) scanf("%d",&a[i]);
    
    //求最小值
    for(int i=0;i<n;i++){
        //1.判定是否在窗口内,若不在窗口内,hh++即窗口后移一位
        if(hh<=tt&&i-k+1>q[hh]) hh++;
        
        //2.判断当遍历的(即窗口外的)更小时,将窗口内的出队列(队尾出队)
        while(hh<=tt&&a[i]<=a[q[tt]]) tt--;
		
        //3.再将窗口外的入队
        q[++tt]=i;
        
        //4.从第一个完整的窗口开始,进行输出
        if(i+1>=k) printf("%d ",a[q[hh]]);
    
    }
    cout<<endl;
    hh=0,tt=-1;
    //求最大值 同理
    for(int i=0;i<n;i++){
        if(hh<=tt&&i-k+1>q[hh]) hh++;
        while(hh<=tt&&a[i]>=a[q[tt]]) tt--;
        q[++tt]=i;
        if(i+1>=k) printf("%d ",a[q[hh]]);
    }
    return 0;                     
} 

KMP

8.KMP字符串

给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串 P 在模式串 S 中多次作为子串出现。

求出模板串 P 在模式串 S 中所有出现的位置的起始下标。

输入格式

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

输出格式

共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

数据范围

1≤N≤105
1≤M≤106

输入样例:
3
aba
5
ababa
输出样例:
0 2
题目分析

next[i]数组表示的含义是以i为终点的后缀和从1开始的前缀两者相等的最大值
next[i]=j
两边往中间延申,达到长度最长即为至少往后移动的距离,求next的数组就是求前缀和后缀相等时的最大值
含义是p[1,j]=p[i-j+1,i]

pababa
下标12345
next[]00123
求next数组的执行过程
ne[1]=0;j=0//就从头比较        
i=2 p[2]==p[1]? 不等于-->ne[2]=0;
i=3 p[3]==p[1] 等于-->j=1 ne[3]=1;
i=4 p[4]==p[2]? 等于-->j=2 ne[4]=2;
i=5 p[5]!=p[3]? 等于-->j=3 ne[5]=3;
#include<iostream>

using namespace std;

const int N=100010,M=1000010;

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++)
    {
        //不等于回溯
        while(j&&p[i]!=p[j+1]) j=ne[j];
        //如果等于就后移
        if(p[i]==p[j+1]) j++;
        //ne[]赋值
        ne[i]=j;
    }
    //kmp匹配过程
    
    for(int i=1,j=0;i<=m;i++){
        //j是有对应的p串元素,失配则回溯
        while(j && s[i] != p[j+1]) j = ne[j];
        //匹配则后移
        if(s[i]==p[j+1]) j++;
        //匹配成功后回溯继续下一个匹配
        if(j == n)
        {
            printf("%d ",i-n);
            j=ne[j];
        }
    }
    return 0;
}

Trie

9.Trie字符串统计

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

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

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

输入格式

第一行包含整数 N,表示操作数。

接下来 N 行,每行包含一个操作指令,指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2∗104

输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
#include<iostream>
using namespace std;
const int N = 1e5+10;
//Trie树:用来高效地存储和查找字符串集合的数据结构。
//son[p][u] p是指某一层,u是指该层真正的结点0~25 表示a~z
//idx是类似一个指针,确定整个树的结点序号,方便给cnt[idx]赋值
int son[N][26],cnt[N];//son存的是整颗树的结点,cnt存的是以当前结点结尾的字符串有多少个
int idx;//下标是0
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;
    cin>>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;
}

10.最大异或对

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

输入格式

第一行输入一个整数 N。

第二行输入 N 个整数 A1~AN

输出格式

输出一个整数表示答案。

数据范围

1≤N≤105,
0≤Ai<231

输入样例:
3
1 2 3
输出样例:
3
//求第k位
//n>>k&1
#include<iostream>
#include<algorithm>
using namespace std;
//N是输入数的个数,每个输入的数可拆解31位,最大存储结点为N*31,有31位但都可以分为0或1
const int N=1e5+10,M=31*N;

int n;
int a[N];
//M是总结点
int son[M][2],idx;
//31位是题目给的
void insert(int x){
    int p=0;
    for(int i=30;i>=0;i--){
        int u=x>>i&1;
        if(!son[p][u]) son[p][u]=++idx; //创建结点
        p=son[p][u];//走到该结点
    }
    //此处不用标记
}
//尽量最大着
int  search(int x){
    int p=0,res=0;
    for(int i=30;i>=0;i--){
        int u=x>>i&1;
        //相反位存在则取相反位,不存在就源路径
        if(son[p][!u]){
            //取相反位,例如u是0,!u是1
            p=son[p][!u];
            res=res*2+1;//此处是最高位,因为它不停的*2累加,看似是最低位,其实累加操作完成后是最高位,下面同理
        } 
        else{
            p=son[p][u];
            res=res*2+0;
        }
    }
    
    return res;
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++) {
        scanf("%d",&a[i]);
        insert(a[i]);
    }
    int res=0;
    for(int i=0;i<n;i++)
        res=max(res,search(a[i]));
    cout<<res<<endl;
    return 0;
}

并查集

11.合并集合

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  1. M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
并查集
1.将两个集合合并
2.询问两个元素是否在一个集合当中

基本原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。

问题1:如何判断树根:if(p[x]==x)
问题2:如何求x的集合编号:while(p[x]!=x) x=p[x];
问题3:合并两个集合:px是x的集合编号,py是y的集合编号。p[x]=y

并查集的优化:路径压缩。
树太高的话遍历时间太长,都指向根节点可解决这个问题,实现路径压缩。
#include<iostream>
using namespace std;
const int N=1e5+10;
int p[N];
int n,m;//n个数、m种操作
int find(int 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];
        int a,b;
        scanf("%s%d%d",op,&a,&b);
        if(op[0]=='M') p[find(a)]=find(b);
        else {
            if(p[find(a)]==p[find(b)])
                cout<<"Yes"<<endl;
            else 
                cout<<"No"<<endl;
        }
        
    }
    return 0;
}

12. 连通块中点的数量

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

  1. C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
  2. Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
  3. Q2 a,询问点 a 所在连通块中点的数量;
输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
#include<iostream>
using namespace std;
const int N=1e5+10;
int n,m;
int p[N],se[N];

int find(int x){
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int main(){
    cin>>n>>m;
    int a,b;
    for(int i=1;i<=n;i++)
    { p[i]=i;
        se[i]=1;
    }
    while(m--){
        char op[5];
        scanf("%s",op);
        //合并
        if(op[0]=='C')
        {
            scanf("%d%d",&a,&b);
            if(find(a)==find(b)) continue;
            se[find(a)]+=se[find(b)];
            p[find(b)]=find(a);
        }
        //查询
        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",se[find(a)]);
        }

    }
    return 0;
}

13.食物链

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中 X 或 Y 比 N 大,就是假话;
  3. 当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤50000,
0≤K≤100000

输入样例:
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5
输出样例:
3
//核心代码理解,主要是看给d[i]赋值的过程
int find(int x)
{
    if(p[x]!=x)
    {
        int tmp=find(p[x]);
        d[x]+ = d[p[x]];
        p[x] = tmp;
    }
    return p[x];
}
//d[i]的正确理解,应是第i个节点到其父节点距离
//find()函数进行路径压缩,当查询某个节点i时,如果i的父节点不为根节点的话,就会进行递归调用,将i节点沿途路径上所有节点均指向父节点,此时的d[i]存放的是i到父节点,也就是根节点的距离


//只有3种动物且构成单方向环形
//有n个动物

//根是0 1吃根 2吃 1  
//1  2  3  1  2 3
//0->1->2->3->4>5->0
//模三余0 与根是同类 
//模三余1 吃根
//模三余0 与根是同类
#include<iostream>
using namespace std;
const int N=5e4+10;
int n,m;
int p[N],d[N];
int find(int x){
    if(p[x]!=x){
        int t=find(p[x]);
        d[x]=d[x]+d[p[x]];
        p[x]=t; 
    } 
    return p[x];
}
//主函数更新集合,d[i]指的是与父节点的关系
//find函数一个是查找根节点,另一个是路径压缩,全部指向根节点,且求出距离长度
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i]=i;
    int res=0;
    while(m--){
        int t,x,y;
        scanf("%d%d%d",&t,&x,&y);
        if(x>n||y>n) res++;
        else{
            int px=find(x),py=find(y);
            if(t==1){
                //在一个集合里面
                if(px==py&& (d[x]-d[y])%3) res++;
                //说明不在一个集合里面,不在一个集合的话不是判断,而是加入,当真话的弄
                else if(px!=py){
                    //py是老大
                    p[px]=py;
                    // (?+d[x]-d[y])%3=0又或者(d[y]-d[x]-?)%3=0
                    d[px]= d[y]-d[x];
                }
            }else{
                if(px==py&& (d[x]-d[y]-1)%3) res++;
                //不在一个集合,姑且认为是真话,直接加进来
                else if(px!=py){
                    p[px]=py;
                    //x吃y (x+1-y)%3=0
                    // (d[x]+?-1-d[y])%3=0
                    d[px]=d[y]-d[x]+1;
                }            
            }
        }       
    }
     cout<<res<<endl;
}

14.堆排序

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

输入格式

第一行包含整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

输出格式

共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围

1≤m≤n≤105
1≤数列中元素≤109

输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
//小根堆
#include<iostream>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],se;
void down(int u)
{	//t类似一个指针
    int t=u;
    if(2*u<=se&&h[u*2]<h[t]) t=u*2;
    if(2*u+1<=se&&h[u*2+1]<h[t]) t=u*2+1;
    if(t!=u)
    {   swap(h[t],h[u]);
        down(t);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&h[i]);
    se=n;
    for(int i= n/2;i;i--) down(i);
    while(m--){
        printf("%d ",h[1]);
        h[1]=h[se];
        se --;
        down(1);
    }

    return 0;
}

15.模拟堆

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

  1. I x,插入一个数 x;
  2. PM,输出当前集合中的最小值;
  3. DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. D k,删除第 k 个插入的数;
  5. C k x,修改第 k 个插入的数,将其变为 x;

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

输入格式

第一行包含整数 N。

接下来 N 行,每行包含一个操作指令,操作指令为 I xPMDMD kC k x 中的一种。

输出格式

对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围

1≤N≤105
−109≤x≤109
数据保证合法。

输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
//1. `I x`,插入一个数 x;
//2. `PM`,输出当前集合中的最小值;
//3. `DM`,删除当前集合中的最小值(数据保证此时的最小值唯一);
//4. `D k`,删除第 k 个插入的数;
//5. `C k x`,修改第 k 个插入的数,将其变为 x;

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

//需要两个额外数组,ph[]和hp[]
//ph存的是第k个插入的点的堆里面的下标
//hp存的是堆里面的点下标对应的插入次序

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int h[N],ph[N],hp[N],se;
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<=se&&h[u*2]<h[t]) t=u*2;
    if(u*2+1<=se&&h[u*2+1]<h[t]) t=u*2+1;
    if(t!=u){
        heap_swap(u,t);
        down(t);
    }
}
void up(int u){
    //   u/2是父节点
    while(u/2&&h[u/2]>h[u]){
        heap_swap(u/2,u);
        u/=2;
    }
}
int main(){
    int n,m=0;
    cin>>n;
    while(n--){
        char op[5];
        scanf("%s",op);
        //strcmp函数等于返回0
        if(!strcmp(op,"I")){
            int x;
            scanf("%d",&x);
            m++;
            h[++se]=x;
            ph[m]=se;hp[se]=m;
            up(se);
        }
        else if(!strcmp(op,"PM"))
            printf("%d\n",h[1]);
        else if(!strcmp(op,"DM")){
            heap_swap(1,se);
            se--;
            down(1);
        }
        else if(!strcmp(op,"D")){
            int k;
            scanf("%d",&k);
            k=ph[k];
            heap_swap(k,se);
            se--;
            down(k),up(k);
        }
        else if(!strcmp(op,"C")){
            int k,x;
            scanf("%d%d",&k,&x);
            k=ph[k];
            h[k]=x;
            down(k),up(k);
        }
    }
    return 0;
}

哈希表

16.模拟散列表

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

  1. I x,插入一个数 x;
  2. Q x,询问数 x 是否在集合中出现过;

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

输入格式

第一行包含整数 N,表示操作数量。

接下来 N 行,每行包含一个操作指令,操作指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤N≤105
−109≤x≤109

输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e5+3;
int h[N],e[N],ne[N],idx;
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;
}
void insert(int x){
	//转换成数学意义的余数
    //最后模N是为了防止正数无缘无故多加一遍
    int k=(x%N+N)%N;
    e[idx]=x;
    ne[idx]=h[k];//h[k]指的是链子
    h[k]=idx;
    idx++;
}
int main(){
    int n;
    scanf("%d",&n);
    //将h数组全部初始化为-1
    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");
        }
    }
    return 0;
    
}

17.字符串哈希

给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

输入格式

第一行包含整数 n 和 m,表示字符串长度和询问次数。

第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。

接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

注意,字符串的位置从 1 开始编号。

输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
//思路 就是全部用数字表达出来,跟求前缀和一样,总数相等即就是字符串一样
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
const int N=1e5+10,P=131;
int n,m;
char str[N];
ULL h[N],p[N];
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;  //p[i]就是p的i次方
        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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Redis常用数据结构有六种,分别是简单动态字符串(SDS)、链表、字典、跳跃表、整数集合和压缩列表。这些数据结构被用来构建Redis的对象系统,包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象。每种对象都有多种不同的数据结构实现,以适应不同的应用场景。字符串类型是Redis最基础的数据结构,它的值可以是简单的字符串、复杂的字符串(如JSON、XML)、数字(整数、浮点数)甚至二进制数据(如图片、音频、视频),但值的大小不能超过512MB。Redis中的字符串是动态字符串,可以通过预分配冗余空间的方式减少内存分配的频率。字符串的长度不超过1MB时,扩容时会加倍现有的空间;而超过1MB时,每次扩容只会增加1MB的空间。需要注意的是,Redis规定字符串的长度不能超过512MB。123 #### 引用[.reference_title] - *1* [Redis常见数据结构](https://blog.csdn.net/weixin_42348609/article/details/102996982)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *2* [Redis-常用数据结构](https://blog.csdn.net/Huangjiazhen711/article/details/127567141)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *3* [Redis的五种基础数据结构](https://blog.csdn.net/yuyuanlai/article/details/106745757)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迟意..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值