【数据结构】纯干货—系统梳理常考数据结构

2,457 阅读15分钟

前言

本文梳理了常见的几种数据结构,针对人群为数据结构入门和系统复习数据结构的小伙伴们!小编从各数据结构的定义、特点、使用场景和方法实现,以及最后附上Leecode中一道相关练习题,帮助大家来更好地掌握对应知识。如果你有这个需求,快来快来~

特点

它是一个**后进先出(LIFO)**的数据结构。按照字面意思就是先进去的后出去,后进去的先出来。联想一下身边这样的事例其实有很多,比如:

  • 煤炉中,最先放进的蜂窝煤在最底部,最后才被取出,而最后放进去的蜂窝煤是在最上面,也是最先被取出的;

  • 收纳箱中的衣服,最先叠放进去的最后取出来;

    ......

在我们JS中是没有栈这个数据结构的,那么要想在JS中实现栈的功能,就需要借助Array来实现了。接下来看看如何通过Array来运用栈吧!

const stack = [];  //栈空间,通过数组来模拟
//入栈
stack.push(1);
stack.push(2);
//出栈
const p1 = stack.pop();
const p2 = stack.pop();

通过断点调试可以看到它的整个过程,stack变化:[1,2] => [1] => [],p1=2,p2=1,这也验证了后进先出。

实现

学会使用后,我们来简单实现一个栈结构:

class Stack{
    container = [];   //容器
	//进栈
	enter(value){
        this.container.unshift(value);
    }
	//出栈
	leave(){
        return this.container.shift();  
    }
	//获取栈的长度
	size(){
        return this.container.length;
    }
	//获取栈中结果
	value(){
        return this.container.slice(0);  //克隆一份防止外部操作修改内部container
    }
}

练习

上面提到了栈的特点就是后进先出,那么也就是需要后进先出的地方就需要用到栈了!

  • 十进制转二进制;
  • 判断字符串的括号是否有效;
  • 函数调用堆栈;

以上是程序中最常见的使用栈的场景,下面以判断字符串括号是否有效为例来感受一下栈的作用吧!

/*
题目描述:
    给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
    有效字符串需满足:
    	左括号必须用相同类型的右括号闭合。
    	左括号必须以正确的顺序闭合。
    注意空字符串可被认为是有效字符串。
示例:
	"()[]{}"  //true
	"(]"      //false
	"{[]}"    //true
	"([)]"    //false
*/

我们来分析下题目,对于没有闭合的左括号,越靠后的左括号其对应的右括号就越靠前,满足后进先出的条件。那我们一步步来看怎样去做。

解题步骤

  • 新建一个栈;
  • 遍历字符串,遇到左括号就将其入栈,遇到和栈顶括号相匹配的右括号就出栈,类型不匹配直接判断为不合法;
  • 最后栈空为合法,否则不合法;
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    if(s.length % 2 === 1) {return false};   //如果字符串长度为奇数,肯定是不合法的
    const stack = [];  //容器
    for(let i=0;i<s.length;i++){
        const item = s[i];
        if(item === '(' || item === '[' || item === '{'){
            stack.push(item);   //左括号入栈
        }else{
            const top = stack[stack.length-1];   //获取栈顶元素
            if(
                //判断栈顶元素是否与当前符号匹配
                (top === '(' && item === ')')||
                (top === '[' && item === ']')||
                (top === '{' && item === '}')
            ){
                stack.pop();
            }else{
                return false;
            }
        }
    }
    return stack.length === 0;   //栈空合法,非空不合法
};

到此我们就解决了这道题,下面分析它的时间复杂度和空间复杂度:

时间复杂度:O(n)【n就是字符串的长度,因为只有一个for循环,push/pop操作都是O(1)复杂度】;

空间复杂度:O(n)【stack在极端情况下会把s所有的字符都push进去,也就是都是左括号,会占用n个空间】;

队列

特点

一个先进先出的数据结构。很容易我们就可以想到生活中的排队比比皆是:食堂打饭排队、医院挂号排队、银行办理业务排队等等。

在我们JS中也没有队列这个数据结构的,要想在JS中实现队列的功能,也需要借助Array来实现了。接下来看看如何通过Array来运用队列吧!

