第六课-数据结构(链表,栈,队列)

本文大约9000字,如果仔细阅读,并读懂代码大约需要45分钟(这个不包括我给的例题的题解(呜呜呜,刚写博客,题解一时半会儿可能供应不上,但是我一定会把我列出来的题目解答提供出来的),仅仅是这篇文章的代码)。如果手动coding一边,大约需要三小时。

参考资料
《算法笔记》–曾磊,胡凡
《数据结构》–高等教育出版社-陈越,魏宝钢,徐镜春
《计算机网络》(主要是补充了滑动窗口的概念-对应于书本的227页)

链表,栈,队列。以上都是基于数组进行模拟实现。当然我会补上链表实现,但是有一点需要去进行考虑,因为new,malloc操作都是动态去申请内存空间,所以当创建大量的节点的时候,时间上就是不太允许了。而且节点释放不放,就会造成内存泄漏
其实我上课听老师讲课以及网课,包括一些数据结构书籍都是使用的是结构体+指针,这里真的是很墙裂很强烈推荐使用数组 去模拟实现链表,栈,队列。

链表题目来源:AcWing,leetcode,pat
AcWing17. 从尾到头打印链表
AcWing826. 单链表
AcWing827. 双链表
AcWing29. 删除链表中重复的节点
PAT1032 Sharing (25分)
PAT1052 Linked List Sorting (25分)
PAT1074 Reversing Linked List (25分)
leetcode2. 两数相加
leetcode19. 删除链表的倒数第N个节点
leetcode21. 合并两个有序链表

单链表的逻辑操作无非就是增删改查,:
代码逻辑如下:

void init()   //初始化链表
{
	head = -1;
	idx = 0;
}
void add_to_head(int x)   //加入元素到表头
{
	e[idx] = x;   //先记录输入节点的权值
	ne[idx] = head;     //让输入节点的指针指向head指向的节点
	head = idx ++;    //让head节点指向该节点,同时记录的下标加一
}
void add(int k, int x)   //加入元素到任意节点后
{
	e[idx] = x;
	ne[idx] = ne[k];     //这里的理解和上面差不多,把head换成了ne[k]而已
	ne[k] = idx ++;
}
void remove(int k)   //删除,注意我们删除的是第k + 1个节点
{
	ne[k]=ne[ne[k]];
}
//这里还要有一点就是链表的遍历,输出或查询链表的时候会用到
void print()//到时候调用函数去打印链表
{
	for(int i = head; i != -1; i = ne[i])
		printf("%d ", e[i]);
	printf("\n");
}

826. 单链表
题解如下:

#include <iostream>
using namespace std;
const int N = 100010;
int e[N], ne[N], head, idx;
void init()   //初始化链表
{
	head = -1;
	idx = 0;
}
void add_to_head(int x)   //加入元素到表头
{
	e[idx] = x;   //先记录输入节点的权值
	ne[idx] = head;     //让输入节点的指针指向head指向的节点
	head = idx ++;    //让head节点指向该节点,同时记录的下标加一
}
void add(int k, int x)   //加入元素到任意节点后
{
	e[idx] = x;
	ne[idx] = ne[k];     //这里的理解和上面差不多,把head换成了ne[k]而已
	ne[k] = idx ++;
}
void remove(int k)   //删除,注意我们删除的是第k + 1个节点
{
	ne[k]=ne[ne[k]];
}
//这里还要有一点就是链表的遍历,输出或查询链表的时候会用到
void print()
{
	for(int i = head; i != -1; i = ne[i])
		printf("%d ", e[i]);
	printf("\n");
}

int main(){
	int m;
	cin>>m;
	//给链表去进行初始化 
	init();
	
	while(m--){
		int k,x;
		char op;
		cin>>op;
		if(op=='H'){
			cin>>x;//读入插入的数是什么 
			add_to_head(x);
		}
		else if(op=='D'){
			cin>>k;
			if(!k) head=ne[head];
			remove(k-1);
		}
		else{
			cin>>k>>x;
			add(k-1,x);
		}
	}
	//链表遍历,从头节点开始 
	print();
	return 0;
} 

双链表:任然使用数组去进行模拟:画图模拟如下:
在这里插入图片描述

代码编写如下:

void init(){
	//0表示左端点,1表示右端点.
	//那么就很容易去想象:0号点的右边是1号点,1号点的左边是0号点
	R[0]=1;//0号点右边的索引为1 
	L[1]=0;//1号点左边的索引为0 
	index=0;
} 
//在第k个点的右边去插入一个值x 
void insert(int k,int x){
	e[index]=x;
	R[index]=R[k];//index的右边的索引,应该是k右边的索引,进行赋值
	L[index]=k;
	//上面已经将原本两个点的指针换掉,指向新的节点
	//底下两个步骤顺序不能错误,否则值就会错误 
	L[R[k]]=index; 
	R[k]=index; 
} 
//删除第k个点 
void del(int k){
	R[L[k]]=R[k];
	L[R[k]]=L[k];
}

