四种基础数据结构的变种应用

最近重新复习了一下一些基础的数据结构,今天介绍四种基础结构的变种应用,分别是,单调栈,单调队列,01字典树和带权并查集,四种基础的数据结构都是很常见的,我们就直接介绍变种后的这些数据结构如何应用。每个部分都介绍一个最经典的例题。

单调栈

首先,顾名思义,单调栈指的就是栈内元素单调增或减的栈,单调栈可以维护的信息就是每个数前后第一个大于/小于他的数。这是单调栈的维护方法。
单调递增栈:在保持栈内元素单调递增的前提下(如果栈顶元素大于要入栈的元素,将将其弹出),将新元素入栈。

单调递减栈:在保持栈内元素单调递减的前提下(如果栈顶元素小于要入栈的元素,则将其弹出),将新元素入栈。

它的时间复杂度是O(n)。

对于单调递增栈,有两个性质常常用到,

1.栈内自己左边的数就是数组左边第一个小于自己的元素。

2.每个元素被弹出是,遇到的就是数组中第一个比自己小的元素。

单调递减栈刚好相反

这里给大家一个维护每个数左边第一个小于它的数的例子


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

输入格式
第一行包含整数 N,表示数列长度。

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

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

数据范围
1≤N≤105
1≤数列中元素≤109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2

这是stl的代码

#include<bits/stdc++.h>
using namespace std;
stack<int>a;
int main()
{
    int n,x;
    cin>>n;
    while (n -- )
    {
        cin>>x;
        while(!a.empty()&&a.top()>=x)a.pop();
        if(a.empty())cout<<"-1"<<" ";
        else cout<<a.top()<<" ";
        a.push(x);
    }
    cout<<endl;
    return 0;
}

如果是手写栈

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
    int n;cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        while (tt && stk[tt] >= x) tt -- ;
        if (!tt) printf("-1 ");
        else printf("%d ", stk[tt]);
        stk[ ++ tt] = x;
    }
    return 0;
}

维护右边第一个大于该元素值时,开一个新数组存储即可

单调队列

同样的,顾名思义,单调队列指的就是具有单调性的队列,他也分为单调递增队列和单调递减队列。如何维护我们放在下面,我们先来看看他能解决什么问题

  • 单调递增队列:保证队列头元素一定是当前队列的最小值,用于维护区间的最小值。
  • 单调递减队列:保证队列头元素一定是当前队列的最大值,用于维护区间的最大值。

什么是区间的最小值,举个简单的例子,给一个长度为5的数组,问区间长度为3的最大值是多少,我们可以分别维护每个区间的最大值,但这样显然复杂度很大,不能很快解决问题,如果用单调队列来维护,就可以保证只用扫一遍就能维护出所有区间段的信息,首先我们要明白单调队列单调的原理。比如单调递增队列。保证队列单调递增的意义在什么呢,实际上,如果后进入队列的值比之前队列的中的值还大,那么无论是当前区间还是之后的区间,当前队列中的值都不可能作为区间的最小值,所以可以直接弹出队列,如果之前的值比现在插入的值小,那就说明,这个区间最小值还是前面的值,但是未来当前插入的值有可能成为最小的值,所以把队列中比他大的数弹出后,在把它插入,可以保证,队头永远是当前区间的最小值,随着移动,队头逐渐弹出,队尾逐渐插入,就可以维护单调队列了

这里用一道单调队列最经典的例题来解释一下

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

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

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

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

以下是一个例子:

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

窗口位置	最小值	最大值
[1 3 -1] -3 5 3 6 7	-1	3
1 [3 -1 -3] 5 3 6 7	-3	3
1 3 [-1 -3 5] 3 6 7	-3	5
1 3 -1 [-3 5 3] 6 7	-3	5
1 3 -1 -3 [5 3 6] 7	3	6
1 3 -1 -3 5 [3 6 7]	3	7
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式
输入包含两行。

第一行包含两个整数 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

题目要求同时维护最大值和最小值,我们就把单调递增队列和单调递减队列一起写了就可以了

#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int a[N], q[N];
int main()
{
    int n, k;
    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++;//q[hh]指的是当前队头元素的原始位置,判断它是否在这个区间中,是否需要删除
        while(hh<=tt&&a[q[tt]]>=a[i])tt--;//如果当前队尾比新插入大,后面不可能用得到,所以可以退出队伍
        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;
}

单调队列还可以维护很多区间信息,多重背包问题也可以用单调队列优化,有兴趣也可以去尝试一下,

01字典树

字典树相信大家都已经很熟悉了,就是给一堆字符串,不断往树上插入,我们统计所有字符串分别出现几次。用一个end数组维护结尾即可。在介绍01字典树之前先给一个字典树的模板题和代码帮大家回忆一下字典树是什么样的

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