const queue = [];  //队列,通过数组来模拟
//入队
queue.push(1);
queue.push(2);
//出队
const p1 = queue.shift();
const p2 = queue.shift();

通过断点调试可以看到它的整个过程,队列变化:[1,2] => [2] => [],p1=1,p2=2,这也验证了先进先出。

实现

学会使用后,我们来简单实现一个队列结构:

class Queue {
  container = [];  //容器
  //入队
  enter(element) {
    this.container.push(element);
  }
  //出队
  leave() {
    return this.container.shift();
  }
  //队列长度
  size() {
    return this.container.length;
  }
  //获取队列中的结果
  value() {
    return this.container.slice(0);
  }
}

练习

使用场景

  • JS异步中的任务队列;【JS是单线程,无法同时处理异步中的并发任务】
  • 计算最近请求次数;【新请求入队,最先发起请求的最先出队,队列的长度就是最近请求次数】

以上是程序中最常见的使用队列的场景,下面以计算最近请求次数为例来感受一下队列的作用吧!

/*  题目描述:
 * 	写一个 RecentCounter 类来计算最近的请求。
	它只有一个方法:ping(int t),其中 t 代表以毫秒为单位的某个时间。
	返回从 3000 毫秒前到现在的 ping 数。
	任何处于 [t - 3000, t] 时间范围之内的 ping 都将会被计算在内,包括当前(指 t 时刻)的 ping。
	保证每次对 ping 的调用都使用比之前更大的 t 值。
	
	示例:
	输入:inputs = ["RecentCounter","ping","ping","ping","ping"], 
		 inputs = [[],[1],[100],[3001],[3002]]
	输出:[null,1,2,3,3]
 */

这么看题目,是不是有点懵啊,很迷惑人!那接下来我们分析下题目,第一个inputs代表了发出请求的记录,第二个inputs代表发出请求的时间,所以上面的输入示例可以理解为:

  • 第1个ping,出现时间为1ms
  • 第2个ping,出现时间为100ms
  • 第3个ping,出现时间为3001ms
  • 第4个ping,出现时间为3002ms

那么要计算的是在当前ping出现的时间及其前3000毫秒出现了多少次ping。好难呐,把自己给绕晕了阿O(∩_∩)O

也就是说,如果当前ping出现的时间为t,我们要求得就是(t-3000) ms~t ms之间的ping有多少次。继续分析可以得出:

t = 1ms:时间范围(0,1),此时只有一个请求,[1];

t = 100ms:时间范围(0,100),此时有两个请求,[1] 和 [100];

t = 3001ms:时间范围(1,3001),此时有3个请求, 3001 - 3000 = 1,因为是闭合的,1在计算在内,所以是[1],[100] 和 [3001]这3个请求;

t = 3002ms:时间范围(2,3002),此时有3个请求, 3002 - 3000 = 2,所以t-1ms得请求出队,所以是[100] ,[3001] 和 [3002]这3个请求;

这样理解应该更容易了吧!读懂题目了,来看看怎样去做。题目中首先写了一个RecentCounter类,在实例化这个类后按照输入的数组顺序依次调用ping方法,每次调用ping传入参数t,将其返回值放到输出数组中。

解题步骤

  • 新建一个队列;
  • 有新请求就入队,如果是3000ms之前发出的请求,将其剔除出队;
  • 队列的长度就是最近请求次数,返回队列长度即可;
var RecentCounter = function() {
    this.q = [];    //把队列挂载到this上,这样在ping就可以访问队列q
};

/** 
 * @param {number} t
 * @return {number}
 */
RecentCounter.prototype.ping = function(t) {
	this.q.push(t);   //新请求入队
    while(this.q[0] < t - 3000){   //判断是否在时间范围内
        this.q.shift();   //不在范围的出队
    }
    return this.q.length;
};

/**
 * Your RecentCounter object will be instantiated and called as such:
 * var obj = new RecentCounter()
 * var param_1 = obj.ping(t)
 */

到此我们就解决了这道题,是不是发现很简单!同样地我们来分析它的时间复杂度和空间复杂度:

