JS版数据结构第一篇(栈)

2,195 阅读8分钟

前端入行门槛低,人员参差不齐

前端就是写页面的

前端的人都不懂数据结构和算法


背景

相信大家在社区经常会听到类似以上的话

由于前端上手比较快,而且平时开发时大部分写的都是业务逻辑以及交互,常常导致我们被一些后端人员'鄙视',这无疑是对我们前端开发人员是不公平的,前端技术更新迭代很快,而且知识点琐碎,想要学好前端是需要一定的持续性学习能力以及创造性和好奇心的,学好前端并不是一件很容易的事情。

我们都知道 程序设计=算法+数据结构

无论是前端还是后端,作为开发人员对这些基础知识的掌握程度决定了你以后的技术道路发展的上限,算法和数据结构对程序员的重要程度不言而喻。

抛开一些偏见,我们不能否认的是:

  1. 有很多非科班出身的前端同学对数据结构的理解并不是很深,甚至一些数据结构类型的定义都不清楚。
  2. 网上利用JS实现的数据结构的资料有限。

针对这种实际情况,我将分六篇博客介绍最常见的几种数据结构,并结合LeetCode上面的经典原题利用js方法进行实际题目的求解,包括:

  • 队列
  • 链表
  • 矩阵
  • 二叉树

如果你也是一名想夯实一下数据结构基础的前端开发人员,在网上又找不到合适的资源的话,那么小弟的博客一定会对你有所帮助。

栈(stack)

栈的定义

作为数据结构中最简单的一种想必大家对栈都有所了解,我们先来看一下百度百科上'栈'的定义


其实说白了就是我们平时讲的'先进后出',只能从一端(栈顶)添加或删除数据。定义还是很抽象,我还是用一个例子比喻一下我理解中的‘栈’。

栈的理解


我这里用抖音上最近比较火的彩虹酒来举例:

如果你去酒吧点了这杯彩虹酒,酒吧小哥哥会先拿来一个干净的空杯子(此时是空栈的状态)

杯口称为栈顶,杯底称为栈底,给你制作这杯酒时他会

  1. 先倒入红色的'烈焰红唇',
  2. 然后是黄色的‘杏仁’鸡尾酒,
  3. 最后倒入'蓝色夏威夷',
这就是一个入栈的过程,

而当你拿到这杯酒时,

  1. 将先会品尝到蓝色妖姬带来的清爽,
  2. 然后是杏仁的清香,
  3. 最后陶醉在红色海洋里,
这也就是一个出栈的过程。

注意:调酒师给你制作酒时他只会从杯子口将酒倒入杯中,你也不会在杯底打一个洞然后将酒倒入你的嘴里。

只能从杯口(栈顶)进只能从杯口(栈顶)出,这也就是上面定义中指的'受限'。

js实现一个简单的栈

其实对于js来讲实现栈再简单不过了,我们只需要定义一个数组,结合Array.prototype.push方法以及Array.prototype.pop方法就实现了。

以下六行代码对应图下的六个过程

var arr = [2,7,1]
arr.push(8) 
arr.push(2)
arr.pop()
arr.pop()
arr.pop()



怎么样,是不是很简单?

接下来我们用LeetCode上的例题看一下利用栈的数据结构可以解决怎样的问题。

例题一:棒球比赛

我们先来看一下题目  原题地址

你现在是棒球比赛记录员。
给定一个字符串列表,每个字符串可以是以下四种类型之一:
1.整数(一轮的得分):直接表示您在本轮中获得的积分数。
2. "+"(一轮的得分):表示本轮获得的得分是前两轮有效 回合得分的总和。
3. "D"(一轮的得分):表示本轮获得的得分是前一轮有效 回合得分的两倍。
4. "C"(一个操作,这不是一个回合的分数):表示您获得的最后一个有效 回合的分数是无效的,应该被移除。

每一轮的操作都是永久性的,可能会对前一轮和后一轮产生影响。
你需要返回你在所有回合中得分的总和。

示例:

输入: ["5","2","C","D","+"]
输出: 30
解释: 
第1轮:你可以得到5分。总和是:5。
第2轮:你可以得到2分。总和是:7。
操作1:第2轮的数据无效。总和是:5。
第3轮:你可以得到10分(第2轮的数据已被删除)。总数是:15。
第4轮:你可以得到5 + 10 = 15分。总数是:30。

分析一下这道题目:

先传入一个数组,然后根据数组中每一项字符串的不同类型计算出真实得分,最后将结果求和。

我们可以根据‘栈’的数据结构分以下几个步骤进行求解:

  1. 先新建一个空数组A(即空栈) 保存真实的得分情况
  2. 然后遍历传入的数组B并根据它每一项的不同类型计算出每一轮的得分情况
  • 如果该项是前三种类型(‘正数’,'+',‘D’) 则按要求计算该轮得分并插入到空数组A中(即入栈)
  • 如果该项是第四种类型('C'),则将数组A最后一项抛出(即出栈)
   3. 对数组A进行遍历求和

代码如下:

