582. Kill Process 🔒
Problem Description
You have n
processes that form a rooted tree structure. Each process has a unique ID and exactly one parent process (except for the root process).
You're given two arrays:
pid[i]
: the ID of the i-th processppid[i]
: the ID of the i-th process's parent
The root process has ppid[i] = 0
, meaning it has no parent.
When a process is killed, all of its descendant processes (children, grandchildren, etc.) are also killed automatically.
Given an integer kill
representing the ID of a process you want to kill, return a list containing all process IDs that will be killed as a result. The order of IDs in the output doesn't matter.
How the solution works:
The solution builds a graph g
where each key represents a parent process ID and its value is a list of direct child process IDs. This is done by iterating through the pid
and ppid
arrays together - for each process i
with parent p
, we add i
to g[p]
.
Then, starting from the process we want to kill, we perform a depth-first search (DFS) to traverse the entire subtree rooted at that process. During the traversal, we collect all visited process IDs in the ans
list. The DFS ensures we visit the killed process and all its descendants, giving us the complete list of processes that will be terminated.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves processes with parent-child relationships, which forms a graph structure. Each process can be viewed as a node, and the parent-child relationships are edges.
Is it a tree?
- Yes: The problem explicitly states we have "n processes forming a rooted tree structure." Each process has exactly one parent (except the root), and there's a single root with
ppid[i] = 0
. This confirms we're dealing with a tree.
DFS
- Since we identified this as a tree problem, the flowchart leads us directly to DFS as the solution approach.
Why DFS is appropriate here:
- We need to traverse from a starting node (the process to kill) and visit all its descendants
- The tree structure guarantees no cycles, making DFS safe and efficient
- DFS naturally explores the entire subtree rooted at the killed process, which is exactly what we need - to find all child processes that will also be killed
Conclusion: The flowchart correctly guides us to use DFS for this tree traversal problem. Starting from the process we want to kill, DFS will systematically visit all descendant processes in the tree, giving us the complete list of processes that will be terminated.
Intuition
When we think about this problem, we need to understand what happens when a process is killed - it creates a cascading effect where all its children die, then all their children die, and so on. This is like cutting a branch from a tree - when you cut it, everything attached to that branch falls off too.
The key insight is that we need to find all processes in the subtree rooted at the process we want to kill. Since each process can have multiple children but only one parent, we're dealing with a tree structure where information flows from parent to children.
The input gives us parent-child relationships in a "bottom-up" format (each child knows its parent through ppid
), but to solve this problem efficiently, we need to think "top-down" (each parent should know its children). This is why we first build an adjacency list g
where g[parent]
contains all direct children of that parent.
Once we have this parent-to-children mapping, finding all processes that will be killed becomes a classic tree traversal problem. Starting from the process to kill, we need to visit it and all its descendants. DFS is perfect for this because:
- It naturally explores one branch completely before moving to another
- The recursive nature of DFS mirrors the recursive structure of the problem (kill a process → kill its children → kill their children, etc.)
- We can collect all visited nodes during the traversal, which gives us our answer
The beauty of this approach is that we're essentially asking: "What are all the nodes in the subtree rooted at the kill node?" - and DFS answers this question directly by visiting exactly those nodes.
Learn more about Tree, Depth-First Search and Breadth-First Search patterns.
Solution Approach
Following the DFS approach identified earlier, let's walk through the implementation step by step:
Step 1: Build the Parent-to-Children Graph
First, we need to transform the input data into a more useful structure. The input gives us pid
(process IDs) and ppid
(parent process IDs), but we need to know which children belong to each parent.
g = defaultdict(list)
for i, p in zip(pid, ppid):
g[p].append(i)
This creates an adjacency list where:
g[p]
contains a list of all direct children of processp
- We use
defaultdict(list)
to automatically handle cases where a process has no children - By zipping
pid
andppid
, we iterate through each processi
and its parentp
, addingi
as a child ofp
Step 2: Implement DFS Traversal
The DFS function visits a process and all its descendants:
def dfs(i: int):
ans.append(i) # Add current process to kill list
for j in g[i]: # Visit all children of current process
dfs(j) # Recursively kill each child
The function works by:
- Adding the current process
i
to our answer list (it will be killed) - Looking up all direct children of process
i
from our graphg
- Recursively calling
dfs
on each child, which will in turn visit their children
Step 3: Execute the Solution
ans = [] dfs(kill) return ans
We initialize an empty list ans
to collect all processes that will be killed, start the DFS from the process we want to kill, and return the collected list.
Time Complexity: O(n)
where n
is the number of processes, as we visit each process at most once.
Space Complexity: O(n)
for storing the graph and the recursion stack in the worst case (when the tree is skewed).
The solution elegantly handles the cascading nature of process termination by leveraging the tree structure and DFS's natural ability to explore all nodes in a subtree.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the solution works.
Example Input:
pid = [1, 3, 10, 5]
ppid = [3, 0, 5, 3]
kill = 5
This represents the following process tree:
3 (root) / \ 1 5 \ 10
Step 1: Build the Parent-to-Children Graph
We iterate through pid
and ppid
together:
- Process 1 has parent 3 → add 1 to
g[3]
- Process 3 has parent 0 → add 3 to
g[0]
- Process 10 has parent 5 → add 10 to
g[5]
- Process 5 has parent 3 → add 5 to
g[3]
Resulting graph g
:
g = { 0: [3], # root's child 3: [1, 5], # process 3's children 5: [10] # process 5's child }
Step 2: Execute DFS from kill = 5
Starting DFS traversal:
-
Visit process 5:
- Add 5 to
ans
→ans = [5]
- Look up children of 5 in graph →
g[5] = [10]
- Recursively visit child 10
- Add 5 to
-
Visit process 10:
- Add 10 to
ans
→ans = [5, 10]
- Look up children of 10 in graph →
g[10] = []
(no children) - Return to parent
- Add 10 to
-
Complete traversal:
- All descendants of process 5 have been visited
- Final answer:
[5, 10]
When we kill process 5, both process 5 and its child process 10 are terminated. The DFS traversal naturally captures this cascading effect by visiting the entire subtree rooted at the killed process.
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
6 """
7 Find all processes that need to be killed when killing a specific process.
8 When a process is killed, all its child processes are killed recursively.
9
10 Args:
11 pid: List of process IDs
12 ppid: List of parent process IDs (ppid[i] is parent of pid[i])
13 kill: The process ID to kill
14
15 Returns:
16 List of all process IDs that will be killed
17 """
18
19 # Build adjacency list: parent -> list of children
20 parent_to_children = defaultdict(list)
21 for process_id, parent_id in zip(pid, ppid):
22 parent_to_children[parent_id].append(process_id)
23
24 # Store all processes to be killed
25 killed_processes = []
26
27 def depth_first_search(process_id: int) -> None:
28 """
29 Recursively find and mark all processes to be killed starting from given process.
30
31 Args:
32 process_id: Current process ID to kill
33 """
34 # Add current process to killed list
35 killed_processes.append(process_id)
36
37 # Recursively kill all child processes
38 for child_process_id in parent_to_children[process_id]:
39 depth_first_search(child_process_id)
40
41 # Start DFS from the process to be killed
42 depth_first_search(kill)
43
44 return killed_processes
45
1class Solution {
2 // Graph to store parent-child relationships (parent ID -> list of child IDs)
3 private Map<Integer, List<Integer>> parentToChildrenMap = new HashMap<>();
4 // Result list to store all processes that need to be killed
5 private List<Integer> killedProcesses = new ArrayList<>();
6
7 /**
8 * Kills a process and all its descendant processes.
9 *
10 * @param pid List of process IDs
11 * @param ppid List of parent process IDs (ppid[i] is parent of pid[i])
12 * @param kill The process ID to kill
13 * @return List of all process IDs that will be killed
14 */
15 public List<Integer> killProcess(List<Integer> pid, List<Integer> ppid, int kill) {
16 int numberOfProcesses = pid.size();
17
18 // Build the parent-child relationship graph
19 for (int i = 0; i < numberOfProcesses; i++) {
20 int parentId = ppid.get(i);
21 int childId = pid.get(i);
22
23 // Add child to parent's children list (create list if doesn't exist)
24 parentToChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(childId);
25 }
26
27 // Perform DFS to find all processes to kill starting from the target process
28 depthFirstSearch(kill);
29
30 return killedProcesses;
31 }
32
33 /**
34 * Recursively kills a process and all its child processes using DFS.
35 *
36 * @param processId The current process ID to kill
37 */
38 private void depthFirstSearch(int processId) {
39 // Add current process to the killed list
40 killedProcesses.add(processId);
41
42 // Recursively kill all child processes
43 for (int childProcessId : parentToChildrenMap.getOrDefault(processId, Collections.emptyList())) {
44 depthFirstSearch(childProcessId);
45 }
46 }
47}
48
1class Solution {
2public:
3 vector<int> killProcess(vector<int>& pid, vector<int>& ppid, int kill) {
4 // Build adjacency list where key is parent process ID and value is list of child process IDs
5 unordered_map<int, vector<int>> parentToChildren;
6 int processCount = pid.size();
7
8 // Iterate through all processes to build parent-child relationships
9 for (int i = 0; i < processCount; ++i) {
10 parentToChildren[ppid[i]].push_back(pid[i]);
11 }
12
13 // Result vector to store all processes that need to be killed
14 vector<int> killedProcesses;
15
16 // Define recursive DFS function to traverse process tree
17 function<void(int)> dfs = [&](int currentProcess) {
18 // Add current process to the kill list
19 killedProcesses.push_back(currentProcess);
20
21 // Recursively kill all child processes
22 for (int childProcess : parentToChildren[currentProcess]) {
23 dfs(childProcess);
24 }
25 };
26
27 // Start DFS from the process to be killed
28 dfs(kill);
29
30 return killedProcesses;
31 }
32};
33
1/**
2 * Kills a process and all its child processes
3 * @param pid - Array of process IDs
4 * @param ppid - Array of parent process IDs (ppid[i] is the parent of pid[i])
5 * @param kill - The process ID to kill
6 * @returns Array of all killed process IDs (including the target and its descendants)
7 */
8function killProcess(pid: number[], ppid: number[], kill: number): number[] {
9 // Build adjacency list: maps parent process ID to list of child process IDs
10 const parentToChildren: Map<number, number[]> = new Map();
11
12 // Iterate through all processes to build parent-child relationships
13 for (let i = 0; i < pid.length; i++) {
14 const parentId = ppid[i];
15 const childId = pid[i];
16
17 // Initialize empty array for parent if not exists
18 if (!parentToChildren.has(parentId)) {
19 parentToChildren.set(parentId, []);
20 }
21
22 // Add current process as child of its parent
23 parentToChildren.get(parentId)!.push(childId);
24 }
25
26 // Store all processes that need to be killed
27 const killedProcesses: number[] = [];
28
29 /**
30 * Depth-first search to find all descendant processes
31 * @param processId - Current process ID to kill
32 */
33 const killProcessAndDescendants = (processId: number): void => {
34 // Add current process to killed list
35 killedProcesses.push(processId);
36
37 // Recursively kill all child processes
38 const children = parentToChildren.get(processId) ?? [];
39 for (const childProcessId of children) {
40 killProcessAndDescendants(childProcessId);
41 }
42 };
43
44 // Start DFS from the target process to kill
45 killProcessAndDescendants(kill);
46
47 return killedProcesses;
48}
49
Time and Space Complexity
Time Complexity: O(n)
The algorithm constructs a graph representation of the process tree and then performs a depth-first search (DFS) starting from the kill
process.
- Building the graph takes
O(n)
time, where we iterate through alln
processes once to create the adjacency list. - The DFS traversal visits each process that needs to be killed exactly once. In the worst case, when we need to kill all processes (if
kill
is the root of all processes), we visit alln
nodes. - Each node is processed once during DFS (appended to the answer list and its children are explored).
Therefore, the overall time complexity is O(n) + O(n) = O(n)
.
Space Complexity: O(n)
The space usage comes from:
- The graph
g
(adjacency list) stores all parent-child relationships, which can have at mostn
entries across all lists combined. - The
ans
list stores all processes to be killed, which in the worst case contains alln
processes. - The recursive DFS call stack can go as deep as the height of the process tree. In the worst case (a linear chain of processes), this could be
O(n)
.
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling Missing Process IDs
A critical pitfall occurs when the kill
parameter refers to a process ID that doesn't exist in the pid
array. The current solution will still return [kill]
even if that process doesn't exist, which could be misleading.
Problem Example:
pid = [1, 3, 10, 5] ppid = [3, 0, 5, 3] kill = 7 # Process 7 doesn't exist! # Current solution returns: [7] # Expected behavior: return [] or handle appropriately
Solution: Add validation to check if the process exists before attempting to kill it:
def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
# Create a set of valid process IDs for O(1) lookup
valid_processes = set(pid)
# Check if the process to kill actually exists
if kill not in valid_processes:
return [] # or raise an exception depending on requirements
# Build adjacency list: parent -> list of children
parent_to_children = defaultdict(list)
for process_id, parent_id in zip(pid, ppid):
parent_to_children[parent_id].append(process_id)
# Rest of the solution remains the same...
2. Stack Overflow with Deep Process Trees
For extremely deep process trees (thousands of levels), the recursive DFS approach can cause a stack overflow due to Python's default recursion limit.
Problem Example:
# Creating a linear chain of 10000 processes
pid = list(range(1, 10001))
ppid = [0] + list(range(1, 10000))
kill = 1
# This could hit Python's recursion limit!
Solution: Use an iterative approach with an explicit stack instead of recursion:
def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
# Build adjacency list
parent_to_children = defaultdict(list)
for process_id, parent_id in zip(pid, ppid):
parent_to_children[parent_id].append(process_id)
# Iterative DFS using explicit stack
killed_processes = []
stack = [kill]
while stack:
current_process = stack.pop()
killed_processes.append(current_process)
# Add all children to the stack
for child in parent_to_children[current_process]:
stack.append(child)
return killed_processes
3. Assuming Single Root Process
The problem states that the root process has ppid[i] = 0
, but there could be multiple processes with parent ID 0 (multiple roots), creating a forest rather than a single tree.
Problem Example:
pid = [1, 2, 3, 4] ppid = [0, 0, 1, 2] # Both process 1 and 2 have parent 0 kill = 1 # This works fine, but be aware of the forest structure
Solution:
The current implementation actually handles this correctly since it only traverses from the kill
node downward. However, if you need to validate that there's only one root:
def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
# Count root processes (those with parent 0)
root_count = sum(1 for parent in ppid if parent == 0)
if root_count > 1:
# Handle multiple roots if needed
pass # Current solution still works correctly
# Continue with existing solution...
These pitfalls highlight the importance of input validation and considering edge cases in tree traversal problems.
In a binary min heap, the maximum element can be found in:
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!