时间复杂度:O(n) 【n是需要被剔除出队的请求个数】;

空间复杂度:O(n) 【n是队列的长度】;

链表

链表是什么?

链表是由多个元素组成的列表,但是元素的存储不连续,用next指针连在一起的一种数据结构。

既然它也是由多个元素组成的列表,为什么不直接用数组,还设计了这么复杂的链表呢?下面我们看看它们之间的区别。

数组和链表的区别

数组:增删非首尾元素时,需要移动元素。

链表:增删非首尾元素时,不需要移动元素,只需要修改next指针的指向即可。

同样地,在JS中没有链表这个数据结构。我们使用Object来模拟链表结构!敲黑板,这里不是Array是Object奥!!!

实现

const a = { val: 'a' }
const b = { val: 'b' }
const c = { val: 'c' }

//通过next建立关联
a.next = b;
b.next = c;

//遍历链表
let p = a;
while (p) {
  console.log(p.val);     //a   b   c
  p = p.next;
}

//插入d到链表中
const d = { val: 'd' };
b.next = d;
d.next = c;

//从链表中删除d
b.next = c;

练习

/*
 * 请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为 要被删除的节点 。
   现有一个链表 -- head = [4,5,1,9],它可以表示为:4 => 5 => 1 => 9
   
   示例 1:
   输入:head = [4,5,1,9], node = 5
   输出:[4,1,9]
   解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
   
   示例 2:
   输入:head = [4,5,1,9], node = 1
   输出:[4,5,9]
   解释:给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
   
   提示:链表至少包含两个节点。链表中所有节点的值都是唯一的。给定的节点为非末尾节点并且一定是链表中的一个有效节点。不要从你的函数中返回任何结果。
 */

先来分析下题目,在这道题中,我们无法直接获取被删除节点的上个节点,它只是指向下个节点。那怎么删除呢?可以将被删除节点转移到下个节点。拿示例1来说,比如我们想删除节点1,先把9赋值到节点1上,此时为4=>5=>9=>9,再把最后一个节点9删掉就可以达到目的了。

解题步骤

  • 将被删节点的值改为下个节点的值;
  • 删除下个节点;
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} node
 * @return {void} Do not return anything, modify node in-place instead.
 */
var deleteNode = function(node) {
    node.val = node.next.val;
    node.next = node.next.next;
};

时间复杂度:O(1) 【无循环】 空间复杂度:O(1) 【无矩阵、数组】

树是什么?

它是一种分层数据的抽象模型。这里可不是说我们平时看到的大树奥O(∩_∩)O,在前端工作中常见的树包括:DOM树、级联选择、树形控件......JS中没有树,因此我们用Object和Array结合来模拟树,下面就来看一下它的基本结构。

{
    label:'parent',
    value:'p1',
    children:[
        {
            label:'child1',
            value:'c1',
            children:[
                label:'c-child1',
                value:'cc1'
            ]
        },{
            label:'child2',
            value:'c2'
        }
    ]
}

常用操作

深度/广度优先遍历

可以把图中的a当作书的目录根节点,b和c是章节目录,d,e,f,g就是对应的每一小节内容。那么深度优先遍历就是一页一页去看,而广度优先遍历是先看章节目录,都看完有哪些章节之后再看里面的具体内容。图中1-7是遍历顺序。先来模拟下上图中的树结构:

const tree = {
    val:'a',
    children:[
        {
            val:'b',
            children:[
                {
                    val:'d',
                    children:[]
                },
                {
                    val:'e',
                    children:[]
                }
            ]
        },
        {
            val:'c',
            children:[
                {
                    val:'f',
                    children:[]
                },
                {
                    val:'g',
                    children:[]
                }
            ]
        }
    ]
}

下面针对这个树结构分别看两种遍历方法如何实现。

深度优先遍历(DFS):尽可能深的搜索树的分支。算法口诀:访问根节点,对根节点的children挨个进行深度优先遍历。

const dfs = (root) => {
    console.log(root.val);
    root.children.forEach(dfs);  //根节点的children递归执行深度优先遍历
}
dfs(tree);   //a b d e c f g

广度优先遍历(BFS):先访问离根节点最近的节点。

