数组上的方法
在聊栈哥与队宝之前,我们先来回顾一下关于JS数组上的方法:
arr.push('f') // 向尾部添加元素
arr.pop() // 移除数组尾部元素
arr.unshift('hello') // 头部添加
arr.shift() // 头部移除
arr.slice(2, 5) // 从下标2删除到下标5(不包括5) [2,5)
arr.splice(1, 0, 'hello') // 删除0个元素添加一个'hello'
arr.splice(0, 2) // 从下标为0的元素开始删除2个
其中slice
与splice
区别在于slice不会改变原数组,而splice会改变原数组。
const arr = [1, 2, 3, 4, 5, 6, 7]
console.log(arr.slice(2, 5)); // [ 3, 4, 5 ]
console.log(arr); // [1, 2, 3, 4, 5, 6, 7]
const arr1 = [1, 2, 3, 4, 5, 6, 7]
console.log(arr1.splice(2, 3)); // [ 3, 4, 5 ]
console.log(arr1); // [ 1, 2, 6, 7 ]
栈
接下来我们来聊一聊栈。栈是一种常见的数据结构,它是什么样子的呢?就相当于一堆叠起来的盘子,你放一个盘子只能往上面添加(正常情况下),而你取盘子也是只能从上面取。栈就相当于这种操作,先进后出,最上面的为栈顶,最下面为栈底。
怎么使用栈呢?在 JS 中,我们可以通过let stack = []
来创建一个栈,这不是一个数组吗?对,就是一个数组,但是我们可以只让它一边进一边出,也就是只使用数组上的push()+pop()
或unshift()+shift()
方法,就是一个栈了,只能在尾部添加或删除,所以可以称栈结构是一个弱化版的数组。
const stack = []
stack.push('可爱多')
stack.push('巧乐兹')
stack.push('小布丁')
stack.push('老冰棍')
while (stack.length) {
const top = stack.pop()
console.log('现在吃:' + top);
}
上面的代码实现了一个简单的入栈出栈以及遍历栈的过程。
先将四种类型不同的冰棒使用数组上的push()
方法顺序压入栈中,然后使用while (stack.length)
遍历栈,每次循环使用stack.pop()
出一个栈顶的冰棒,会返回栈顶元素的值,并且打印出来这个冰棒,这时候stack.length
就会-1,当减到0时栈空,即遍历完毕。
栈的应用
既然知道了栈这种数据结构,那么我们可以用栈来干嘛呢?,来看一道leetcode第20题——有效的括号问题。
s = "()[]{}" // 输出 true
s = "([])" // 输出 true
s = "([" //输出 false
s = "([)]" //输出 false
这就是一个非常适合用栈来解决的问题。但是我们如何将左括号与右括号判断配对呢?这就可以用到对象中的键值对配对。可以创建一个对象,里面的属性为左边括号,属性值为右边括号。
const lToR = {
'(': ')',
'[': ']',
'{': '}'
}
然后遍历字符串,左边括号就入栈,遇到右边括号就判断当前是否有栈顶元素(栈空了直接返回false
)且栈顶的元素是否有相应的左括号(lToR[stack.pop()] == s[i]
),一旦有一个不等于或遍历完栈没空(左括号多了,比如([]
,此时(
还在栈中,就返回false
。
具体代码如下;
let s = '([{}])'
var isValid = function (s) {
const lToR = {
'(': ')',
'[': ']',
'{': '}'
}
const stack = []
for (let i = 0; i < s.length; i++) {
//如果是左括号就入栈
if (s[i] === '(' || s[i] === '[' || s[i] === '{') {
stack.push(s[i])
} else { //当前获取到的是右边的括号 //考虑右边括号多,没有左边的括号配对
if (!stack.length || lToR[stack.pop()] !== s[i]) { // '{' === '}'
return false
}
}
}
return !stack.length //考虑左边括号多了
};
console.log(isValid(s)); // true
除了这个问题,许多关于对称的问题都可以先想着用栈来解决。
队列
相比栈,队列就是一种先进先出的结构了,就比如超市结账排队,先排队先进去的就先结账出来,也就是先进先出,也是通过let queue = []
创建,也是弱化版的数组,只使用push()+shift()
或unshift()+pop()
方法,只能在尾部添加,头部删除取出。
const queue = []
queue.push('辣椒炒肉')
queue.push('黄豆鸡脚')
queue.push('剁椒鱼头')
while (queue.length) {
const food = queue.shift()
console.log('现在开始做:' + food);
}
上面的代码展示了队列的入队出队和遍历队列的过程。
使用
push()
方法从尾部添加,shift()
方法从头部弹出。
队列的应用
再来看一道leetcode的第225题算法题,用队列来实现栈。
我们可以先创建两个队列(
queue1
和queue2
),且只能在队尾添加,队头删除,只能获取队头的元素。
push()
方法直接往queue1中直接添加即可完成(this.queue1.push(x);
)
再来看pop()
和top()
方法的实现。以上图为例,先push()进去1234
,pop()
方法就是获取出来4,并且把4出队列,根据上图,可以先将123
压入queue2
中,然后再弹出queue1队头的元素4,如下。
但是当接着pop()
此时queue1
为空队列,即输出错误,所以我们应该判断如果此时queue1
为空队列,将俩个队列交换,这样下次pop()
操作就可以同上操作一样弹出queue1队头元素3了。
if (!this.queue1.length) {
[this.queue1, this.queue2] = [this.queue2, this.queue1];
}
交换完如下;
而
top()
获取栈顶的元素可以将前面的pop()
与push()
方法结合。先使用pop()
将栈顶元素出栈并且此时用变量x 接收返回值,然后再将push(x)
入栈(弹出一个栈顶元素,用变量接收该值再加入进去),再return出 x 的值即可获取栈顶元素。
let x = this.pop();
this.queue1.push(x);
return x;
最后判断是否为空empty()
可以直接用俩个队列长度是否为0,可简写成两个队列长度加起来为0即为空,返回true,反之false。
具体代码如下;
var MyStack = function () {
this.queue1 = [];
this.queue2 = [];
};
// 压入栈
MyStack.prototype.push = function (x) {
this.queue1.push(x);
};
// 弹出栈顶元素
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();
};
// 获取栈顶元素
MyStack.prototype.top = function () {
let x = this.pop();
this.queue1.push(x);
return x;
};
// 判断是否为空
MyStack.prototype.empty = function () {
return !(this.queue1.length + this.queue2.length)
};
会了这题相信你对队列有了更加深刻的认识。还有一道leecode232题用俩个栈实现队列可以做一下。
滑动窗口最大值
接下来我们再来看一道leetcode第239题,关于滑动窗口的题目;
给出一个数组,维护一个固定长度的窗口,每次向右移动一位,选取滑动窗口的最大值。比较容易想到的解法有双指针,不过其每次都要遍历固定长度选取最大值,事件复杂度会超过题目要求。我们可以用双端队列(俩端都能进出)来解决。
首先:
const res = [] // 遍历到长度大于等于k时,存储每次的队头
const deque = [] // 存的是当前维护队列(长度k)的每个下标
一直维护一个队头为最大值的双端队列,当加入一个值时,与队尾的元素比较,如果该值更大,则接着与下一个比较,直到比到有一个比它大的在前面为止,用下面代码可以实现。
while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop()
}
也就是维护一个单调递减的队列,但是维护队列时需判断队头元素的下标是否在窗口内。
比如;
当加入2时,队头的元素已经超过了滑动窗口的范围,所以要移除掉队头元素(deque存的是维护的队列元素的下标),所以判断是否deque[0]<=i-k
然后deque.shift()
移除头部的元素。
具体实现代码如下;
let nums = [1, 3, -1, -3, 2, 3, 6, 7], k = 3 //[3,3,5,5,6,7]
var maxSlidingWindow = function (nums, k) {
const len = nums.length
const res = [] // 遍历到长度大于等于k时,存储每次的队头
const deque = [] // 存的是当前维护队列(长度k)的每个下标
for (let i = 0; i < len; i++) {
// 比较当前元素与队尾的元素,如果新的>队尾,pop移除
while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop()
}
deque.push(i)
// 当队列头部存放的值 和 i 形成的区间大于窗口宽度时
while (deque.length && deque[0] <= i - k) {
deque.shift()
}
// 该取最大值
if (i >= k - 1) {
res.push(nums[deque[0]])
}
}
return res
};
好了,关于栈与队列就聊到这里了,如果觉得文章对你有所帮助的话可以点点赞o(●'◡'●)