目录
栈和队列
我们以下的使用的栈或队列都是作为一个工具来解决其他问题的,我们可以把栈或队列的声明和操作写的很简单,而不必分函数写出。
- 顺序栈
-
声明一个栈并初始化:
ElemType stack[maxSize]; //存放栈中的元素 int top = -1; //栈顶指针(指向栈顶元素) -
元素进栈:
stack[++top] = x; -
元素出栈:
x = stack[top--]; -
判断栈空:
top == -1; //栈空 top > -1; //栈非空
-
- 顺序队列
-
声明一个队列并初始化:
ElemType queue[maxSize]; //存放队列中元素 int front = -1, rear = -1; //队头(指向队头元素的前一个位置),队尾(指向队尾元素) -
元素入队:
queue[++rear] = x; -
元素出队:
x = queue[++front]; -
判断队空:
front == rear; //队空 front < rear; //队非空
-
- 使用“栈”遍历:是用来从最近访问的一个结点开始,访问其他结点
- 使用“队列”遍历:是用来按照访问的顺序开始,访问其他结点
卡特兰(Catalan)数:${{1}\over{n+1}}C_{2n}^{n}$
应用:对于n个不同元素进栈,出栈序列的个数为${{1}\over{n+1}}C_{2n}^{n}$
- 栈的应用:括号匹配、表达式求值(后缀表达式)、递归、迷宫求解等。
-
中缀表达式转换为前缀或后缀表达式:(手工做法)
- 按照运算符的优先级对所有的运算单位加括号。
-
转换为前缀或后缀表达式
- 前缀:把运算符号移动到对应的括号前面
- 后缀:把运算符号移动到对应的括号后面
-
在中缀表达式转化为相应的后缀表达式,需要根据操作符
的优先级来进行栈的变化: 栈外优先级icp(in coming priority, icp):表示当前扫描到的运算符ch的优先级;
栈内优先级isp(in stack priority, isp):为该运算符进栈后的优先级。这个优先级其实也很简单,就是一般的运算优先级,有括号先算括号、先乘除后加减、同级运算从左往右依次运算。
操作符 # ( *,/ +,- ) 栈外优先级icp 0 6 4 2 1 栈内优先级isp 0 1 5 3 6 -
icp>isp:进栈,读下一个字符
icp=isp:pop,不输出(#,(,))
icp<isp:出栈并输出如a+b-a*((c+d)/e-f)+g转换为ab+acd+e/f-*-g
+-*((+(按优先级进入的)
+号遇到-号之后运算了,后面的不能算得看下一个,遇到)能算了。所以栈中存的暂时还不能确定运算次序的操作符最多则5个。
-
- 中缀表达式转换为后缀表达式:(程序做法)
- 从左向右开始扫描中缀表达式
- 遇到数字时,加入后缀表达式
- 遇到运算符时:按照运算符的优先级进行操作
- 若当前扫描元素优先级>栈顶元素,那么当前扫描元素就入栈,先处理当前扫描元素,再处理栈顶元素
- 若当前扫描元素优先级<栈顶元素,那么栈顶元素就出栈,先处理栈顶元素,再处理当前扫描元素
-
若当前扫描元素优先级=栈顶元素,那么pop,不输出
- 若为'(',入栈
- 若为')',则依次把栈中的运算符出栈,并加入后缀表达式,直到出现'(',从栈中删除'(';
- 若为除括号外的其他运算符,当其优先级高于除'('以外的栈顶运算符时,直接入栈;
否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或遇到了一个左括号为止。
- 当扫描的中缀表达式结束时,栈中的所有运算符依次出栈加入后缀表达式。
待处理序列 当前扫描元素 后缀表达式 栈 动作 依次扫描,扫描元素优先级高的入栈;扫描元素优先级低,先让优先级较高的栈顶元素处理
以元素。。开头的序列个数是,把该元素出栈,再将剩下的元素一个个插到现有栈中元素之间,即可算出个数。
顺序栈
基本概念:
采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指向当前栈顶的位置。- 存储结构:
#define MaxSize 50 //定义栈中元素的最大个数
typedef struct {
ElemType data[MaxSize]; //存放栈中元素
int top; //栈顶指针
}SqStack; //Sequence Stack
- 基本操作:
- 栈顶指针:S.top,初始时设置S.top=-1;(即 指向了栈顶元素)
- 栈顶元素:S.data[S.top];
- 进栈操作:栈不满是,栈顶指针先加1,再送值到栈顶元素;
- 出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1;
- 栈空条件:S.top == -1;
- 栈满条件:S.top == MaxSize-1;
- 栈长:S.top+1
共享栈
基本概念:
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数据空间,将两个站的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。- 基本操作:
-
栈空条件:
- 0号栈:top0=-1
- 1号栈:top1=MaxSize
栈满条件:top1 - top0 = 1(即 当两个栈顶指针相邻时栈满)
-
进栈操作:
- 0号栈:top0先加1再赋值
- 1号栈:top1先减1再赋值
-
出栈操作:栈非空时,
- 0号栈:先取栈顶元素值,再将top0减1;
- 1号栈:先取栈顶元素值,再将top1加1
-
栈空条件:
链栈
在表头入栈出栈
基本概念:
采用链式存储的栈称为链栈,通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。- 存储结构:
typedef struct LinkNode {
ElemType data; //数据域
struct LinkNode *next; //指针域
}*LiStack; //List Stack
- 操作:
//插入x结点
x->next = top;
top = x;
- 优点:
便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。便于结点的插入与删除。
队列
- 队列的应用:层次遍历、解决主机与外部设备之间速度不匹配的问题、解决由多用户引起的资源竞争的问题、页面替换算法等。
顺序:先头后尾、左头右尾(头尾)
顺序队列
基本概念:
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针front和rear分别指向队头元素和队尾元素的位置。(这里队头指针指向队头元素的前一个位置,队尾指针指向队尾元素)(避免队空与队列中只有一个元素时无法区分)- 存储结构:
#define MaxSize 50 //定义栈中元素的最大个数
typedef struct {
ElemType data[MaxSize]; //存放队列元素
int front, rear; //队头指针和队尾指针
}SqQueue; //Sequence Queue
- 基本操作:
- 初始状态;Q.front = -1; Q.rear = -1;
- 队空条件:Q.front == Q.rear; (因为队头指针指向队头元素的前一个位置,所以当与队尾指针相等时,队空)
- 进队操作:队不满时,先送值到队尾元素,再将队尾指针加1;
- 出队操作:队不空时,先取队头元素值,再将队头指针加1;
-
队满条件:无法判断队满,当rear > maxsize-1,front > -1时,其实队中依然有空位(front之前还有空位)可以存放元素,只是由于被指针欺骗了,这种现象称为“假溢出”。
循环队列
基本概念:
将顺序队列臆造为一个环状的空间,即吧存储队列元素的表从逻辑上视为一个环,称为循环队列。当队首指针Q.front=MaxSize-1后,再前进一个位置就自动到0,利用取余运算(%)。- 基本操作:
- 初始时:Q.front = Q.rear = 0;
- 入队操作:Q.rear = (Q.rear+1)%MaxSize; (在队尾排队)
- 出队操作:Q.front = (Q.front+1)%MaxSize; (在队头出队)
- 队列长度:(Q.rear+MaxSize-Q.front)%MaxSize。
- 出队入队时:指针都按顺时针方向进1
为了区分队空队满的情况,有三种处理方式:但是无法区分队空与队满(都为Q.front == Q.rear)
- 牺牲一个单元来区分队空队满,入队时少用一个队列单元。约定以“队头指针在队尾指针的下一位置作为队满的标志”
- 队满条件:(Q.rear+1)%MaxSize == Q.front;
- 队空条件:Q.front == Q.rear;
- 队列中元素的个数:(Q.rear-Q.front+MaxSize)%MaxSize。
- 类型中增设表示元素个数的数据成员。
- 队空条件:Q.size == 0;
-
队满条件:Q.size == MaxSize。
这两种情况都有Q.front == Q.rear。
- 类型中增设tag数据成员,以区分是队满还是队空。
- 队空条件:tag == 0时,若因删除导致Q.front == Q.rear,则为队空;
- 队满条件:tag == 1时,若因插入导致Q.front == Q.rear,则为队满。
链队列
头出尾进
基本概念:
队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表(通常设计成带头结点的单链表,方便操作)。头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点(注意与顺序队列不同)。- 存储结构:
typedef struct LinkNode{//链式队列结点
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct { //链式队列
LinkNode *front, *rear; //队列的队头和队尾指针
}LinkQueue;
- 基本操作:
- 初始化:Q.front = Q.rear = 头结点;
- 队空条件:Q.front == NULL且Q.rear == NULL;
- 入队操作:尾插法;
- 出队操作:删除头。
- 适用性:
链队列特别适合于数据元素变动比较大的情形,而且不存在队列满且产生溢出的问题。
另外,假如程序中要使用多个队列,与多个栈的情形一样,最好使用链队列,这样就不会出现存储分配不合理和“溢出”的问题。
双端队列
受限的唯一一端,最好放在左边,无论是输出还是输出,好看一些。
- 基本概念:
双端队列是指允许两端都可以进行入队和出队操作的队列。 - 受限的双端队列:
- 输出受限:只有一端能输出,两端都能输入
- 输入受限:只有一端能输入,两端都能输出
-
技巧:
- 对于这种受限的双端队列,左右仅此一个的操作,为解题关键。
-
输入受限:只有一端能进行输入操作,输入操作唯一
∵入队序列唯一
∴看能否出成选项当中的序列 -
输出受限:只有一端能进行输出操作,输出操作唯一
∵出队序列唯一
∴看能否入成这种队列
栈的相关算法
剑指 Offer 09. 用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例 1:
输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:
输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
答案
方法一:双栈
思路和算法
维护两个栈,第一个栈支持插入操作,第二个栈支持删除操作。
根据栈先进后出的特性,我们每次往第一个栈里插入元素后,第一个栈的底部元素是最后插入的元素,第一个栈的顶部元素是下一个待删除的元素。为了维护队列先进先出的特性,我们引入第二个栈,用第二个栈维护待删除的元素,在执行删除操作的时候我们首先看下第二个栈是否为空。如果为空,我们将第一个栈里的元素一个个弹出插入到第二个栈里,这样第二个栈里元素的顺序就是待删除的元素的顺序,要执行删除操作的时候我们直接弹出第二个栈的元素返回即可。
成员变量
- 维护两个栈 stack1 和 stack2,其中 stack1 支持插入操作,stack2 支持删除操作
构造方法
- 初始化 stack1 和 stack2 为空
插入元素
插入元素对应方法 appendTail
- stack1 直接插入元素
删除元素
删除元素对应方法 deleteHead
- 如果 stack2 为空,则将 stack1 里的所有元素弹出插入到 stack2 里
- 如果 stack2 仍为空,则返回 -1,否则从 stack2 弹出一个元素并返回
class CQueue {
Deque<Integer> stack1;
Deque<Integer> stack2;
public CQueue() {
stack1 = new LinkedList<Integer>();
stack2 = new LinkedList<Integer>();
}
public void appendTail(int value) {
stack1.push(value);
}
public int deleteHead() {
// 如果第二个栈为空
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
if (stack2.isEmpty()) {
return -1;
} else {
int deleteItem = stack2.pop();
return deleteItem;
}
}
}
复杂度分析
时间复杂度:对于插入和删除操作,时间复杂度均为 $O(1)$。插入不多说,对于删除操作,虽然看起来是 $O(n)$ 的时间复杂度,但是仔细考虑下每个元素只会「至多被插入和弹出 stack2 一次」,因此均摊下来每个元素被删除的时间复杂度仍为 $O(1)$。
空间复杂度:$O(n)$。需要使用两个栈存储已有的元素。
我的答案:
class CQueue {
Stack<Integer> s1;
Stack<Integer> s2;
public CQueue() {
s1 = new Stack<>();
s2 = new Stack<>();
}
public void appendTail(int value) {
s1.push(value);
}
public int deleteHead() {
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
}
if (s2.isEmpty()) {
return -1;
} else {
return s2.pop();
}
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
剑指 Offer 30. 包含min函数的栈
使用栈或队列可以保存当前状态下的最大最小值,实现最大最小值的O(1)查找。
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
示例:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2.
答案
辅助栈
思路
要做出这道题目,首先要理解栈结构先进后出的性质。
对于栈来说,如果一个元素 a 在入栈时,栈里有其它的元素 b, c, d,那么无论这个栈在之后经历了什么操作,只要 a 在栈中,b, c, d 就一定在栈中,因为在 a 被弹出之前,b, c, d 不会被弹出。
因此,在操作过程中的任意一个时刻,只要栈顶的元素是 a,那么我们就可以确定栈里面现在的元素一定是 a, b, c, d。
那么,我们可以在每个元素 a 入栈时把当前栈的最小值 m 存储起来。在这之后无论何时,如果栈顶元素是 a,我们就可以直接返回存储的最小值 m。
算法
按照上面的思路,我们只需要设计一个数据结构,使得每个元素 a 与其相应的最小值 m 时刻保持一一对应。因此我们可以使用一个辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值。
当一个元素要入栈时,我们取当前辅助栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入辅助栈中;
当一个元素要出栈时,我们把辅助栈的栈顶元素也一并弹出;
在任意一个时刻,栈内元素的最小值就存储在辅助栈的栈顶元素中。
class MinStack {
Deque<Integer> xStack;
Deque<Integer> minStack;
public MinStack() {
xStack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
minStack.push(Integer.MAX_VALUE);
}
public void push(int x) {
xStack.push(x);
minStack.push(Math.min(minStack.peek(), x));
}
public void pop() {
xStack.pop();
minStack.pop();
}
public int top() {
return xStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
复杂度分析
时间复杂度:对于题目中的所有操作,时间复杂度均为 O(1)。因为栈的插入、删除与读取操作都是 O(1),我们定义的每个操作最多调用栈操作两次。
空间复杂度:O(n),其中 n 为总操作数。最坏情况下,我们会连续插入 n 个元素,此时两个栈占用的空间为 O(n)。
我的答案:
class MinStack {
Stack<Integer> stack;
Stack<Integer> minStack; // 辅助栈
/** initialize your data structure here. */
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
minStack.push(Integer.MAX_VALUE);
}
public void push(int x) {
stack.push(x);
minStack.push(Math.min(minStack.peek(), x));
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int min() {
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.min();
*/
剑指 Offer 31. 栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
答案
如下图所示,给定一个压入序列 pushed 和弹出序列 popped,则压入 / 弹出操作的顺序(即排列)是 唯一确定 的。
如下图所示,栈的数据操作具有 先入后出 的特性,因此某些弹出序列是无法实现的。
考虑借用一个辅助栈 stackstack ,模拟 压入 / 弹出操作的排列。根据是否模拟成功,即可得到结果。
- 入栈操作: 按照压栈序列的顺序执行。
- 出栈操作: 每次入栈后,循环判断 “栈顶元素 == 弹出序列的当前元素” 是否成立,将符合弹出序列顺序的栈顶元素全部弹出。
由于题目规定 栈的所有数字均不相等 ,因此在循环入栈中,每个元素出栈的位置的可能性是唯一的(若有重复数字,则具有多个可出栈的位置)。因而,在遇到 “栈顶元素 = 弹出序列的当前元素” 就应立即执行出栈。
算法流程:
- 初始化: 辅助栈 stack,弹出序列的索引 i;
- 遍历压栈序列: 各元素记为 num;
- 元素 num 入栈;
- 循环出栈:若 stack 的栈顶元素 = 弹出序列元素 popped[i] ,则执行出栈与 i++;
- 返回值: 若 stack 为空,则此弹出序列合法。
复杂度分析:
- 时间复杂度 O(N): 其中 N 为列表 pushed 的长度;每个元素最多入栈与出栈一次,即最多共 2N 次出入栈操作。
- 空间复杂度 O(N): 辅助栈 stack 最多同时存储 N 个元素。
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack = new Stack<>();
int i = 0;
for(int num : pushed) {
stack.push(num); // num 入栈
while(!stack.isEmpty() && stack.peek() == popped[i]) { // 循环判断与出栈
stack.pop();
i++;
}
}
return stack.isEmpty();
}
}
我的答案:
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> s = new Stack<>();
int j = 0;
// 1. 不断地将数组压入栈
for (int i = 0; i < pushed.length; i++) {
s.push(pushed[i]);
// 2. 之后循环判断栈顶元素是否与pop数组元素一致
while (!s.isEmpty() && s.peek() == popped[j]) {
// 3. 如果一致,那就出栈
s.pop();
j++;
}
}
if (s.isEmpty()) {
return true;
}
return false;
}
}
面试题 03.05. 栈排序
栈排序。 编写程序,对栈进行排序使最小元素位于栈顶。最多只能使用一个其他的临时栈存放数据,但不得将元素复制到别的数据结构(如数组)中。该栈支持如下操作:push、pop、peek 和 isEmpty。当栈为空时,peek 返回 -1。
示例1:
输入:
["SortedStack", "push", "push", "peek", "pop", "peek"]
[[], [1], [2], [], [], []]
输出:
[null,null,null,1,null,2]
示例2:
输入:
["SortedStack", "pop", "pop", "push", "pop", "isEmpty"]
[[], [], [], [1], [], []]
输出:
[null,null,null,null,null,true]
答案
未完待续
队列的相关算法
剑指 Offer 59 - II. 队列的最大值
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例 1:
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:
输入:
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]
答案
如何思考
我们知道对于一个普通队列,push_back 和 pop_front 的时间复杂度都是 $\mathcal{O}(1)$,因此我们直接使用队列的相关操作就可以实现这两个函数。
对于 max_value 函数,我们通常会这样思考,即每次入队操作时都更新最大值:
但是当出队时,这个方法会造成信息丢失,即 当最大值出队后,我们无法知道队列里的下一个最大值。
解题思路
为了解决上述问题,我们只需记住当前最大值出队后,队列里的下一个最大值即可。
具体方法是使用一个双端队列 deque,在每次入队时,如果 deque 队尾元素小于即将入队的元素 value,则将小于 value 的元素全部出队后,再将 value 入队;否则直接入队。
这时,辅助队列 deque 队首元素就是队列的最大值。
答案
维护一个单调的双端队列
思路
本算法基于问题的一个重要性质:当一个元素进入队列的时候,它前面所有比它小的元素就不会再对答案产生影响。
举个例子,如果我们向队列中插入数字序列 1 1 1 1 2,那么在第一个数字 2 被插入后,数字 2 前面的所有数字 1 将不会对结果产生影响。因为按照队列的取出顺序,数字 2 只能在所有的数字 1 被取出之后才能被取出,因此如果数字 1 如果在队列中,那么数字 2 必然也在队列中,使得数字 1 对结果没有影响。
按照上面的思路,我们可以设计这样的方法:从队列尾部插入元素时,我们可以提前取出队列中所有比这个元素小的元素,使得队列中只保留对结果有影响的数字。这样的方法等价于要求维持队列单调递减,即要保证每个元素的前面都没有比它小的元素。
那么如何高效实现一个始终递减的队列呢?我们只需要在插入每一个元素 value 时,从队列尾部依次取出比当前元素 value 小的元素,直到遇到一个比当前元素大的元素 value 即可。
- 上面的过程保证了只要在元素 value 被插入之前队列递减,那么在 value 被插入之后队列依然递减。
- 而队列的初始状态(空队列)符合单调递减的定义。
- 由数学归纳法可知队列将会始终保持单调递减。
上面的过程需要从队列尾部取出元素,因此需要使用双端队列来实现。另外我们也需要一个辅助队列来记录所有被插入的值,以确定 pop_front 函数的返回值。
保证了队列单调递减后,求最大值时只需要直接取双端队列中的第一项即可。
class MaxQueue {
Queue<Integer> q;
Deque<Integer> d;
public MaxQueue() {
q = new LinkedList<Integer>();
d = new LinkedList<Integer>();
}
public int max_value() {
if (d.isEmpty()) {
return -1;
}
return d.peekFirst();
}
public void push_back(int value) {
while (!d.isEmpty() && d.peekLast() < value) {
d.pollLast();
}
d.offerLast(value);
q.offer(value);
}
public int pop_front() {
if (q.isEmpty()) {
return -1;
}
int ans = q.poll();
if (ans == d.peekFirst()) {
d.pollFirst();
}
return ans;
}
}