养成编程思维——栈与队列的运用

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 ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。
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]......

我想,聊到这儿,你应该会在算法中使用栈与队列去解决问题了吧!

可以送上你的一键三连吗?

啊哈哈哈哈~我们下期再见!

相关推荐
天宇&嘘月2 小时前
web第三次作业
前端·javascript·css
小王不会写code2 小时前
axios
前端·javascript·axios
尼尔森系3 小时前
排序与算法:希尔排序
c语言·算法·排序算法
发呆的薇薇°3 小时前
vue3 配置@根路径
前端·vue.js
luckyext4 小时前
HBuilderX中,VUE生成随机数字,vue调用随机数函数
前端·javascript·vue.js·微信小程序·小程序
小小码农(找工作版)4 小时前
JavaScript 前端面试 4(作用域链、this)
前端·javascript·面试
AC使者4 小时前
A. C05.L08.贪心算法入门
算法·贪心算法
冠位观测者4 小时前
【Leetcode 每日一题】624. 数组列表中的最大距离
数据结构·算法·leetcode
前端没钱4 小时前
前端需要学习 Docker 吗?
前端·学习·docker
前端郭德纲4 小时前
前端自动化部署的极简方案
运维·前端·自动化