【实战篇】 第13期 算法竞赛_数据结构超详解(上)

【你奶奶都能听懂的算法数据结构】 第13 期 数据结构

目录

开头:

今天这一期来更新一下实战篇,就如标题所见,在竞赛算法题中,很多时候我们要使用一些数据结构来帮助我们进行对数据进行增、删、查、改的操作,前面我们在学习篇里已经学习过了一些数据结构,那为什么 要在实战篇里又要学习数据结构呢?

那当然是因为有很大的不同。前面的数据结构偏向于工程,实现起来很复杂,但是在竞赛算法中,我们追求的是速度,不可能为了实现一个数据结构,从头开始写相关的函数实现吧?

所以有了这一期算法竞赛中的数据结构,这是上半部分,我们将一起学习:顺序表、链表、双向链表、栈、队列这几个数据结构,还会介绍C++中给我们提供的相关标准模板库(STL),当然,因为是实战篇,例题一定是不会少的。那就来喽!

一.顺序表

要想学习顺序表,先要知道线性表是什么?

线性表是具有相同数据类型的 n 个数据元素的有限序列,数据中元素的逻辑关系是一一对应的

而顺序表是线性表的顺序存储方式,顺序表使用一段连续的内存空间来存储数据,可以说顺序表是线性表的一种具体实现方式。

数组就是一种顺序表

按照数组的申请空间的方式,可以分为两种:

  1. 数组采⽤静态分配,此时的顺序表称为静态顺序表
  2. 数组采⽤动态分配,此时的顺序表称为动态顺序表

因为动态分配创建的数组申请空间会有时间的开销,所以在算法竞赛中,我们一般会创建一个足够大的静态数组

由于实现方式很简单,所以这里不做解释

1.实现方式

尾插:

头插

在任意下标处插入

尾删

头删

删除任意下标处的元素

查找某个元素,返回下标

这样的查找的方法时间复杂度是 O(n),在竞赛中我们可以采用空间换时间的策略

开一个 mp 查询数组,每一次插入、删除数据的时候,也要相应的修改 mp 数组

2.STL_vector

如果需要⽤动态顺序表,有更好的⽅式: C++ 的 STL 提供了⼀个已经封装好的容器 - vector ,

有的地⽅也叫作可变⻓的数组。 vector 的底层就是⼀个会⾃动扩容的顺序表,其中创建以及增删查改等等的逻辑已经实现好了,并且也完成了封装

vector 创建的数组初始值为0

接下来重点学习一下 vector 的使用:

首先当然是要包含下头文件

接着我们来看看 vector 的几种创建方式

< > 尖括号里面是要存储的数据类型,比如:int,char 或者结构体类型

后面紧跟的是此 vector 的名字

重点来解释一下 "vector<int > a(10)""vector<int> a[10]" 这两者的区别

  1. "vector<int > a(10)" 这个相当于是创建了一个初始大小为10的数组
  2. "vector<int> a[10]" 这个相当于可以看成------"变量类型 a(数组名)[大小]" 也就是说 a 数组存储的数据类型是 vector ------一个可变长数组

下面我们来看看 STL 提供的相关函数:

(调整元素数量 v.resize( n,val ) ,如果是扩大容量,多出来的元素用 val 填充)

3.例题

(1)芬兰国旗问题

本题要求是不能借助排序

这是经典的 "三色排序问题(芬兰国旗问题)",要将一组数按照特定的规则排列成三个大部分,还记得之前我们在双指针算法讲的移动 0 问题吗?------双指针算法

这里我们要借助三个指针将数组分成四个部分

我们通过指针 i 来遍历数组,结束条件是当 i 与 right 相遇时

当 i 指向的数为 1 时,符合【left+1, i -1】全是1,就让 i++

当 i 指向的数为 0 时,就要进入左区间,先 left++,再交换 left 和 i 指向的两个数,然后再 i++,继续判断

当 i 指向的数为 2 ,这时就要让这个数添加到最右区间,right--,交换此时 right 和 i 指向的数,然后 i++

但是这会有一个问题,如图,这时候如果 i++ 的话,会将未处理范围内的一个数越过,所以这种情况下不能直接将 i++,要继续判断此时 i 指向的数

当 i 与 right 相遇时,就结束循环,这时候数组就被分成了三个部分

