2020年12月

栈是什么?

  • 一个后进先出的数据结构
  • JavaScript中没有栈,但可以用Array实现栈的所有功能

    栈示意图.jpeg

栈的应用场景

  • 需要后进先出的场景
  • 比如: 十进制转二进制,判断字符串的括号是否有效、函数的调用栈

场景一、 十进制转二进制

  • 后出来的余数要先出来
  • 把余数依次入栈,然后再出栈,就可以实现余数倒序输出

    十进制转二进制.jpeg

场景二、 有效的括号

  • 越靠后的左括号,对应的右括号越靠前
  • 左括号入栈,右括号出栈,最后栈空了就是合法的。

场景三、 函数调用堆栈

  • 最后调用的函数,最先执行完成
  • JS解释器使用栈来控制函数的调用顺序

LeetCode #20题

20 有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。

解题思路:

  1. 对于没有闭合的左括号而言,越靠后的左括号,对应的右括号越靠前。
  2. 满足后进先出,考虑用栈的思路

步骤:

  1. 新建一个栈
  2. 扫描字符串。遇到左括号入栈,遇到和栈顶括号类型匹配的右括号出栈,类型不匹配直接判定为不合法
  3. 最后栈控了合法,不空就不合法

时间复杂度 O(n)

空间复杂度 O(n)

/**
 * @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+=1){
        const c = s[i];
        if(c === '(' || c === '{' || c === '['){
            stack.push(c);
        }else{
            const top = stack[stack.length-1];
            if(
                (top === '(' && c === ')') ||
                (top === '{' && c === '}') ||
                (top === '[' && c === ']')
            ){
                stack.pop();
            }else{
                return false;
            }
        }
    }
    return stack.length === 0;
};

总结

  • 栈是一个后进先出的数据结构
  • JavaScript中没有栈,但是可以用array实现栈的所有功能
  • 栈的常用操作: push、pop、stack[length-1]

用ES6的class,封装一个stack类,包括push、pop、peek方法

```JavaScript
// 使用es6的class来封装一个stack类
class farkStack{ // 定义一个名字为Stack的类
  constructor(){
    this.dataStore = []
  }
  push(item){
    this.dataStore.push(item);
  }
  pop(){
    return this.dataStore.pop();
  }
  peek(){ // 探出
    return this.dataStore[this.dataStore.length - 1];
  }
  size(){
    return this.dataStore.length;
  }
  clear(){
    this.dataStroe = []
  }
}
```

用栈这个数据结构,将100这个十进制数字转为2进制

```JavaScript
function  switchBinary(decNum){
  let stack = new farkStack();
  while(decNum > 0){
    stack.push(decNum % 2);
    decNum = Math.floor(decNum/2)
  }
  let binaryStr = ''
  while(stack.size() !== 0) {
    binaryStr += stack.pop()
  }
  return binaryStr;
}

switchBinary(125)
```

时间复杂度是什么?

  • 一个函数,用大O表示,比如O(1)、O(n)、 O(logN)...
  • 定性描述该算法的运算时间
  • 需要关注的 O(1) < O(log₂n)< O(n)< O(n²)

    时间复杂度阶梯.jpeg

主要的时间复杂度有:

  • 常数阶 O(1):无论代码执行了多行,只要没有循环等复杂结构,那这个代码的复杂度就是 O(1)

  • 对数阶 O(logN):在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2(n) 也就是说当循环 log2(n) 次以后,这个代码就结束了。因此这个代码的时间复杂度趋势为:O(logn)

  • 线性阶 O(n): 在 单层 循环体内,代码会随着每次循环执行,它消耗的时间是随着n的变化而变化的,因此这是一个线性相关趋势,时间复杂度为 O(n)。

  • 线性对数阶 O(nlogN):线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。

    for (let i = 0; i < n; i++) {
    let m = 1;
    while(m < n) {
        i = i * 2;
    }
    }
  • 平方阶 O(n^2): 将 O(n) 的代码再循环嵌套一遍,它的时间复杂度就是 O(n^2)。

    for (let i = 0; i < n; i++) {
    for (let j = 0; i < n; i++) {
        console.log(i, j)
    }
    }

    如果将其中一层循环的n改成m,即:

    for (let i = 0; i < m; i++) {
    for (let j = 0; i < n; i++) {
        console.log(i, j)
    }
    }

    那么它的时间复杂度就变成了 O(m*n)

  • 立方阶 O(n^3): O(n³)相当于三层n循环,其它的类似

  • K次方阶 O(n^k)

  • 指数阶 O(2^n)

空间复杂度是什么?

  • 一个函数,用大O表示,比如O(1)、O(n)、O(n²)
  • 算法在运行过程中临时占用存储空间大小的量度

常见空间复杂度:

  • 空间复杂度 O(1),如果算法执行所需要的临时空间不随着某个变量 n 的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1);

  • 空间复杂度 O(n)

    const list = []
    for(let i = 0; i < n; i +=1 ){
      list.push(i);
    }
  • 空间复杂度 O(n^2)
    const matrix = []
    for (let i = 0; i < n; i++) {
        matrix.push([]);
        for (let j = 0; i < n; j++) {
          matrix[i].push(j);
        }
    }

    开始定义了一个数组,而后使用 for 循环再次定义了一个数组,相当于创建了一个二维数组(矩阵),这个数组中的每一项都是一个空间度量为 n 的数组,因此它的空间复杂度为 O(n^2)

阶段思考题:

  1. 如果一段代码中有3个循环,它们的循环次数都是n,那么这段代码的时间复杂度是O(3n)还是O(n)?
    • 如果是 3 个并列循环的代码,那么总的时间复杂度趋势为 O(n)
    • 如果是 3 个循环嵌套,那么总的时间复杂度趋势为 O(n^3)
  2. 假设每天睡觉前,你都会数2的次方,1、2、4、8...,每次你都数到n才能睡着,那么你数了几个数?时间复杂度是多少?
    • 数了:2^x = n ==> x = log2(n) ; x 从 0 算起 所以数了 log2(n) + 1 个数
    • 时间复杂度的趋势为 O(logN)