0% found this document useful (0 votes)
30 views51 pages

Linked List

The document discusses three linked list problems: reversing a linked list, merging two sorted linked lists, and detecting a cycle in a linked list. It provides problem statements, example inputs and outputs, and detailed approaches for each problem, including iterative and recursive methods for reversing, a two-pointer approach for merging, and Floyd’s Tortoise and Hare algorithm for cycle detection. Each section includes Java solutions, explanations of the logic, time and space complexities, and test cases.

Uploaded by

gaurav5998soni
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
30 views51 pages

Linked List

The document discusses three linked list problems: reversing a linked list, merging two sorted linked lists, and detecting a cycle in a linked list. It provides problem statements, example inputs and outputs, and detailed approaches for each problem, including iterative and recursive methods for reversing, a two-pointer approach for merging, and Floyd’s Tortoise and Hare algorithm for cycle detection. Each section includes Java solutions, explanations of the logic, time and space complexities, and test cases.

Uploaded by

gaurav5998soni
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 51

Reverse Linked List Problem

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 = [1, 2]


Output: [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.

Steps for the Iterative Approach:

1. Traverse through the linked list.


2. For each node:
○ Save the next node (next).
○ Change the current node’s next to the previous node.
○ Move previous to the current node.
○ Move current to the next node.
3. Once the traversal is complete, the previous node will point to the new head of the
reversed list.

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.

Steps for the Recursive Approach:

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 {

// 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; }
}

public ListNode reverseList(ListNode head) {


ListNode previous = null;
ListNode current = head;

// Traverse the list and reverse the links


while (current != null) {
ListNode nextNode = current.next; // Save the next node
current.next = previous; // Reverse the current node's pointer
previous = current; // Move previous to the current node
current = nextNode; // Move current to the next node
}

return previous; // At the end, previous will be the new head of the reversed list
}

public static void main(String[] args) {


ReverseLinkedList solution = new ReverseLinkedList();

// Create a linked list: 1 -> 2 -> 3 -> 4 -> 5


ListNode head = solution.new ListNode(1);
head.next = solution.new ListNode(2);
head.next.next = solution.new ListNode(3);
head.next.next.next = solution.new ListNode(4);
head.next.next.next.next = solution.new ListNode(5);

// Reverse the list


ListNode reversedHead = solution.reverseList(head);

// Print the reversed list


ListNode current = reversedHead;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
}
}

Recursive Approach:
public class ReverseLinkedList {

// 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; }
}

public ListNode reverseList(ListNode head) {


// Base case: if head is null or only one node, return head
if (head == null || head.next == null) {
return head;
}

// Recursively reverse the rest of the list


ListNode rest = reverseList(head.next);

// After recursion, reverse the link


head.next.next = head;
head.next = null;

return rest; // The new head after the reversal


}

public static void main(String[] args) {


ReverseLinkedList solution = new ReverseLinkedList();

// Create a linked list: 1 -> 2 -> 3 -> 4 -> 5


ListNode head = solution.new ListNode(1);
head.next = solution.new ListNode(2);
head.next.next = solution.new ListNode(3);
head.next.next.next = solution.new ListNode(4);
head.next.next.next.next = solution.new ListNode(5);

// Reverse the list


ListNode reversedHead = solution.reverseList(head);

// Print the reversed list


ListNode current = reversedHead;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
}
}

Explanation of the Solution:

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).

Merge Two Sorted Lists Problem

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.

Return the head of the merged 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 {

// 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; }
}

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {


// Create a dummy node to simplify the merging process
ListNode dummy = new ListNode(-1);
ListNode current = dummy;

// Traverse both lists and merge them in sorted order


while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next; // Move the current pointer
}

// If there are remaining nodes in list1 or list2, attach them


if (list1 != null) {
current.next = list1;
} else {
current.next = list2;
}

// Return the merged list, starting from the next of dummy node
return dummy.next;
}

// Helper method to print the list (for testing purposes)


public void printList(ListNode head) {
ListNode current = head;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}