算法口诀:

  • 新建一个队列,把根节点入队;
  • 把队头出队并访问;
  • 把队头的children挨个入队;
  • 重复2,3步,直到队列为空;
const bfs = (root) => {
    const q = [root];
    while(q.length>0){
        const header = q.shift();
        console.log(header.val);
        header.children.forEach(child=>{
            q.push(child);
        })
    }
}
bfs(tree);   //a b c d e f g

二叉树的先中后序遍历

什么是二叉树呢?树中每个节点最多只能有两个子节点。在JS中,我们通常用Object来模拟二叉树,下面看下数据结构:

const binaryTree = {
    val:1,
    left:{
        val:2,
        left:{
            val:4,
            left:null,
            right:null
        },
        right:{
            val:5,
            left:null,
            right:null
        }
    },
    right:{
        val:3,
        left:{
            val:6,
            left:null,
            right:null
        },
        right:{
            val:7,
            left:null,
            right:null
        }
    }
}

了解完二叉树之后,根据上面定义的这个树结构,我们来看看它的三种遍历方式以及具体是实现的。

先序遍历

  • 访问根节点
  • 对根节点的左子树进行先序遍历;
  • 对根节点的右子树进行先序遍历;
const prevOrder = (root) => {
  if (!root) return;
  console.log(root.val);  //根节点
  prevOrder(root.left);   //左子树
  prevOrder(root.right);  //右子树
}
prevOrder(binaryTree);    //1 2 4 5 3 6 7

中序遍历

  • 对根节点的左子树进行中序遍历;
  • 访问根节点
  • 对根节点的右子树进行中序遍历;
const inOrder = (root) => {
  if (!root) return;
  inOrder(root.left);   //左子树
  console.log(root.val);    //根节点
  inOrder(root.right);  //右子树

}
inOrder(binaryTree);     //4 2 5 1 6 3 7

后序遍历

  • 对根节点的左子树进行后序遍历;
  • 对根节点的右子树进行后序遍历;
  • 访问根节点
const postOrder = (root) => {
  if (!root) return;
  postOrder(root.left);   //左子树
  postOrder(root.right);  //右子树
  console.log(root.val);  //根节点
}
postOrder(binaryTree);   //4 5 2 6 7 3 1

练习

/*
 * 题目描述:
   给定一个二叉树,返回它的中序 遍历。
   输入: [1,null,2,3]
       1
        \
         2
        /
       3
   输出: [1,3,2]
 */

有了之前的中序介绍,这里我们就不多加赘述,直接上代码:

//递归版
var inorderTraversal = function(root) {
    const res = [];
    const rec = (n) => {
        if(!n) return;   //节点不存在直接返回
        rec(n.left);     //左子树
        res.push(n.val);  //访问根节点
        rec(n.right);    //右子树
    }
    rec(root)
    return res;
};

递归版本的实现比较简单,下面使用迭代算法实现一下!

var inorderTraversal = function(root) {
    const res = [];
    const stack = [];   //栈空间
    let p = root;   //指针
    while(stack.length || p){
        while(p){
            stack.push(p);
            p = p.left;   //指向左节点
        }
        const top = stack.pop();  
        res.push(top.val);
        p = top.right;     //访问右节点
    }
    return res;
};

继续分析一下迭代版的时间复杂度和空间复杂度:

时间复杂度:O(n) 【中序遍历到所有节点,n为二叉树所有节点数量】

空间复杂度:O(n)

堆是什么?

它是一种特殊的完全二叉树。那什么又是完全二叉树呢?每层节点都完全填满,最后一层如果不是满的,只是缺少右边的若干,满足这个特点的就是完全二叉树。堆与普通的完全二叉树的区别在于所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。在JS中通常用数组来表示堆,之前树都是用Object来模拟的,这里怎么又换成数组了,是不是有点乱了。别慌,稳住,接着往下看。

可以发现:左侧子节点的位置是2 * index + 1,右侧子节点的位置是2 * index + 2,父节点位置是(index - 1) / 2;

