【ICPC备战||Day5】数据结构(一)

Content:

  1. 单链表和邻接表

  1. 栈&队列

Part 1 单链表和邻接表

单链表学习的主要作用就是用来构建邻接表,而制作邻接表的目的就在于存储图和树,而双链表一般的作用是优化问题。

链表中每一个节点如无特殊要求,一般都存储两个值,一个是自身的value,一个是next指针,也就是下一个节点所指向的位置。同时还要定义一个中间变量,使这个变量存储下一个指针可以开辟的最小坐标。

我在csdn上经常看到的initlinklist的方式就是使用struct结构体,但实际上由于点运算符和括号迭代的使用,会使代码变得非常复杂,因此不如采用数组来代替struct,毕竟数组是联系起结构体和指针的最好的工具。

#include<iostream>
using namespace std;
const int N = 1000;

// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx++;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}
//双链表 —— 模板题 AcWing 827. 双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx++;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

由于链表非常注重顺序的先后,在更新指针的指向时,一定要注意先建立新指针还是先将原指针的值进行调用,比如在双链表中,如果想要进行insert操作,有如下程序:

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx++;
}

当我们需要insert一个新结点的时候,毫无疑问idx是新节点创始之初应该指向的位置,也就是说不能在a右边直接取a+1,需要将idx位置的节点建立之后再插入到双链表当中才有效。但在插入之前,不能直接将r[a]=idx写在前面,而是要先将r【a】的值赋给r【idx】,否则先存在的r【a】这个值就将丢失,idx也就没有相应的右节点了,idx的值在+1之前是不会改变的,但是idx的左右节点的值都需要用到原先指向的值,所以应将idx的赋值放在最后。

在linklist代码中很关键的一种思想就是,永远不要把a+1,a-1当作左右节点,尤其是在双链表当中,巧用l[a]和r[a]可以尽量减少出错的可能。比如以下代码:

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

Part 2 栈和队列

栈的建立非常简单,由于是一个先进后出的木桶型结构,因此在实现时不需要大量的顺序操作,很多基本的操作都是在top运行的,不涉及栈底的改变,整个结构相对来说也更加稳定,除了insert和delete操作之外,pop和push都是小范围的数据改动。

// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;//原先tt处的值自然delete

// 栈顶的值
stk[tt];

// 判断栈是否为空
if (tt > 0)
{
not empty;
}
else empty;

Simple queue:

// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

hh代表queue的左值,tt代表最右端的值

循环队列:

// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

接下来介绍一种比较特殊的栈和队列,一般是用于构造单调增减的顺序

//单调栈
#include<iostream>
using namespace std;
const int N = 1000;

int n;
int stk[N], tt;

int main()
{
    cin.tie(0);
    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;
}

以下是一道有关于单调队列的滑动窗口问题:

#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n, k, q[N], a[N];//q[N]存的是数组下标
int main()
{
    int tt = -1, hh=0;//hh队列头 tt队列尾
    cin.tie(0);
    ios::sync_with_stdio(false);
    cin>>n>>k;
    for(int i = 0; i <n; i ++) cin>>a[i];
    for(int i = 0; i < n; i ++)
    {
        //维持滑动窗口的大小
        //当队列不为空(hh <= tt) 且 当当前滑动窗口的大小(i - q[hh] + 1)>我们设定的
        //滑动窗口的大小(k),队列弹出队列头元素以维持滑动窗口的大小
        if(hh <= tt && k < i - q[hh] + 1) hh ++;//最多也只会超出去一个元素
        //构造单调递增队列
        //当队列不为空(hh <= tt) 且 当队列队尾元素>=当前元素(a[i])时,那么队尾元素
        //就一定不是当前窗口最小值,删去队尾元素,加入当前元素(q[ ++ tt] = i)
        while(hh <= tt && a[q[tt]] >= a[i]) tt --;
        q[ ++ tt] = i;
        if(i + 1 >= k) printf("%d ", a[q[hh]]);
    }
    puts("");
    hh = 0,tt = -1;
    for(int i = 0; i < n; i ++)
    {
        if(hh <= tt && k < i - q[hh] + 1) hh ++;
        while(hh <= tt && a[q[tt]] <= a[i]) tt --;
        q[ ++ tt] = i;
        if(i + 1 >= k ) printf("%d ", a[q[hh]]);
    }
    return 0;
}

i代表了当前的终点,也就是窗口的tail

这道题目中最难理解也是最难想的一部分就在于:

 for(int i=0;i<n;i++)
    {
        if(hh<=tt && i-q[hh]+1>k) hh++;//如果超出可容纳元素个数
        while(hh<=tt && a[q[tt]]>=a[i]) tt--;//非空且小于队尾
        q[++tt]=i;
        if(i+1>=k) cout<<a[q[hh]]<<' ';
    }
    cout<<endl;

这一块的实质作用是什么呢?

首先明确,由于我们需要对每个窗口中的元素进行一一比对,同时通过分析数据之间的大小关系而进行排序,需要设置两个数组,一个用以存储下标,另一个则用来存储数据。首先,窗口是不断滑动的,也就是说窗口的head和tail一直在发生改变,那这就需要进行一轮判断是否超出窗口内元素个数后hh++,随后再进行队列的单调化处理,如果此时队尾tt所对应的下标为q[tt]的a值相比于当前遍历到的这个数字更大,那么就需要进行tt--,也就是将目前队尾的元素pop出去,同时加入当前的元素q[++tt]=i。

详细说一下这里的妙处吧,while循环的好处是不达目的不罢休,只要你不是当前最大的元素,那就要一直向head遍历,而每次循环后为了保证下标也跟着向head移动,tt就必须--,所以q[++tt]刚好是将新元素安插在了合适的排序位置上。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值