public static void main(String[] args) {


MergeTwoSortedLists solution = new MergeTwoSortedLists();

// Create two sorted linked lists: [1, 2, 4] and [1, 3, 4]


ListNode list1 = solution.new ListNode(1);
list1.next = solution.new ListNode(2);
list1.next.next = solution.new ListNode(4);

ListNode list2 = solution.new ListNode(1);


list2.next = solution.new ListNode(3);
list2.next.next = solution.new ListNode(4);

// Merge the two lists


ListNode mergedList = solution.mergeTwoLists(list1, list2);

// Print the merged list


solution.printList(mergedList); // Output: 1 1 2 3 4 4
}
}

Explanation of the Solution:

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:

○ We start by creating a dummy node to simplify the process. The current


pointer is initialized to point to this dummy node.
○ We iterate through both list1 and list2 simultaneously. In each iteration, we
compare the values at the current nodes of both lists. The smaller value is
appended to the merged list, and the corresponding pointer (list1 or list2) is
moved to the next node.
○ If one of the lists is exhausted, we simply append the remaining nodes of the
other list to the merged list.
3. Final Result:

○ 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:

○ Input: list1 = [1, 2, 4], list2 = [1, 3, 4]


○ Output: 1 -> 1 -> 2 -> 3 -> 4 -> 4
2. Test Case 2:

○ Input: list1 = [], list2 = [0]


○ Output: 0
3. Test Case 3:

○ Input: list1 = [1], list2 = []


○ Output: 1
4. Test Case 4:

○ Input: list1 = [1, 5, 9], list2 = [2, 6, 10]


○ Output: 1 -> 2 -> 5 -> 6 -> 9 -> 10

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).

Linked List Cycle Problem

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).

Input: head = [1, 2], pos = 0


Output: true
Explanation: There is a cycle in the linked list, where the tail connects to the node at position 0
(0-indexed).

Input: head = [1], pos = -1


Output: false
Explanation: There is no cycle in the linked list.

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:

● The slow pointer moves one step at a time.


● The fast pointer moves two steps at a time.

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 {

// 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; }
}

public boolean hasCycle(ListNode head) {


// If the list is empty or has only one node, there can't be a cycle
if (head == null || head.next == null) {
return false;
}

ListNode slow = head;


ListNode fast = head;

// Traverse the list with two pointers


while (fast != null && fast.next != null) {
slow = slow.next; // Move slow pointer by 1 step
fast = fast.next.next; // Move fast pointer by 2 steps

if (slow == fast) { // If slow and fast meet, there's a cycle


return true;
}
}

return false; // No cycle if fast pointer reaches the end of the list
}

// Helper method to create a cycle in the linked list (for testing)


public ListNode createListWithCycle() {
ListNode head = new ListNode(3);
ListNode node2 = new ListNode(2);
ListNode node0 = new ListNode(0);
ListNode nodeMinus4 = new ListNode(-4);

head.next = node2;
node2.next = node0;
node0.next = nodeMinus4;
nodeMinus4.next = node2; // Create a cycle by connecting the last node to node2

return head;
}

public static void main(String[] args) {


LinkedListCycle solution = new LinkedListCycle();

// Create a linked list with a cycle for testing


ListNode head = solution.createListWithCycle();

// Check if the list has a cycle


boolean result = solution.hasCycle(head);
System.out.println("Has Cycle: " + result); // Output: true
}
}

Explanation of the Solution:

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:

○ Input: head = [3, 2, 0, -4], cycle at position 1.


○ Output: true
○ Explanation: The list has a cycle where the last node (-4) points back to the
second node (2).
2. Test Case 2:

○ Input: head = [1, 2], cycle at position 0.


○ Output: true
○ Explanation: The list has a cycle where the second node (2) points back to the
first node (1).
3. Test Case 3:

○ Input: head = [1], no cycle.


○ Output: false
○ Explanation: The list contains only one node, and no cycle is formed.
4. Test Case 4:

○ Input: head = [], empty list.


○ Output: false
○ Explanation: An empty list cannot have a cycle.

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.

