Before attempting this problem, you should be comfortable with:
Tree Data Structures - Understanding parent-child relationships and tree traversal concepts
Hash Maps - Using dictionaries/maps for O(1) lookups to build adjacency lists
Depth First Search (DFS) - Recursively traversing tree structures to visit all descendants
Breadth First Search (BFS) - Level-order traversal using queues as an alternative to DFS
1. Depth First Search
Intuition
When we kill a process, all its descendants must also be killed. This is naturally a tree traversal problem. The brute force approach scans through all processes for each recursive call to find children of the current process. While this works, it repeatedly searches the entire list, making it inefficient for large inputs.
Algorithm
Start with the process to kill and add it to the result list.
Scan through the ppid array to find all children of the current process.
For each child found, recursively call the function to kill that process and its descendants.
Return the accumulated list of all killed processes.
publicclassSolution{publicIList<int>KillProcess(IList<int> pid,IList<int> ppid,int kill){var result =newList<int>();if(kill ==0)return result;
result.Add(kill);for(int i =0; i < ppid.Count; i++){if(ppid[i]== kill){
result.AddRange(KillProcess(pid, ppid, pid[i]));}}return result;}}
funckillProcess(pid []int, ppid []int, kill int)[]int{if kill ==0{return[]int{}}
result :=[]int{kill}for i :=0; i <len(ppid); i++{if ppid[i]== kill {
result =append(result,killProcess(pid, ppid, pid[i])...)}}return result
}
class Solution {funkillProcess(pid: List<Int>, ppid: List<Int>, kill: Int): List<Int>{if(kill ==0)returnemptyList()val result =mutableListOf(kill)for(i in ppid.indices){if(ppid[i]== kill){
result.addAll(killProcess(pid, ppid, pid[i]))}}return result
}}
classSolution{funckillProcess(_ pid:[Int],_ ppid:[Int],_ kill:Int)->[Int]{if kill ==0{return[]}var result =[kill]for i in0..<ppid.count {if ppid[i]== kill {
result.append(contentsOf:killProcess(pid, ppid, pid[i]))}}return result
}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n)
Where n is the length of the pid and ppid.
2. Tree Simulation
Intuition
Instead of repeatedly scanning arrays, we can build an actual tree structure upfront. By creating node objects with parent-child relationships, we transform the implicit tree (represented by parallel arrays) into an explicit one. Once built, collecting all descendants becomes a simple tree traversal.
Algorithm
Create a Node object for each process ID and store them in a hash map.
Iterate through the ppid array to establish parent-child relationships.
Starting from the node to kill, perform a DFS to collect all descendant nodes.
classSolution{/**
* @param {number[]} pid
* @param {number[]} ppid
* @param {number} kill
* @return {number[]}
*/killProcess(pid, ppid, kill){const mp =newMap();for(const id of pid){
mp.set(id,{val: id,children:[]});}for(let i =0; i < ppid.length; i++){if(ppid[i]>0){
mp.get(ppid[i]).children.push(mp.get(pid[i]));}}const result =[kill];this.getAllChildren(mp.get(kill), result);return result;}getAllChildren(node, result){for(const child of node.children){
result.push(child.val);this.getAllChildren(child, result);}}}
publicclassSolution{classNode{publicint val;publicList<Node> children =newList<Node>();publicNode(int v){ val = v;}}publicIList<int>KillProcess(IList<int> pid,IList<int> ppid,int kill){var mp =newDictionary<int, Node>();foreach(int id in pid){
mp[id]=newNode(id);}for(int i =0; i < ppid.Count; i++){if(ppid[i]>0){
mp[ppid[i]].children.Add(mp[pid[i]]);}}var result =newList<int>{ kill };GetAllChildren(mp[kill], result);return result;}privatevoidGetAllChildren(Node node,List<int> result){foreach(var child in node.children){
result.Add(child.val);GetAllChildren(child, result);}}}
type Node struct{
val int
children []*Node
}funckillProcess(pid []int, ppid []int, kill int)[]int{
mp :=make(map[int]*Node)for_, id :=range pid {
mp[id]=&Node{val: id, children:[]*Node{}}}for i :=0; i <len(ppid); i++{if ppid[i]>0{
mp[ppid[i]].children =append(mp[ppid[i]].children, mp[pid[i]])}}
result :=[]int{kill}getAllChildren(mp[kill],&result)return result
}funcgetAllChildren(node *Node, result *[]int){for_, child :=range node.children {*result =append(*result, child.val)getAllChildren(child, result)}}
class Solution {classNode(val value: Int){val children = mutableListOf<Node>()}funkillProcess(pid: List<Int>, ppid: List<Int>, kill: Int): List<Int>{val mp = mutableMapOf<Int, Node>()for(id in pid){
mp[id]=Node(id)}for(i in ppid.indices){if(ppid[i]>0){
mp[ppid[i]]!!.children.add(mp[pid[i]]!!)}}val result =mutableListOf(kill)getAllChildren(mp[kill]!!, result)return result
}privatefungetAllChildren(node: Node, result: MutableList<Int>){for(child in node.children){
result.add(child.value)getAllChildren(child, result)}}}
classSolution{classNode{var val:Intvar children:[Node]=[]init(_ val:Int){self.val = val }}funckillProcess(_ pid:[Int],_ ppid:[Int],_ kill:Int)->[Int]{var mp =[Int:Node]()for id in pid {
mp[id]=Node(id)}for i in0..<ppid.count {if ppid[i]>0{
mp[ppid[i]]!.children.append(mp[pid[i]]!)}}var result =[kill]getAllChildren(mp[kill]!,&result)return result
}privatefuncgetAllChildren(_ node:Node,_ result:inout[Int]){for child in node.children {
result.append(child.val)getAllChildren(child,&result)}}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
Where n is the length of the pid and ppid.
3. HashMap + Depth First Search
Intuition
We can simplify the tree simulation by using a hash map that directly maps each parent to its list of children. This avoids creating node objects while still giving us O(1) access to any process's children. The DFS traversal then becomes straightforward.
Algorithm
Build a hash map where each key is a parent process ID and the value is a list of its child process IDs.
Start with the kill process and add it to the result.
Look up the children of the current process in the hash map.
For each child, recursively add it and all its descendants to the result.
publicclassSolution{publicIList<int>KillProcess(IList<int> pid,IList<int> ppid,int kill){var map =newDictionary<int, List<int>>();for(int i =0; i < ppid.Count; i++){if(ppid[i]>0){if(!map.ContainsKey(ppid[i])){
map[ppid[i]]=newList<int>();}
map[ppid[i]].Add(pid[i]);}}var result =newList<int>{ kill };GetAllChildren(map, result, kill);return result;}privatevoidGetAllChildren(Dictionary<int, List<int>> map,List<int> result,int kill){if(map.ContainsKey(kill)){foreach(int childId in map[kill]){
result.Add(childId);GetAllChildren(map, result, childId);}}}}
funckillProcess(pid []int, ppid []int, kill int)[]int{
mp :=make(map[int][]int)for i :=0; i <len(ppid); i++{if ppid[i]>0{
mp[ppid[i]]=append(mp[ppid[i]], pid[i])}}
result :=[]int{kill}var getAllChildren func(kill int)
getAllChildren =func(kill int){if children, ok := mp[kill]; ok {for_, childId :=range children {
result =append(result, childId)getAllChildren(childId)}}}getAllChildren(kill)return result
}
class Solution {funkillProcess(pid: List<Int>, ppid: List<Int>, kill: Int): List<Int>{val map = mutableMapOf<Int, MutableList<Int>>()for(i in ppid.indices){if(ppid[i]>0){
map.getOrPut(ppid[i]){mutableListOf()}.add(pid[i])}}val result =mutableListOf(kill)getAllChildren(map, result, kill)return result
}privatefungetAllChildren(map: Map<Int, List<Int>>, result: MutableList<Int>, kill: Int){
map[kill]?.forEach{ childId ->
result.add(childId)getAllChildren(map, result, childId)}}}
classSolution{funckillProcess(_ pid:[Int],_ ppid:[Int],_ kill:Int)->[Int]{var map =[Int:[Int]]()for i in0..<ppid.count {if ppid[i]>0{
map[ppid[i],default:[]].append(pid[i])}}var result =[kill]getAllChildren(map,&result, kill)return result
}privatefuncgetAllChildren(_ map:[Int:[Int]],_ result:inout[Int],_ kill:Int){iflet children = map[kill]{for childId in children {
result.append(childId)getAllChildren(map,&result, childId)}}}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
Where n is the length of the pid and ppid.
4. HashMap + Breadth First Search
Intuition
BFS offers an alternative to DFS for traversing the process tree. Instead of going deep into one branch before backtracking, BFS processes all children at the current level before moving to the next. Both approaches yield the same result but BFS can be more intuitive when thinking about the spread of the kill signal.
Algorithm
Build a hash map mapping each parent process ID to its children.
Initialize a queue with the kill process.
While the queue is not empty:
Dequeue a process and add it to the result.
Enqueue all of its children.
Return the result containing all killed processes.
classSolution:defkillProcess(self, pid: List[int], ppid: List[int], kill:int)-> List[int]:
map_dict ={}for i inrange(len(ppid)):if ppid[i]>0:if ppid[i]notin map_dict:
map_dict[ppid[i]]=[]
map_dict[ppid[i]].append(pid[i])
queue = deque([kill])
result =[]while queue:
r = queue.popleft()
result.append(r)if r in map_dict:for child_id in map_dict[r]:
queue.append(child_id)return result
classSolution{publicList<Integer>killProcess(List<Integer> pid,List<Integer> ppid,int kill){HashMap<Integer,List<Integer>> map =newHashMap<>();for(int i =0; i < ppid.size(); i++){if(ppid.get(i)>0){List<Integer> l = map.getOrDefault(ppid.get(i),newArrayList<Integer>());
l.add(pid.get(i));
map.put(ppid.get(i), l);}}Queue<Integer> queue =newLinkedList<>();List<Integer> l =newArrayList<>();
queue.add(kill);while(!queue.isEmpty()){int r = queue.remove();
l.add(r);if(map.containsKey(r))for(int id: map.get(r))
queue.add(id);}return l;}}
classSolution{/**
* @param {number[]} pid
* @param {number[]} ppid
* @param {number} kill
* @return {number[]}
*/killProcess(pid, ppid, kill){const map =newMap();for(let i =0; i < ppid.length; i++){if(ppid[i]>0){if(!map.has(ppid[i])){
map.set(ppid[i],[]);}
map.get(ppid[i]).push(pid[i]);}}const queue =[kill];const result =[];while(queue.length >0){const r = queue.shift();
result.push(r);if(map.has(r)){for(const childId of map.get(r)){
queue.push(childId);}}}return result;}}
publicclassSolution{publicIList<int>KillProcess(IList<int> pid,IList<int> ppid,int kill){var map =newDictionary<int, List<int>>();for(int i =0; i < ppid.Count; i++){if(ppid[i]>0){if(!map.ContainsKey(ppid[i])){
map[ppid[i]]=newList<int>();}
map[ppid[i]].Add(pid[i]);}}var queue =newQueue<int>();var result =newList<int>();
queue.Enqueue(kill);while(queue.Count >0){int r = queue.Dequeue();
result.Add(r);if(map.ContainsKey(r)){foreach(int childId in map[r]){
queue.Enqueue(childId);}}}return result;}}
funckillProcess(pid []int, ppid []int, kill int)[]int{
mp :=make(map[int][]int)for i :=0; i <len(ppid); i++{if ppid[i]>0{
mp[ppid[i]]=append(mp[ppid[i]], pid[i])}}
queue :=[]int{kill}
result :=[]int{}forlen(queue)>0{
r := queue[0]
queue = queue[1:]
result =append(result, r)if children, ok := mp[r]; ok {
queue =append(queue, children...)}}return result
}
class Solution {funkillProcess(pid: List<Int>, ppid: List<Int>, kill: Int): List<Int>{val map = mutableMapOf<Int, MutableList<Int>>()for(i in ppid.indices){if(ppid[i]>0){
map.getOrPut(ppid[i]){mutableListOf()}.add(pid[i])}}val queue = java.util.LinkedList<Int>()val result = mutableListOf<Int>()
queue.add(kill)while(queue.isNotEmpty()){val r = queue.poll()
result.add(r)
map[r]?.forEach{ childId ->
queue.add(childId)}}return result
}}
classSolution{funckillProcess(_ pid:[Int],_ ppid:[Int],_ kill:Int)->[Int]{var map =[Int:[Int]]()for i in0..<ppid.count {if ppid[i]>0{
map[ppid[i],default:[]].append(pid[i])}}var queue =[kill]var result =[Int]()while!queue.isEmpty {let r = queue.removeFirst()
result.append(r)iflet children = map[r]{
queue.append(contentsOf: children)}}return result
}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
Where n is the length of the pid and ppid.
Common Pitfalls
Forgetting to Include the Kill Process Itself
A common mistake is only collecting the children of the process to kill, but forgetting to include the kill process itself in the result. The killed process must be added to the result list before traversing its descendants.
Not Handling Processes With No Children
When building the parent-to-children map, some processes may have no children (leaf processes). Failing to check if a key exists in the map before accessing its children can cause null pointer exceptions or key errors. Always verify the map contains the key before iterating over children.
Using Linear Search for Each Recursive Call
The brute force approach scans the entire ppid array for each recursive call, leading to O(n^2) complexity. This becomes a problem for large inputs. Building a hash map upfront to store parent-child relationships reduces this to O(n) by providing O(1) child lookups.