707. Design Linked List

We’re required to desing our own implementation of a Linked List. It can be singly or doubly linked, doesn’t matter, just has to meet the requirements. The biggest trick is to go ahead and use a doubly linked list. It’s easier to reason about and faster than a singly linked list to traverse.

First we start off by declaring the ListNode class. Then we create the init method for the MyLinkedList class, creating a head and tail with two dummy (sentinel) nodes. We also have to track the size of the linked list.

Adding nodes at the head and tail are pretty straightforward. Just realize the head and tail will always be dummy nodes and the predecessor and successor of the new node, respectively.

When indexing into the doubly linked list, we’ll calculate the distance from the head or tail to the requested index. This helps us decide whether to traverse from the head or tail, decreasing the amount of time it takes for us to conduct an operation.

The solution is as follows:

class ListNode:
    def __init__(self, val, pred=None, succ=None):
        self.val = val
        self.prev, self.next = pred, succ
 
class MyLinkedList:
    def __init__(self):
        self.size = 0
        self.head, self.tail = ListNode(0), ListNode(0)
        self.head.next, self.tail.prev = self.tail, self.head
 
    def get(self, index: int) -> int:
        if index < 0 or index >= self.size:
            return -1
 
        if index + 1 < self.size - index:
            curr = self.head
            for _ in range(index + 1):
                curr = curr.next
        else:
            curr = self.tail
            for _ in range(self.size - index):
                curr = curr.prev
 
        return curr.val
 
    def addAtHead(self, val: int) -> None:
        pred, succ = self.head, self.head.next
        self.size += 1
        pred.next = succ.prev = ListNode(val, pred, succ)
 
    def addAtTail(self, val: int) -> None:
        pred, succ = self.tail.prev, self.tail
        self.size += 1
        pred.next = succ.prev = ListNode(val, pred, succ)
 
    def addAtIndex(self, index: int, val: int) -> None:
        if index > self.size:
            return
        if index < 0:
            index = 0
 
        if index < self.size - index:
            pred = self.head
            for _ in range(index):
                pred = pred.next
            succ = pred.next
        else:
            succ = self.tail
            for _ in range(self.size - index):
                succ = succ.prev
            pred = succ.prev
 
        self.size += 1
        pred.next = succ.prev = ListNode(val, pred, succ)
 
    def deleteAtIndex(self, index: int) -> None:
        if index < 0 or index >= self.size:
            return
 
        if index < self.size - index:
            pred = self.head
            for _ in range(index):
                pred = pred.next
            succ = pred.next.next
        else:
            succ = self.tail
            for _ in range(self.size - index - 1):
                succ = succ.prev
            pred = succ.prev.prev
 
        self.size -= 1
        pred.next, succ.prev = succ, pred

_ Time Complexity:

O(1) - For adding at the tail or head. O(min(k, N-k)) - For adding at an index, getting, or deleting a node, where k is the index.

_ Space Complexity:

O(1) - We maintain two pointers, slow and fast.