数据结构与算法-栈二

176 阅读6分钟

文章简要概述

  • 本文主要进行栈相关的算法题刷题题解记录,记录栈相关算法以及如何解。
  • 本文一共有5道题,主要介绍leetcode中二叉树的后序遍历验证二叉树的前序序列化基本计算器 II函数的独占时间表现良好的最长时间段的解题思路。

与栈相关算法

二叉树的后序遍历

二叉树的后序遍历--leetcode

题目大意:

给定一个二叉树,返回它的 *后序* 遍历。
后序遍历指的是根节点的位置, 左右根,这样的顺序显示。

示例

输入: [1,null,2,3]

输出: [3,2,1]

解题思路:

  • 根据题意后序遍历树,这里采用递归的方式
  • 先将树的根放入栈中,再将树的右子树放入到栈中,树的左子树放入栈中。
  • 重复以上操作,知道根节点为空。
  • 这里用数组模拟操作,最后返回数组的值即可。

代码:

// 遍历节点
function postorder (root, list) {
  if (!root) return null;
  postorder(root.left, list);
  postorder(root.right, list);
  list.push(root.val);
  return root;
}
function postorderTraversal (root) {
   const list = [];
   postorder(root, list);
   return list;
};

验证二叉树的前序序列化

验证二叉树的前序序列化--leetcode

题目大意:

序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。

     _9_
    /   \
   3     2
  / \   / \
 4   1  #  6
/ \ / \   / \
# # # #   # #

例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#",其中 # 代表一个空节点。

给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。

每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 '#' 。

你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 "1,,3" 

示例:

输入: "9,3,4,#,#,1,#,#,2,#,6,#,#" 输出: true