代码实现:

(2)移动木块

这题目又臭又长,其实简单来说就是要完成两个操作:归位、叠放

总结一下题目:

当有 "move"时,就要将 a 上的木块归位

当有 "onto"时,就要将 b 上的木块归位

最后都要将 a 及以上的放到 b 上

根据题意我们要创建一个 vector<int> a[ n ] 的数组,就拿事例举例:

先走前四步:

接着是:pile 8 over 6

要将 8 及它后面接的数都要放到 6 ,但是此时它们放在同一个木块上,就不用管,跳过此步

接下来是:pile 8 over 5

要将 8 及它后面接的数都要放到 5

接下来的两步:

和最后的结果是一样的

解释完这是一个什么样的过程,接下来我们就要去思考,如何用代码实现归位,叠放操作

要实现归位 a 或 b 上的所有木块,首先要找到它们的位置在哪里吧,找到位置后,就可以用一个循环遍历往后的数,将他们依次尾插到本身对应的位置

要实现叠放,其实就是尾插到相应的位置

另外,要记录一个数的位置,要有两个信息,一个是在 a 数组的第几个 vector 数组中,另一个就是在此 vector 数组的第几个

就可以用 pair:

pair 是 C++ 标准库中的⼀个模板类,⽤于将两个值组合成⼀个单⼀对象,通常⽤于存储键值对或返回多个值。它有两个公有成员 first 和 second ,分别表⽰第⼀个值和第⼆个值
我们可以把 pair 理解成 C++ 为我们提供⼀个结构体,⾥⾯有两个变量:

这样一个框架就搭建好了,此时我们就要来实现 find(查询)、clean(归位)、Move(叠放)这三个函数

一个简简单单的循环遍历

要注意的是,归位以后,原 vector 的大小就要收缩,防止访问到已经归位的数据

完整代码:

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

const int N=1e5;
vector<int>p[N];
int n;

typedef pair<int,int> PII;

PII find(int x)
{
    for(int i=0;i<n;i++)
    {
       for(int j=0;j<p[i].size();j++)
       {
          if(p[i][j]==x)  return {i,j};
       }
    }
}

void clean(int x,int y)
{
    for(int i=y+1;i<p[x].size();i++)
    {
       int t=p[x][i];
       p[t].push_back(t);
    }
    p[x].resize(y+1);
}


void Move(int x1,int y1,int x2)
{
    for(int i=y1;i<p[x1].size();i++)
    {
        int t=p[x1][i];
        p[x2].push_back(t);
    }
    p[x1].resize(y1);
}



int main()
{
    cin>>n;
    for(int i=0;i<n;i++)  p[i].push_back(i);

    string s1,s2;
    int a,b;
    while(cin>> s1 >> a >> s2 >> b)
    {
       PII pos_a=find(a);
       int x1=pos_a.first;
       int y1=pos_a.second;

       PII pos_b=find(b);
       int x2=pos_b.first;
       int y2=pos_b.second;

       if(x1==x2)  continue;

       if(s1=="move") //此时要将 a 上的全部归位
       {
          clean(x1,y1);
       }
       if(s2=="onto") //此时将 b 上的全部归位
       {
         clean(x2,y2);
       }
       Move(x1,y1,x2);
    }

    for(int i=0;i<n;i++)
    {
        cout<< i <<":";
        for(int j=0;j< p[i].size();j++)
        {
            cout<<" "<< p[i][j];
        }
        cout<< endl;
    }
}

二.链表

单向链表

前面的学习篇我们一起手撕链表------手撕链表

在算法竞赛中,使用链表可以通过两个足够大的数组来模拟实现:

此时的下标就可以当作此前我们学习链表的地址,下标为 0 的位置当作哨兵位.

h 指针始终指向哨兵位,id 每次指向链表的尾

1.实现方式

如上图,先创建了两个数组,a 数组是专门存放数据的数据域, 而 Next 是存放下一位地址(下标)的指针域

为了可以快速的访问某一个数据,可以开一个足够的数组 mp ,将数本身作为下标,存放的是其在 a 数组的下标,以此来实现快速访问

头插:

现在有这样一段初始序列,要进行头插,也就是说要插在 h 指向的哨兵位后面那一位