Reorder List Problem

The Reorder List problem involves rearranging a singly linked list in a specific order. The goal
is to reorder the list such that:

1. The first element is followed by the last element.


2. The second element is followed by the second-to-last element.
3. The third element is followed by the third-to-last element, and so on.

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]

Input: head = [1, 2, 3, 4]


Output: [1, 4, 2, 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 {

// 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; }
}

public void reorderList(ListNode head) {


if (head == null || head.next == null) {
return; // If the list is empty or has only one node, no reordering is needed.
}

// 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

// Step 3: Reverse the second half of the list


secondHalf = reverseList(secondHalf);

// Step 4: Merge the two halves


ListNode firstHalf = head;
while (secondHalf != null) {
ListNode temp1 = firstHalf.next;
ListNode temp2 = secondHalf.next;

firstHalf.next = secondHalf; // Link first half to second half


secondHalf.next = temp1; // Link second half to first half's next node

firstHalf = temp1; // Move first half pointer


secondHalf = temp2; // Move second half pointer
}
}

// Helper method to reverse the linked list


private ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode nextTemp = current.next;
current.next = prev;
prev = current;
current = nextTemp;
}
return prev;
}

// Helper method to print the linked list (for testing)


public void printList(ListNode head) {
ListNode current = head;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}
public static void main(String[] args) {
ReorderList solution = new ReorderList();

// Create a linked list: 1 -> 2 -> 3 -> 4 -> 5


ListNode head = solution.new ListNode(1);
head.next = solution.new ListNode(2);
head.next.next = solution.new ListNode(3);
head.next.next.next = solution.new ListNode(4);
head.next.next.next.next = solution.new ListNode(5);

// Reorder the list


solution.reorderList(head);

// Print the reordered list: 1 -> 5 -> 2 -> 4 -> 3


solution.printList(head);
}
}

Explanation of the Solution:

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:

○ Input: head = [1, 2, 3, 4, 5]


○ Output: 1 -> 5 -> 2 -> 4 -> 3
○ Explanation: After reordering, the list alternates between the first and last nodes,
then the second and second-last nodes, and so on.
2. Test Case 2:

○ Input: head = [1, 2, 3, 4]


○ Output: 1 -> 4 -> 2 -> 3
○ Explanation: The list is reordered by alternating the nodes between the first and
last, then the second and second-last.
3. Test Case 3:

○ Input: head = [1, 2, 3]


○ Output: 1 -> 3 -> 2
○ Explanation: The list is reordered in a similar alternating fashion.
4. Test Case 4:

○ Input: head = [1]


○ Output: 1
○ Explanation: A single-node list remains unchanged after reordering.
Conclusion:

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.

Remove Nth Node From End of List Problem

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.

Input: head = [1], n = 1


Output: []
Explanation: The list has only one node, so after removal, the list is empty.

Approach:

To solve this problem efficiently in O(n) time and O(1) space, we can use the two-pointer
technique:

1. Step 1: Initialize two pointers:

○ 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 {

// 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; }
}

public ListNode removeNthFromEnd(ListNode head, int n) {


// Step 1: Create a dummy node to handle edge case where the head is removed
ListNode dummy = new ListNode(0);
dummy.next = head;

// Step 2: Initialize the fast and slow pointers


ListNode fast = dummy;
ListNode slow = dummy;

// Step 3: Move the fast pointer n+1 steps ahead


for (int i = 1; i <= n + 1; i++) {
fast = fast.next;
}

// 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;

// Step 6: Return the new head (dummy.next is the new head)


return dummy.next;
}

// Helper method to create a linked list from an array (for testing)


public ListNode createList(int[] values) {
ListNode head = new ListNode(values[0]);
ListNode current = head;
for (int i = 1; i < values.length; i++) {
current.next = new ListNode(values[i]);
current = current.next;
}
return head;
}

// Helper method to print the linked list (for testing)


public void printList(ListNode head) {
ListNode current = head;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}

