Singly Linked Lists

Let's use Design Linked List to design our Singly Linked List.

from typing import Generic, Optional, TypeVar

T = TypeVar("T")


class ListNode(Generic[T]):
    def __init__(self, val: T, next_ptr: Optional["ListNode"] = None) -> None:
        self.val = val
        self.next = next_ptr


class MyLinkedList:
    def __init__(self):
        self.size = 0
        self.head = ListNode(-1)

    def get(self, index: int) -> int:
        if index < 0 or index >= self.size:
            return -1

        return self._getNodeAtIndex(index + 1).val

    def addAtHead(self, val: int) -> None:
        self.addAtIndex(0, val)

    def addAtTail(self, val: int) -> None:
        self.addAtIndex(self.size, val)

    def addAtIndex(self, index: int, val: int) -> None:
        if index < 0 or index > self.size:
            return

        p = self._getNodeAtIndex(index)
        node = ListNode(val)
        node.next = p.next
        p.next = node

        self.size += 1

    def deleteAtIndex(self, index: int) -> None:
        if index < 0 or index >= self.size:
            return

        p = self._getNodeAtIndex(index)
        p.next = p.next.next

        self.size -= 1

    def _getNodeAtIndex(self, index: int) -> ListNode[int]:
        p = self.head

        for _ in range(index):
            p = p.next

        return p

The key idea here is to use a dummy head node to avoid to write a lot of code, which would be used to check the validity of the head itself.

Except for addAtHead, which is for the time complexity, all the other operations are .

The space complexity is for each operation, however, after operations we might end up having a list of elements (assuming that they are all valid consecutive insert operations).