export default (arr) => {  
    // 新建空栈,保存处理后的结果  
        let result = []  
    // 上一轮的数据  
        let pre1  
    // 上上轮的数据  
        let pre2  
    // 对传进来的数组进行遍历,遍历的目的是处理得分  
    arr.forEach(item => {    
        switch (item) {      
            case 'C':        
                if (result.length) {          
                    result.pop()        
                }        
                break      
            case 'D':        
                pre1 = result.pop()        
                result.push(pre1, pre1 * 2)       
                break      
            case '+':       
                pre1 = result.pop()       
                pre2 = result.pop()      
                result.push(pre2, pre1, pre2 + pre1)      
                break     
            default:
                // *1是为了将item转为number类型       
                result.push(item * 1)    
        }  
    })
     //利用reduce进行求和 
     return result.reduce((prev, cur) =>  prev + cur )}

例题二:最大矩形

原题地址

给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

示例:

输入:
[
  ["1","0","1","0","0"],
  ["1","0","1","1","1"],
  ["1","1","1","1","1"],
  ["1","0","0","1","0"]
]
输出: 6

实现思路:

将二维数组arr1的每一项2个及以上连续1的索引范围转换成新的二维数组arr2,如

[  
    ['1', '1', '0', '1', '1'],
    ['1', '1', '1', '1', '1'],
    ['1', '1', '0', '1', '1']
]

可以转换为:

[ 
    [[0,1],[3,4]],
    [[0,4]],
    [[0,1],[3,4]]
]

这样的话我们先写一个函数,功能是查找二维数组里面每相邻两项的交集,接受两个参数,分别是需要求交集的数组和已经遍历的行数。

函数内部这样实现的:

函数先将接收的数组的最后两项推出(出栈两次),然后将两项遍历取交集,

  • 如果有交集则将取到的交集入栈,再次递归,传入此时的数组和n
  • 没有交集则将数组删除最后一项(出栈),然后将新的数组传入函数
每次取到交集时要根据交集数组的长度和已遍历的行数计算此时矩形面积,最后将最大的矩形面积返回。

更多的注释写在代码里,我把源码发出来供大家参考

export default arr => {
  // 用来保存处理过后的二维数组  
  let changeArr = []
  // 将传入的二维数组根据每一项的连续1的索引位置遍历  
  arr.forEach((item, i) => {
    //每一项的索引    
    let itemArr = []    
    let str = item.join('')    
    let reg = /1{2,}/g
    // 连续1的匹配结果    
    let execRes = reg.exec(str)
    //如果不为null就继续匹配    
    while (execRes) {
      // 将第i项匹配到的第j组1的起止位置推入      
      itemArr.push([execRes.index, execRes.index + execRes[0].length - 1])      
      execRes = reg.exec(str)    
    }
    // 将第i项的匹配结果推入    
    changeArr.push(itemArr)  
  })
  // 用来保存面积  
  let area = []  
  // 取交集函数  
  let find = (arr, n = 1) => {    
      // 浅拷贝arr数组 防止由于引用导致每次执行函数被改变    
      let copyArr = [].concat(arr)    
      // 数组最后一项    
      let last = copyArr.pop()    
      // 数组倒数第二项    
      let next = copyArr.pop()
      // 最后一项和倒数第二项的每一项   
      let lastItem, nextItem
      // 取到的交集数组    
      let recuArr = []
      // 作为每次取到交集的临时变量    
      let item = []
      // 已遍历的行数    
      n++
      // 将每相邻两项的每个区间分别取交集    
      for (let i = 0; i < last.length; i++) {      
          lastItem = last[i]      
          for (let j = 0; j < next.length; j++) {        
            nextItem = next[j]        
            // 开始取交集       
            // 若有交集        
            if (!(Math.max(...lastItem) <= Math.min(...nextItem) ||                   
                    Math.max(...nextItem) <= Math.min(...lastItem))) {           
                item = [Math.max(lastItem[0], nextItem[0]), Math.min(lastItem[1], nextItem[1])]          
                recuArr.push(item)
            // 求此时交集的面积并推入area           
            area.push((item[1] - item[0] + 1) * n)        
            }      
         }    
      }    
      // 若遍历完了所有情况都没有交集,则返回false
      if (recuArr.length === 0) {
         return false
      } else {
         // 有交集,继续递归遍历
         if (copyArr.length > 0) {
           copyArr.push(recuArr)
           find(copyArr, n)
         }
      }
    }
    //将数组一直递减,每一行都作为第一行找一次交集
    while (changeArr.length > 1) {
        find(changeArr)
        changeArr.pop()
    }
    //最后将保存面积的矩形数组的最大值返回
    return area.length === 0 ? 0 : Math.max(...area)
  }

参考

  1. 百度百科
  2. 今日头条视频架构前端负责人银国徽老师的《js数据结构与算法》
  3. leetCode

总结

作为一种比较简单的数据结构,栈在js中很常见,也比较容易理解,我们可以利用js原生的数组API就可以实现。

下一篇我将介绍队列的使用方法。