I x 向集合中插入一个字符串 x;
Q x 询问一个字符串在集合中出现了多少次。
共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。

输入格式
第一行包含整数 N,表示操作数。

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

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

每个结果占一行。

数据范围
1≤N≤2104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1

题目就是最基础的字典树裸题,直接套板子就可以,

#include <bits/stdc++.h>
using namespace std;
int trie[1000010][26],tot=1,end1[1000010];
void insert(string s)
{
	int len=s.size(),p=1;
	for(int i=0;i<len;i++)
	{
		int c=s[i]-'a';
		if(trie[p][c]==0)trie[p][c]=++tot; 
		p=trie[p][c];
	}
	end1[p]++;
	return;
}
int finds(string s)
{
	int len=s.size(),p=1;
	for(int i=0;i<len;i++)
	{
		if(trie[p][s[i]-'a']==0)return 0;
		p=trie[p][s[i]-'a'];
	}
	if(end1[p])return p;
 } 
int main()
{
	string s;
	char aa;
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
	    cin>>aa;
	    if(aa=='I')
	{	cin>>s;
		insert(s);
	}
	else{
	 	cin>>s;
	 	int q=finds(s);
	 	cout<<end1[q]<<endl;
	 }
	}
    return 0;
}

那么什么是01字典树呢,其实他就是一种特殊的字典树,整个树里只有两种字符,0和1,用来表示二进制的数,我们可以简单总结一下它的性质

1.01字典树是一棵最多 32层的二叉树,其每个节点的两条边分别表示二进制的某一位的值为 0 还是为 1. 将某个路径上边的值连起来就得到一个二进制串。

2.节点个数为 1 的层(最高层)节点的边对应着二进制串的最高位。

那么它可以解决什么问题呢,一般我们用它来解决异或最大值类的问题,因为异或性质是最好相反。所以我们可以优先找和 x二进制的未处理的最高位值不同的边对应的点,这样保证结果最大。如果可以保证高位不同,那么一定会更大,从高层开始往下,可以不同就不同,不能不同就相同,可以保证找到一个数组种两个数异或结果最大。

给一个例题感受一下他的性质

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

输入格式
第一行输入一个整数 N。

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

输出格式
输出一个整数表示答案。

数据范围
1≤N≤105,
0≤Ai<231
输入样例:
3
1 2 3
输出样例:
3

方法就和上面提到的一样,先构造出字典树,然后让每个位置的数都在字典树上跑一遍,就能找到两两之间异或的最大值了。

#include<bits/stdc++.h>
using namespace std;
int trie[3000010][2],a[3000010],tot=1;
void insert(int a) 
{
    int p = 0;
    for (int i =31;~i; i -- )
    {
        if (!trie[p][a>>i&1]) trie[p][a>>i&1] = ++tot;
        p = trie[p][a>>i&1];
    }
}
int finds(int a)  
{
    int p = 0,ans1=0;
    for (int i =31;~i; i -- )
    {
        int s=a>>i&1;
        if (trie[p][!s]) 
        {
            p=trie[p][!s];
            ans1=ans1*2+!s;
        }
        else 
        {
            ans1=ans1*2+s;
            p=trie[p][s];
        }
    }
    return ans1;
}
int main()
{
    int n,x;
    scanf("%d", &n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d", &a[i]);
        insert(a[i]);
    }
    int ans=0;
    for(int i=1;i<=n;i++)ans=max(ans,a[i]^finds(a[i]));
    printf("%d\n",ans);
    return 0;
}

带权并查集

并查集相信大家都很熟悉了,就不介绍普通并查集的性质了,我们直接来看带权并查集是干什么的。

用并査集,但是维护多一个数组 d,用来计算当前节点到父节点的 距离,初始化为 0(因为自身到自身的距离为 0)这是带权并查集的一种最基础的应用,就是额外多维护一个信息数组d,用来保存需要的数据,我们一般用它来保存字结点到父节点的距离,用来灵活的解决问题,如果要维护这个距离,那么路径压缩的时候就要注意,把距离改成两段距离之和

int find(int x) 
{
    if(fa[x]==x){
        return x;
    }
    else {
        int t=find(fa[x]);
        d[x]+=d[fa[x]];//路径压缩后维护距离
        fa[x]=t;
        return fa[x];
    }
}

直到了可以多维护一个信息,那么这个信息可以用来干什么呢,提到带权并查集,不得不提到2001年Noi中的一道题,食物链,我们先一起看一下题面

动物王国中有三类动物 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 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
当前的话与前面的某些真的话冲突,就是假话;
当前的话中 X 或 Y 比 N 大,就是假话;
当前的话表示 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

