Linked List
Linked List
The Reverse Linked List problem involves reversing a singly linked list, such that the first
element becomes the last and vice versa. This problem is a common interview question and can
be solved iteratively or recursively.
Problem Statement:
Given the head of a singly linked list, reverse the list and return its reversed head.
Example:
Input: head = [1, 2, 3, 4, 5]
Output: [5, 4, 3, 2, 1]
Input: head = []
Output: []
Approach:
We can solve this problem using two common approaches: Iterative approach and
Recursive approach.
1. Iterative Approach:
In this approach, we traverse the linked list and reverse the links one by one. We maintain three
pointers:
● previous: Initially null, this will hold the reversed part of the list.
● current: Initially pointing to the head of the list, this will traverse through the list.
● next: This will store the next node before changing the current node’s pointer.
2. Recursive Approach:
In this approach, we reverse the rest of the list and then fix the links between the nodes after the
recursion unwinds.
1. If the node is null or it’s the last node (head.next == null), return the node as it is.
2. Recursively call the function for the next node.
3. When recursion reaches the end, reverse the link between the current node and the next
node.
4. Return the new head of the reversed list.
Java Solution:
Iterative Approach:
public class ReverseLinkedList {
return previous; // At the end, previous will be the new head of the reversed list
}
Recursive Approach:
public class ReverseLinkedList {
1. Iterative Approach:
○ We start by initializing two pointers: previous (set to null) and current (set
to the head of the list).
○ As we iterate through the list:
■ We store the next node of the current node in the nextNode pointer.
■ Then we reverse the link by setting current.next to previous.
■ We move previous to the current node and current to the
nextNode.
○ Once the loop completes, previous will point to the new head of the reversed
list.
2. Recursive Approach:
○ The base case checks if head is null or if the list has only one node
(head.next == null).
○ The recursive step reverses the rest of the list by calling reverseList on
head.next.
○ After the recursion, we reverse the link by setting head.next.next = head
(i.e., making the next node point back to the current node).
○ We set head.next = null to mark the end of the list.
○ The recursive call returns the new head of the reversed list.
Time Complexity:
● O(n), where n is the number of nodes in the linked list. We visit each node once, either
iteratively or recursively.
Space Complexity:
● O(1) for the iterative approach, as we only use a constant amount of extra space for
pointers.
● O(n) for the recursive approach due to the recursion stack. The depth of recursion is
proportional to the number of nodes in the list.
Test Cases:
1. Test Case 1:
○ Input: 1 -> 2 -> 3 -> 4 -> 5
○ Output: 5 -> 4 -> 3 -> 2 -> 1
2. Test Case 2:
○ Input: 2 -> 4
○ Output: 4 -> 2
3. Test Case 3:
○ Input: 1
○ Output: 1 (Single node list, remains unchanged)
4. Test Case 4:
○ Input: []
○ Output: [] (Empty list, remains unchanged)
Conclusion:
Reversing a linked list is a fundamental problem that can be solved either iteratively or
recursively. The iterative approach is generally preferred because it uses constant space,
whereas the recursive approach requires additional space due to the recursion stack. Both
approaches work efficiently with a time complexity of O(n).
The Merge Two Sorted Lists problem involves merging two sorted linked lists into a single
sorted linked list. Both linked lists are already sorted in ascending order, and you are required to
combine them in a way that the resulting list is also sorted.
Problem Statement:
Given the heads of two sorted linked lists list1 and list2, merge the two lists into a single
sorted linked list.
Example:
Input:
list1 = [1, 2, 4], list2 = [1, 3, 4]
Output:
[1, 1, 2, 3, 4, 4]
Input:
list1 = [], list2 = [0]
Output:
[0]
Input:
list1 = [1], list2 = []
Output:
[1]
Approach:
To solve this problem, we will use a two-pointer approach to traverse both linked lists,
compare their current nodes, and merge them in sorted order.
Steps:
1. Create a dummy node: This dummy node will act as a placeholder to simplify the
merging process. We will maintain a current pointer to build the merged list.
2. Iterate through both lists: Compare the current nodes of list1 and list2. Append
the smaller node to the merged list and move the corresponding pointer to the next node
in that list.
3. Handle remaining elements: If one list is exhausted before the other, append the
remaining nodes of the non-exhausted list to the merged list.
4. Return the merged list: The merged list starts from the node next to the dummy node.
Java Solution:
public class MergeTwoSortedLists {
// Return the merged list, starting from the next of dummy node
return dummy.next;
}
1. ListNode Class:
○ A simple class to represent each node of the linked list, which contains a value
(val) and a reference to the next node (next).
2. Merge Logic:
○ The merged list starts from dummy.next because the dummy node is just a
placeholder to simplify the code logic.
4. Helper Method:
○ The printList method is provided to print the elements of the linked list, which
is helpful for testing.
Time Complexity:
● O(n + m), where n is the length of list1 and m is the length of list2. We traverse
both lists once, making comparisons at each step.
Space Complexity:
● O(1) if we do not count the space for the input lists themselves. The space complexity is
constant because we are only using a few extra pointers (dummy and current) and the
merged list is formed in-place.
Test Cases:
1. Test Case 1:
Conclusion:
Merging two sorted linked lists is a common problem that can be efficiently solved with a
two-pointer approach. The solution provided handles edge cases, such as when one of the
lists is empty, and merges the lists in O(n + m) time complexity. This approach ensures the
merged list is sorted, using constant space (ignoring input storage).
The Linked List Cycle problem involves detecting if a given singly linked list has a cycle. A
cycle in a linked list occurs when a node’s next pointer points back to a previous node in the
list, creating a loop.
Problem Statement:
Given the head of a linked list, determine if the list has a cycle. A cycle occurs when a node’s
next points to a previous node in the list.
Return true if there is a cycle in the linked list, otherwise return false.
Example:
Input: head = [3, 2, 0, -4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where the tail connects to the node at position 1
(0-indexed).
Approach:
The most efficient way to detect a cycle in a linked list is by using Floyd’s Tortoise and Hare
algorithm. This algorithm uses two pointers, slow and fast, that move through the list at
different speeds:
If there is no cycle, the fast pointer will eventually reach the end of the list (null). However, if
there is a cycle, the fast pointer will eventually meet the slow pointer within the cycle.
Steps:
1. Initialize two pointers, slow and fast. Both start at the head of the list.
2. Move the slow pointer by one step and the fast pointer by two steps in each iteration.
3. If the fast pointer or fast.next becomes null, return false, indicating no cycle.
4. If the slow pointer equals the fast pointer at any point, return true, indicating a cycle.
Java Solution:
public class LinkedListCycle {
return false; // No cycle if fast pointer reaches the end of the list
}
head.next = node2;
node2.next = node0;
node0.next = nodeMinus4;
nodeMinus4.next = node2; // Create a cycle by connecting the last node to node2
return head;
}
1. ListNode Class: This class represents the node in the linked list, with a val and a next
pointer to the next node.
2. hasCycle Method:
○ We first check if the list is empty or has only one node. In such cases, there can
be no cycle, so we return false.
○ We then initialize two pointers, slow and fast, both starting at the head of the
list.
○ We enter a while loop where the fast pointer moves two steps and the slow
pointer moves one step. If the slow and fast pointers meet at any point, we
return true, indicating a cycle.
○ If the fast pointer or fast.next becomes null, it means we reached the end
of the list without encountering a cycle, and we return false.
3. createListWithCycle Method (for testing):
○ This method is used to create a linked list with a cycle for testing purposes. It
connects the last node to an earlier node (in this case, the second node), forming
a cycle.
4. main Method:
○ The main method creates a linked list with a cycle and checks if the list has a
cycle using the hasCycle method. It prints the result.
Time Complexity:
● O(n), where n is the number of nodes in the linked list. We traverse the list at most once
with the two pointers.
Space Complexity:
● O(1), since we are only using a constant amount of extra space (two pointers) regardless
of the size of the list.
Test Cases:
1. Test Case 1:
Conclusion:
The Floyd’s Tortoise and Hare algorithm provides an efficient solution for detecting cycles in
a linked list with a time complexity of O(n) and space complexity of O(1). The approach uses
two pointers to traverse the list at different speeds, and it guarantees detecting a cycle if one
exists.
The Reorder List problem involves rearranging a singly linked list in a specific order. The goal
is to reorder the list such that:
Problem Statement:
Given the head of a singly linked list, reorder the list in-place as described above.
You must solve it in O(n) time complexity and O(1) space complexity.
Example:
Input: head = [1, 2, 3, 4, 5]
Output: [1, 5, 2, 4, 3]
Approach:
To solve this problem efficiently in O(n) time and O(1) space, we can follow these steps:
1. Step 1: Find the middle of the linked list:
○ Use the tortoise and hare method (slow and fast pointers) to find the middle of
the list. The slow pointer moves one step at a time, and the fast pointer moves
two steps at a time. When the fast pointer reaches the end, the slow pointer will
be at the middle.
2. Step 2: Split the list into two halves:
○ Once we have the middle, we split the linked list into two halves:
■ The first half starts from the head to the middle.
■ The second half starts from the node after the middle.
3. Step 3: Reverse the second half of the list:
○ Reverse the second half of the list so that we can start merging it in the required
order.
4. Step 4: Merge the two halves:
○ Merge the first half and the reversed second half, alternating nodes between the
two halves.
Java Solution:
public class ReorderList {
// Step 1: Find the middle of the list using slow and fast pointers
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// Step 2: Split the list into two halves
ListNode secondHalf = slow.next;
slow.next = null; // Cut off the first half from the second half
1. ListNode Class:
○ A simple class representing a node in the singly linked list, which holds an integer
value (val) and a reference to the next node (next).
2. reorderList Method:
○ Step 1: We use two pointers (slow and fast) to find the middle of the list. The
slow pointer moves one step at a time, while the fast pointer moves two steps at
a time. When the fast pointer reaches the end, the slow pointer will be at the
middle.
○ Step 2: After finding the middle, we split the list into two halves by setting
slow.next to null.
○ Step 3: The reverseList method is used to reverse the second half of the list.
○ Step 4: We then merge the two halves by alternating nodes from the first half and
the reversed second half.
3. reverseList Method:
○ This method reverses the given linked list. We keep track of the previous node,
the current node, and the next node to reverse the list.
4. printList Method:
○ A helper method to print the linked list values for testing purposes.
5. main Method:
○ In the main method, we create a linked list with values [1, 2, 3, 4, 5] and
reorder it using the reorderList method. The reordered list is then printed: 1
-> 5 -> 2 -> 4 -> 3.
Time Complexity:
● O(n), where n is the number of nodes in the linked list. We traverse the list a constant
number of times (once for finding the middle, once for reversing the second half, and
once for merging the two halves).
Space Complexity:
● O(1), since we are rearranging the list in-place with only a few pointer variables (slow,
fast, firstHalf, secondHalf), and we do not use any extra space proportional to
the input size.
Test Cases:
1. Test Case 1:
The solution efficiently reorders the list in O(n) time and O(1) space by leveraging the
two-pointer technique to find the middle, list reversal, and in-place merging. This approach
ensures that the list is reordered correctly while maintaining the constraints of time and space
complexity.
The Remove Nth Node From End of List problem involves removing the nth node from the
end of a singly linked list. The goal is to remove the node in such a way that we only traverse
the list once and do not use extra space for storing the nodes.
Problem Statement:
Given the head of a linked list and an integer n, remove the nth node from the end of the list and
return its head.
You must solve it in O(n) time complexity and O(1) space complexity.
Example:
Input: head = [1, 2, 3, 4, 5], n = 2
Output: [1, 2, 3, 5]
Explanation: The second node from the end (value 4) is removed.
Approach:
To solve this problem efficiently in O(n) time and O(1) space, we can use the two-pointer
technique:
○ Use two pointers, fast and slow, both starting at the head of the list.
○ Move the fast pointer n steps ahead of the slow pointer. This ensures that
when the fast pointer reaches the end, the slow pointer will be at the node just
before the one that needs to be removed.
2. Step 2: Move both pointers:
○ Move both the fast and slow pointers one step at a time until the fast pointer
reaches the end of the list.
○ The slow pointer will be just before the node that needs to be removed.
3. Step 3: Remove the nth node:
○ The slow pointer’s next will be the node to remove. Simply update slow.next
to slow.next.next, which effectively removes the node.
4. Edge Case:
○ If the node to be removed is the head (i.e., the list length is n), simply return the
head’s next as the new head.
Java Solution:
public class RemoveNthNodeFromEnd {
// Step 4: Move both fast and slow pointers until fast reaches the end
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// Step 5: Remove the nth node from the end
slow.next = slow.next.next;
1. ListNode Class:
○This class represents a node in the singly linked list, with a val and a next
pointer.
2. removeNthFromEnd Method:
○ Dummy Node: A dummy node is used to simplify the edge case where the head
node is removed. The dummy node points to the head of the list.
○ Two Pointers: We use two pointers, fast and slow, both starting from the
dummy node. The fast pointer is moved n + 1 steps ahead of the slow
pointer.
○ Moving the Pointers: Both pointers are moved one step at a time until the fast
pointer reaches the end of the list. At this point, the slow pointer will be just
before the node that needs to be removed.
○ Removing the Node: The node to be removed is slow.next, so we update
slow.next to slow.next.next, effectively skipping the node.
○ Return: Finally, we return dummy.next as the new head of the list, which may
have been modified if the head was removed.
3. createList Method:
○ This helper method creates a linked list from an array of values for testing
purposes.
4. printList Method:
○ We create a linked list with the values [1, 2, 3, 4, 5], then remove the 2nd
node from the end (node with value 4). The new list [1, 2, 3, 5] is printed.
Time Complexity:
● O(n), where n is the number of nodes in the linked list. We only traverse the list twice
(once for moving the fast pointer and once for moving both pointers to the end).
Space Complexity:
● O(1), since we only use a constant amount of extra space (for the dummy node and two
pointers), regardless of the size of the list.
Test Cases:
1. Test Case 1:
Conclusion:
This solution efficiently removes the nth node from the end of a linked list using a two-pointer
technique with O(n) time complexity and O(1) space complexity. It handles edge cases like
removing the head node by using a dummy node, ensuring a clean solution.
The Copy List With Random Pointer problem involves creating a deep copy of a given linked
list, where each node in the list has two pointers:
1. Next Pointer: Points to the next node in the list (standard singly linked list pointer).
2. Random Pointer: Points to any random node in the list or null.
You need to create a deep copy of this list such that the next and random pointers of the
copied list are correctly set up, without modifying the original list.
Problem Statement:
You are given a head of a linked list, where each node has a next pointer and a random
pointer. You need to return a deep copy of this list.
You must solve it in O(n) time complexity and O(1) space complexity (no extra space for
hashmaps).
Example:
Input:
head = [[7,null], [13,0], [11,4], [10,2], [1,0]]
Output:
[[7,null], [13,0], [11,4], [10,2], [1,0]]
Explanation:
The list contains 5 nodes. The values of the nodes are [7, 13, 11, 10, 1].
The random pointers are set as follows:
- The random pointer of node 1 (value 7) is null.
- The random pointer of node 2 (value 13) points to node 1.
- The random pointer of node 3 (value 11) points to node 5 (value 1).
- The random pointer of node 4 (value 10) points to node 3 (value 11).
- The random pointer of node 5 (value 1) points to node 1.
Approach:
To solve this problem efficiently in O(n) time complexity and O(1) space complexity (without
extra space for hashmaps), we can use the following approach:
○ For each node in the original list, create a new node with the same value.
○ Insert this new node immediately after the original node. The new node's next
pointer will point to the next node of the original node.
○
2. Step 2: Set random pointers:
○ Traverse the modified list again, and for each original node, set the random
pointer of the newly created node (the node after the original node) to the
corresponding random node's next.
○ If the original node’s random pointer is null, then the new node's random
pointer will also be null.
3. Step 3: Separate the two lists:
○ Finally, we need to separate the interwoven list back into two separate lists: the
original list and the deep copy list.
○ The original list's next pointers remain intact, while the newly created list will
follow the next pointers of the interwoven nodes.
Java Solution:
public class CopyListWithRandomPointer {
Node() {}
Node(int val) {
this.val = val;
}
// Step 1: Create new nodes and interweave them with the original list
Node current = head;
while (current != null) {
Node copy = new Node(current.val);
copy.next = current.next;
current.next = copy;
current = copy.next;
}
return copyHead;
}
// Example linked list: [7,null] -> [13,0] -> [11,4] -> [10,2] -> [1,0]
Node head = solution.new Node(7);
head.next = solution.new Node(13);
head.next.next = solution.new Node(11);
head.next.next.next = solution.new Node(10);
head.next.next.next.next = solution.new Node(1);
1. Node Class:
○ A simple class for the list node with val, next, and random pointers.
2. copyRandomList Method:
○ Step 1: We traverse the original list and for each node, we create a new node
(copy) with the same value. The new node is inserted right after the original
node.
○ Step 2: We then traverse the list again and set the random pointers for the
copied nodes. If the random pointer of an original node points to a node, we set
the random pointer of the copied node to the next node of the original node's
random.
○ Step 3: We restore the original list and separate the copied list. We do this by
setting the next pointers of the original list to skip the copied nodes, and we
build the new list by setting the next pointers of the copied nodes.
3. printList Method:
○ This helper method is used to print the list for testing. It prints both the val and
the random pointer's val (if it exists).
4. main Method:
○ We create an example list with nodes having random pointers, then we make a
deep copy of the list and print both the original and the copied list.
Time Complexity:
● O(n), where n is the number of nodes in the linked list. We traverse the list three times
(once for copying nodes, once for setting random pointers, and once for separating the
two lists).
Space Complexity:
● O(1), since we do not use any extra space proportional to the input size. We only use a
constant amount of space for the pointers.
Test Cases:
1. Test Case 1:
The solution efficiently creates a deep copy of the linked list with random pointers using O(n)
time and O(1) space, leveraging the technique of interweaving the original and copied nodes
and separating them afterward.
The Add Two Numbers problem involves adding two numbers represented by linked lists. Each
node of the linked list contains a single digit of a number, and the digits are stored in reverse
order (i.e., the ones place is at the head). The goal is to add the two numbers and return the
result as a linked list, where the digits of the sum are also stored in reverse order.
Problem Statement:
You are given two non-empty linked lists representing two non-negative integers. The digits are
stored in reverse order, and each of their nodes contains a single digit. Add the two numbers
and return the result as a linked list.
You may assume the two numbers do not contain any leading zeros, except the number 0
itself.
Example:
Input:
l1 = [2, 4, 3] // represents the number 342
l2 = [5, 6, 4] // represents the number 465
Output:
[7, 0, 8] // represents the number 807
Approach:
1. Initialization:
○ Use two pointers, one for each list (l1 and l2), and start from the head of each
list.
○ Initialize a dummy node to store the result. The current pointer will be used to
build the result list.
○ Initialize a carry variable to handle sums that exceed 9 (since digits in a number
are between 0 and 9).
2. Iterate through the lists:
○Traverse both lists in parallel. For each pair of digits, calculate their sum along
with the carry from the previous addition.
○ If the sum exceeds 9, set the carry to 1 (since this will be carried over to the next
sum). Otherwise, set the carry to 0.
○ Add the digit of the current sum (mod 10) to the result list.
○ Move to the next nodes of the two lists.
3. Handle remaining carry:
○After the main loop, if there is a carry left (i.e., the sum of the last digits was
greater than 9), create a new node with the carry value.
4. Return the result:
○ The result list will start from the node after the dummy node, as the dummy node
was only a placeholder.
Java Solution:
public class AddTwoNumbers {
1. ListNode Class:
○Represents a node in the linked list with an integer value (val) and a next
pointer.
2. addTwoNumbers Method:
○ Dummy Node: A dummy node is used to simplify edge cases (like when the
result is a single node). The current pointer is used to build the resulting linked
list.
○ Carry Handling: A carry variable is used to store values greater than or equal
to 10 from previous sums. It ensures that the digits are correctly carried over to
the next node.
○ Traversal: We traverse both linked lists (l1 and l2) until we’ve processed all
nodes in both lists and the carry is 0.
○ Result Construction: The new nodes are created with the digits obtained from
the sum of corresponding nodes in l1 and l2. These new nodes are linked
together via the next pointer.
3. printList Method:
○ This helper method prints the values of the linked list in order for testing.
4. main Method:
○ This method creates two sample linked lists (l1 and l2), adds them together
using addTwoNumbers, and prints the result.
Time Complexity:
● O(n), where n is the maximum length of the two input lists. We process each list node
once and perform constant-time operations for each node.
Space Complexity:
● O(n), where n is the number of nodes in the result linked list. We create a new linked list
to store the sum, and its length is proportional to the longer of the two input lists.
Test Cases:
1. Test Case 1:
Conclusion:
The solution efficiently adds two numbers represented by linked lists in O(n) time complexity
and O(n) space complexity. It handles carry and edge cases, such as lists of different lengths or
a carry over at the end of the calculation.
The Find the Duplicate Number problem involves finding the duplicate number in an array that
contains n + 1 integers. These integers are in the range from 1 to n, meaning that one of the
numbers between 1 and n is duplicated.
Problem Statement:
You are given an integer array nums of length n + 1, where each element is an integer
between 1 and n (inclusive). There is exactly one duplicate number in the array, and you need to
find that duplicate number.
You are not allowed to modify the input array, and you need to solve the problem in O(n) time
and O(1) extra space.
Example:
Input:
nums = [1, 3, 4, 2, 2]
Output:
2
Approach:
To solve this problem efficiently in O(n) time and O(1) space, we can use Floyd’s Tortoise and
Hare (Cycle Detection) Algorithm, which is often used to detect cycles in linked lists. The idea
is based on treating the array indices as pointers and discovering a cycle formed by the
duplicate number.
1. Cycle Detection:
○ Treat each number in the array as an index, so nums[i] points to the next index
nums[nums[i]].
○ Since there is exactly one duplicate number, there must be a cycle in the array,
and the task becomes finding the entry point to that cycle (which is the duplicate
number).
2. Phase 1: Finding the Intersection Point (Tortoise and Hare):
○ Start with two pointers: slow (tortoise) and fast (hare). Initially, both start at the
first index nums[0].
○ The slow pointer moves one step at a time, and the fast pointer moves two
steps at a time. The idea is that if there is a cycle, the fast pointer will
eventually meet the slow pointer.
3. Phase 2: Finding the Entrance to the Cycle:
○ Once an intersection is found, reset one pointer to the start of the array and keep
the other pointer at the intersection. Move both pointers one step at a time; the
point at which they meet is the duplicate number.
Java Solution:
public class FindTheDuplicateNumber {
// Example input
int[] nums = {1, 3, 4, 2, 2};
1. Initialization:
○ We use two pointers, slow and fast, to traverse the array. Both pointers initially
start at nums[0].
2. Phase 1: Detect Cycle:
○ The slow pointer moves one step at a time (slow = nums[slow]), while the
fast pointer moves two steps at a time (fast = nums[nums[fast]]).
○ The do-while loop continues until the slow and fast pointers meet, indicating
that a cycle exists.
3. Phase 2: Find the Entrance to the Cycle:
○ Once an intersection is found, we reset one pointer to the beginning of the array
(pointer1 = nums[0]).
○ The other pointer (pointer2) remains at the intersection point.
○ Both pointers move one step at a time (pointer1 = nums[pointer1] and
pointer2 = nums[pointer2]).
○ The point where they meet is the duplicate number because that is the entrance
to the cycle in the array.
4. Return the Duplicate Number:
○ Once the two pointers meet, the pointer1 (or pointer2) will point to the
duplicate number, which we return.
Time Complexity:
● O(n), where n is the length of the array. In the worst case, we make two full passes over
the array: one to find the intersection and one to find the entrance of the cycle.
Space Complexity:
● O(1), because we only use a constant amount of extra space (just a few pointers).
Test Cases:
1. Test Case 1:
Conclusion:
The Floyd’s Tortoise and Hare Algorithm is an efficient way to find the duplicate number in an
array in O(n) time and O(1) space, making it optimal for solving the problem with the given
constraints.
The LRU (Least Recently Used) Cache problem asks to design a data structure that supports
the following operations:
● get(key): Returns the value of the key if the key exists, otherwise returns -1.
● put(key, value): Inserts or updates the value of the key. If the number of keys exceeds
the capacity, evict the least recently used key.
An LRU Cache works by storing only a limited number of elements, and when it reaches its
capacity, it evicts the least recently used element. The most recently used elements are moved
to the front.
Problem Statement:
Design and implement an LRU cache that supports the following operations:
● get(key): Returns the value of the key if the key exists, otherwise -1.
● put(key, value): Inserts or updates the value. If the number of keys exceeds the
capacity, evict the least recently used key.
You must implement the LRUCache class with the following methods:
● LRUCache(int capacity) Initializes the LRU cache with the given capacity.
● int get(int key) Returns the value of the key if the key exists, otherwise returns -1.
● void put(int key, int value) Inserts the value. If the number of keys exceeds
the capacity, evict the least recently used key.
Constraints:
● 1 ≤ capacity ≤ 3000
● 0 ≤ key ≤ 3000
● 0 ≤ value ≤ 10^4
Approach:
The core idea is to maintain the order of usage and ensure that the least recently used item is
evicted when the cache exceeds its capacity. We can use a doubly linked list to maintain the
order of usage and a hash map for fast lookups.
● Doubly Linked List: The nodes of the linked list will store the key-value pairs. The most
recently used items will be moved to the front of the list, and the least recently used
items will be at the end of the list.
1. Put operation: Insert the new key-value pair at the front. If the cache is full, remove the
last node (which is the least recently used).
2. Get operation: Move the accessed key to the front of the list to mark it as the most
recently used.
Java Solution:
import java.util.*;
class LRUCache {
// Get the value for the key, and move the node to the front
public int get(int key) {
if (!cache.containsKey(key)) {
return -1; // Key not found
}
return node.value;
}
// Insert the node at the front of the list (most recently used)
private void insertAtFront(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
1. Node Class:
○This class represents each node in the doubly linked list and stores the key-value
pair. Each node has a prev and next pointer to facilitate the movement of
nodes in the list.
2. LRUCache Constructor:
○ If the key exists in the cache, we retrieve the corresponding node, remove it from
its current position, and insert it at the front to mark it as the most recently used.
○ If the key does not exist, return -1.
4. put Method:
○ If the key already exists, we remove the existing node, update its value, and then
insert it at the front.
○ If the key doesn't exist, we insert the new key-value pair at the front. If the cache
exceeds its capacity, we evict the least recently used node (which is at the tail of
the list).
5. Helper Methods:
Time Complexity:
● get: O(1) - The get operation involves only accessing the hash map and moving the
corresponding node to the front of the list.
● put: O(1) - The put operation involves checking if the key exists, inserting/removing
nodes, and updating the cache, all of which take constant time.
Space Complexity:
● O(capacity): The space complexity is determined by the number of nodes in the cache,
which is at most equal to the capacity. The hash map stores capacity nodes, and
the doubly linked list also stores capacity nodes.
1. Test Case 1:
○ Input: lruCache.get(1);
○ Output: 1 — The value for key 1 is returned, and key 1 is moved to the front as
the most recently used.
3. Test Case 3:
○ Input: lruCache.put(3, 3);
○ Output: {1=1, 3=3} — Key 2 is evicted because it's the least recently used.
4. Test Case 4:
○ Input: lruCache.get(2);
○ Output: -1 — Key 2 was evicted in the previous step.
Conclusion:
The LRU Cache problem can be efficiently solved using a combination of a doubly linked list
and a hash map. This ensures O(1) time complexity for both the get and put operations while
maintaining the correct order of usage for eviction.
The Merge K Sorted Lists problem asks to merge k sorted linked lists into a single sorted
linked list. Each of the k lists is sorted in non-decreasing order.
Problem Statement:
You are given an array of k sorted linked lists. Merge all the linked lists into one sorted linked list
and return the merged list.
● Input: An array of k linked lists, where each list is sorted in non-decreasing order.
● Output: A single sorted linked list.
Example:
Input:
[
1 -> 4 -> 5,
1 -> 3 -> 4,
2 -> 6
]
Output:
1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6
Approach:
○ A more efficient approach would be to use a min-heap (or priority queue). The
idea is to add the first element of each list to the heap. The heap will allow us to
always extract the smallest element.
○ Once we extract the smallest element from the heap, we add the next element
from the same list to the heap.
○ This process ensures that the elements are merged in sorted order. The heap
ensures efficient extraction of the smallest element in O(log k) time.
3. Detailed Steps:
return dummy.next;
}
1. ListNode Class: This class represents a node in a linked list. It has an integer value
(val) and a pointer to the next node (next).
2. PriorityQueue (Min-Heap):
○The min-heap is used to efficiently find and remove the smallest node. We
compare nodes based on their val field, ensuring that the smallest node is
always at the top of the heap.
○ When we extract the smallest node, we add its next node (if it exists) back into
the heap.
3. mergeKLists Method:
○ First, we initialize the min-heap and add the head node of each linked list to it.
○ We then create a dummy node (dummy) to help simplify the code for constructing
the result list. current is used to track the last node in the merged list.
○ We keep extracting the smallest node from the heap and add it to the merged list.
If the extracted node has a next node, we add it to the heap.
○ Finally, we return the merged list starting from dummy.next.
4. Helper Function:
○ The printList method is used to print the resulting merged linked list for
testing.
Time Complexity:
● Building the Heap: Adding the first node of each list to the heap takes O(k log k),
where k is the number of lists.
● Processing All Nodes: For each of the n nodes across all lists, the heap operations
(inserting and extracting) take O(log k) time. Hence, the overall time complexity is O(n
log k), where n is the total number of nodes and k is the number of lists.
Space Complexity:
● The space complexity is O(k) due to the space used by the heap, which stores at most k
nodes at any time (one from each list).
● Additionally, we use O(n) space for storing the merged result list, so the overall space
complexity is O(n + k).
[
1 -> 4 -> 5,
1 -> 3 -> 4,
2 -> 6
]
1.
2. Step-by-step Execution:
○ First, all the heads of the lists are added to the min-heap:
■ minHeap = [1, 1, 2] (sorted by node values).
○ Extract the smallest node (1) from the heap:
■ minHeap = [1, 2], result list: 1
■ Add the next node (4) from the extracted list to the heap:
■ minHeap = [1, 2, 4].
○ Extract the next smallest node (1):
■ minHeap = [2, 4], result list: 1 -> 1
■ Add the next node (3) from the extracted list to the heap:
■ minHeap = [2, 3, 4].
○ Continue this process until the heap is empty, producing the final merged list:
■ 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6
Output:
3.
Conclusion:
The Merge K Sorted Lists problem can be efficiently solved using a min-heap (priority queue).
This approach ensures the merging process is done in O(n log k) time, where n is the total
number of nodes, and k is the number of linked lists. This method provides an optimal solution
for merging multiple sorted linked lists into one.
Reverse Nodes in K-Group Problem
The Reverse Nodes in K-Group problem asks to reverse the nodes of a linked list in groups of
size k. If there are fewer than k nodes remaining, leave them as is.
Problem Statement:
Given a linked list, reverse the nodes in groups of k and return its modified list. If the number of
nodes in the group is less than k, do not reverse those nodes.
Example:
Input: head = [1,2,3,4,5], k = 3
Output: [3,2,1,4,5]
Explanation: The first group of 3 nodes [1,2,3] is reversed to [3,2,1]. The remaining nodes [4,5]
are not reversed since there are fewer than k nodes.
Approach:
1. Traverse the list to determine the total length of the linked list.
2. Reverse groups of size k. For each group:
○ Reverse the nodes in the current group.
○ Connect the previous group's tail to the head of the newly reversed group.
○ After processing a full group, move the pointer to the next group.
3. Handle the last group: If there are fewer than k nodes left, do not reverse them. Just
connect them to the previous reversed part.
Detailed Steps:
1. First, check if the length of the linked list is less than k. If so, return the list as is.
2. Reverse the nodes in each group of k. To reverse a group of nodes:
○ Keep track of the previous, current, and next nodes.
○ Reverse the links in the group.
3. Once a group is reversed, connect it to the previous group’s tail.
4. After processing a group, move to the next k-group and repeat the process.
Java Solution:
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
// Connect the reversed group with the next part of the list
groupStart.next = nextGroupStart;
return dummy.next;
}
return prev;
}
// Output: 3 2 1 4 5
solution.printList(result);
}
}
1. ListNode Class: This class represents a node in the linked list. It contains the node's
value (val) and a pointer to the next node (next).
2. reverseKGroup Method:
○ This method reverses a linked list by iterating through the nodes and updating
their next pointers.
4. Helper Method (printList):
○ This method prints the linked list to help with testing and validation.
Time Complexity:
● Time Complexity: The algorithm traverses the linked list once to calculate the length
(O(n)), and then it processes each group of k nodes. For each group, it reverses the
nodes, which also takes O(k) time. In the worst case, the entire list is processed in
groups of size k. Therefore, the time complexity is O(n), where n is the number of nodes
in the linked list.
● Space Complexity: The space complexity is O(1), since we're only using a constant
amount of extra space, aside from the input linked list.
1. Input:
Conclusion:
The Reverse Nodes in K-Group problem can be efficiently solved by reversing nodes in
groups of k and connecting the groups together. The solution uses a helper method to reverse
each group and connects them in the correct order. The time complexity is linear, O(n), and the
space complexity is constant, O(1), making this solution both time and space efficient.