public static void main(String[] args) {


RemoveNthNodeFromEnd solution = new RemoveNthNodeFromEnd();

// Create a linked list: 1 -> 2 -> 3 -> 4 -> 5


ListNode head = solution.createList(new int[]{1, 2, 3, 4, 5});

// Remove the 2nd node from the end


head = solution.removeNthFromEnd(head, 2);

// Print the modified list: 1 -> 2 -> 3 -> 5


solution.printList(head);
}
}
Explanation of the Solution:

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:

○ A helper method to print the values of the linked list.


5. main 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:

○ Input: head = [1, 2, 3, 4, 5], n = 2


○ Output: [1, 2, 3, 5]
○ Explanation: The 2nd node from the end (node with value 4) is removed.
2. Test Case 2:

○ Input: head = [1], n = 1


○ Output: []
○ Explanation: The list only contains one node, so after removal, the list is empty.
3. Test Case 3:

○ Input: head = [1, 2, 3, 4, 5], n = 5


○ Output: [2, 3, 4, 5]
○ Explanation: The 5th node from the end (node with value 1) is removed.
4. Test Case 4:

○ Input: head = [1, 2], n = 1


○ Output: [1]
○ Explanation: The last node (value 2) is removed.

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.

Copy List With Random Pointer Problem

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.

● Each node in the list has the following attributes:


○ val: an integer value.
○ next: a pointer to the next node in the list.
○ random: a pointer to any node in the list (could be null).

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:

1. Step 1: Copy nodes and interweave with the original list:

○ 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.

After this step, the list will look like:


Original -> New -> Original -> New -> Original -> ...


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 {

// Definition for a node in the linked list


public class Node {
int val;
Node next;
Node random;

Node() {}

Node(int val) {
this.val = val;
}

Node(int val, Node next, Node random) {


this.val = val;
this.next = next;
this.random = random;
}
}

public Node copyRandomList(Node head) {


if (head == null) {
return null;
}

// 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;
}

// Step 2: Set the random pointers for the copied nodes


current = head;
while (current != null) {
if (current.random != null) {
current.next.random = current.random.next;
}
current = current.next.next;
}

// Step 3: Separate the two lists


Node original = head;
Node copyHead = head.next;
Node copy = copyHead;

while (original != null) {


original.next = original.next.next; // Restore the original list
if (copy.next != null) {
copy.next = copy.next.next; // Separate the copy list
copy = copy.next;
}
original = original.next;
}

return copyHead;
}

// Helper method to print the list (for testing)


public void printList(Node head) {
Node current = head;
while (current != null) {
System.out.print("[" + current.val + ", ");
System.out.print(current.random != null ? current.random.val : "null");
System.out.print("] ");
current = current.next;
}
System.out.println();
}
public static void main(String[] args) {
CopyListWithRandomPointer solution = new CopyListWithRandomPointer();

// 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);

// Setting random pointers


head.random = null;
head.next.random = head; // 13's random points to 7
head.next.next.random = head.next.next.next.next; // 11's random points to 1
head.next.next.next.random = head.next.next; // 10's random points to 11
head.next.next.next.next.random = head; // 1's random points to 7

// Copy the list


Node copiedHead = solution.copyRandomList(head);

// Print the original and copied list


System.out.println("Original List:");
solution.printList(head);
System.out.println("Copied List:");
solution.printList(copiedHead);
}
}

Explanation of the Solution:

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:

○ 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 is copied correctly, and the random pointers are maintained.
2. Test Case 2:

○ Input: head = [1, null]


○ Output: [1, null]
○ Explanation: A single-node list, with no random pointer, is copied.
3. Test Case 3:

○ Input: head = [] (empty list)


○ Output: []
○ Explanation: The list is empty, so the output is also empty.
Conclusion:

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.

Add Two Numbers Problem

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

Explanation: 342 + 465 = 807.

Approach:

To solve this problem:

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 {

// 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; }
}

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {


// Dummy node to store the result
ListNode dummy = new ListNode(0);
ListNode current = dummy; // Pointer to build the result list
int carry = 0; // To handle the carry from previous sum

// Traverse both lists while there are still digits to process


while (l1 != null || l2 != null || carry != 0) {
// Get the current values (if the node exists, otherwise 0)
int x = (l1 != null) ? l1.val : 0;
int y = (l2 != null) ? l2.val : 0;

// Calculate the sum and the new carry


int sum = x + y + carry;
carry = sum / 10; // Calculate the carry for the next iteration
int digit = sum % 10; // The current digit (sum % 10)

// Add the digit as a new node in the result list


current.next = new ListNode(digit);
current = current.next; // Move the current pointer

// Move to the next nodes if they exist


if (l1 != null) l1 = l1.next;
if (l2 != null) l2 = l2.next;
}

// Return the result list (skip the dummy node)


return dummy.next;
}

// Helper method to print the list (for testing)


public void printList(ListNode head) {
ListNode current = head;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}

public static void main(String[] args) {


AddTwoNumbers solution = new AddTwoNumbers();

// Create two linked lists representing 342 and 465


ListNode l1 = solution.new ListNode(2);
l1.next = solution.new ListNode(4);
l1.next.next = solution.new ListNode(3);

ListNode l2 = solution.new ListNode(5);


l2.next = solution.new ListNode(6);
l2.next.next = solution.new ListNode(4);

// Add the two numbers


ListNode result = solution.addTwoNumbers(l1, l2);

// Print the result


solution.printList(result); // Output: 7 0 8
}
}

Explanation of the Solution:

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:

○ Input: l1 = [2, 4, 3], l2 = [5, 6, 4]


○ Output: [7, 0, 8]
○ Explanation: 342 + 465 = 807. The result is returned in reverse order.
2. Test Case 2:

○ Input: l1 = [0], l2 = [0]


○ Output: [0]
○ Explanation: 0 + 0 = 0.
3. Test Case 3:

○ Input: l1 = [9, 9, 9], l2 = [1]


○ Output: [0, 0, 0, 1]
○ Explanation: 999 + 1 = 1000, and the result is in reverse order.
4. Test Case 4:

○ Input: l1 = [5], l2 = [5]


○ Output: [0, 1]
○ Explanation: 5 + 5 = 10, so the result is [0, 1] with carry over.

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.

Find the Duplicate Number Problem

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

Explanation: The duplicate number in the array is 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.

Here’s the idea:

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 {

public int findDuplicate(int[] nums) {


// Phase 1: Find the intersection point (Tortoise and Hare)
int slow = nums[0];
int fast = nums[0];

// Move slow by 1 step and fast by 2 steps until they meet


do {
slow = nums[slow]; // Move slow pointer 1 step
fast = nums[nums[fast]]; // Move fast pointer 2 steps
} while (slow != fast);

// Phase 2: Find the entrance to the cycle


// Reset one pointer to the start and keep the other at the intersection
int pointer1 = nums[0];
int pointer2 = slow;

// Move both pointers one step at a time until they meet


while (pointer1 != pointer2) {
pointer1 = nums[pointer1]; // Move pointer1 1 step
pointer2 = nums[pointer2]; // Move pointer2 1 step
}

// The duplicate number is where they meet


return pointer1;
}

public static void main(String[] args) {


FindTheDuplicateNumber solution = new FindTheDuplicateNumber();

// Example input
int[] nums = {1, 3, 4, 2, 2};

// Find the duplicate number


int result = solution.findDuplicate(nums);

// Print the result


System.out.println("The duplicate number is: " + result); // Output: 2
}
}

Explanation of the Solution:

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:

○ Input: nums = [1, 3, 4, 2, 2]


○ Output: 2
○ Explanation: The duplicate number is 2.
2. Test Case 2:

○ Input: nums = [3, 1, 3, 4, 2]


○ Output: 3
○ Explanation: The duplicate number is 3.
3. Test Case 3:

○ Input: nums = [1, 1]


○ Output: 1
○ Explanation: The duplicate number is 1.
4. Test Case 4:

○ Input: nums = [5, 4, 3, 2, 1, 5]


○ Output: 5
○ Explanation: The duplicate number is 5.

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.

