hello,友友们,这里是哆啦美玲为你讲解我今天学到的编程思想------栈与队列的运用。
栈和队列都是一种数据结构,在JavaScript中,其实没有任何一种符号或者方法能够直接创建栈或者队列。但是,在许多的编程思想中又需要用到它们,所以我们是如何在js中创建并使用她们呢?
栈和队列的定义
1. 栈
栈实际上是一种线性表,它只允许在固定的一端进行插入或者删除元素,在进行数据插入或者删除的一端称之为栈顶,剩下的一端称之为栈底。其遵循的原则是数据实现先进后出。 其实你可以把它看作是一个没有盖子的杯子,每次放和杯子一样大的东西进去,要取最先放进去的出来,只能从杯子口开始一个一个取出来。
在JavaScript中,我们知道数组可以在其任意位置添加或者删除元素,常见的一些方法有:
JavaScript代码
let arr = ['a', 'b', 'c', 'd', 'e']
arr.push('f') // 尾部增加元素f -> ['a', 'b', 'c', 'd', 'e', 'f']
arr.pop() // 尾部移除f -> ['a', 'b', 'c', 'd', 'e']
arr.unshift('hello') // 头部增加元素'hello' -> ['hello', 'a', 'b', 'c', 'd', 'e']
arr.shift() // 头部移除元素hello -> ['a', 'b', 'c', 'd', 'e']
arr.splice(1,0,'hello') // 往下标1后面增加了一个元素;参数1是下标,参数0是移除的元素个数,最后是增加的元素 -> [ 'a', 'b', 'hello', 'c', 'd', 'e']
arr.splice(1,1) // 从下标1处开始,移除一个元素 -> [ 'a', 'b', 'c', 'd', 'e']
console.log(arr) // [ 'a', 'b', 'c', 'd', 'e']
根据栈的定义,我们严格让数组的数据一边进一边出,那我们就可以称这个弱化的数组是一个栈。所以这个数组在增加或者删除数据时,只通过push() + pop() 或者 unshift() + shift() 来实现,就创建了一个栈。举个例子:
JavaScript代码
const stack = []
// 入栈
stack.push('可爱多')
stack.push('巧乐兹')
stack.push('小布丁')
stack.push('老布丁')
stack.push('老冰棍')
// 出栈 后进先出
while(stack.length){
const top = stack.pop()
console.log('现在吃:'+ top)
}
上面代码我们实现了一个简单的入栈出栈过程:
stack使用push()方法,只从尾部添加每个元素,这个过程也叫入栈
,最后一个元素"老冰棍"在栈顶;
随后在while循环下,stack使用pop()方法,只从尾部删除元素,这个过程叫出栈
。
从打印结果可以看出,先进入数组的"可爱多"是最后一个被数组移除的;而最后进入数组的"老冰棍"是第一个被移除的,遵循栈先入后出的规则。
2. 队列
队列也是一种特殊的线性表,它允许在一端进行插入数据,在另一端进行删除数据。其遵循的原则是先进先出。队列你可以看作是一根竖着的水管,从上边放水进去,只能从下边流出来。
所以按照队列的原则,我们可以创建一个数组,让其只能使用 push() + shift() 或者 unshift() + pop() 来增加或删除数据,这个弱化的数组就可以称之为队列。
举个例子:
JavaScript代码
const queue = []; // 队列,也是一个弱化的数组,头尾可以知道,中间不知道
// 一边添加 另一边移除
// queue.push() // 尾部添加
// queue.shift() // 头部取出
queue.push('辣椒炒肉')
queue.push('辣椒炒辣椒')
queue.push('黄豆鸡脚')
while(queue.length){
const top = queue.shift()
console.log(`我爱吃:${top}`)
}
在上面代码中,我们实现了数组queue只从尾部添加数据,然后循环使用shift方法从头部删除元素的过程,这个数组我们就叫它是队列。
栈与队列的运用
1. leetcode 第20题------有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
JavaScript代码
let s = '()[]{}'
let a = '(([])'
let b = '([]))'
// 栈解决 匹配可以使用键值对"(":")"
var isValid = function(s){
const stack = []
// 自己创建的匹配规则,引入键值对,左类括号为key,对应value为对应的右类括号
const lTop = {
'(':')',
'[':']',
'{':'}'
}
// 遍历字符串
for(let i = 0; i < s.length; i++){
// 左类括号(、[、{ 进栈
if(s[i] === "(" || s[i] === "[" || s[i] === "{"){
stack.push(s[i])
}else{
// 当有右类括号,但是栈为空,stack.pop()会显示undefined,lTop[stack.pop()]与其不相等。例如:")({}"
// 当有右类括号,但是栈内不是对应左类括号,lTop[stack.pop()]与其不相等。例如:"(]"
if(lTop[stack.pop()] !== s[i]){
return false
}
}
}
// 当循环结束,栈内还有左类括号,则一定不是有效字符串。例如:"(([])"
if(stack.length){
return false
}
return true
};
console.log(isValid(s));
console.log(isValid(a));
console.log(isValid(b));
我们会发现,其实是栈可以处理对称问题。
大对称:例如"( [ { } ] )"遍历过程入栈的数据占左边一半,另一半对应的数据不在栈内,但是出栈的顺序刚好与后一半数据遍历时候相对应;
小对称:例如"( ) [ ] { }"遍历过程左边先入栈,对称的数据与其出栈时对应。
2. leetcode 第225题------两个队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
- void push(int x)将元素 x 压入栈顶。
- int pop()移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
在上述问题中,我先思考了队列的原则和栈的原则,并在草稿上画图示意:
-
push的过程
-
pop和top的实现
其实,3移除后,会发现它和第一张图的push后很像,如果直接在这时候push的话,那再次进入的值会在队列一,那此时的栈顶元素不是最新添加的值,如果再次pop的话,就不遵守栈的原则。
所有,在pop的过程我们需要进行判断我们让队列身份互换一下。
完整代码如下:
JavaScript代码
// 有两个队列实现栈,先进后出
var MyStack = function() {
this.queue1 = []
this.queue2 = []
};
// 入栈
MyStack.prototype.push = function(x) {
// 队列一尾部依次添加全部值,队列二为空
this.queue1.push(x)
};
// 移除栈顶元素
MyStack.prototype.pop = function() {
// 判断队列二为空,循环遍历队列一的元素并从头部移除,依次从队列二尾部添加,只保留队列一的尾部一个值,即栈顶元素
if( this.queue2.length == 0 ){
while(this.queue1.length > 1){
this.queue2.push(this.queue1.shift())
}
}
// 得到要移除的栈顶元素
const top = this.queue1.shift()
// 队列一为空,让队列一和队列二互换,总是保持队列二为空
const queue = this.queue1
this.queue1 = this.queue2
this.queue2 = queue
return top
};
// 读取栈顶,
MyStack.prototype.top = function() {
return this.queue1[this.queue1.length - 1]
};
// 栈是否为空
MyStack.prototype.empty = function() {
return !(this.queue1.length + this.queue2.length)
};
3. leetcode 第232题------两个栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
- void push(int x)将元素 x 推到队列的末尾
- int pop()从队列的开头移除并返回元素
- int peek() 返回队列开头的元素
- boolean empty() 如果队列为空,返回 true ;否则,返回 false
在上述问题中,我还是在草稿上画图示意:
- push方法的呈现直接存入栈一
- pop的实现原理,如图先全部栈一的元素依次使用pop从尾部移除,与此同时让栈二从尾部添加。栈二再从尾部移除的元素就是队列中最先添加的元素。
但是有个问题,如果在移除1后,我们再次push添加一个元素4进入栈一,想要再次移除栈顶元素,就要按照pop方法让4添加在栈二尾部。我们会发现,栈4变成了栈顶元素,但是它不是最先进入队列的元素,移除的话就不符合队列的原则。
所以,在栈二不是空的情况下,我们不能直接让栈一的值从尾部移除。
代码如下:
JavaScript代码
// 打造一个构造函数,new一下得到的实例对象是一个队列(用两个栈实现)
var MyQueue = function() {
this.stack1 = []
this.stack2 = []
};
// push方法将元素添加到尾部
MyQueue.prototype.push = function(x) {
this.stack1.push(x)
};
// pop方法将头部元素移除并返回
MyQueue.prototype.pop = function() {
// 栈2不为空,不可以直接放进去
if(this.stack2.length <= 0){
while(this.stack1.length){
this.stack2.push(this.stack1.pop())
}
}
return this.stack2.pop()
};
// peek方法返回队列开头的元素(不取出来直接读取)
MyQueue.prototype.peek = function() {
if(this.stack2.length <= 0){
while(this.stack1.length){
this.stack2.push(this.stack1.pop())
}
}
return this.stack2[this.stack2.length-1]
};
// 判断队列是否为空
MyQueue.prototype.empty = function() {
// !取反,有值返回的是false,不是空
return !(this.stack1.length + this.stack2.length)
};
4. leetcode 第239题------双端队列应用
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值。
JavaScript代码
// 滑动窗口的最大值
// 输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
// 输出: [3,3,5,5,6,7]
// 方法一:左指针指向最左边,右指针指向最右边,找两个指针里面的最大值
// 方法二:双端队列
let nums = [1,3,-1,-3,2,5,3,6,7], k = 3
var maxSlidingWindow = function(nums, k) {
const len = nums.length
const res = []
const deque = [] // 存数组的下标
for(let i = 0; i < len; i++){
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]])
}
}
};
看代码有点懵逼,不急。 首先,我们要知道一个概念,双端队列:两端都能进出。
我还是画图给你看:
- 高光区域相当于是窗口在滑动,每次移动一个值;
- 图中红颜色写的数组中,是我用元素代替了存入的下标,更好理解这个代码;
- 红色数组相当于一个双端队列,可以同时使用push、pop、unshift和shift方法;
- 队列deque为空,直接push添加第一个元素 deque = [1]
- 队列长度不等于0,每个添加的元素与队列尾部元素对比,新的 > 尾部,pop移除尾部,再push添加在尾部deque = [3,1],依次类推;
- 在遍历元素大于等于3时,res开始添加窗口的最大值,即队列的头部读取下标,res数组push添加 res = [3];
- 例如元素2,在上述步骤中队列会变成 deque = [3,2],最大值就是3,但是这样窗口就好像是四个值的宽度了,因为3的下标不在窗口区间,所以要移除头部,所以 deque = [2],res = [3,3,2]......
我想,聊到这儿,你应该会在算法中使用栈与队列去解决问题了吧!
可以送上你的一键三连吗?
啊哈哈哈哈~我们下期再见!