解题思路:

  • 如输入: "9,3,4,#,#,1,#,#,2,#,6,#,#" ,当遇到 x,#,# 的时候,就把它变为 #。

  • 模拟一遍过程:

    [9,3,4,#,#] => [9,3,#],继续

    [9,3,#,1,#,#] => [9,3,#,#] => [9,#] ,继续

    [9,#2,#,6,#,#] => [9,#,2,#,#] => [9,#,#] => [#],结束

1615551708-uxodPT-331.gif

  • 上面的思路是解题的一个理解,然后我们思考一个问题,如果一个节点有数值,那么对应的他有两个子节点,同时会占据父节点对应的一个子节点位置。如果这个节点是空节点,那么他会占据父节点对应的一个子节点位置。

代码:

function isValidSerialization (preorder) {
   const preorderList = preorder.split(',');
   const l = preorderList.length;
   const stack = [1];
   let i = 0;
   while(i < l) {
       if (!stack.length) return false;
       if (preorderList[i] === '#') {
           stack[stack.length - 1]--;
           if (stack[stack.length - 1] === 0) {
               stack.pop();
           }
           i++;
       } else {
           stack[stack.length - 1]--;
           if (stack[stack.length - 1] === 0) {
               stack.pop();
           }
           stack.push(2);
           i++;
       }
   }
   return !stack.length
};

基本计算器 II

基本计算器 II--leetcode

题目大意:

给你一个字符串表达式 `s` ,请你实现一个基本计算器来计算并返回它的值。

整数除法仅保留整数部分。

示例:

输入: s = "3+2*2"

输出: 7

解题思路:

  • 遍历字符串,判断字符是不是运算操作符
  • 如果是+将数值放入到栈中
  • 如果是-将数值的对应的负数放入到栈中
  • 如果是*将栈顶中的元素弹出与当前数值乘,放入栈中
  • 如果是/将栈顶中的元素弹出除以当前数值,放入栈中
  • 将栈中的元素求和

1615444023-jpFiWE-无标题1.png

代码:

function calculate (s) {
   s = s.trim();
   let stack = [];
   let num = 0;
   let preSign = '+';
   for(let i in s) {
       if (!isNaN(s[i]) && s[i] !== ' ') {
         num = num * 10 + Number(s[i]);
       }
       if (isNaN(s[i])|| i == s.length - 1) {
           switch(preSign) {
               case '+':
                stack.push(num);
                break;
               case '-':
                stack.push(-num);
                break;
               case '*':
                stack.push(stack.pop() * num);
                break;
               default:
                stack.push(stack.pop() / num | 0);
                break;
           }
           num = 0;
           preSign = s[i];
       }
   }
   return stack.reduce((a,b) => a+b);
};

函数的独占时间

函数的独占时间--leetcode

题目大意:

有一个 单线程 CPU 正在运行一个含有 n 道函数的程序。每道函数都有一个位于  0 和 n-1 之间的唯一标识符。

函数调用 存储在一个 调用栈 上 :当一个函数调用开始时,它的标识符将会推入栈中。而当一个函数调用结束时,它的标识符将会从栈中弹出。标识符位于栈顶的函数是 当前正在执行的函数 。每当一个函数开始或者结束时,将会记录一条日志,包括函数标识符、是开始还是结束、以及相应的时间戳。

给你一个由日志组成的列表 logs ,其中 logs[i] 表示第 i 条日志消息,该消息是一个按 "{function_id}:{"start" | "end"}:{timestamp}" 进行格式化的字符串。例如,"0:start:3" 意味着标识符为 0 的函数调用在时间戳 3 的 起始开始执行 ;而 "1:end:2" 意味着标识符为 1 的函数调用在时间戳 2 的 末尾结束执行。注意,函数可以 调用多次,可能存在递归调用 。

函数的 独占时间 定义是在这个函数在程序所有函数调用中执行时间的总和,调用其他函数花费的时间不算该函数的独占时间。例如,如果一个函数被调用两次,一次调用执行 2 单位时间,另一次调用执行 1 单位时间,那么该函数的 独占时间 为 2 + 1 = 3 。

以数组形式返回每个函数的 独占时间 ,其中第 i 个下标对应的值表示标识符 i 的函数的独占时间。

示例:

diag1b.png

输入:n = 2, logs = ["0:start:0","1:start:2","1:end:5","0:end:6"]

输出:[3,4]

解释:

函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,于时间戳 1 的末尾结束执行。

函数 1 在时间戳 2 的起始开始执行,执行 4 个单位时间,于时间戳 5 的末尾结束执行。

函数 0 在时间戳 6 的开始恢复执行,执行 1 个单位时间。

所以函数 0 总共执行 2 + 1 = 3 个单位时间,函数 1 总共执行 4 个单位时间。

解题思路:

  • 可以这么理解题目,在一个单线程中执行函数,当执行过程中遇到子函数,就开始执行子函数,子函数完成后在执行当前函数。
  • 我们需要一个计算当前函数的执行的时间,递归得到每一个子函数的执行时间dure
  • 当前函数的执行时间是endTime - startTime + 1 - dure
  • 遍历每一项任务即可。

代码:

function exclusiveTime (n, logs) {
   let res = new Array(n).fill(0);
   let i = 0;
    function getNext() {
        // 得到执行时间
        let dure = 0;
        const starts = logs[i].split(':');
        while(i < logs.length - 1 && logs[++i].indexOf('s') !== -1) {
            dure = dure + getNext();
        }
        const ends = logs[i].split(':');
        const totalDure = Number(ends[2]) - Number(starts[2]) + 1;
        const sum = totalDure - dure;
        res[Number(starts[0])] += sum;
        return totalDure;
    }
    while(i < logs.length) {
       getNext();
       i++;
   }
   return res;
};

表现良好的最长时间段

表现良好的最长时间段--leetcode

题目大意:

给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。

我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。

所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。

请你返回「表现良好时间段」的最大长度。

示例:

输入: hours = [9,9,6,0,6,6,9]

输出: 3

解释: 最长的表现良好时间段是 [9,9,6]。

解题思路:

  • 首先根据得到数组,按照大于8的计数+1,否则计数-1,等到处理后数据,列出前缀和。
  • 得到单调栈,这里应该是取前缀和的比较小的值,因为最后是获取区间值,减去负数 会相应得到较大的值
  • 与单调栈中的元素比较,取出区间跨度最大的值

5baaaa25c9b0158663cd3757f59e28c516ed6f867a3acc5a73abb509cc8a422f-1124-1.gif

代码:

function longestWPI (hours) {
   let preSum = new Array(hours.length + 1).fill(0);
   for(let i = 0; i < hours.length; i++) {
       preSum[i + 1] = preSum[i] + (hours[i] > 8 ? 1 : -1);
   }
   let stack = [0];
   for(let i = 1; i < preSum.length; i++) {
       if (preSum[i] < preSum[stack[stack.length - 1]]) stack.push(i);
   }
   let max = 0;
   for(let i = preSum.length - 1; i > max; i--) {
     while (stack.length && preSum[i] > preSum[stack[stack.length - 1]]) {
       max = Math.max(max, i - stack.pop());
     }
   }
   return max;
};

结束语

数据结构与算法相关的练习题会持续输出,一起来学习,持续关注。当前是栈部分。后期还会有其他类型的数据结构,题目来源于leetcode。

往期文章:

数据结构与算法-栈一

数据结构与算法-队列一

数据结构与算法-队列二

数据结构与算法-链表一

数据结构与算法-链表二

数据结构与算法-链表三

有兴趣的可以一起来刷题,感谢点赞👍 , 关注!