题目
难度中等
有一个 单线程 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:
输入: n = 2, logs = ["0:start:0","1:start:2","1:end:5","0:end:6"]
输出: [3,4]
示例 2:
输入: n = 1, logs = ["0:start:0","0:start:2","0:end:5","0:start:6","0:end:6","0:end:7"]
输出: [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]
示例 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]
思路
函数是在函数栈上调用的,我们需要做的就是模拟这个栈的操作,通过模拟函数调用栈,将日志里的信息还原,以此计算出每个函数的独立运行时间
我们可以这么考虑:
-
每个 start 记录都表示有一个新函数开始执行,我们将 start 记录相关的信息入栈。函数调用栈顶的函数就是正在运行的函数。在新的 start 记录入栈时,我们要检查一下是否有正在运行的函数,如果有,需要将其暂停,统计一下他已经运行的时间,累加到结果集中。
-
每个 end 记录都表示有一个函数执行结束了,由于栈的特性,在遇到一个 end 记录时,一定会有与其对应的 start 记录在栈顶,将栈顶的 start 记录弹出表示这个函数执行结束。此时会有两种情况,第一是栈顶的这个函数在执行中没有被打断过,此时用这个函数的开始时间和结束时间就可以直接算出它的独立运行时间了。第二种情况就是这个函数在执行过程中被打断过,我们需要知道函数被打断之后重新开始执行的时间戳是多少,用这个时间戳和结束时间计算打断后的运行时间,函数被打断前运行了多长时间在被打断时已经进行过了计算。
为了知道当前函数有没有被打断,我们需要记录一下上一个记录的内容,如果上一个记录为 end,说明函数在运行过程中被打断了,再利用这个 end 记录的时间戳就能计算出打断结束后函数的独立运行时间。
需要留意的一点是:end 的时间戳是一个时间片的末尾,而 start 的时间戳是一个时间片的开始,用这两个时间戳直接相减来算时间间隔是错误的。为了计算方便可以把所有时间戳的表示都统一为一个时间片的开始。
这个思路就是稍微没这么脉络清晰,可能是当时一边想一边写的缘故吧,这个思路在实现中的代码是 方法1
题解里的思路就清晰很多,在实现中的代码是 方法2,下面也进行简单的分析
优化主要体现在遇到 end 记录时的处理逻辑。当 遇到 end 记录,栈顶函数结束运行并返回,弹出栈顶记录的同时更新即将开始运行的函数的开始时间,这样就能避免了关于栈顶函数运行过程中是否被打断的相关讨论,因为我们其实只关注打断后的函数运行了多长时间,打断前相关的时间信息是没有用的。
实现
方法1 :
func exclusiveTime(n int, logs []string) []int {
type funcLog struct {FuncID, Timestamp int}
stack := make([]funcLog, 0) // go 原生没有栈,但是用切片模拟很容易
ret := make([]int, n)
state := "" // 记录上一个操作是什么
lastTimestamp := 0 // 记录上一个操作的时间戳
for _, log := range logs {
data := strings.Split(log, ":")
fID, _ := strconv.Atoi(data[0])
timestamp, _ := strconv.Atoi(data[2])
op := data[1]
if op == "start" {
if len(stack) != 0 {
fid := stack[len(stack)-1].FuncID
ret[fid] += (timestamp - lastTimestamp)
}
stack = append(stack, funcLog{
FuncID: fID,
Timestamp: timestamp,
})
} else if data[1] == "end" {
if state == "start" { // 连续执行了一个完整的任务
ret[fID] += (timestamp - stack[len(stack) - 1].Timestamp + 1)
stack = stack[:len(stack)-1]
} else if state == "end" { // 切换回来执行并结束任务
ret[fID] += (timestamp - lastTimestamp + 1)
stack = stack[:len(stack)-1]
}
}
lastTimestamp = timestamp
state = op
if op == "end" {
lastTimestamp += 1 // 把 timestamp 统一为用时间片的起始位置表示
}
}
return ret
}
方法2:
func exclusiveTime(n int, logs []string) []int {
type funcLog struct {FuncID, Timestamp int}
stack := make([]funcLog, 0) // go 原生没有栈,但是用切片模拟很容易
ret := make([]int, n)
for _, log := range logs {
data := strings.Split(log, ":")
fID, _ := strconv.Atoi(data[0])
timestamp, _ := strconv.Atoi(data[2])
op := data[1]
if op == "start" {
// 如果有正在运行的函数就计算运行时间,将其暂停,
if len(stack) > 0 {
f := stack[len(stack)-1]
ret[f.FuncID] += (timestamp - f.Timestamp)
}
// 执行新函数
stack = append(stack, funcLog{
FuncID: fID,
Timestamp: timestamp,
})
} else if data[1] == "end" {
// 结束正在运行的函数
ret[fID] += (timestamp - stack[len(stack) - 1].Timestamp + 1)
stack = stack[:len(stack)-1]
// 如果有暂停的函数就更新开始时间戳,让其重新开始运行
if len(stack) > 0 {
stack[len(stack)-1].Timestamp = timestamp + 1
}
}
}
return ret
}