[2021-10-07]数据结构第4章-栈和队列

数据结构第4章 - 栈和队列

写在开头

尽管听起来很陌生,但是栈和队列在数据结构中也是十分常用且使用广泛的数据结构。
其实举两个例子,一个是:一摞盘子,每次只能从最上面拿,那么后放的必然先被拿走:这是一个先进后出的结构(First In Last Out)
另一个是队列:你在食堂排队拿饭,先排的先付完钱吃饭:这就是一个先进先出的结构(First In First Out)
就是这么简单啦~ 你还想栈和队列有什么特别牛X的定义哦

在开头我们已经知道了,栈的主要规则就是先进后出。我们使用一个数组存储这个栈中的元素,并用指针指向栈顶(就是这堆盘子的顶上)。定义代码如下:

template<class DataType>
class Stack {
	public:
		DataType data[MAX_SIZE];
		int top = -1;
};

栈有4个常用操作,在stl的栈容器中也是这样:压栈、出栈、取栈顶、判空

入栈

和顺序表中的添加元素非常像,入栈操作将top向后移动一位:

void push(DataType d) {
	if(++top == MAX_SIZE) throw "stack full";
	data[top] = d;
}

出栈

出栈就是将栈顶元素移除,为了和stl中的栈统一,这边的出栈将不会返回任何值:

void pop() {
	if(top == -1) throw "stack empty";
	data[top--] = NULL; //其实只要top--就行
}

取栈顶

取栈顶只要读出data中位于top位置的元素的值就行了:

DataType peek() {
	if(top == -1) throw "stack empty";
	return data[top];
}

判空

看代码:

bool empty() {
	return top == -1;
}

链式栈

一般的栈的定义中,data区是大小不可变的,这也就导致了像先前顺序结构中的问题,我们使用链式存储能够很好地解决这一问题。
链式栈在入栈时将新的节点插在头节点后,出栈只要指向头节点所指的节点的后一个节点就可以了。
代码大同小异,这边不再给出,可以自己写写看。

两个栈公用一个空间

这其实很简单,就是一个top1从头往后,一个top2从后往前,如果相遇或者越界就是满了,代码自己写写看吧~

栈应用0:各种现实模拟

可以用栈模拟一些现实中的情况。

栈应用1:求前缀、中缀、后缀表达式

前缀和后缀表达式比较好做,先讲后缀了:

后缀表达式(逆波兰表达式)

从前向后扫描表达式,遇到数字则压入栈中,遇到运算符则从栈中同时弹出两个数字并用该运算符对其进行计算(不要忘记运算合法性判断),将计算结果重新压入栈中,最后栈顶的元素(运算完成后栈中只有一个元素了)就是表达式的结果。

前缀表达式(波兰表达式)

首先讲一种奇淫巧计:从后往前扫描表达式,然后规则和计算后缀表达式相同,最后栈顶的元素就是表达式的结果啦~
正规做法:
从前往后扫描表达式,遇到符号压入符号栈,如果符号后同时连着两个数字,则弹出一个符号并用该符号去计算这个两个数字,结果保存在一个变量中,然后继续向后扫描直到结尾,此时栈应当为空否则表达式就有问题。

中缀表达式

中缀表达式一般不会把它拿出来单独计算,一般是转换成前缀或者后缀表达式然后进行计算。
在转换过程中运算符就像正常的计算一样有优先级,*、/ > +、-
下面给出转化为后缀表达式的过程:

  • 如果是数字,则直接输出;
  • 如果是左括号,则压入栈中;
  • 如果是操作符,若栈为空或者优先级高于栈顶元素,则压入栈中;
  • 如果是操作符,栈不为空而且优先级小于等于栈顶元素,输出运算符并出栈;
  • 如果是右括号,则将栈中操作符输出直到遇到左括号,将左括号出栈而不输出;
  • 扫描完后如果栈中还有操作符,则将操作符输出。

栈应用2,栈实现队列

这边直接给出传送门和我的代码:

传送门:剑指 Offer 09. 用两个栈实现队列
代码:

class CQueue {
public:
    stack<int> a, b;
    CQueue() {}
    
    void appendTail(int value) {
        a.push(value);
    }
    
    int deleteHead() {
        if(b.empty()) {
            while(!a.empty()) {
                b.push(a.top());
                a.pop();
            }
        }
        if(b.empty()) return -1;
        else{
            int top = b.top();
            b.pop();
            return top;
        }

    }
};

栈应用3,单调栈

顾名思义,元素是单调上升或者下降的栈,这个应用非常广泛,题目难度跨度也非常大,在这边不进行举例;P

栈应用4,优化深度优先搜索(dfs)

大家都知道大爆搜(dfs)的递归过程就是由系统的栈辅助执行的,但是这个递归栈非常容易溢出,然后就寄了。。。我们使用非递归方式并用栈来存储步骤,这样就可以便于程序的执行。
题目也有很多,最常见的就是走迷宫了。

队列

队列和栈类似,只是将其变成先进先出结构就行了。
注意,由于是从前往后取元素,队列需要同时记录插入位置rear和队顶位置front:

template<class DataType>
class Queue {
	public:
		DataType data[MAX_SIZE];
		int front = -1;
		int rear = -1;
};

入队(push),出队(pop),取队顶(front),判空(empty)

void push(DataType d) {
	if(++rear == MAX_SIZE) throw "queue full";
	data[rear] = d;
}
void pop() {
	if(front == rear) throw "queue empty";
	data[++front] = NULL; //直接++front,个人习惯清空数据;P
}
DataType front() {
	if(front == rear) throw "queue empty";
	return data[front];
}
bool empty() {
	return front == rear;
}

循环队列

从上面的代码不难看出,判空仅仅是比较front和rear,如果数据已经存到MAX_SIZE且全部出队了,这时候数组中没有任何数据,但是依然会抛出queue empty的异常,这时就发生了“假溢出”。为了解决假溢出的问题,我们可以使用循环队列。
和普通队列的不同之处就在于,循环队列使用了%MAX_SIZE来对新位置进行计算,如果已经存到MAX_SIZE - 1了,那么下一个位置是MAX_SIZE % MAX_SIZE=0,于是将从头开始存储,这样就不会导致空间浪费。

void push(DataType d) {
	if((rear + 1) % MAX_SIZE == front) throw "queue full";
	rear = (rear + 1) % MAX_SIZE;
	data[rear] = d;
}
void pop() {
	if(front == rear) throw "queue empty";
	data[front] = NULL;
	front = (front + 1) % MAX_SIZE;
}

PS:取值、判空还是一样的

链式队列

相较于链式栈,链式队列存储了头指针的同时存储了尾指针rear负责向队尾添加元素,其入队、出队操作大同小异,可以试着自己写写。
特别地,还可以将链式队列的结尾指向头节点这样可以减少一个指针。

队列应用0,现实的各种模拟

。。。

队列应用1,滑动窗口

对于一些数据,可以使用队列先进先出的特性方便地实现滑动窗口而不是使用双指针。
队列此时就是那个滑动的窗口,从头出来,从尾进去。

队列应用2,广度优先搜索(bfs)

和深度优先不同,广度优先枚举了下一步的每种可能并对这种可能再来一次枚举,这时我们就使用了队列来记录第n步的每一种可能,这样队列中的每个数据都是成组且步骤数是递增的,也就是相较于上一步不断向外扩展,然后这就是bfs了(???,但确实就是这样)

写在结尾

尽管栈和队列概念很简单,但是由其衍生出的题目和技巧很多。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值