动物王国中有三种动物,他们之间可以互相有捕食关系,成了一个环,说很多局话,问有多少是假话,其实就是让我们维护真话的信息,如果后面遇到冲突的。就按照假话去处理。那么我们如何维护这个捕食关系呢,实际上,我们可以建立一个并查集,如果某句话让两个动物联系起来就让他们加入同一个并查集,用“距离”来描述关系、判断关系,所有的距离都以根节点为基准,按照mod类别数(3)分为3类。“距离”:x吃y表示y到x的距离为1. y是第0代,吃y的x是第1代,吃x的是第2代…根节点是第0代三种关系:用点到根节点之间的距离表示其余根节点之间的关系
mod 3 = 1:可以吃根节点
mod 3 = 2:可以被根节点吃
mod 3 = 0:和根节点同类
把集合中所有的点划分为上述三类。每局话,我们先判断是否在同一个集合中,以及是否和之前满足过的关系冲突,来判断是不是假话,如果是真话,我们要把他所表达的信息也维护起来,用来判断后面的话会不会和它发生冲突。具体细节可以参考代码的注释,注意在维护d数组时搞清楚把两个没连接的区间相连需要改变根节点的位置。之后在路径压缩的过程中,其余结点的距离会自动改变为合适的值。

#include<bits/stdc++.h>
using namespace std;
int t,n,m,a,b,ans,fa[100010],d[100010];
void init()
{
    for(int i=1;i<=n;i++)
        fa[i]=i;
}
int find(int x) 
{
    if(fa[x]==x){
        return x;
    }
    else {
        int t=find(fa[x]);
        d[x]+=d[fa[x]];//路径压缩后维护距离
        fa[x]=t;
        return fa[x];
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m;
    init();
    for(int i=1;i<=m;i++)
    {
        cin>>t>>a>>b;
        if(a>n||b>n)ans++;
        else
        {
            int fx=find(a),fy=find(b);
            if(t==1)
            {
                if(fx==fy&&(d[a]-d[b])%3)ans++;//相差是3的倍数,说明是同类。
                else if(fx!=fy){
                    fa[fx]=fy;
                    d[fx] = d[b] - d[a]; //设fx到fy节点距离是dist,那么(d[x]+dist-d[y])%3应该等于0因为同类,连接后要保证在mod3意义下相等
                                        //然后我们解出来dist应该是 d[y]-d[x](解同余方程)
                }
            }
            else {
                if(fx==fy&&(d[a]-d[b]-1)%3)ans++;
                else if(fx!=fy)
                {
                    fa[fx]=fy;
                    d[fx]=d[b]-d[a]+1;
                }
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

写在后面

四种基础数据结构都可以用来维护很多信息,只有理解他们的原理后,才能灵活的应用他们。本文基本只给出每个数据结构最经典的题目或者模板题,想要更熟悉这些数据结构,大家可以尝试用这些数据结构做更多题目,希望可以帮到大家

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码下载:完整代码,可直接运行 ;运行版本:2022a或2019b或2014a;若运行有问题,可私信博主; **仿真咨询 1 各类智能优化算法改进及应用** 生产调度、经济调度、装配线调度、充电优化、车间调度、发车优化、水库调度、三维装箱、物流选址、货位优化、公交排班优化、充电桩布局优化、车间布局优化、集装箱船配载优化、水泵组合优化、解医疗资源分配优化、设施布局优化、可视域基站和无人机选址优化 **2 机器学习和深度学习方面** 卷积神经网络(CNN)、LSTM、支持向量机(SVM)、最小二乘支持向量机(LSSVM)、极限学习机(ELM)、核极限学习机(KELM)、BP、RBF、宽度学习、DBN、RF、RBF、DELM、XGBOOST、TCN实现风电预测、光伏预测、电池寿命预测、辐射源识别、交通流预测、负荷预测、股价预测、PM2.5浓度预测、电池健康状态预测、水体光学参数反演、NLOS信号识别、地铁停车精准预测、变压器故障诊断 **3 图像处理方面** 图像识别、图像分割、图像检测、图像隐藏、图像配准、图像拼接、图像融合、图像增强、图像压缩感知 **4 路径规划方面** 旅行商问题(TSP)、车辆路径问题(VRP、MVRP、CVRP、VRPTW等)、无人机三维路径规划、无人机协同、无人机编队、机器人路径规划、栅格地图路径规划、多式联运运输问题、车辆协同无人机路径规划、天线线性阵列分布优化、车间布局优化 **5 无人机应用方面** 无人机路径规划、无人机控制、无人机编队、无人机协同、无人机任务分配 **6 无线传感器定位及布局方面** 传感器部署优化、通信协议优化、路由优化、目标定位优化、Dv-Hop定位优化、Leach协议优化、WSN覆盖优化、组播优化、RSSI定位优化 **7 信号处理方面** 信号识别、信号加密、信号去噪、信号增强、雷达信号处理、信号水印嵌入提取、肌电信号、脑电信号、信号配时优化 **8 电力系统方面** 微电网优化、无功优化、配电网重构、储能配置 **9 元胞自动机方面** 交通流 人群疏散 病毒扩散 晶体生长 **10 雷达方面** 卡尔曼滤波跟踪、航迹关联、航迹融合
### 回答1: 二叉树是一种树形数据结构,它由一个根节点和最多两个子树组成,这两个子树被称为左子树和右子树。二叉树的每个节点最多有两个子节点,如果一个节点只有一个子节点,那么这个子节点必须是左子节点。 二叉树有很多种类型,最基本的二叉树是二叉搜索树。在二叉搜索树,左子树的所有节点的值都小于根节点的值,右子树的所有节点的值都大于根节点的值。这使得在二叉搜索树进行查找、插入和删除操作非常高效。 除了二叉搜索树,还有平衡二叉树、红黑树、B树等多种二叉树类型,每种类型的二叉树都有其特定的应用场景和优缺点。 二叉树的遍历方式有三种:前序遍历(先访问根节点,然后遍历左子树和右子树)、序遍历(先遍历左子树,然后访问根节点,最后遍历右子树)和后序遍历(先遍历左子树和右子树,最后访问根节点)。二叉树的遍历方式是解决很多问题的基础,比如查找二叉树的最大值、计算二叉树的深度等。 ### 回答2: 二叉树是一种重要的数据结构,它由一组称为节点的元素组成,每个节点最多可以连接到两个子节点,分别称为左子节点和右子节点。二叉树的一个节点可以表示一个值或者一条数据。 二叉树具有以下特点: 1. 根节点:二叉树的顶部节点称为根节点,它是整个树的起点。 2. 叶子节点:没有子节点的节点称为叶子节点,它们位于树的末端。 3. 分支节点:有子节点的节点称为分支节点,它们是树的间节点。 4. 子树:以某个节点为根节点,将其及其后代节点组成的树称为子树。 5. 左子树和右子树:一个节点的左右子节点分别构成左子树和右子树。 6. 高度:树节点的最大层次称为树的高度。 二叉树有多种变种,如满二叉树、完全二叉树等。满二叉树是一种每个节点都有两个子节点的二叉树,而完全二叉树是一种除了最后一层外,其他层都是满的二叉树。 二叉树的应用十分广泛,常见的应用场景包括文件系统、数据库索引等。在二叉树,插入、删除、查找等操作效率很高,时间复杂度通常为O(logN)。然而,如果二叉树退化成链表,操作效率会大大降低,时间复杂度为O(N)。 总的来说,二叉树是一种简单但十分重要的数据结构。它能够高效地存储、操作数据,被广泛应用于各个领域。 ### 回答3: 二叉树是一种常见的数据结构,它由节点组成,每个节点最多有两个子节点。 二叉树的特点是左子节点小于父节点,而右子节点大于父节点,这样的特性方便在树进行排序和搜索操作。 二叉树有多种常见的类型,包括满二叉树、完全二叉树和平衡二叉树等。 满二叉树是指除了叶子节点外,每个节点都有两个子节点的二叉树。 完全二叉树是指除了最后一层以外的其他层都是满的,最后一层的节点从左到右依次填满。 平衡二叉树是指左子树和右子树的高度差不超过1的二叉树,这样可以保证在最坏情况下的搜索时间复杂度为O(logn)。 二叉树可以使用数组或链表实现,具体选择取决于应用场景和需求。 在二叉树,我们可以使用递归或迭代的方式进行遍历操作,包括先序遍历、序遍历和后序遍历。 先序遍历是指先访问根节点,然后递归遍历左子树和右子树。 序遍历是指先递归遍历左子树,然后访问根节点,最后递归遍历右子树。 后序遍历是指先递归遍历左子树和右子树,然后访问根节点。 二叉树还可以进行插入、删除和查找操作。插入操作一般按照二叉搜索树的规则进行,即比根节点小的值插入左子树,比根节点大的值插入右子树。删除操作需要考虑不同情况,包括删除叶子节点、删除有一个子节点的节点和删除有两个子节点的节点。查找操作可以根据二叉搜索树的性质进行递归或迭代实现。 总之,二叉树是一种常见且重要的数据结构,能够方便地进行排序、搜索和插入等操作,同时还有多种类型和遍历方式供选择。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值