Facebook Pixel

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)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 functions
  • ans: An array of size n initialized with zeros to store the exclusive time of each function
  • pre: A variable to track the previous timestamp

Algorithm Steps:

  1. Initialize the data structures:

    stk = []
    ans = [0] * n
    pre = 0
  2. 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)
  3. 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)
  4. 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
  • Process "1:end:5":
    • Function 1 ran for 5 - 2 + 1 = 4 units, ans[1] = 4
    • Stack becomes [0], pre = 6
  • Process "0:end:6":
    • Function 0 ran for 6 - 6 + 1 = 1 more unit, ans[0] = 2 + 1 = 3
    • Stack becomes empty

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 Evaluator

Example 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
  • 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, &currentTime);
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, requiring O(n) space where n 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 to O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More