Content:
单链表和邻接表
栈&队列
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]刚好是将新元素安插在了合适的排序位置上。