那么堆有哪些应用呢?它还是很有用地,能高效快速地找出最大值和最小值,时间复杂度为O(1);还是拿上图的堆来说,如果要找出它的最小值,直接返回堆顶元素即可。除此之外,堆还可以找出第K个最大(小)元素,下面练习题会对它进行具体描述。

实现

class MinHeap{
    constructor(){
        this.heap = [];
    }
    swap(i1,i2){
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp;
    }
    getParentIndex(i){
        return ( i - 1) >> 1;
    }
    getLeftIndex(i){
        return i * 2 + 1;
    }
    getRightIndex(i){
        return i * 2 + 2;
    }
    shiftUp(index){
        if(index == 0) return;
        const parentIndex = this.getParentIndex(index);
        if(this.heap[parentIndex] > this.heap[index]){
            this.swap(parentIndex,index);
            this.shiftUp(parentIndex);
        }
    }
    shiftDown(index){
        const leftIndex = this.getLeftIndex(index);
        const rightIndex = this.getRightIndex(index);
        if(this.heap[leftIndex] < this.heap[index]){
            this.swap(leftIndex,index);
            this.shiftDown(leftIndex);
        }
        if(this.heap[rightIndex] < this.heap[index]){
            this.swap(rightIndex,index);
            this.shiftDown(rightIndex);
        }
    }
    insert(value){
        this.heap.push(value);
        this.shiftUp(this.heap.length-1);
    }
    pop(){
        this.heap[0] = this.heap.pop();
        this.shiftDown(0);
    }
    peek(){
        return this.heap[0];
    }
    size(){
        return this.heap.length;
    }
}

练习

/*
 * 题目描述:
 	在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不  同的元素。

    示例 1:
    输入: [3,2,1,5,6,4] 和 k = 2
    输出: 5
    
    示例 2:
    输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
    输出: 4
    
	说明:你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
 */

分析一下这道题怎样去做,看到 " 第K个最大元素 ",之前提到过考虑使用最小堆实现。

解题步骤

  • 构建一个最小堆,并将元素依次插入堆中;
  • 当堆的容量超过K,就删除堆顶;
  • 插入结束后,堆顶就是第K个最大元素;
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    const h = new MinHeap();   //构建最小堆实例
    nums.forEach(n=>{
        h.insert(n);        //将元素插入堆中
        if(h.size() > k){   //堆的大小超过k
            h.pop();        //删除堆顶元素
        }
    })
    return h.peek();   //返回堆顶元素
};

时间复杂度:O(n*logK)【循环套循环,第一层循环为n,insert和pop嵌套在循环中,时间复杂度为logK】

空间复杂度:O(k) 【堆数据结构存放元素,堆的大小就是空间复杂度,也就是参数k】

堆学的有点吃力了嗷~(⊙﹏⊙) ,还需要再多琢磨琢磨!

图是什么?

它是网络结构的抽象模型,是一组由连接的节点。图可以用来表示任何二元关系,比如道路、航班......JS中没有图这个数据结构,可以通过Object和Array来构建一个图结构。图的表示法有邻接矩阵、邻接表、关联矩阵等等。

集合

集合是什么?

它是一种无序并且唯一的数据结构。在ES6中有集合,名为Set,下面看看它的具体使用方法。

//去重
const arr = [1, 2, 3, 3];
const newArr = [...new Set(arr)];
console.log(newArr);  //[1,2,3]

//判断元素是否在集合中
const s1 = new Set(arr);
const has1 = s1.has(1);
const has2 = s1.has(8);
console.log(has1, has2);     //true false

//求交集
const s2 = new Set([2, 4, 5]);
const s3 = new Set([...s1].filter(item => s2.has(item)));
console.log(s3);  //Set { 2 }

以上是Set的基本的使用场景,下面来看看Set的常用操作还有哪些。

ES6中的Set

add

let mySet = new Set();

mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add('hello');
let obj = { name: 'tt' };
mySet.add(obj);
mySet.add([1, 3]);
mySet.add({ name: 'tt' });  
/**
 注意:集合的唯一性,5没有被添加进去,而obj和{name:'tt'}都在集合中,是因为两个对象看起来一样,但实际在内存中存储位置不一样,本质上来说属于不同的对象。
*/