LRU Cache Problem

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.

● HashMap: This allows us to access any key in O(1) time.

To implement the cache, the idea is:

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 {

// Doubly linked list node


class Node {
int key, value;
Node prev, next;

Node(int key, int value) {


this.key = key;
this.value = value;
}
}
private final int capacity;
private final Map<Integer, Node> cache; // Map to store the key and its corresponding node
private final Node head, tail; // Dummy head and tail nodes for easier manipulation of the list

public LRUCache(int capacity) {


this.capacity = capacity;
cache = new HashMap<>();
head = new Node(0, 0); // Dummy head
tail = new Node(0, 0); // Dummy tail
head.next = tail;
tail.prev = head;
}

// 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
}

Node node = cache.get(key);


remove(node); // Remove the node from its current position
insertAtFront(node); // Insert it at the front (most recently used)

return node.value;
}

// Put the key-value pair into the cache


public void put(int key, int value) {
if (cache.containsKey(key)) {
// If key already exists, remove it
Node node = cache.get(key);
remove(node);
} else if (cache.size() == capacity) {
// If the cache is full, remove the least recently used item
cache.remove(tail.prev.key);
remove(tail.prev);
}

// Insert the new node at the front


Node newNode = new Node(key, value);
insertAtFront(newNode);
cache.put(key, newNode);
}
// Remove the node from the doubly linked list
private void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}

// 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;
}

public static void main(String[] args) {


LRUCache lruCache = new LRUCache(2);

lruCache.put(1, 1); // Cache is {1=1}


lruCache.put(2, 2); // Cache is {1=1, 2=2}
System.out.println(lruCache.get(1)); // returns 1, Cache is {2=2, 1=1}
lruCache.put(3, 3); // Evicts key 2, Cache is {1=1, 3=3}
System.out.println(lruCache.get(2)); // returns -1 (not found)
lruCache.put(4, 4); // Evicts key 1, Cache is {3=3, 4=4}
System.out.println(lruCache.get(1)); // returns -1 (not found)
System.out.println(lruCache.get(3)); // returns 3, Cache is {4=4, 3=3}
System.out.println(lruCache.get(4)); // returns 4, Cache is {3=3, 4=4}
}
}

Explanation of the Solution:

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:

○ We initialize the cache with a given capacity.


○ We also create a dummy head and tail for the doubly linked list, which simplifies
the logic for adding and removing nodes from the list.
3. get Method:

○ 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:

○ remove: Removes a node from the doubly linked list.


○ insertAtFront: Inserts a node at the front of the doubly linked list.

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.

Test Case Explanation:

1. Test Case 1:

○ Input: lruCache.put(1, 1); lruCache.put(2, 2);


○ Output: {1=1, 2=2} — The cache contains two elements with keys 1 and 2.
2. Test Case 2:

○ 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.

Merge K Sorted Lists Problem

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:

1. Naive Approach (Merging Two Lists at a Time):


○We could repeatedly merge two lists at a time. This would involve merging two
lists using a helper function and then merging the result with the next list. The
time complexity would be O(k * n), where n is the number of elements and k is
the number of lists.
2. Optimized Approach (Using Min-Heap or Priority Queue):

○ 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:

○ Initialize a min-heap (priority queue).


○ Add the first node of each linked list into the heap.
○ Extract the smallest element from the heap, and add it to the result list.
○ If the extracted node has a next node, add that to the heap.
○ Repeat the above steps until all nodes are processed.

Java Solution (Using Min-Heap):


import java.util.PriorityQueue;

// Definition for singly-linked list.


class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