首先肯定是先让 id++ , 去指向一块新的空间,然后将对应位置的值赋值成要插入的值

即 a [ id ] = 4 ( 假设要将 4 进行头插)

但是这样只是在数据域中申请了空间来存放值,要想在逻辑结构上进行头插,其实就和动态链表的实现一样,只不过这里的地址就是数组的下标( id )

先 Next [ id ] =Next [ h ],就是让头插进来的新元素的 Next 指向原先的头节点

再 Next [ h ] = id,就是让哨兵位的下一个指向新的头节点的地址------ id


遍历链表:

依旧和链表的循环遍历一样,直接上代码

查询:

因为我们使用了 mp 数组记录每个数据的下标信息,所以我们可以用 O(1)的时间复杂度快速查找某个值的下标

在任意位置之后插入

注意这里的 "位置" 指的是数组中的下标

要在给定的位置的后面插入( 在 2 后面插入一个 5)

首先当然是 id++ ,让 id 指向一个新的空间,接着依旧让 a [ id ] =5

因为 2 的下标是 1 ,先让 id 申请的新的元素的 Next 指向 2 原本的后一位

即 Next [ 1 ],接着让 2 的后面指向新的元素,即 Next [1]= id


删除指定位置后的元素

首先这个位置不能是链表的末尾,如果是链表的尾元素,其 Next 指针域存的是 0,指向的是哨兵位,不能把哨兵位删了吧

所以代码是:

完整代码:

2.STL_list

C++的 STL 中也提供了关于链表的标准模板库,我们也来学习一下:

首先当然要包含头文件

初始化:

如上图,有两种初始化方式,可以直接初始化,也可以放几个值进行初始化

< > 尖括号里面依旧是存储的变量类型

相关函数:

双向链表

接下来我们来学习下与单向链表相对应的------双向链表

这里我们也是要用数组来进行模拟实现:

1.代码实现:

尾插

头插

查找

在指定位置后插入

在指定位置前插入

删除指定位置

打印

完整代码:

例题:

(1)排队顺序

这道题就是很经典的链表题,直接展示代码:

(2)队列安排

因为会在指定位置的左或右插入元素,我们可以用双向链表:

(3)约瑟夫问题

因为前面的模拟实现链表时,数组的初始值均为 0, 这就导致了,最后一个元素的 Next 指针域存的是 0 ,发现是指向哨兵位的,也就是说这是一个循环链表

这就非常适合解决约瑟夫问题

而解决这道题可以简化,只使用一个 Next 数组

此时数组 1~10 的下标代表的就是 1~10 个数,对应位存储的是下一位的地址(下标),这样其实就是构建了一个循环链表

每次要找的是第 m-1 个数,也就是这一轮要出圈人的上一个人(找到上一个人这样好进行删除操作),我们先定义一个指针 t 来指向下标为 n 的这个位置(也就是第一个人的上一个)

因为最终每个人都会出圈,一共会进行 n 轮出圈操作,先来个 for 循环

在每一轮的出圈操作中,要从 1 开始计数,直到 m-1

每一次要遍历这个链表,t=Next [ t ]

出了这层循环后,此时 t 指向的下一个位置就是本轮要出圈的,先将其打印,接着就是删除指定位置之后元素的操作

完整代码:

三.栈

栈是⼀种只允许在⼀端进⾏数据插⼊和删除操作的线性表

• 进⾏数据插⼊或删除的⼀端称为栈顶,另⼀端称为栈底。不含元素的栈称为空栈

• 进栈就是往栈中放⼊元素,出栈就是将元素弹出栈顶。

大白话讲:栈是一种访问受限的线性表,核心特点是后进先出

所以栈用数组模拟实现起来非常简单

1.实现方式

入栈

出栈

返回栈顶元素

判空

栈的大小

完整代码:


2.STL_stack

依旧是头文件

依旧大差不差的初始化

相关函数:


3.例题

(1)有效括号

学习栈一定就会学习这个有效括号问题,思路就是,如果遇到左括号 "( " 、" { "、" [ "

就要进栈,如果遇到右括号 " )"、" } "、" ] ",就让其与栈顶元素进行比较,如果可以匹配就继续判断,如果不能就是 false

代码就很容易写了:

当然这里还有些细节,如果遇到右括号,但是此时栈为空,没有元素与此进行匹配,这时候也要 return false,以及当遍历完字符串后,如果此时栈中还有元素,也要return false

(2)验证栈序列

这道题的意思就是给定两段序列,第一段是入栈序列,判断第二段是否是其的出栈序列

要解决这个问题,我们要借助栈:

如上图,有 a , b 两个数组

大体思路,定义两个指针 i,j 分别指向两个数组的 "头",每一次将 a 数组中 i 指向的数入栈,接着判断 b 数组中 j 指向的数和栈里面的元素进行匹配,如果匹配失败,继续将 a 中的数入栈,如果相等匹配成功,先 pop 掉栈顶元素,接着就让 j++,循环判断,我们画图演示下

让 1 入栈,然后判断,发现 1 和 2 不相等,i++,继续入栈

此时栈顶元素与 b 中 j 指向的数相等,就 pop 栈顶元素,接着 j++

4 不等于栈顶元素 1,就继续让 i 指向的数入栈,不断重复此操作

当 i 遍历完 a 数组,如果栈为空,就说明是可以以 b 数组的序列输出,就是 "yes"

下面是代码实现:

(3)后缀表达式

这道题意思其实很简单,数字之间会用 " . " 分隔开,当遇到运算符号时,就计算离其最近的两个数,将计算结果代替到原来的两个数,重复计算。

这也是用栈来解决,遇到数字就进行入栈操作,遇到运算符就拿两次栈顶的元素进行计算,然后将计算结果继续入栈

代码实现:

四.队列

队列也是⼀种访问受限的线性表

它只允许在表的⼀端进⾏插⼊操作,在另⼀端进⾏删除操作

• 允许插⼊的⼀端称为队尾,允许删除的⼀端称为队头。

• 先进⼊队列的元素会先出队,故队列具有先进先出(First In First Out)的特性

1.实现方式

用数组模拟队列也是很简单的,直接展示代码:

2.STL_queue

头文件:

初始化:

相关函数:

3.例题

(1)队列

就是基本的队列模拟,直接上代码:

(2)机器翻译

这道题这么长,简单来说就是,有一个长度仅为 m 的队列,每次查询一个数,如果这个数不存在于这段队列,就进行入队,但注意要维护队列长度始终 <= m

直接上代码:

(3)海港

根据题意,我们要统计出 24 小时之内国籍的种数,因为是一段区间,如果新来船的时间减一开始的时间大于 86400s ,就要收缩统计区间

这里我们可以使用 pair ,队列中存放的类型为 pair<int ,int> ,第一个存到达的时间,第二个存国籍,因为要动态的管理 24h 内国籍种数,不可能每一次更新时间我们都要去重新计算一下

就得使用一个 mp 数组,和一个记录种数的 kind 变量

每一次将国籍存到 mp 里,当 mp 值为 0 ,kind++

如果进行收缩时间时,此国籍对应的 mp 值为 1 是,kind--

代码实现:

结语:

ok了,这一期关于算法竞赛中的数据结构就到这里了,制作不易,内容较长,可以点歌收藏,如果对你的学习有所帮助,关注我,我们一起学习进步

往期博客:

1.全站最全七大排序超超超详解

2.手撕二叉树

3.什么!你居然还不会建堆???

相关推荐
小李子呢02111 小时前
Node.js
开发语言·前端·学习·node.js
KakiNakajima1 小时前
Java结合AI技术学习总结【kaki与时俱进】
学习
期待のcode1 小时前
JVM 中对象进入老年代的时机
java·开发语言·jvm
嘿嘿嘿x31 小时前
Modbus TCP 数据结构(发送和返回/读/写)
数据结构·网络协议·tcp/ip
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于java的医院床位管理系统的设计与开发 为例,包含答辩的问题和答案
java·开发语言
啊阿狸不会拉杆2 小时前
《数字图像处理》第 12 章 - 目标识别
图像处理·人工智能·算法·计算机视觉·数字图像处理
lly2024062 小时前
AJAX PHP 实践指南
开发语言
Never_Satisfied2 小时前
在JavaScript / HTML中,cloneNode()方法详细指南
开发语言·javascript·html
huwei8532 小时前
python设计通用表格类 带右键菜单
开发语言·windows·python