栈与队列
用数组表示栈或队列( js ),
栈:push()、pop()方法分别用来入栈、出栈
队列:push()、shift()方法分别用来入队、出队
1. 用栈实现队列
题目:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x)将元素 x 推到队列的末尾int pop()从队列的开头移除并返回元素int peek()返回队列开头的元素boolean empty()如果队列为空,返回true;否则,返回false
说明:
- 你 只能 使用标准的栈操作 —— 也就是只有
push to top,peek/pop from top,size, 和is empty操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
思路:
用数组表示栈,其push()、pop()方法分别用来入栈、出栈( js )
- 两个栈,一个为进栈——队列元素入口栈,一个为出栈——队列元素出口栈
- 入队时,元素直接进入进栈即可
- 出队时,若出栈不空,则栈顶元素直接出出栈并返回其值;若空,则将进栈中的元素全部出栈并进入出栈中,然后栈顶元素出出栈并返回其值
- 返回队列开头的元素——将开头元素出队并保存其值,然后又让该元素进入出栈中(因为出队最终都是从出栈中出),最后返回其值
- 队列判空——判断两个栈是否都空,若都空,则返回
true;否则,返回false
动态效果:
代码:
var MyQueue = function () {
this.inStack = []
this.outStack = []
};
/**
* @param {number} x
* @return {void}
*/
MyQueue.prototype.push = function (x) {
this.inStack.push(x)
};
/**
* @return {number}
*/
MyQueue.prototype.pop = function () {
if (!this.outStack.length) {
while (this.inStack.length) {
this.outStack.push(this.inStack.pop())
}
}
return this.outStack.pop()
};
/**
* @return {number}
*/
MyQueue.prototype.peek = function () {
let val = this.pop()
this.outStack.push(val)
return val
};
/**
* @return {boolean}
*/
MyQueue.prototype.empty = function () {
return !this.inStack.length && !this.outStack.length
};
/**
* Your MyQueue object will be instantiated and called as such:
* var obj = new MyQueue()
* obj.push(x)
* var param_2 = obj.pop()
* var param_3 = obj.peek()
* var param_4 = obj.empty()
*/
2. 用队列实现栈
题目:
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x)将元素 x 压入栈顶。int pop()移除并返回栈顶元素。int top()返回栈顶元素。boolean empty()如果栈是空的,返回true;否则,返回false。
注意:
- 你只能使用队列的基本操作 —— 也就是
push to back、peek/pop from front、size和is empty这些操作。 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
思路:
用数组表示队列,其push()、shift()方法分别用来入队、出队( js )
-
双队列
- 两个队列,一个用来实现元素出入栈(出入队列),一个用来辅助实现元素出栈(辅助队列)
- 入栈时,元素直接进入出入队列
- 出栈时,若出入队列为空,则将辅助队列元素出队并进入出入队列中;若不空,则不必操作。然后,若出入队列元素个数等于1,则直接出队并返回该元素;若出入队列元素个数大于1,则将出入队列元素出队并进入辅助队列,直至出入队列元素个数等于1,然后让该元素出队并返回其值
- 返回栈顶元素——将栈顶元素出栈并保存其值,然后又让该元素进入出入队列中或入栈(因为出栈最终都是从出入队列中出),最后返回其值
- 栈判空——判断两个队列是否都空,若都空,则返回
true;否则,返回false
动态效果:
-
单队列
即,双队列的优化。
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部即可。移动次数为队列元素个数减1。
代码:
-
双队列
var MyStack = function () { this.queue1 = [] this.queue2 = [] }; /** * @param {number} x * @return {void} */ MyStack.prototype.push = function (x) { this.queue1.push(x) }; /** * @return {number} */ MyStack.prototype.pop = function () { if (!this.queue1.length) { [this.queue1, this.queue2] = [this.queue2, this.queue1] } while (this.queue1.length > 1) this.queue2.push(this.queue1.shift()) return this.queue1.shift() }; /** * @return {number} */ MyStack.prototype.top = function () { let val = this.pop(); this.queue1.push(val); return val; }; /** * @return {boolean} */ MyStack.prototype.empty = function () { return !this.queue1.length && !this.queue2.length }; /** * Your MyStack object will be instantiated and called as such: * var obj = new MyStack() * obj.push(x) * var param_2 = obj.pop() * var param_3 = obj.top() * var param_4 = obj.empty() */ -
单队列
var MyStack = function () { this.queue = [] }; /** * @param {number} x * @return {void} */ MyStack.prototype.push = function (x) { this.queue.push(x) }; /** * @return {number} */ MyStack.prototype.pop = function () { let n = this.queue.length - 1 while (n--) this.queue.push(this.queue.shift()) return this.queue.shift() }; /** * @return {number} */ MyStack.prototype.top = function () { let val = this.pop(); this.queue.push(val); return val; }; /** * @return {boolean} */ MyStack.prototype.empty = function () { return !this.queue.length }; /** * Your MyStack object will be instantiated and called as such: * var obj = new MyStack() * obj.push(x) * var param_2 = obj.pop() * var param_3 = obj.top() * var param_4 = obj.empty() */
3. 有效的括号
题目:
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
考察点:栈的应用(括号匹配是使用栈解决的经典问题)
思路:
-
分析括号不匹配的情况有哪些?
-
左括号多余
-
右括号多余
-
括号类型不匹配
-
-
处理不匹配或匹配的情况
- 若字符为左括号,则直接入栈
- 若字符为右括号,则判断栈顶元素是否与之匹配(类型或方向),若匹配,则栈顶元素出栈(相消操作);若不匹配,则直接返回
false - 最终,判断栈是否为空,若空,则返回
true;若不空,则说明有多余的括号,返回false
代码:
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function (s) {
let stack = []
for (const i of s) {
if (i === '(' || i === '[' || i === '{')
stack.push(i)
else if (i === ')' && stack[stack.length - 1] === '(')
stack.pop()
else if (i === ']' && stack[stack.length - 1] === '[')
stack.pop()
else if (i === '}' && stack[stack.length - 1] === '{')
stack.pop()
else
return false
}
return !stack.length
};
4. 删除字符串中的所有相邻重复项
题目:
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
考察点:栈的应用(此题仍为匹配问题)
思路:
- 将字符串的首字符加入栈中
- 利用循环遍历字符串中剩余字符
- 若当前字符与栈顶元素相同,则弹出栈顶元素(相消操作);若不同,则将该字符入栈
- 遍历结束后,将栈内元素出栈,连接成字符串并返回(注意出栈的字符顺序为目标值的逆序,所以还需要颠倒顺序再返回)
代码:
/**
* @param {string} s
* @return {string}
*/
var removeDuplicates = function (s) {
let stack = [], r1 = "", r2 = ""
stack.push(s[0])
for (let i = 1; i < s.length; i++) {
if (s[i] === stack[stack.length - 1])
stack.pop()
else
stack.push(s[i])
}
while (stack.length)
r1 += stack.pop()
for (let i = r1.length - 1; i >= 0; i--)
r2 += r1[i]
return r2
};
5. 逆波兰表达式求值
题目:
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式(后缀表达式)。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'、'-'、'*'和'/'。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
考察点:栈的应用(和上题做法类似,不过是把相消操作换成算数操作以及判断逻辑的变化)
思路:
首先,对逆波兰表达式要有一定的了解:
逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )。 - 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *也可以依据次序计算出正确结果。 - 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
故,依照上述内容,利用循环遍历字符串数组,用栈操作运算,最终返回栈顶元素。
注意点:
-
'+'运算需要将字符串转换成数字;转换方法参考:js字符串转数字
-
'-'、'/'运算需要注意参与运算数字的顺序,先出栈的应为减数或除数; -
两个整数之间的除法总是 向零截断,即当商为小数时,结果应保留较靠近零的整数。所以,当商为正小数时,则需要向下取整;但当商为负小数时,则需要向上取整,即所得商无论正负直接截取整数部分即可。
代码:
-
普通版
/** * @param {string[]} tokens * @return {number} */ var evalRPN = function (tokens) { let stack = [], n for (const i of tokens) { switch (i) { case '+': stack.push(stack.pop() + stack.pop()) break; case '-': n = stack.pop() stack.push(stack.pop() - n) break; case '*': stack.push(stack.pop() * stack.pop()) break; case '/': n = stack.pop() stack.push(Math.trunc(stack.pop() / n)) break; default: stack.push(i * 1) } } return stack.pop() }; -
优化版
/** * @param {string[]} tokens * @return {number} */ var evalRPN = function (tokens) { let stack = [] let s = new Map([ ['+', (a, b) => a * 1 + b * 1], ['-', (a, b) => b - a], ['*', (a, b) => b * a], // | 0 是一个位运算,用于将浮点数结果截断为整数 ['/', (a, b) => (b / a) | 0], ]) for (const i of tokens) { if (!s.has(i)) stack.push(i) else stack.push(s.get(i)(stack.pop(), stack.pop())) } return stack.pop() };
6. 滑动窗口最大值
题目:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
思路:
用队列存放元素——队首元素应为当前队列中所有元素的最大值,且元素顺序不能变,故需要自定义一个符合需求的队列。
- 设计队列(关键):
constructor():添加一个双队列push(val):若双队列非空且队尾元素相比要加入队列的元素较小,则将队尾元素出队,直至队尾元素相对较大或队空,则加入元素到队尾;否则,说明队列中无比要加入队列的元素更小的元素,故直接加入元素到队尾(维护:队首元素为当前队列中所有元素的最大值,且元素顺序不变)pop(val):若要出队的元素还在队列中,即队首元素等于val,则队首元素出队getMax():队首元素为当前队列中所有元素的最大值,故直接返回队首元素
- 利用循环将最初始的滑动窗口中的元素放入队列中,并获取最大值放入保存结果的数组中
- 利用循环移动滑动窗口,即元素出队、入队。每移动一次,则收集一次最大值
- 最终返回保存结果的数组
动态效果:
代码:
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
class deque {
constructor() {
this.queue = []
}
push(val) {
while (this.queue.length && this.queue[this.queue.length - 1] < val)
this.queue.pop()
this.queue.push(val)
}
pop(val) {
if (this.queue.length && this.queue[0] === val)
this.queue.shift()
}
getMax() {
return this.queue[0]
}
}
let myDeque = new deque()
let i = 0, j = 0
let res = []
while (i < k)
myDeque.push(nums[i++])
res.push(myDeque.getMax())
while (i < nums.length) {
myDeque.pop(nums[j++])
myDeque.push(nums[i++])
res.push(myDeque.getMax())
}
return res
};
7. 前 K 个高频元素
题目:
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
解决步骤:
- 统计各元素出现频率
- 对频率进行排序
- 找出前K个高频元素
思路:
- 用哈希法(Map)统计各元素出现频率
- 用优先级队列(小 顶/根 堆)对部分频率进行排序(时间复杂度较低)
- 最后将优先队列中的元素对应的 key 值返回
堆相关内容可以参考:数据结构——【堆】详解
代码:
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function (nums, k) {
let map = {}
for (const i of nums) {
map[i] = (map[i] || 0) + 1
}
let pq = new priorityQueue((a, b) => a[1] - b[1])
for (const i of Object.entries(map)) {
pq.push(i)
if (pq.size() > k)
pq.pop()
}
let res = []
for (let i = pq.size() - 1; i >= 0; i--)
res[i] = pq.pop()[0]
return res
};
class priorityQueue {
constructor(compareFn) {
this.queue = []
this.compareFn = compareFn
}
push(item) {
this.queue.push(item)
let index = this.queue.length - 1
let parent = Math.floor((index - 1) / 2)
while (parent >= 0 && this.compare(parent, index) > 0) {
[this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]]
index = parent
parent = Math.floor((index - 1) / 2)
}
}
pop() {
let r = this.queue[0]
this.queue[0] = this.queue.pop()
let index = 0, left = 1
let child = this.compare(left, left + 1) > 0 ? left + 1 : left
while (child != undefined && this.compare(index, child) > 0) {
[this.queue[index], this.queue[child]] = [this.queue[child], this.queue[index]]
index = child
left = 2 * index + 1
child = this.compare(left, left + 1) > 0 ? left + 1 : left
}
return r
}
size() {
return this.queue.length
}
compare(i, j) {
if (this.queue[i] === undefined)
return 1
if (this.queue[j] === undefined)
return -1
return this.compareFn(this.queue[i], this.queue[j])
}
}