数据结构
JavaScript 中,栈和队列的实现依赖于数组,也就是说,栈和队列就是特殊的数组。
栈
栈的特点是先进后出,用到数组的两个方法:
stack.push(item); // 进栈
stack.pop(); // 出栈
队列
队列的特点是先进先出,用到数组的两个方法:
stack.push(item); // 进队
stack.shift(); // 出队
算法
栈的应用
关键词:对称性、递减栈
20. 有效的括号
括号的成立意味着对称性,所以我们使用栈来解决。当遇到左括号时,压入对应的右括号,遇到右括号时,弹出栈顶元素并进行校验,得到结果:
const l2r = {
"(":")",
"[":"]",
"{":"}"
}
var isValid = function(s) {
const stack=[];
for(let i =0;i<s.length;i++){
const value=s[i];
if(value==="("||value==='['||value==='{'){
stack.push(l2r[value]);
} else if(!stack.length|| stack.pop()!==value){
return false;
}
}
return !stack.length;
};
剑指 Offer II 038. 每日温度
可以使用两层遍历,更好的方法是使用栈来避免重复操作,就是要及时将不必要的元素出栈。
维护一个递减栈,保持温度单调递减。具体做法是,遍历温度数组,将新读取的温度值与栈顶温度比较,若读取的温度值较低或栈为空,则将它的下标压入栈中;若读取的温度值较高,则将读取的温度值下标与栈顶元素的值求差,填入结果数组栈顶下标对应的元素中,重复这一步直到读取的温度值比栈顶温度值低或栈为空。
相较于两层遍历,递减栈的时间复杂度由 O(n^2) 变为了 O(n)。
var dailyTemperatures = function(temperatures) {
const len = temperatures.length;
const stack=[];
const res= (new Array(len)).fill(0);
for(let i=0;i<len;i++){
while(stack.length&&temperatures[i]>temperatures[stack[stack.length-1]]){
const index=stack.pop();
res[index]= i-index;
}
stack.push(i);
}
return res;
};
155. 最小栈
这道题的关键在于 getMin 函数。普通的想法是使用遍历来找最小元素,时间复杂度是 O(n):
var MinStack = function() {
this.stack=[];
};
MinStack.prototype.push = function(val) {
this.stack.push(val)
};
MinStack.prototype.pop = function() {
this.stack.pop();
};
MinStack.prototype.top = function() {
return this.stack[this.stack.length-1];
};
MinStack.prototype.getMin = function() {
let min = Infinity;
for(let i =0;i<this.stack.length;i++){
const cur=this.stack[i];
if(cur<min){
min=cur;
}
}
return min;
};
这么写也能通过测试,但是我们可以利用栈来将时间复杂度降到 O(1)。
为了降低时间复杂度,我们使用空间换时间。使用一个辅助栈作为递减栈,用于存储当前的最小元素序列,调用 getMin 时,只需要取出栈顶的元素:
var MinStack = function() {
this.stack=[];
// 用空间换时间,定义最小数的栈,使 getMin 方法时间复杂度为 O(1)
this.stack2=[];
};
MinStack.prototype.push = function(val) {
this.stack.push(val);
// 若当前元素比递减栈的栈顶还小,压栈
if(!this.stack2.length||this.stack2[this.stack2.length-1]>=val){
this.stack2.push(val);
}
};
MinStack.prototype.pop = function() {
const value = this.stack.pop();
// 若弹出的元素也存在于递减栈,将递减栈的栈顶一并弹出
if(value===this.stack2[this.stack2.length-1]){
this.stack2.pop();
}
return value;
};
MinStack.prototype.top = function() {
return this.stack[this.stack.length-1];
};
MinStack.prototype.getMin = function() {
// 直接获取递减栈的栈顶
return this.stack2[this.stack2.length-1]
};
队列的应用
关键词:逆序、双指针、双端队列
232. 用栈实现队列
本题的关键是让一个栈逆序输出,这里用两个栈来解决:一个栈用于入队操作,一个栈用于出队操作。当想要出队或者查看队首元素时,若出队的栈为空,则将入队栈的元素逐个弹出并压入出队栈,再执行相关操作,这样就实现了已输入的元素的逆序:
var MyQueue = function() {
this.stack1=[];
this.stack2=[];
};
MyQueue.prototype.push = function(x) {
this.stack1.push(x);
};
MyQueue.prototype.pop = function() {
if(!this.stack2.length){
while(this.stack1.length){
this.stack2.push(this.stack1.pop())
}
}
return this.stack2.pop();
};
MyQueue.prototype.peek = function() {
if(!this.stack2.length){
while(this.stack1.length){
this.stack2.push(this.stack1.pop())
}
}
return this.stack2[this.stack2.length-1];
};
MyQueue.prototype.empty = function() {
return !this.stack1.length&&!this.stack2.length;
};
剑指 Offer 59 - I. 滑动窗口的最大值
为了约束窗口的范围,我们使用双指针,两个指针分别指向左和右,遍历指针包含的区域得到结果,这种方法的时间复杂度为 O(kn):
var maxSlidingWindow = function(nums, k) {
const res =[];
let i =0,j=k-1;
if(!nums||!nums.length){
return res;
}
while(j<nums.length){
let max=calMax(nums,i,j);
res.push(max);
i++;
j++;
}
return res;
};
function calMax(nums,i,j){
let max=nums[i];
for(let k=i+1;k<=j;k++){
if(nums[k]>max) {
max=nums[k];
}
}
return max;
}
O(kn) 中 k 的产生来源于我们需要通过遍历来得到最大值,可以使用双端队列将时间复杂度降到 O(n)。
本题中的双端队列是一个有效的递减队列,只在窗口移动时更新元素的最大值。遍历数组时,若当前元素大于队尾元素,则将队尾小于当前元素的元素依次出队(双端队列可从队尾出队);若小于队尾元素,则将其下标入队。
当遍历到第 k 个值时,第一个滑动窗口的最大值就是当前的队尾元素所对应的值。将它压入结果数组中,重复此步骤直到读完最后一个元素。
同时,每次得到结果时需要检查窗口之前的元素是否在队列中,若在则出队。这一步是为了维持队列的有效性。
var maxSlidingWindow = function(nums, k) {
// 双端队列,时间复杂度O(n)
const deque=[];
const res=[];
for(let i=0;i<nums.length;i++){
// 若遍历元素大于队尾元素对应的值且队不空,队尾元素出队
while(deque.length&&nums[deque[deque.length-1]]<nums[i]){
deque.pop();
}
// 直到遍历元素小于队尾元素或队空,遍历元素的下标入队
deque.push(i);
// 及时移除窗口之前的元素
while(deque.length&&deque[0]<=i-k){
deque.shift();
}
// 若遍历到 k,则证明第一个窗口的结果已经产生
if(i>=k-1){
res.push(nums[deque[0]]);
}
}
return res;
};
(使用 API 来解决:
var maxSlidingWindow = function(nums, k) {
const res=[];
let i=0;
while(nums[i+k-1]!=null){
const temp = nums.slice(i,i+k);
res.push(Math.max(...temp))
i++;
}
return res;
};