This problem asks you to design and implement your own linked list from scratch, either as a singly linked list or a doubly linked list, depending on your choice. Here’s a simplified explanation of the requirements:
- Constructor (MyLinkedList):
- get(int index)
- addAtHead(int val)
- addAtTail(int val)
- addAtIndex(int index, int val)
- deleteAtIndex(int index)
Tradeoffs Between Singly and Doubly Linked List
1. Singly Linked List:
• Simpler to implement.
• Requires less memory because each node only stores next.
• Cannot traverse backward.
class SinglyLinkedList {
class Node {
int value;
Node next;
Node(int value) {
this.value = value;
}
}
private Node head;
public SinglyLinkedList() {
this.head = null;
}
public void addAtHead(int val) {
Node newNode = new Node(val);
newNode.next = head;
head = newNode;
}
public void addAtTail(int val) {
if (head == null) {
head = new Node(val);
return;
}
Node curr = head;
while (curr.next != null) {
curr = curr.next;
}
curr.next = new Node(val);
}
public void deleteAtIndex(int index) {
if (index == 0 && head != null) {
head = head.next;
return;
}
Node curr = head;
for (int i = 0; i < index - 1 && curr != null; i++) {
curr = curr.next;
}
if (curr == null || curr.next == null) return;
curr.next = curr.next.next;
}
}
Line-By-Line Walkthrough
Step 1: Create Node
class Node {
int value;
Node next;
Node(int value) {
this.value = value;
}
}
- A linked list is made up of nodes, each storing a value and a pointer to the next node.
Step 2: State
private Node head;
private int size;
- The head allows the list to keep track of its starting point for traversals and operations.
Step 3: Constructor
public SinglyLinkedList() {
this.head = null;
this.size = 0;
}
- Initializes an empty linked list
- Sets head to null and size to 0;
Step 4: Get
public int get(int index) {
if (index < 0 || index >= size) return -1;
Node curr = head;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
return curr.value;
}
if(index < 0 || index >= size)
- Validates the index and prevents invalid memory access.
Node curr = head;
- Start traversal at the head of the linked list.
for(int i = 0; i < index; i++)
- Traverses the list until the desired index.
Step 5: Add At Head
public void addAtHead(int val) {
Node newNode = new Node(val);
newNode.next = head;
head = newNode;
size++;
}
Node newNode = new Node(val)
;- Creates a new node with the given value
newNode.next = head;
- Links the new node to the current head
head = newNode;
- Updates the head to point to the new node.
size++
- Increment our state so that we can access it later.
Step 5: Add At Tail
public void addAtTail(int val) {
if (head == null) {
head = new Node(val);
} else {
Node curr = head;
while (curr.next != null) {
curr = curr.next;
}
curr.next = new Node(val);
}
size++;
}
if(head == null)
- Handles edge case where the list is empty
Node curr = head
- Assign new pointer so that we don’t manipulate the actual head.
while(curr.next != null)
- Traverses the list to find the last ndoe
curr.next = new Node(val)
- Adds the new node at the end of the list.
Step 6: AddAtIndex
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) return;
if (index == 0) {
addAtHead(val);
} else {
Node curr = head;
for (int i = 0; i < index - 1; i++) {
curr = curr.next;
}
Node newNode = new Node(val);
newNode.next = curr.next;
curr.next = newNode;
size++;
}
}
if(index < 0 || index > size)
- Validates the index
if(index == 0)
- Add to head
for(int i = 0; i < index - 1; i++)
- Traverses to the node just before the target index
newNode.next = curr.next; curr.next = newNode;
- Inserts the new node at the desired position.
Step 7: Delete At Index
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) return;
if (index == 0) {
head = head.next;
} else {
Node curr = head;
for (int i = 0; i < index - 1; i++) {
curr = curr.next;
}
curr.next = curr.next.next;
}
size--;
}
if(index < 0 || index >= size)
- Validates the index. If invalid, do nothing.
if(index == 0)
- Special case: Adding at the head.
for(int i = 0; i < index - 1; i++)
- Traverses to the node just before the target index.
curr.next = curr.next.next;
- Inserts the new node at the desired position
Doubly-Linked List
2. Doubly Linked List:
• Allows bi-directional traversal, making some operations (e.g., deletion) faster.
• Uses more memory because each node stores prev and next.
• Slightly more complex to manage pointers during insertion and deletion.
class DoublyLinkedList {
class Node {
int value;
Node prev, next;
Node(int value) {
this.value = value;
}
}
private Node head, tail;
private int size;
public DoublyLinkedList() {
this.head = new Node(0); // Dummy head
this.tail = new Node(0); // Dummy tail
this.head.next = this.tail;
this.tail.prev = this.head;
this.size = 0;
}
public int get(int index) {
if (index < 0 || index >= size) return -1;
Node curr = head.next;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
return curr.value;
}
public void addAtHead(int val) {
Node newNode = new Node(val);
Node next = head.next;
newNode.next = next;
newNode.prev = head;
head.next = newNode;
next.prev = newNode;
size++;
}
public void addAtTail(int val) {
Node newNode = new Node(val);
Node prev = tail.prev;
newNode.prev = prev;
newNode.next = tail;
prev.next = newNode;
tail.prev = newNode;
size++;
}
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) return;
Node curr = head;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
Node newNode = new Node(val);
Node next = curr.next;
newNode.prev = curr;
newNode.next = next;
curr.next = newNode;
next.prev = newNode;
size++;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) return;
Node curr = head;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
Node prev = curr;
Node next = curr.next.next;
prev.next = next;
if (next != null) {
next.prev = prev;
}
size--;
}
}
Line-By-Line Explanation
Step 1: Create Node
class Node {
int value;
Node prev, next;
Node(int value) {
this.value = value;
}
}
- Represents an individual node in the list
- Each node contains:
- value: The data stored in the node.
- prev: A reference to the previous node.
- next: A reference to the next node.
Step 2: State
private Node head, tail;
private int size;
- head: Dummy node representing the start of the list.
- tail: Dummy node representing the end of the list.
- size: Help validate indices and improves efficiency when checking the list’s length.
Step 3: Constructor
public DoublyLinkedList() {
this.head = new Node(0); // Dummy head
this.tail = new Node(0); // Dummy tail
this.head.next = this.tail;
this.tail.prev = this.head;
this.size = 0;
}
- Dummy nodes simplify operations (adding or removing nodes) by eliminating the need for special cases when the list is empty or has one element
Step 4: Get
public int get(int index) {
if (index < 0 || index >= size) return -1;
Node curr = head.next;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
return curr.value;
}
if(index < 0 || index >= size)
- Validates the index
Node curr = head.next;
- Starts traversal from the first real node (after the dummy head).
for(int i = 0; i < index; i++)
- Traverses the list until the desired index is reached.
Step 5: Add at head
public void addAtHead(int val) {
Node newNode = new Node(val);
Node next = head.next;
newNode.next = next;
newNode.prev = head;
head.next = newNode;
next.prev = newNode;
size++;
}
Node newNode = new Node(val)
- Creates a new node with the given value.
Node next = head.next;
- Stores a reference to the current first node.
- Link Updates
- newNode.next = next: Points the new node to the current first node.
- newNode.prev = head: Points the new node back to the dummy head.
- head.next = newNode: Updates the dummy head to point to the new node.
- next.prev = newNode: Updates the old first node to point back to the new node.
- size++: Increments the size of the list.
Step 6: Add at tail
public void addAtTail(int val) {
Node newNode = new Node(val);
Node prev = tail.prev;
newNode.prev = prev;
newNode.next = tail;
prev.next = newNode;
tail.prev = newNode;
size++;
}
Node newNode = new Node(val);
- Creates a new node with the given value.
Node prev = tail.prev;
- Retrieves the current last node.
- Link Updates:
- newNode.prev = prev: Points the new node back to the current last node.
- newNode.next = tail: Points the new node forward to the dummy tail.
- prev.next = newNode: Updates the last node to point to the new node.
- tail.prev = newNode: Updates the dummy tail to point back to the new node.
- size++
- Increments the size of the list.
Step 7: Add At Index
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) return;
Node curr = head;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
Node newNode = new Node(val);
Node next = curr.next;
newNode.prev = curr;
newNode.next = next;
curr.next = newNode;
next.prev = newNode;
size++;
}
if(index < 0 || index > size)
- Validates the index
Node curr = head;
- Assigns pointer so that the head is not directly manipulated during traversal
Node newNode = new Node(val)
- Creates the new node.
- Link Updates:
- Same as in addAtHead or addAtTail, but generalized for any position.
Step 8: Delete At Index
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) return;
Node curr = head;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
Node prev = curr;
Node next = curr.next.next;
prev.next = next;
if (next != null) {
next.prev = prev;
}
size--;
}
if(index < 0 || index > size)
- Validates the index
Node curr = head;
- Assigns pointer so that the head is not directly manipulated during traversal
Node newNode = new Node(val)
- Creates the new node.
- Link Updates:
- prev.next = next skips the target node.
- next.prev = prev updates the back pointer
Why This Code Works
• Use singly linked lists when memory efficiency and simple operations are priorities.
• Use doubly linked lists when you need fast insertions/deletions at both ends or bi-directional navigation.
Both implementations demonstrate a clear understanding of data structure fundamentals and strike a balance between performance, clarity, and adaptability, making them excellent solutions for LeetCode and real-world scenarios alike.