console.log(mySet);
//Set { 1, 5, 'hello', { name: 'tt' }, [ 1, 3 ], { name: 'tt' } }

has

let mySet = new Set();
mySet.add(1);
mySet.add(5);
const has1 = mySet.has(3);
const has2 = mySet.has(5);
console.log(has1, has2);  //false true

delete

let mySet = new Set();
mySet.add(1);
mySet.add(5);
console.log(mySet);     //Set {1,5}
mySet.delete(5);
console.log(mySet);     //Set {1}

迭代

for...of

for(let item of mySet){
    console.log(item);   
}

keys() / values() / entries()

for(let item of mySet.keys()){
    console.log(item);   
}

for(let item of mySet.values()){
    console.log(item);   
}

for(let [key,value] of mySet.entries()){
    console.log(key,value);
}
/*
    1 1
    hello hello
    { name: 'tt' } { name: 'tt' }
    [ 1, 3 ] [ 1, 3 ]
    { name: 'tt' } { name: 'tt' }
*/

可以看出,keys()和values()的内容是一样的。

集合与数组的相互转换

//集合转数组
const myArr = [...mySet];
const myArr = Array.from(mySet);

//数组转集合
const mySet2 = new Set([1,2,3,4]);

求交集

const intersection = new Set([...mySet1].filter(item => mySet2.has(item)));

求差集

求差集,和交集很类似。交集是判断集合2中是否有集合1的元素,现在只需要判断集合2中不包含集合1中的元素即可。

Emm,应该没有被绕晕吧!绕晕的话那就来贴图:

const difference = new Set([...mySet1]).filter(item => !mySet.has(item));

练习

/* 题目描述:
 * 给定两个数组,编写一个函数来计算它们的交集。
    示例 1:
    输入:nums1 = [1,2,2,1], nums2 = [2,2]
    输出:[2]

    示例 2:
    输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
    输出:[9,4]
    
    说明:
    输出结果中的每个元素一定是唯一的。
    我们可以不考虑输出结果的顺序。
 */

分析下题目:指出元素唯一,不考虑顺序,求交集,自然而然就联想到了集合。

解题步骤

  • 用集合对nums1去重;
  • 遍历nums1,筛选出nums2也包含的值;
/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
  let s1 = new Set(nums1);   //通过Set来去重nums1
  return [...s1].filter(item => nums2.includes(item));  //filter过滤出nums2数组中包含nums1元素地内容
};

同样地我们来分析它的时间复杂度和空间复杂度:

时间复杂度:O(m*n) 【m指filter循环消耗时间,n指includes消耗时间】;

空间复杂度:O(m) 【m是去重后nums1的长度】;

字典

字典是什么?

它与集合类似,也是一种存储唯一值的数据结构,但它是以键值对的形式来存储的。键值对是用来做映射关系的,ES6中有字典,名为Map。下面看它的具体操作:

const m = new Map();

//增
m.set('a','a1');   
m.set('b','b1');   
m.set('c','123');   
//删
m.delete('b');
//m.clear();    //删除全部元素
//改
m.set('a','hello');   

console.log(m);   //Map { 'a' => 'hello', 'c' => '123' }

//查
m.get('c');     //123

到此就是它基本的增删改查操作了。

练习

还记得在栈这一小节的练习题吗?学完本小节后,我们来对之前这道题做一下优化。

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    if(s.length % 2 === 1) return false;
    
    const stack = [];
    const map = new Map();
    map.set('(',')');
    map.set('[',']');
    map.set('{','}');
    
    for(let i=0;i<s.length;i++){
        let item = s[i];
        if(map.get(item)){
            stack.push(item);
        }else{
            let top = stack[stack.length-1];
            if(map.get(top) === item){
                stack.pop();
            }else{
                return false;
            }
        }
    }
    return stack.length === 0;
};

时间复杂度:O(n)【n就是字符串的长度,因为只有一个for循环,push/pop操作都是O(1)复杂度】;

空间复杂度:O(n)【虽然多了map,但是map是常量级O(1)复杂度,所以不会改变空间复杂度】;

以上就是全部内容了,小编在自己复习过程中梳理总结出这篇文章,希望可以帮助到大家!如有错误,希望各位前辈指证!