栈和队列
622. 设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
思路:
成员变量:
- 队头指针
head
、当前队列中的元素数count
、队列容量capacity
、队列queue
主要操作:
MyCircularQueue(k)
: 构造器,设置队列长度为k
。初始化各个成员变量。Front
: 从队首获取元素,直接通过head
指针返回。如果队列为空,返回 -1 。Rear
: 获取队尾元素,利用 (head+count-1)%capacity来计算得到队尾的位置。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素,插入队尾,位置是 (head+count-1)%capacity,如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素,相当于将head
指针向后移一位,但同时也要考虑下一位是否到了最后一个**(head+1)%capacity**,如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空,看当前的元素数是否等于0。isFull()
: 检查循环队列是否已满,看当前元素数是否等于容量。
代码
class MyCircularQueue {
public:
//队列头指针
int head;
//队列中元素数量
int count;
//队列容量
int capacity;
//用数组创建队列
vector<int>queue;
//构造函数
MyCircularQueue(int k) {
queue.resize(k);
capacity=k;
head=0;
count=0;
}
//入队
bool enQueue(int value) {
if(isFull())
return false;
//循环队列,队尾位置以队头和元素数量和容量来推断
queue[(head+count)%capacity]=value;
count++;
return true;
}
bool deQueue() {
if(isEmpty())
return false;
//head向后移一个
head=(head+1)%capacity;
count--;
return true;
}
int Front() {
if(isEmpty())
return -1;
return queue[head];
}
int Rear() {
if(isEmpty())
return -1;
return queue[(head+count-1)%capacity];
}
bool isEmpty() {
return count==0;
}
bool isFull() {
return count==capacity;
}
};
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue* obj = new MyCircularQueue(k);
* bool param_1 = obj->enQueue(value);
* bool param_2 = obj->deQueue();
* int param_3 = obj->Front();
* int param_4 = obj->Rear();
* bool param_5 = obj->isEmpty();
* bool param_6 = obj->isFull();
*/
广度优先遍历(BFS)
应用场景:在一幅图中找到从起点start
到终点target
的最近距离。
模板来自于题解
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj())
if (x not in visited) {//已遍历过的就跳过
q.offer(x);
visited.add(x);
}
}
/* 划重点:更新步数在这里 */
step++;
}
}
cur.adj()
泛指cur
相邻的节点,比如说二维数组中,cur
上下左右四面的位置就是相邻节点;visited
的主要作用是防止走回头路,遍历过的结点就跳过。大部分时候都是必须的(特别是在图中),但是像一般的二叉树结构,没有子节点到父节点的指针,不会走回头路就不需要visited
。
752. 打开转盘锁
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000'
,一个代表四个拨轮的数字的字符串。
列表deadends
包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target
代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1
。
思路
- 利用广度优先搜索
- 以字符串
‘0000’
为起点,将它的每一位都分别加1和减1,总共有8
种结果 - 逐个考察这
8
个数,看是否是死亡数字或是目标数字,死亡数字的话就跳过,目标的话就返回当前层数。 - 用一个哈希表来存储死亡数组和遍历过的数字,方便查询。
- 利用队列,如果当前数字既不是死亡数组也没有遍历过的话就将它压入队列中
- 本题的重点还在于如何“拨动”,让某一位数字加一和减一,比较巧妙,我不会,看代码吧。以下代码可实现
0
向后“拨动”变成9
,9
向前“拨动”变成0
// 完美的字符处理方式,利用ascⅡ码之差之后加上t并取余作为新得到的整型
//然后再加上0的ascⅡ码值返回字符
char a = (curr[j] -'0' + 10 + t) % 10 + '0';
代码
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
// 哈希表vis中存储不可能入队的结点,包括deadends和已访问过的结点
unordered_set<string> vis;
vis.insert(deadends.begin(), deadends.end());
if(vis.count("0000"))
return -1;
//记步数
int step = 0;
//队列
queue<string> st;
//0000作为起始节点
st.push("0000");
//循环处理队列,直到队列中没有元素
while(!st.empty()){
int length = st.size();
for(int i = 0; i < length; i++){
//取出队首元素
string curr = st.front();
st.pop();
// 找到目标元素,直接返回答案
if(curr == target)
return step;
// 没有找到的话就继续向下看
// 重点:处理curr周围的八个相邻结点,将没有在死亡数组中出现的数字加入到队列
for(int j = 0; j < 4; ++j){
// 每一位自增1与自减1,所以有八种结果(4×2)
for(int t = -1; t < 2; t += 2){
// 完美的字符处理方式,利用ascⅡ码之差之后加上t并取余作为新得到的整型,然后再加上0的ascⅡ码值返回字符
char a = (curr[j] -'0' + 10 + t) % 10 + '0';
string newOne = curr;
newOne[j] = a;
// 若哈希集中找不到此状态,则加入哈希集同时入队
if(!vis.count(newOne)){
st.push(newOne);
vis.emplace(newOne);
}
}
}
}
// 本层队列中元素处理完成,到达下一转动步数,步数加1
step++;
}
return -1;
}
};
739. 每日温度
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
,你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]
。
提示:气温 列表长度的范围是[1, 30000]
。每个气温的值的均为华氏度,都是在[30, 100]
范围内的整数。
思路
- 这道题要用到单调栈,栈中存放的是元素下标。
- 先将第一个元素下标0入栈,然后遍历数组中的元素,逐个与栈顶数字所代表的数组元素进行比较。
- 如果小于栈顶,就将当前元素下标入栈,如果大于的话,就弹出栈顶元素。也就是要保证从栈底到栈顶是单调递减的。
- 我解释不清楚,还是看官方题解
代码
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& T) {
//利用单调栈存储元素下标
//当前元素小于栈顶元素时,下标入栈
//大于栈顶元素时,出栈,并在result数组对应位置赋值i-indx.top()
stack<int>indx;
vector<int>result(T.size(),0);
for(int i=0;i<T.size();i++)
{
while(!indx.empty()&&T[i]>T[indx.top()])
{
result[indx.top()]=i-indx.top();
indx.pop();
}
indx.push(i);
}
return result;
}
};
150. 逆波兰表达式求值(栈)
根据 逆波兰表示法,求表达式的值。
有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。整数除法只保留整数部分。
思路
这道题有几个比较关键的地方
- 给定的表达式是
string
类型,对于整数字符串,应该将其转化为对应的整数,可以使用内置的函数atoi( )
,在此之前还得先把string
类型转变为char*
类型,用c_str( )
- 遍历字符串数组,对每个字符串进行判断,看是否是运算符或是数字,将
非+、-、*、/
的字符串都认为是数字,对其处理后压入栈 - 如果是运算符,就从栈中弹出两个元素,用当前的运算符做运算,然后将计算结果压入栈。
- 最终的返回值是栈顶元素。
代码
class Solution {
public:
int evalRPN(vector<string>& tokens) {
//遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
stack<int>st;
for(string i:tokens)
{
if(!(i == "+" || i == "-" || i == "*" || i == "/"))
{
//压入数字
//c_str() 以 char* 形式传回 string 内含字符串
//atoi()将字符串转化为对应整数
st.push(atoi(i.c_str()));
}
else
{
//弹出两个元素
int num2=st.top();
st.pop();
int num1=st.top();
st.pop();
//switch的参数只能为char或int型,所以取字符串的第0个字符
switch(i[0])
{
//根据不同运算符做相应运算
//然后将结果压入栈
case '+':
st.push(num1+num2);
break;
case '-':
st.push(num1-num2);
break;
case '*':
st.push(num1*num2);
break;
case '/':
st.push(num1/num2);
break;
}
}
}
return st.top();
}
};
394. 字符串解码(栈)
- 给定一个经过编码的字符串,返回它解码后的字符串。
- 编码规则为:
k[encoded_string]
,表示其中方括号内部的encoded_string
正好重复k
次。注意k
保证为正整数。 - 输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
- 原始数据不包含数字,所有的数字只表示重复的次数
k
,例如不会出现像3a
或2[4]
的输入。
思路:
- 利用辅助栈
st
和存储数字的变量multi
,存储字符串的res
,遍历字符串中每个字符,分四种情况 - 如果当前字符是数字,那就将它解析成数值后放入
multi
中,作为重复的倍数,但是这里要注意,有可能出现多个连续的数字组成两位及其以上的整数,对multi
要有一个累加的过程,这里的小技巧不太容易想到 - 如果是字母,就将字母加到
res
后面 - 如果是左括号
'['
,说明遇到了要重复的字符串,将遇到这个括号之前记录下来的res
和multi
压入栈st
,然后将这两个变量之置0
- 每个栈元素是一个
int
和一个string
组成的对子,注意用pair<>
和make_pair()
- 如果是右括号
']'
,说明到了要重复的字符串的尾部,此时res
中的字符串就是一个[ ]
中的要重复的内容(因为[
之前的字符在遇到[
后被压入栈了)
代码
class Solution {
public:
string decodeString(string s) {
//利用辅助栈
//注意使用pair<>来组成对子
stack<pair<int,string>> st;
int multi=0;
string res="";
//逐个读取字符串的字符
for(int i=0;i<s.length();i++)
{
//数字
if(s[i]>='0'&&s[i]<='9')
{
//可能有多位数字,要进行累加
multi*=10;
multi+=(s[i]-'0');
}
//如果是左括号,将左括号之前读到的数字和字符串入栈,注意压入栈后要置0
else if(s[i]=='[')
{
//注意使用make_pair()组成对子
st.push(make_pair(multi,res));
multi=0;
res="";
}
//如果是右括号,弹出栈顶元素,res=栈顶res+当前读到的res*栈顶multi
else if(s[i]==']')
{
//取出栈顶元素的两个值,都是成员变量,不用加括号
//n是[]中字符串重复次数,3[a2[c]]中的2
//tmp是当前[]和上一个[之间的字符,比如3[a2[c]]中的a
int n=st.top().first;
string tmp=st.top().second;
st.pop();
//重复括号内的字符串,3[a2[c]]中的c
while(n>0)
{
tmp+=res;
n--;
}
res=tmp;
}
//如果是字母,加入到res,当遇到括号就将res暂时压入栈
else
res+=s[i];
}
return res;
}
};
733. 图像渲染(遍历邻接点,队列)
一个图像(二维数组),每个元素的数值在0
到65535
之间,给定坐标(sr,sc)
,表示图像渲染开始的位置,以及一个新的颜色值newColor
,如果该位置原本的值就等于新颜色值,那就不进行渲染,直接返回image
渲染从初始坐标开始,考察它的上下左右四个方向的值,如果等于初始坐标的值,就对他进行渲染,然后再看它的邻接点,返回经过上色渲染后的图像
思路
广度优先遍历
- 用一个辅助队列来保存没有访问过并且值等于初始坐标值的点
- 官方题解对于访问邻接点有个很好的方法,先定义两个数组
const int dx[4] = {1, 0, 0, -1};
const int dy[4] = {0, 1, -1, 0};
- 然后只需要在中心坐标的基础上加数组元素,就能实现上下左右四个邻接点的遍历
for (int i = 0; i < 4; i++) {
int mx = x + dx[i], my = y + dy[i];
if (mx >= 0 && mx < n && my >= 0 && my < m && image[mx][my] == currColor) {
que.emplace(mx, my);
image[mx][my] = newColor;
}
}
代码
class Solution {
public:
//用于访问邻接点
const int dx[4] = {1, 0, 0, -1};
const int dy[4] = {0, 1, -1, 0};
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
int currColor = image[sr][sc];
if (currColor == newColor) return image;
int n = image.size(), m = image[0].size();
//辅助队列
queue<pair<int, int>> que;
//初始点入队
que.emplace(sr, sc);
//渲染初始点
image[sr][sc] = newColor;
while (!que.empty()) {
int x = que.front().first, y = que.front().second;
que.pop();
for (int i = 0; i < 4; i++) {
//考察当前位置的上下左右坐标
int mx = x + dx[i], my = y + dy[i];
//满足条件的结点入队
if (mx >= 0 && mx < n && my >= 0 && my < m && image[mx][my] == currColor) {
que.emplace(mx, my);
image[mx][my] = newColor;
}
}
}
return image;
}
};
深度优先搜索
class Solution {
public:
//上下左右坐标
int x[4]={1,0,0,-1};
int y[4]={0,1,-1,0};
//深度优先遍历
void DFS(vector<vector<int>>& image,int sr, int sc, int newColor,int old,int r,int c)
{
image[sr][sc]=newColor;
for(int i=0;i<4;i++)
{
int m_x=sr+x[i];
int m_y=sc+y[i];
if(m_x<r&&m_x>=0&&m_y<c&&m_y>=0)
{
if(image[m_x][m_y]==old)
{
DFS(image,m_x,m_y,newColor,old,r,c);
}
}
}
}
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
//深度优先,一条路走到黑
if(image[sr][sc]==newColor)
return image;
int m=image.size();
int n=image[0].size();
int oldcolor=image[sr][sc];
//启动深度优先遍历
DFS(image,sr,sc,newColor,oldcolor,m,n);
return image;
}
};
542. 01 矩阵(队列)
给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。
两个相邻元素间的距离为 1
思路
- 参照官方题解
- 题目要求是对于每个1,找到离他最近的0
- 题目中可能存在多个0,为了找到离1最近的那个0,可以从0的位置开始进行广度优先搜索
- 广度优先搜索可以找到从起点到其余所有点的 最短距离,因此如果我们从 0 开始搜索,每次搜索到一个 1,就可以得到 0 到这个 11 的最短距离
- 对于多个0,将这些0全部加入队列中,并且作为已访问过的点
代码
class Solution {
public:
int dx[4]={1,0,0,-1};
int dy[4]={0,1,-1,0};
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
//对矩阵中的每个1开启广度优先搜索,一层一层地向外扩张找0
//开启的搜索次数是该1到最近的0的距离
int r=mat.size();
int c=mat[0].size();
//结果矩阵
vector<vector<int>>res(r,vector<int>(c,0));
//已访问的结点
vector<vector<int>>V(r,vector<int>(c,0));
//辅助队列
queue<pair<int,int>>Q;
//遍历原矩阵的每个元素,将0加入队列
for(int i=0;i<r;i++)
{
for(int j=0;j<c;j++)
{
if(mat[i][j]==0)
{
Q.emplace(i,j);
V[i][j]=1;
}
}
}
while(!Q.empty())
{
int x=Q.front().first;
int y=Q.front().second;
Q.pop();
for(int i=0;i<4;i++)
{
int mx=x+dx[i];
int my=y+dy[i];
//遇到没访问过得非0点
if(mx>=0 && mx<r && my>=0 && my<c && !V[mx][my])
{
res[mx][my]=res[x][y]+1;
Q.emplace(mx,my);
V[mx][my]=1;
}
}
}
return res;
}
//广度优先搜索,找0
int BFS(vector<vector<int>>& mat,vector<vector<int>>& V,int nr,int nc)
{
int r=mat.size();
int c=mat[0].size();
//辅助队列
queue<pair<int,int>>Q;
Q.emplace(nr,nc);
V[nr][nc]=1;
int n=1;
while(!Q.empty())
{
int x=Q.front().first;
int y=Q.front().second;
Q.pop();
for(int i=0;i<4;i++)
{
int mx=x+dx[i];
int my=y+dy[i];
if(mx>=0 && mx<r && my>=0 && my<c && !V[mx][my])
{
if(mat[mx][my]==1)
{
Q.emplace(mx,my);
V[mx][my]=1;
}
else if(mat[mx][my]==0)
return n;
}
}
n++;
}
return 0;
}
};
剑指 Offer 09. 用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail
和 deleteHead
,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead
操作返回 -1
)
思路
- 简单题,一个输入栈
S_in
一个输出S_out
appendTail
将新元素直接压入输入栈,deleteHead
则需要弹出的是输入栈的栈底元素,可以将输入栈中的所有元素依次弹出后再依次压入输出栈,此时输出栈的栈顶就是队列的头部。- 当输出栈非空时,每次
deleteHead
都直接从输出栈中弹出元素 - 当输出栈为空而输入栈非空时,继续将输入栈中的全部元素弹出后依次加入输出栈作为补充
- 当两个栈都为空时,返回
-1
代码
class CQueue {
public:
//两个辅助栈,输入栈和输出栈
stack<int>S_in;
stack<int>S_out;
CQueue() {
}
void appendTail(int value) {
//新元素压入输入栈
S_in.push(value);
}
int deleteHead() {
if(S_in.empty()&&S_out.empty())
return -1;
//将输入栈中元素弹出压入输出栈
if(S_out.empty())
{
while(!S_in.empty())
{
S_out.push(S_in.top());
S_in.pop();
}
}
int res=S_out.top();
S_out.pop();
return res;
}
};
/**
* Your CQueue object will be instantiated and called as such:
* CQueue* obj = new CQueue();
* obj->appendTail(value);
* int param_2 = obj->deleteHead();
*/
225. 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack
类:
void push(int x)
将元素 x
压入栈顶。
int pop()
移除并返回栈顶元素。
int top()
返回栈顶元素。
boolean empty()
如果栈是空的,返回 true
;否则,返回 false
。
思路:
- 用一个辅助队列Q就可以实现
push
压入元素时,直接压入Q
- 然后将新加元素之前的所有数都依次出队,然后再依次入队,这样的话,队首就是新压入的那个
- 每次压入元素之后都将它前面的元素放出来再压进去
pop
返回栈顶元素时,直接出队就好了,front
同理
class MyStack {
public:
queue<int>Q;
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
//将队列前面的数依次都出队,然后再依次重新加入队列
//保证队首是新加的数
int n=Q.size();
Q.push(x);
while(n>0)
{
int tmp=Q.front();
Q.pop();
Q.push(tmp);
n--;
}
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
int tmp=Q.front();
Q.pop();
return tmp;
}
/** Get the top element. */
int top() {
return Q.front();
}
/** Returns whether the stack is empty. */
bool empty() {
return Q.empty();
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/