636. Exclusive Time of Functions
Problem Description
You have a single-threaded CPU that executes a program with n
functions, where each function has a unique ID from 0
to n-1
.
The execution follows a call stack pattern:
- When a function starts, its ID is pushed onto the stack
- When a function ends, its ID is popped from the stack
- The function at the top of the stack is the one currently executing
You're given a list of logs where each log entry is formatted as: "{function_id}:{"start" | "end"}:{timestamp}"
For example:
"0:start:3"
means function 0 started at the beginning of timestamp 3"1:end:2"
means function 1 ended at the end of timestamp 2
Important details:
- Functions can be called multiple times, including recursively
- "Start" logs occur at the beginning of a timestamp
- "End" logs occur at the end of a timestamp
The exclusive time of a function is the total time it spends executing, excluding the time when other functions are executing within it. If a function is called multiple times, its exclusive time is the sum of all its execution times.
Your task is to return an array where the value at index i
represents the exclusive time of the function with ID i
.
For instance, if function 0 starts at timestamp 0, then function 1 starts at timestamp 2, ends at timestamp 5, and function 0 ends at timestamp 6:
- Function 1's exclusive time is
5 - 2 + 1 = 4
units - Function 0's exclusive time is
(2 - 0) + (6 - 5 - 1 + 1) = 2 + 1 = 3
units (it runs from 0-1, then pauses while function 1 runs, then resumes from 6-6)
Intuition
The key insight is that we need to track which function is currently executing and accumulate time for it whenever another function interrupts or when it completes.
Think of it like a stopwatch for each function. When a function starts, we start its stopwatch. But when another function begins (nested call), we need to pause the current function's stopwatch and start the new one. When the nested function ends, we stop its stopwatch and resume the previous one.
Since functions follow a Last-In-First-Out pattern (the most recently started function must end before its caller can continue), a stack is the natural data structure to track the currently executing functions.
The tricky part is handling the timestamps correctly. Notice that:
- A "start" event happens at the beginning of a timestamp
- An "end" event happens at the end of a timestamp
This means if a function starts at timestamp 3 and ends at timestamp 5, it actually runs during timestamps 3, 4, and 5 - that's 3 time units, not 2. This is why we add 1
when calculating the time for an "end" event.
We maintain a pre
variable to track the last processed timestamp. This helps us calculate the time elapsed since the last event:
- When we encounter a new event, we can calculate how much time the currently executing function (top of stack) has been running since
pre
- We add this time to that function's exclusive time
- Then we update
pre
to the current timestamp (or current + 1 for "end" events)
By processing events sequentially and always updating the exclusive time of the function at the top of the stack before handling the current event, we ensure each function gets credited for exactly the time it was actively executing.
Learn more about Stack patterns.
Solution Approach
We implement the solution using a stack to track the currently executing functions and simulate the execution process.
Data Structures:
stk
: A stack to store the IDs of currently executing functionsans
: An array of sizen
initialized with zeros to store the exclusive time of each functionpre
: A variable to track the previous timestamp
Algorithm Steps:
-
Initialize the data structures:
stk = [] ans = [0] * n pre = 0
-
Process each log entry: For each log, we parse it to extract:
i
: function ID (converted to integer)op
: operation type ("start" or "end")cur
: current timestamp (converted to integer)
-
Handle "start" operations: When
op[0] == "s"
:- If the stack is not empty, there's a function currently executing that needs to be paused
- Add
cur - pre
to the exclusive time of the function at the top of the stack (this is the time it ran since the last event) - Push the new function ID
i
onto the stack - Update
pre = cur
(the new function starts at the beginning of this timestamp)
-
Handle "end" operations: When
op[0] == "e"
:- The function at the top of the stack is completing
- Add
cur - pre + 1
to its exclusive time (the+1
accounts for the fact that "end" happens at the end of the timestamp) - Pop the function from the stack
- Update
pre = cur + 1
(the next timestamp available for the previous function to resume)
Example Walkthrough:
Consider logs: ["0:start:0", "1:start:2", "1:end:5", "0:end:6"]
- Process
"0:start:0"
: Stack becomes[0]
,pre = 0
- Process
"1:start:2"
:- Function 0 ran for
2 - 0 = 2
units,ans[0] = 2
- Stack becomes
[0, 1]
,pre = 2
- Function 0 ran for
- Process
"1:end:5"
:- Function 1 ran for
5 - 2 + 1 = 4
units,ans[1] = 4
- Stack becomes
[0]
,pre = 6
- Function 1 ran for
- Process
"0:end:6"
:- Function 0 ran for
6 - 6 + 1 = 1
more unit,ans[0] = 2 + 1 = 3
- Stack becomes empty
- Function 0 ran for
Final result: [3, 4]
The time complexity is O(m)
where m
is the number of logs, and the space complexity is O(n)
for the answer array and O(n)
for the stack in the worst case.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example with 2 functions and the following logs:
["0:start:0", "1:start:2", "1:end:5", "0:end:6"]
Initial State:
- Stack:
[]
(empty) - Answer array:
[0, 0]
(for functions 0 and 1) - Previous timestamp:
0
Step 1: Process "0:start:0"
- Parse: function ID = 0, operation = "start", timestamp = 0
- Stack is empty, so no function to pause
- Push function 0 onto stack:
[0]
- Update previous timestamp:
pre = 0
Step 2: Process "1:start:2"
- Parse: function ID = 1, operation = "start", timestamp = 2
- Stack has function 0 on top, which has been running
- Calculate time for function 0:
2 - 0 = 2
units - Add to function 0's exclusive time:
ans[0] = 0 + 2 = 2
- Push function 1 onto stack:
[0, 1]
- Update previous timestamp:
pre = 2
Step 3: Process "1:end:5"
- Parse: function ID = 1, operation = "end", timestamp = 5
- Function 1 is ending (top of stack)
- Calculate time for function 1:
5 - 2 + 1 = 4
units- The
+1
accounts for the end happening at the end of timestamp 5
- The
- Add to function 1's exclusive time:
ans[1] = 0 + 4 = 4
- Pop function 1 from stack:
[0]
- Update previous timestamp:
pre = 5 + 1 = 6
Step 4: Process "0:end:6"
- Parse: function ID = 0, operation = "end", timestamp = 6
- Function 0 is ending (top of stack)
- Calculate time for function 0:
6 - 6 + 1 = 1
unit - Add to function 0's exclusive time:
ans[0] = 2 + 1 = 3
- Pop function 0 from stack:
[]
(empty) - Update previous timestamp:
pre = 6 + 1 = 7
Final Result: [3, 4]
- Function 0 ran exclusively for 3 units (timestamps 0-1, then 6)
- Function 1 ran exclusively for 4 units (timestamps 2-5)
Solution Implementation
1class Solution:
2 def exclusiveTime(self, n: int, logs: List[str]) -> List[int]:
3 """
4 Calculate exclusive time of each function based on start/end logs.
5
6 Args:
7 n: Number of functions
8 logs: List of log entries in format "function_id:start/end:timestamp"
9
10 Returns:
11 List of exclusive execution times for each function
12 """
13 # Stack to track currently running functions
14 function_stack = []
15
16 # Initialize result array with exclusive times for each function
17 exclusive_times = [0] * n
18
19 # Track the previous timestamp for calculating time intervals
20 previous_timestamp = 0
21
22 # Process each log entry
23 for log_entry in logs:
24 # Parse log entry: function_id:operation:timestamp
25 function_id, operation, timestamp = log_entry.split(":")
26 function_id = int(function_id)
27 current_timestamp = int(timestamp)
28
29 # Check if this is a start operation
30 if operation == "start":
31 # If there's a function currently running, update its exclusive time
32 if function_stack:
33 top_function = function_stack[-1]
34 exclusive_times[top_function] += current_timestamp - previous_timestamp
35
36 # Push new function onto stack
37 function_stack.append(function_id)
38 previous_timestamp = current_timestamp
39
40 else: # operation == "end"
41 # Pop the ending function and update its exclusive time
42 # Add 1 because end timestamp is inclusive
43 ending_function = function_stack.pop()
44 exclusive_times[ending_function] += current_timestamp - previous_timestamp + 1
45
46 # Move to next timestamp after the end
47 previous_timestamp = current_timestamp + 1
48
49 return exclusive_times
50
1class Solution {
2 public int[] exclusiveTime(int n, List<String> logs) {
3 // Array to store the exclusive execution time for each function
4 int[] result = new int[n];
5
6 // Stack to keep track of currently executing functions
7 Deque<Integer> functionStack = new ArrayDeque<>();
8
9 // Previous timestamp to calculate time intervals
10 int previousTime = 0;
11
12 // Process each log entry
13 for (String logEntry : logs) {
14 // Parse the log entry: "functionId:start/end:timestamp"
15 String[] logParts = logEntry.split(":");
16 int functionId = Integer.parseInt(logParts[0]);
17 String action = logParts[1];
18 int currentTime = Integer.parseInt(logParts[2]);
19
20 if (action.charAt(0) == 's') { // "start" action
21 // If there's a function currently running, update its time
22 if (!functionStack.isEmpty()) {
23 int runningFunctionId = functionStack.peek();
24 result[runningFunctionId] += currentTime - previousTime;
25 }
26
27 // Push the new function onto the stack
28 functionStack.push(functionId);
29 previousTime = currentTime;
30
31 } else { // "end" action
32 // Pop the ending function and update its exclusive time
33 // Add 1 because end timestamp is inclusive
34 int endingFunctionId = functionStack.pop();
35 result[endingFunctionId] += currentTime - previousTime + 1;
36
37 // Update previous time to the next unit after the end
38 previousTime = currentTime + 1;
39 }
40 }
41
42 return result;
43 }
44}
45
1class Solution {
2public:
3 vector<int> exclusiveTime(int n, vector<string>& logs) {
4 // Initialize result array to store exclusive execution time for each function
5 vector<int> result(n, 0);
6
7 // Stack to keep track of currently executing functions
8 stack<int> functionStack;
9
10 // Previous timestamp to calculate time intervals
11 int previousTime = 0;
12
13 // Process each log entry
14 for (const auto& log : logs) {
15 // Parse log entry: "functionId:start/end:timestamp"
16 int functionId;
17 int currentTime;
18 char operation[10];
19 sscanf(log.c_str(), "%d:%[^:]:%d", &functionId, operation, ¤tTime);
20
21 // Check if it's a start operation
22 if (operation[0] == 's') { // "start"
23 // If there's a function currently running, update its execution time
24 if (!functionStack.empty()) {
25 int topFunction = functionStack.top();
26 result[topFunction] += currentTime - previousTime;
27 }
28
29 // Push new function onto stack
30 functionStack.push(functionId);
31 previousTime = currentTime;
32 }
33 else { // "end"
34 // Update execution time for the function that's ending
35 // Add 1 because end time is inclusive
36 int topFunction = functionStack.top();
37 result[topFunction] += currentTime - previousTime + 1;
38
39 // Remove the ended function from stack
40 functionStack.pop();
41
42 // Update previous time to the next timestamp after end
43 previousTime = currentTime + 1;
44 }
45 }
46
47 return result;
48 }
49};
50
1/**
2 * Calculate the exclusive execution time for each function based on logs
3 * @param n - Number of functions
4 * @param logs - Array of log strings in format "function_id:start_or_end:timestamp"
5 * @returns Array where index i contains the exclusive execution time of function i
6 */
7function exclusiveTime(n: number, logs: string[]): number[] {
8 // Initialize result array to store exclusive time for each function
9 const result: number[] = Array(n).fill(0);
10
11 // Track the previous timestamp for calculating time intervals
12 let previousTimestamp: number = 0;
13
14 // Stack to track currently executing functions (stores function IDs)
15 const functionStack: number[] = [];
16
17 // Process each log entry
18 for (const logEntry of logs) {
19 // Parse log entry: [functionId, operation, timestamp]
20 const logParts: string[] = logEntry.split(':');
21 const functionId: string = logParts[0];
22 const operation: string = logParts[1];
23 const currentTimestamp: string = logParts[2];
24
25 // Check if this is a start operation
26 if (operation[0] === 's') {
27 // If stack is not empty, add elapsed time to the function at top of stack
28 if (functionStack.length > 0) {
29 const topFunctionId: number = functionStack[functionStack.length - 1];
30 result[topFunctionId] += Number(currentTimestamp) - previousTimestamp;
31 }
32
33 // Push current function to stack
34 functionStack.push(Number(functionId));
35
36 // Update previous timestamp
37 previousTimestamp = Number(currentTimestamp);
38 } else {
39 // End operation: pop function from stack and add its execution time
40 const poppedFunctionId: number = functionStack.pop()!;
41 result[poppedFunctionId] += Number(currentTimestamp) - previousTimestamp + 1;
42
43 // Update previous timestamp to next unit after end
44 previousTimestamp = Number(currentTimestamp) + 1;
45 }
46 }
47
48 return result;
49}
50
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the logs array. Each log entry is processed exactly once in a single pass through the array. The operations performed for each log entry (splitting the string, stack operations like append and pop, and arithmetic operations) all take constant time O(1)
. Therefore, the overall time complexity is O(n)
.
The space complexity is O(n)
. This consists of two main components:
- The
ans
array which stores the exclusive time for each function, requiringO(n)
space wheren
is the number of functions - The
stk
(stack) which in the worst case could contain all function IDs if they are all nested, requiring up toO(n)
space
Note that while the problem parameter n
represents the number of functions and the reference answer uses n
to denote the length of the logs array, the space complexity remains O(n)
in both interpretations since the stack size is bounded by the number of functions, and the answer array size equals the number of functions.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding Timestamp Boundaries
The most common pitfall is incorrectly handling the difference between "start" and "end" timestamps:
- "start" occurs at the beginning of a timestamp
- "end" occurs at the end of a timestamp
Incorrect approach:
# Wrong: Treating both start and end the same way if operation == "end": exclusive_times[function_stack.pop()] += current_timestamp - previous_timestamp previous_timestamp = current_timestamp # Wrong!
Why it's wrong: If function A ends at timestamp 5 and function B resumes, function B can start executing from timestamp 6, not timestamp 5. The end timestamp is inclusive.
Correct approach:
if operation == "end": exclusive_times[function_stack.pop()] += current_timestamp - previous_timestamp + 1 previous_timestamp = current_timestamp + 1 # Next available timestamp
2. Forgetting to Update the Currently Running Function
When a new function starts, developers often forget to update the exclusive time of the function that was previously running.
Incorrect approach:
if operation == "start": # Wrong: Directly pushing without updating the current function's time function_stack.append(function_id) previous_timestamp = current_timestamp
Correct approach:
if operation == "start": # Update the exclusive time of the currently running function first if function_stack: exclusive_times[function_stack[-1]] += current_timestamp - previous_timestamp function_stack.append(function_id) previous_timestamp = current_timestamp
3. Using Wrong Data Type for Parsing
Forgetting to convert string values to integers can lead to incorrect calculations or comparison errors.
Incorrect approach:
function_id, operation, timestamp = log_entry.split(":") # Using timestamp as string in arithmetic operations exclusive_times[function_id] += timestamp - previous_timestamp # Type error!
Correct approach:
function_id, operation, timestamp = log_entry.split(":")
function_id = int(function_id)
current_timestamp = int(timestamp)
4. Off-by-One Error in End Timestamp Calculation
A subtle but critical error is forgetting the +1
when calculating exclusive time for an ending function.
Example to illustrate: If a function starts at timestamp 2 and ends at timestamp 5:
- Wrong calculation:
5 - 2 = 3
units - Correct calculation:
5 - 2 + 1 = 4
units (it runs during timestamps 2, 3, 4, and 5)
The function executes during the entire duration of timestamp 5, so we need to include that full unit of time.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!