827. 双链表
题解如下:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int e[N], l[N], r[N], idx;
void init()
{
    r[0] = 1, l[1] = 0;
    idx = 2;
}
void add(int k, int x)
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx;
    idx ++ ;
}
void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}
int main()
{
    int m;
    cin >> m;
    init();//链表初始化
    while (m -- )
    {
        string op;
        int k, x;
        cin >> op;
        if  (op == "L")
        {
            cin >> x;
            add(0, x);
        }
        else if (op == "R")
        {
            cin >> x;
            add(l[1], x);
        }
        else if (op == "D")
        {
            cin >> k;
            remove(k + 1);
        }
        else if (op == "IL")
        {
            cin >> k >> x;
            add(l[k + 1], x);
        }
        else
        {
            cin >> k >> x;
            add(k + 1, x);
        }
    }
    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] <<  ' ';
    cout << endl;
    return 0;
}

栈:栈是一种先进后出的数据结构,还是挺有用的。还是按照本文的贯穿,依然使用数组取进行模拟。

呜呜,这里的题解我还没有完全写完,我会尽快补上的。嘿嘿

栈及其应用leetcode,pat,acwing
pat1009 说反话 (20分)
AcWing830. 单调栈
leetcode896. 单调数列
leetcode42. 接雨水
leetcode84. 柱状图中最大的矩形
leetcode496. 下一个更大元素 I
leetcode503. 下一个更大元素 II
void clear(){
	top=-1;
} 
//获取栈里面的元素 
int getSize(){
	return top+1; 
}
bool empty(){
	if(top==-1) return true;
	else return false;
} 
//出栈 
void push(int x){
//	st[top]=x;
//	top++;
	st[top++]=x;
}
void pop(){
	top--;
}
//取栈顶元素 
int Top(){
	return st[top]; 
}

热身一下: 1009 说反话 (20分)
题解如下:

#include <iostream>
#include <stack>
using namespace std;
int main(){
    ios::sync_with_stdio(false);
    stack<string> st;
    string s;
    while(cin>>s){
        st.push(s);
    }
    while(! st.empty()){
        cout<<st.top();//输出最上的元素
        st.pop();//弹栈
        cout<<(st.empty()?"":" ");
    }
    return 0;
}

栈的应用:单调栈:给定一个序列,求这个序列当中。
单调栈主要回答这样的几种问题:
比当前元素更大的下一个元素
比当前元素更大的前一个元素
比当前元素更小的下一个元素
比当前元素更小的前一个元素

似乎这样说起来很苦涩,举一个栗子吧:
给定一个序列如下:3 4 2 7 5
比如:
3左边不存在数,返回-1
4比3大,返回3
3,4都大于2,返回-1
7比前面的数都要大,返回最小值2
5比2,3,4都大,返回最小值2。
输出结果为-1 3 -1 2 2

但是,但是,但是,怎么有代码写出来,而且最优化呢?

暴力做法:核心代码如下:我没有去进行整个代码的书写,但是核心代码都写出来,具体的操作注释都在上面

a[5]={3,4,2,7,5};//假设数列是这个样子的 
int position=-1;//可以使用position记录位置
int num=a[0];//记录最小的数字 
for(int i=0;i<n;i++)
	for(int j=i-1;j<=i;j++)
		if(a[j]<a[i])
			num=a[j];
			postion=j;
			cout<<postion<<" "<<a[j];//输出i最近的一个比他小的数 
			break;
		else 
			cout<<"-1";

如果用栈呢?使用栈,把索引位置为i,的前i个数,全部压入栈中,如果使用栈顶的元素和i去继续比较,如果找到比a[i]小,返回该元素的数值或者索引的位置。

但是这样是一种暴力的写法,看看这样是否具有某些性质,使得栈里面的某些元素永远不会输出出来。
其实总体上的性质就是如下:
数组的下标索引值,在一个二维平面整体是离散的状态。
如下图:
在这里插入图片描述
比如这个图显然a[5]是最大的,但是又发现其实比a[5]小的第一个元素是a[4],且又发现:a[3]>a[4],所以a[3]肯定不会输出出来。那么我们就可以剔除某一些点(逆序的点,比如a[1]和a[2],那就删除了a[1]),使得整个连续区间是单调的。那么就方便啦。哈哈哈哈哈…,这就是单调栈的好处。
在这里插入图片描述
那么就十分容易看出,a[4]是比a[5]小的第一个元素了。
那么代如下:注释这里面写了:

#include <iostream>
using namespace std;
const int N=10010;
int n;
int stk[N],top;
int main(){
	//cin.tie(0);
	//ios::sync_with_stdio(false);
	//上面的那两行是为了使字符单个进入缓冲区
	cin>>n;
	for(int i=0;i<n;i++){
		int x;cin>>x;
		//stk[top]>=x:栈顶元素≥a[i]
		while(top&&stk[top]>=x) top--;
		if(top) cout<<stk[top]<<' ';//输出栈顶元素
		else cout<<"-1"<<' ';
		stk[++top]=x;//表示的是栈顶元素比i位置上的元素小,x插入栈中
	}
	return 0;
} 

队列:队列是一种先进先出的一种数据结构。 使用的地方还是很多的,比如在图的遍历(其实就是树的层序遍历,不要把图想的那么复杂,其实图就是树的变异体)中,BFS(广度优先搜索)就要用到队列,关于树图的遍历,我会在之后的博文中进行详细讨论。

队列题目来源PAT,leetcode,Acwing
pat1014 Waiting in Line (30分)
pat1056 Mice and Rice (25分)
leetcode239. 滑动窗口最大值
leetcode480. 滑动窗口中位数
leetcode718. 最长重复子数组

首先先介绍队列的数组模拟方式:其实也是很简单的:模拟代码如下:

//清空队列 
void clear(){
	front=rear=-1;
}
//获取队列的长度 
int size(){
	return rear-front;
}
//对队列进行判空操作 
bool empty(){
	if(front==rear) return true;
	else return false;
}
//进队 
void push(int x){
	q[++rear]=x;
}
//出队 
void pop(){
	front++;
}
//得到队列的队头元素 
int get_front(){
	return q[front+1];
}
//得到队列的队尾元素 
int get_rear(){
	return q[rear];
}

单调队列的使用,举一个**滑动窗口**的栗子:
关于滑动窗口的概念,请查阅计算机网络的相关书籍.
比如:给定一段序列是: 1 3 -1 -3 5 3 6 7,滑动窗口是3,求出每一个滑动窗口的最小值。
1 3 -1 -3 5 3 6 7
滑动窗口1 为:1 3 -1 最小值为:-1 最大值为:3
1 3 -1 -3 5 3 6 7
滑动窗口2 为:3 -1 -3 最小值为:-3 最大值为:3
1 3 -1 -3 5 3 6 7
滑动窗口3 为:-1 -3 5 最小值为:-3 最大值为:5
1 3 -1 -3 5 3 6 7
滑动窗口4 为:-3 5 3 最小值为:-3 最大值为:5
1 3 -1 -3 5 3 6 7
滑动窗口5 为:5 3 6 最小值为:3 最大值为:6
1 3 -1 -3 5 3 6 7
滑动窗口6 为:3 6 7 最小值为:3 最大值为:7
所以最终输出为3

其实暴力解法也是可以的,但是真的很不好。第一层循环对整个序列去进行操作,第二层循环是截取范围为k的窗口。然后第二层循环里面是有algorithm底下的max和min函数,选出该窗口下最大或者最小的数。这个做虽然逻辑上很简单,但是从时间复杂度上考虑是不完美的。

**如果使用单调队列呢?**其实还是类推单调栈,看看能否去掉一些数,使其具有单调性。
比如看看滑动窗口二:

3-1-3

3和-1依次进入队列,当-3在进入队列的时候,3在队头,-3与对头的元素去进行比较。3>-3,队头元素3不会作为结果而被输出。-1在对头,而且-1>-3,队头元素-1也不会作为结果而被输出。
总结一下:如果前面的数字比后面的数组大,那么断定,前面的数字肯定是没有用的。那么就是删除逆序对。那么删除完逆序对,剩下的点一定是单调的(可以是单调递增或者是单调递减)。
如果队列中的序列具有单调性,那么就太棒了。比如,这次我们开的滑动窗口为n,且假设为+∞。滑动窗口内的元素是离散的,结果去逆序之后,此时滑动窗口是一个单调队列(递增or递减)。那么可以去求最大值,最小值,或者具体到某一个数字(利用二分查找)。
在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e6 + 10;
int a[MAXN];
deque < int > q;//q存放编号 

int main()
{
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    q.clear();
    //单调递增 求最小值
    for (int i = 1; i <= n; i++)
    {
        while (!q.empty() && a[q.back()] >= a[i]) q.pop_back();
        q.push_back(i);
        while (!q.empty() && i - k >= q.front()) q.pop_front();
        if (i >= k) cout << a[q.front()] << " ";
    }
    cout << endl;
    q.clear();
    for (int i = 1; i <= n; i++)
    {
        while (!q.empty() && a[q.back()] <= a[i]) q.pop_back();
        q.push_back(i);
        while (!q.empty() && i - k >= q.front()) q.pop_front();
        if (i >= k) cout << a[q.front()] << " ";
    }
    cout << endl;
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值