函数的独占时间

标签: 数组

难度: Medium

有一个 单线程 CPU 正在运行一个含有 n 道函数的程序。每道函数都有一个位于  0n-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:

输入: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]

示例 5:

输入:n = 1, logs = ["0:start:0","0:end:0"]
输出:[1]

 

提示:

  • 1 <= n <= 100
  • 1 <= logs.length <= 500
  • 0 <= function_id < n
  • 0 <= timestamp <= 109
  • 两个开始事件不会在同一时间戳发生
  • 两个结束事件不会在同一时间戳发生
  • 每道函数都有一个对应 "start" 日志的 "end" 日志

Submission

运行时间: 28 ms

内存: 16.7 MB

class Solution:
    def exclusiveTime(self, n: int, logs: List[str]) -> List[int]:
        stack = []
        res = [0] * n
        for log in logs:
            thread, status, ts = log.split(":")
            if status == "start" or not stack:
                stack.append([thread, int(ts), 0])
            else:
                prev, prevts, b = stack.pop()
                s = int(ts) - prevts + 1
                res[int(thread)] += s - b
                if stack:
                    stack[-1][-1] += s


        return res
                

Explain

这个题解的思路是用一个栈来模拟函数调用过程。每当遇到一个 start 日志时,就将对应的线程号、时间戳和临时时间 0 压入栈中。当遇到 end 日志时,就从栈中弹出之前压入的函数信息,计算该函数的执行时间(当前时间戳 - 之前函数的时间戳 + 1),并将这个时间累加到对应线程的总执行时间中。同时,如果栈不为空,还需要将当前函数的执行时间累加到栈顶函数的临时时间中,因为当前函数的执行时间其实是包含在栈顶函数的执行时间内的。

时间复杂度: O(m),其中 m 为日志的数量。

空间复杂度: O(m+n),其中 m 为日志的数量,n 为函数的数量。

class Solution:
    def exclusiveTime(self, n: int, logs: List[str]) -> List[int]:
        stack = []
        res = [0] * n
        for log in logs:
            thread, status, ts = log.split(":")
            if status == "start" or not stack:
                # 如果是 start 日志或者栈为空,直接将当前函数信息压入栈中
                stack.append([thread, int(ts), 0])
            else:
                # 如果是 end 日志,则从栈中弹出上一个函数的信息
                prev, prevts, b = stack.pop()
                # 计算当前函数的执行时间
                s = int(ts) - prevts + 1
                # 将执行时间累加到对应线程的总执行时间中
                res[int(thread)] += s - b
                if stack:
                    # 如果栈不为空,则将当前函数的执行时间累加到栈顶函数的临时时间中
                    stack[-1][-1] += s
        
        return res

Explore

在函数调用嵌套非常深的情况下,栈的大小将随着调用深度增加而增加。虽然这会增加内存的使用,但在大多数现代计算机系统中,这通常不是问题,因为它们拥有充足的内存资源。然而,在非常极端的情况下,如果调用栈非常深,可能会导致栈溢出,尤其是在有限内存的系统或者是递归深度异常深的情况下。性能瓶颈主要由于每次函数调用和返回时都需要进行栈操作(压栈和出栈),如果栈操作非常频繁,且每次操作都涉及大量计算,那么这会影响整体的执行效率。但在大多数情况下,栈的这种使用是算法效率的一部分,不会成为主要的性能瓶颈。

栈为空并压入新的函数信息,通常发生在处理日志的第一条记录时,因为这是处理的第一个函数调用。这种情况符合函数调用栈的正常行为,即首次调用函数时,调用栈是空的。此外,如果在处理完一系列函数调用后所有函数都已返回,再次遇到一个新的函数调用也会出现栈为空的情况。这并不意味着日志记录存在问题,而是反映了函数调用的实际情况和顺序。只有在日志格式正确且没有丢失任何记录的前提下,这种处理方式才是合理的。