本文大约9000字,如果仔细阅读,并读懂代码大约需要45分钟(这个不包括我给的例题的题解(呜呜呜,刚写博客,题解一时半会儿可能供应不上,但是我一定会把我列出来的题目解答提供出来的),仅仅是这篇文章的代码)。如果手动coding一边,大约需要三小时。
参考资料
:
《算法笔记》–曾磊,胡凡
《数据结构》–高等教育出版社-陈越,魏宝钢,徐镜春
《计算机网络》(主要是补充了滑动窗口的概念-对应于书本的227页)
链表,栈,队列
。以上都是基于数组进行模拟实现。当然我会补上链表实现,但是有一点需要去进行考虑,因为new,malloc操作都是动态去申请内存空间,所以当创建大量的节点的时候,时间上就是不太允许了。而且节点释放不放,就会造成内存泄漏
其实我上课听老师讲课以及网课,包括一些数据结构书籍都是使用的是结构体+指针,这里真的是很墙裂很强烈推荐使用数组 去模拟实现链表,栈,队列。
链表题目 | 来源:AcWing,leetcode,pat |
---|---|
AcWing | 17. 从尾到头打印链表 |
AcWing | 826. 单链表 |
AcWing | 827. 双链表 |
AcWing | 29. 删除链表中重复的节点 |
PAT | 1032 Sharing (25分) |
PAT | 1052 Linked List Sorting (25分) |
PAT | 1074 Reversing Linked List (25分) |
leetcode | 2. 两数相加 |
leetcode | 19. 删除链表的倒数第N个节点 |
leetcode | 21. 合并两个有序链表 |
单链表的逻辑操作无非就是增删改查,:
代码逻辑如下:
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 |
---|---|
pat | 1009 说反话 (20分) |
AcWing | 830. 单调栈 |
leetcode | 896. 单调数列 |
leetcode | 42. 接雨水 |
leetcode | 84. 柱状图中最大的矩形 |
leetcode | 496. 下一个更大元素 I |
leetcode | 503. 下一个更大元素 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 |
---|---|
pat | 1014 Waiting in Line (30分) |
pat | 1056 Mice and Rice (25分) |
leetcode | 239. 滑动窗口最大值 |
leetcode | 480. 滑动窗口中位数 |
leetcode | 718. 最长重复子数组 |
首先先介绍队列的数组模拟方式:其实也是很简单的:模拟代码如下:
//清空队列
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;
}