Remove Nodes From Linked List
MedPrompt
You are given the head of a linked list.
Remove every node which has a node with a greater value anywhere to the right side of it.
Return the head of the modified linked list.
Example 1:
Input: head = [5,2,13,3,8]
Output: [13,8]
Explanation: The nodes that should be removed are 5, 2 and 3.
- Node 13 is to the right of node 5.
- Node 13 is to the right of node 2.
- Node 8 is to the right of node 3.Example 2:
Input: head = [1,1,1,1]
Output: [1,1,1,1]
Explanation: Every node has value 1, so no nodes are removed.
Constraints:
- The number of the nodes in the given list is in the range
[1, 105]. 1 <= Node.val <= 105
Approaches
4 approaches with complexity analysis and trade-offs.
This approach uses a straightforward, brute-force method. It iterates through each node of the linked list, and for each node, it performs a second scan through all the subsequent nodes to check if there exists any node with a strictly greater value. If a greater value is found, the current node is removed from the list.
Algorithm
- Create a
dummynode that points to theheadto simplify edge cases like removing the original head. - Use two pointers,
prevandcurrent.prevwill point to the last node that was kept, andcurrentwill iterate through the list. - For each
currentnode, start another pointerrunnerfromcurrent.next. - Iterate with
runnerthrough the rest of the list. If anyrunner.val > current.val, it meanscurrentmust be removed. - If
currentneeds to be removed, updateprev.nextto point tocurrent.next, effectively skippingcurrent. - If
currentdoes not need to be removed (i.e., the inner loop finishes without finding a greater value), thencurrentis a valid node in the final list, so we updateprevtocurrent. - Advance
currentto the next node in the list and repeat the process. - Finally, return
dummy.nextwhich points to the head of the modified list.
Walkthrough
The core idea is to check the removal condition for each node one by one. We can use a dummy head to make the removal of the first node easier. We iterate through the list with a current pointer. For each current node, we use a runner pointer to scan the rest of the list to its right. If the runner finds any node with a value greater than current.val, we know current must be removed. To remove it, we need a pointer to the node before current, let's call it prev. We update prev.next to current.next. If the entire scan to the right of current completes without finding a greater node, we keep current and simply advance prev to current. This nested loop structure leads to a quadratic time complexity.
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode removeNodes(ListNode head) { ListNode dummy = new ListNode(0, head); ListNode current = head; ListNode prev = dummy; while (current != null) { boolean foundGreater = false; ListNode runner = current.next; while (runner != null) { if (runner.val > current.val) { foundGreater = true; break; } runner = runner.next; } if (foundGreater) { // Remove current node by linking prev to current's next prev.next = current.next; } else { // Keep current node, so update prev prev = current; } // Move to the next node in the original list current = current.next; } return dummy.next; }}Complexity
Time
O(N^2) - For each node in the list (N nodes), we may traverse the rest of the list (up to N-1 nodes). This results in a nested loop structure, giving a quadratic time complexity.
Space
O(1) - We only use a constant number of extra pointers (`dummy`, `prev`, `current`, `runner`) regardless of the list size.
Trade-offs
Pros
The logic is simple and easy to understand.
It operates in-place and uses O(1) extra space.
Cons
The time complexity of O(N^2) is highly inefficient and will likely result in a 'Time Limit Exceeded' error for large inputs as specified in the constraints (N up to 10^5).
A more efficient approach uses recursion. The nature of the problem—where a node's fate depends on the nodes to its right—lends itself well to a post-order traversal pattern, which can be elegantly implemented with recursion. We process the list from the end to the beginning. The recursive function modifies the sublist starting from the end and returns the head of the modified sublist.
Algorithm
- Define a recursive function that takes the
headof a list (or sublist) as input. - Base Case: If the
headisnullorhead.nextisnull, it means we are at the end of the list. No node can be to its right, so we return theheaditself. - Recursive Step: Make a recursive call for the rest of the list:
removeNodes(head.next). This call will return the head of the processed sublist that follows the currenthead. - Let the returned node be
nextNode. ThisnextNodeis the first node in the valid, modified remainder of the list. - Compare the current
head.valwithnextNode.val. SincenextNodeis the head of the modified sublist, all nodes that were smaller than it have already been removed. Thus,nextNodeholds the largest value at the start of the sublist. - If
head.val < nextNode.val, the currentheadmust be removed. We do this by returningnextNodefrom the current function call, effectively bypassinghead. - If
head.val >= nextNode.val, the currentheadshould be kept. We link it to the modified rest of the list by settinghead.next = nextNodeand returnhead. - The initial call to
removeNodes(head)will return the head of the final, fully modified list.
Walkthrough
The function removeNodes(head) will solve the problem for the list starting at head. The key insight is to first solve the problem for the rest of the list, head.next. The recursive call removeNodes(head.next) returns a nextNode which is the head of the already-processed remainder of the list. Now, we only need to decide the fate of the current head. The head should be removed if its value is less than the value of nextNode (since nextNode is a node to its right). If head is kept, its next pointer should point to nextNode. This post-order processing ensures that when we decide about head, the list to its right has already been correctly filtered.
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode removeNodes(ListNode head) { if (head == null || head.next == null) { return head; } ListNode nextNode = removeNodes(head.next); if (head.val < nextNode.val) { // The current node should be removed. // Return the head of the modified rest of the list. return nextNode; } else { // The current node should be kept. // Link it to the modified rest of the list. head.next = nextNode; return head; } }}Complexity
Time
O(N) - Each node in the linked list is visited exactly once during the recursion.
Space
O(N) - The space is consumed by the recursion call stack. In the worst case (a skewed list), the recursion depth can be N.
Trade-offs
Pros
The code is very concise and elegant.
Achieves a linear time complexity of O(N).
Cons
Uses O(N) space for the recursion call stack, which can lead to a
StackOverflowErrorfor very long lists (e.g., N = 10^5).Function call overhead can make it slightly slower in practice than an equivalent iterative solution.
This approach is an iterative equivalent of the recursive solution, using a monotonic stack. We traverse the list from left to right. A stack is used to maintain a sequence of nodes whose values are monotonically decreasing. When we encounter a new node, we pop all nodes from the stack that have a smaller value than the new node, because the new node is a 'greater value to their right'.
Algorithm
- Initialize an empty stack, which will store
ListNodeobjects. - Iterate through the linked list from
headtotailwith acurrentpointer. - For each
currentnode:- While the stack is not empty and the value of the node at the top of the stack (
stack.peek().val) is less thancurrent.val, pop from the stack. This step removes all nodes that are followed by a greater node (current). - After the loop, push the
currentnode onto the stack.
- While the stack is not empty and the value of the node at the top of the stack (
- After iterating through the entire list, the stack contains the nodes of the final list, but in reverse order.
- To construct the final list, create a new
head(initiallynull). Repeatedly pop from the stack, and for each popped node, set itsnextpointer to the currentheadand then updateheadto be the popped node. This reverses the order and links the nodes correctly. - Return the new
head.
Walkthrough
By using a stack, we can efficiently find the next greater element for a sequence of nodes. As we traverse the list, we maintain a stack of nodes that are potential candidates for the final list. When we encounter a current node, we compare it with the node at the top of the stack. If current.val is greater, it means the node on the stack must be removed. We continue popping until the stack is empty or the node at the top is greater than or equal to current. Then, we push current onto the stack. This ensures the stack always holds a sequence of nodes with decreasing values. After one pass, the nodes remaining in the stack are precisely the ones that should be in the final list, but in reverse order. A second pass over the stack elements allows us to rebuild the linked list in the correct order.
import java.util.ArrayDeque;import java.util.Deque; /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode removeNodes(ListNode head) { Deque<ListNode> stack = new ArrayDeque<>(); ListNode current = head; while (current != null) { while (!stack.isEmpty() && stack.peek().val < current.val) { stack.pop(); } stack.push(current); current = current.next; } ListNode newHead = null; while (!stack.isEmpty()) { ListNode node = stack.pop(); node.next = newHead; newHead = node; } return newHead; }}Complexity
Time
O(N) - Each node is pushed onto the stack once and popped at most once. The list traversal and stack reconstruction are both linear operations.
Space
O(N) - In the worst-case scenario (a list with strictly decreasing values like `[5, 4, 3, 2, 1]`), all nodes will be pushed onto the stack.
Trade-offs
Pros
Efficient O(N) time complexity.
Being iterative, it avoids the risk of stack overflow that a recursive solution might face with very long lists.
Cons
Requires O(N) extra space to store the nodes in the stack.
This is the most optimal approach in terms of both time and space complexity. The key idea is to transform the problem by reversing the linked list. After reversing, the condition 'a node with a greater value to the right' becomes 'a node with a greater value to the left'. This new problem is much easier to solve in a single pass. After filtering the nodes in the reversed list, we reverse it back to get the final answer.
Algorithm
- Step 1: Reverse the list. Implement a helper function to reverse the linked list. Call it on the original
head. The original tail is now the new head. - Step 2: Filter the nodes. Traverse the reversed list. Keep track of the maximum value seen so far (
max_so_far), initialized to the value of the new head.- Use a
prevpointer to track the last node that was kept and acurrentpointer to iterate. - If
current.valis less thanmax_so_far, it means this node has a greater value to its left (which was its right in the original list). Remove it by settingprev.next = current.next. - If
current.valis greater than or equal tomax_so_far, keep this node. Updatemax_so_far = current.valand advanceprevtocurrent.
- Use a
- Step 3: Reverse the list again. The filtered list is now correct but in reverse order. Call the reverse function again on the head of this modified list to restore the original relative order of the kept nodes.
Walkthrough
First, we reverse the entire linked list. For an input of [5,2,13,3,8], this gives us [8,3,13,2,5]. Now, we can iterate through this reversed list and keep only the nodes that form a non-increasing sequence when read from left to right. We can do this by keeping track of the maximum value seen so far. We start with max_so_far = 8. Then we see 3, which is less than 8, so we remove it. Then we see 13, which is greater than 8, so we keep it and update max_so_far = 13. Then we see 2 and 5, both less than 13, so they are removed. The resulting list is [8, 13]. Finally, we reverse this list again to get [13, 8], which is the correct output. This entire process takes two full passes over the list data (two reversals and one filtering pass), resulting in linear time complexity, but crucially, it uses only a constant amount of extra space.
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode removeNodes(ListNode head) { // Step 1: Reverse the linked list ListNode reversedHead = reverseList(head); ListNode current = reversedHead; int maxVal = Integer.MIN_VALUE; ListNode prev = null; // Step 2: Traverse the reversed list and remove nodes while (current != null) { if (current.val >= maxVal) { // Keep this node maxVal = current.val; prev = current; } else { // Remove this node by skipping it prev.next = current.next; } current = current.next; } // Step 3: Reverse the list again to get the final result return reverseList(reversedHead); } private ListNode reverseList(ListNode node) { ListNode prev = null; ListNode current = node; while (current != null) { ListNode nextTemp = current.next; current.next = prev; prev = current; current = nextTemp; } return prev; }}Complexity
Time
O(N) - The algorithm consists of three passes over the list (reverse, filter, reverse back), each taking O(N) time. The total time complexity is O(N) + O(N) + O(N) = O(N).
Space
O(1) - The list reversal and filtering are done in-place, using only a few extra pointers.
Trade-offs
Pros
Optimal solution with O(N) time complexity.
Most space-efficient solution with O(1) extra space.
Cons
The logic involves multiple steps (two reversals and a traversal), which can be slightly more complex to implement correctly compared to a single-pass recursive solution.
Solutions
Solution
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeNodes ( ListNode head ) { List < Integer > nums = new ArrayList <>(); while ( head != null ) { nums . add ( head . val ); head = head . next ; } Deque < Integer > stk = new ArrayDeque <>(); for ( int v : nums ) { while (! stk . isEmpty () && stk . peekLast () < v ) { stk . pollLast (); } stk . offerLast ( v ); } ListNode dummy = new ListNode (); head = dummy ; while (! stk . isEmpty ()) { head . next = new ListNode ( stk . pollFirst ()); head = head . next ; } return dummy . next ; } }/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode * removeNodes ( ListNode * head ) { vector < int > nums ; while ( head ) { nums . emplace_back ( head -> val ); head = head -> next ; } vector < int > stk ; for ( int v : nums ) { while ( ! stk . empty () && stk . back () < v ) { stk . pop_back (); } stk . push_back ( v ); } ListNode * dummy = new ListNode (); head = dummy ; for ( int v : stk ) { head -> next = new ListNode ( v ); head = head -> next ; } return dummy -> next ; } };# Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution : def removeNodes ( self , head : Optional [ ListNode ]) -> Optional [ ListNode ]: nums = [] while head : nums . append ( head . val ) head = head . next stk = [] for v in nums : while stk and stk [ - 1 ] < v : stk . pop () stk . append ( v ) dummy = ListNode () head = dummy for v in stk : head . next = ListNode ( v ) head = head . next return dummy . nextVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.