public class MergeKSortedLists {

// Approach: Using Min-Heap (Priority Queue)


public ListNode mergeKLists(ListNode[] lists) {
// Create a min-heap
PriorityQueue<ListNode> minHeap = new PriorityQueue<>((a, b) -> a.val - b.val);

// Add the head node of each list into the min-heap


for (ListNode list : lists) {
if (list != null) {
minHeap.add(list);
}
}

// Create a dummy node for the result linked list


ListNode dummy = new ListNode(0);
ListNode current = dummy;

// Process the heap


while (!minHeap.isEmpty()) {
// Get the smallest node from the heap
ListNode node = minHeap.poll();
current.next = node;
current = current.next;

// If the extracted node has a next node, add it to the heap


if (node.next != null) {
minHeap.add(node.next);
}
}

return dummy.next;
}

// Helper function to print the list (for testing)


public void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static void main(String[] args) {


MergeKSortedLists solution = new MergeKSortedLists();

// Test Case: Example lists


ListNode list1 = new ListNode(1, new ListNode(4, new ListNode(5)));
ListNode list2 = new ListNode(1, new ListNode(3, new ListNode(4)));
ListNode list3 = new ListNode(2, new ListNode(6));

ListNode[] lists = {list1, list2, list3};

ListNode result = solution.mergeKLists(lists);


// Print the merged list
solution.printList(result); // Output: 1 1 2 3 4 4 5 6
}
}

Explanation of the Solution:

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.

So, the total time complexity is O(n log k).

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).

Test Case Walkthrough:


Input:

[
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:

1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6

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.

● Input: A linked list and an integer k (the size of the group).


● Output: The linked list with the nodes reversed in groups of size k.

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.

Input: head = [1,2,3,4,5], k = 2


Output: [2,1,4,3,5]

Approach:

The solution involves the following steps:

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; }
}

public class ReverseNodesInKGroup {

public ListNode reverseKGroup(ListNode head, int k) {


// Check if the list has at least k nodes
ListNode current = head;
int length = 0;
while (current != null) {
length++;
current = current.next;
}

// If the number of nodes is less than k, return the head as is


if (length < k) {
return head;
}

// Initialize a dummy node to serve as the previous node of the head


ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prevGroupEnd = dummy;

// Process the list in groups of k


while (length >= k) {
ListNode groupStart = prevGroupEnd.next;
ListNode groupEnd = groupStart;

// Move the groupEnd to the last node of the group


for (int i = 1; i < k; i++) {
groupEnd = groupEnd.next;
}

// Save the node after the group


ListNode nextGroupStart = groupEnd.next;

// Reverse the current group


groupEnd.next = null;
prevGroupEnd.next = reverseList(groupStart);

// Connect the reversed group with the next part of the list
groupStart.next = nextGroupStart;

// Move the prevGroupEnd pointer to the end of the reversed group


prevGroupEnd = groupStart;

// Update the remaining length of the list


length -= k;
}

return dummy.next;
}

// Helper method to reverse a linked list


private ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;

while (curr != null) {


ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}

return prev;
}

// Helper function to print the list (for testing)


public void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static void main(String[] args) {


ReverseNodesInKGroup solution = new ReverseNodesInKGroup();

// Test Case: Example list [1,2,3,4,5] with k = 3


ListNode list = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new
ListNode(5)))));
solution.printList(list);

ListNode result = solution.reverseKGroup(list, 3);

// Output: 3 2 1 4 5
solution.printList(result);
}
}

Explanation of the Solution:

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:

○ Step 1: We first calculate the length of the linked list.


○ Step 2: If the list has fewer than k nodes, we return the list as is.
○ Step 3: We reverse each group of k nodes. For each group:
■ Find the start and end nodes of the group.
■ Reverse the group using the helper method reverseList.
■ Connect the reversed group to the rest of the list.
○ Step 4: We keep track of the end of the previous group (prevGroupEnd) to
correctly link the reversed groups together.
○ Step 5: The loop runs until there are fewer than k nodes left to reverse.
3. reverseList 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.

Test Case Walkthrough:

1. Input:

○ Linked list: [1 -> 2 -> 3 -> 4 -> 5]


○ k = 3
2. Execution:

○ First, we reverse the group [1, 2, 3] to get [3, 2, 1].


○ Then, the remaining nodes [4, 5] are not reversed because the number of
nodes is less than k.
3. Output:

○ Final list: [3 -> 2 -> 1 -> 4 -> 5]

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.

You might also like