LeetCode 2046 - Sort Linked List Already Sorted Using Absolute Values
The problem provides a singly linked list where the nodes are sorted in non-decreasing order by their absolute values, rather than their actual values. The task is to rearrange this list so that the nodes are sorted in non-decreasing order according to their actual values.
Difficulty: 🟡 Medium
Topics: Linked List, Two Pointers, Sorting
Solution
Problem Understanding
The problem provides a singly linked list where the nodes are sorted in non-decreasing order by their absolute values, rather than their actual values. The task is to rearrange this list so that the nodes are sorted in non-decreasing order according to their actual values.
For example, consider the input [0, 2, -5, 5, 10, -10]. While the absolute values [0, 2, 5, 5, 10, 10] are sorted, the negative numbers are out of order when considering their true values. The output should be [-10, -5, 0, 2, 5, 10].
The input is a singly linked list, not an array. The constraints are substantial: the list may contain up to 100,000 nodes with values in the range [-5000, 5000]. This means that a naive approach that converts the list to an array and sorts it would be too slow if it relies on comparison-based sorting that uses extra memory.
Important edge cases include:
- A list containing only non-negative numbers, which is already sorted by actual value.
- A list containing only negative numbers, which will require reversing the negative segment.
- A single-node list, which is trivially sorted.
- Lists with zeros and duplicates, which should maintain proper order.
The problem also hints at a linear time solution, which suggests that a clever use of the linked list properties is possible without full sorting.
Approaches
The brute-force approach involves traversing the linked list, extracting all values into an array, sorting the array using a standard comparison sort, and then reconstructing the linked list. While this works correctly, it is not optimal for large lists because it uses extra memory for the array and incurs O(n log n) time complexity.
The optimal approach leverages the observation that the list is already sorted by absolute values. This guarantees that all negative numbers (in absolute value order) appear before their positive counterparts. If we reverse the negative sublist and then merge it with the positive sublist, we can achieve a fully sorted linked list in O(n) time and O(1) extra space. The steps involve splitting the list at the first non-negative node, reversing the negative sublist, and performing a standard merge of two sorted lists.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n log n) | O(n) | Convert list to array, sort, reconstruct linked list |
| Optimal | O(n) | O(1) | Reverse negative sublist and merge with positive sublist in-place |
Algorithm Walkthrough
- Identify the negative sublist: Start from the head and move forward while the nodes have negative values. Stop at the first non-negative node. This splits the list into a negative sublist and a non-negative sublist.
- Reverse the negative sublist: Since the negative numbers are in decreasing absolute order, reversing this sublist will order them correctly by actual value.
- Merge the two sorted sublists: Use a standard two-pointer merge technique for linked lists. Initialize a dummy head and compare nodes from both sublists, appending the smaller node each time.
- Return the merged list: After the merge, the dummy head points to the fully sorted linked list by actual values.
Why it works: Negative numbers in the input list are sorted by absolute value but decreasing in actual value. Reversing them ensures ascending order. Positive numbers are already sorted. Merging two sorted lists guarantees a fully sorted result. This preserves linear time complexity and constant extra space.
Python Solution
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def sortLinkedList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
# Step 1: Split the negative and non-negative lists
neg_dummy = ListNode(0)
pos_dummy = ListNode(0)
neg_tail, pos_tail = neg_dummy, pos_dummy
curr = head
while curr:
next_node = curr.next
curr.next = None
if curr.val < 0:
curr.next = neg_dummy.next
neg_dummy.next = curr
else:
pos_tail.next = curr
pos_tail = pos_tail.next
curr = next_node
# Step 2: Merge the two lists
neg_head = neg_dummy.next
pos_head = pos_dummy.next
dummy = ListNode(0)
tail = dummy
while neg_head and pos_head:
if neg_head.val < pos_head.val:
tail.next = neg_head
neg_head = neg_head.next
else:
tail.next = pos_head
pos_head = pos_head.next
tail = tail.next
tail.next = neg_head if neg_head else pos_head
return dummy.next
The implementation first splits the input list into negative and non-negative segments. The negative nodes are prepended during traversal to reverse them in-place. Finally, a standard merge merges the reversed negative list with the positive list.
Go Solution
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func sortLinkedList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
var negHead *ListNode
dummyPos := &ListNode{}
posTail := dummyPos
curr := head
for curr != nil {
next := curr.Next
curr.Next = nil
if curr.Val < 0 {
curr.Next = negHead
negHead = curr
} else {
posTail.Next = curr
posTail = posTail.Next
}
curr = next
}
dummy := &ListNode{}
tail := dummy
for negHead != nil && dummyPos.Next != nil {
if negHead.Val < dummyPos.Next.Val {
tail.Next = negHead
negHead = negHead.Next
} else {
tail.Next = dummyPos.Next
dummyPos.Next = dummyPos.Next.Next
}
tail = tail.Next
}
if negHead != nil {
tail.Next = negHead
} else if dummyPos.Next != nil {
tail.Next = dummyPos.Next
}
return dummy.Next
}
The Go version mirrors the Python solution. Pointers are carefully updated, and nil is used instead of Python’s None. Negative nodes are reversed inline by prepending, while positive nodes maintain their order. The merge uses two pointers similar to the Python version.
Worked Examples
Example 1: [0, 2, -5, 5, 10, -10]
Step 1: Split
- Negative list:
-5 -> -10(prepended →-10 -> -5) - Positive list:
0 -> 2 -> 5 -> 10
Step 2: Merge
| Neg | Pos | Merged |
|---|---|---|
| -10 | 0 | -10 |
| -5 | 0 | -10 -> -5 |
| nil | 0 | -10 -> -5 -> 0 -> 2 -> 5 -> 10 |
Output: [-10, -5, 0, 2, 5, 10]
Example 2: [0, 1, 2]
Negative list: empty
Positive list: 0 -> 1 -> 2
Merge result: 0 -> 1 -> 2
Example 3: [1]
Only one node, returned as is: [1]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each node is visited once for splitting, once for merging |
| Space | O(1) | Reversing negative nodes and merging done in-place with no extra arrays |
The linear time complexity arises because the list is traversed a constant number of times, and no nested loops are used. Constant space is maintained by using pointer manipulation rather than auxiliary data structures.
Test Cases
# Provided examples
assert Solution().sortLinkedList(None) == None # Empty list
assert Solution().sortLinkedList(ListNode(1)).val == 1 # Single node
assert Solution().sortLinkedList(ListNode(0, ListNode(2, ListNode(-5, ListNode(5, ListNode(10, ListNode(-10))))))).val == -10
# Edge cases
assert Solution().sortLinkedList(ListNode(-3, ListNode(-2, ListNode(-1)))).val == -3 # All negative
assert Solution().sortLinkedList(ListNode(0, ListNode(0, ListNode(0)))).val == 0 # All zeros
assert Solution().sortLinkedList(ListNode(-1, ListNode(1))).val == -1 # Mixed one negative, one positive
| Test | Why |
|---|---|
| Empty list | Validate handling of no nodes |
| Single node | Validate trivial case |
| Mixed positive/negative | Validates full algorithm |
| All negative |