[路飞]_算法_栈_函数的独占时间

258 阅读5分钟

题目描述

有一个 单线程 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 的函数的独占时间。

示例 1:

image.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 个单位时间。 

示例 2:

输入:n = 1, logs = ["0:start:0","0:start:2","0:end:5","0:start:6","0:end:6","0:end:7"]
输出:[8]
解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,并递归调用它自身。
函数 0(递归调用)在时间戳 2 的起始开始执行,执行 4 个单位时间。
函数 0(初始调用)恢复执行,并立刻再次调用它自身。
函数 0(第二次递归调用)在时间戳 6 的起始开始执行,执行 1 个单位时间。
函数 0(初始调用)在时间戳 7 的起始恢复执行,执行 1 个单位时间。
所以函数 0 总共执行 2 + 4 + 1 + 1 = 8 个单位时间。

示例 3:

输入:n = 2, logs = ["0:start:0","0:start:2","0:end:5","1:start:6","1:end:6","0:end:7"]
输出:[7,1]
解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,并递归调用它自身。
函数 0(递归调用)在时间戳 2 的起始开始执行,执行 4 个单位时间。
函数 0(初始调用)恢复执行,并立刻调用函数 1 。
函数 1在时间戳 6 的起始开始执行,执行 1 个单位时间,于时间戳 6 的末尾结束执行。
函数 0(初始调用)在时间戳 7 的起始恢复执行,执行 1 个单位时间,于时间戳 7 的末尾结束执行。
所以函数 0 总共执行 2 + 4 + 1 = 7 个单位时间,函数 1 总共执行 1 个单位时间。 

示例 4:

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

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/ex… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路

  • 注意,是单线程cpu,一道程序开始执行时,如果有程序正在执行,会结束掉前一个程序,后面自动开始执行
  • 根据题目,如果在前面函数还没结束时新的函数就开始调用时,这个函数一定遵从后开始的先结束的原则。
  • 综上,与 “栈” 后进先出的原理相似,所以我们用栈的原理来解此题,详细步骤看代码注释

代码

**
 * @param {number} n
 * @param {string[]} logs
 * @return {number[]}
 */
var exclusiveTime = function(n, logs) {
    let result=new Array(n).fill(0);//创建一个有n个元素的数组,并初始化值为0来储存最终计算的独占时间结果,结果下标与 logs里的函数id对应
    let stack=[];//用来储存进行中函数的栈
    for(let i=0;i<logs.length;i++){
        let el=logs[i].split(':');
        let now={
            id:Number(el[0]),//函数id
            type:el[1],//类型start|end
            time:Number(el[2]),//结算的时间戳,当节点类型为start,遇到与这个节点对应end节点时,用时间相减就是这次函数持续的独占时间
        };
        if(now.type==='start'){//如果是开始节点,无脑丢到栈的前面(后进要先出,从前面遍历)
          stack.unshift(now);
        }else if(now.type==='end'){//如果是结束节点,需要从栈里去寻找对应的开始节点,并为其结算独占时间
          //如何找到对应的开始节点?根据思路中提到后进先出的原则,我们只需要从栈的末尾开始遍历,第一个同id的开始节点就是要找的结算开始节点
           let reTime=null //记录是否找到
           stack.forEach((node,index)=>{
               if(node.id===now.id&&!reTime){//id相同且第一次找到的,就是我们要找的结算节点
                   reTime=now.time-node.time+1;
                   result[now.id]+=reTime;//结算累加,同类型的时间
               }else if(reTime){
                   //除了给自己结算以外,因为结算外层函数执行时间时候,需要减去内层执行函数的独占时间,所以计算出结算出时间之后,把这个时间累加到所有外层节点的time上,当外层节点结算时就能一起减去这部分时间
                   node.time+=reTime;
               }
           })
           if(reTime){
               stack.shift()//记住结算后,一定要删除这个开始节点,从栈前面删
            }
        }
    }
    return result;
};