Data Structures

Arrays

A contiguous memory block that stores elements of the same data type. The memory address of the first element in the array is the base address and any other element’s address can be calculated using the base address and the size of each element.

array = []
array.append(1)
array.append(3)
print(array[1]) # 3
array.append(2)
print(array.remove(3)) # 3
print(array.index(2)) # 1

Operations

  • Access by index: O(1) by calculating the position
  • Access by value: O(n) by traversal
  • Write in the beginning: O(n), requires shifting elements
  • Write in the end: O(1), no shifting required

Pros And Cons

Pros: Direct access using indexes, contiguous storage minimizes memory overhead, easy to sort. Cons: Fixed (limited) size, inefficient writing due to shifting, unfilled memory keep allocated.

Linked List

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class LinkedList:
    def __init__(self):
        self.head = None
# implements the following methods:
#    def append(self, val):
#    def remove(self, val):
#    def display(self):

A linear data structure where elements (nodes) are connected through pointers. Doesn’t require contiguous memory locations and each node contains a reference to the next one, allowing to be placed anywhere in the memory.

Operations

  • Access by index: O(n) by traversal
  • Access by value: O(n) by traversal
  • Write in the beginning: O(1) by pointing the new node to the old head
  • Write in the end: O(n) by traversing and then pointing the last node to the new

Pros And Cons

Pros: Dynamic size, no waste of memory, more efficient writing operations. Cons: Access always requires traversing, node requires extra memory for pointers, traversing have a higher constant factor compared to arrays.

Stack

It’s a LIFO-behaved data structure with a pointer to the last element for fast access. The stack itself is a pre-allocated region of memory with a fixed size, and the memory for individual function calls on the stack is dynamically managed. So the nature of the allocation depends on the type of stack and the context in which it’s used.

stack = []
stack.append(1)
stack.append(3)
print(stack.pop()) # 3
stack.append(2)
print(stack.pop()) # 2
print(stack.pop()) # 1

Operations

  • Access in the end: O(1) by stack pointer
  • Write in the end: O(1) by accessing via stack pointer and updating it

Queue

It’s a FIFO-behaved data structure that also can be pre-allocated or dynamically allocated depending on the type and context.

from collections import deque

queue = deque()
deque.append(1)
deque.append(3)
print(queue.popleft()) # 1
deque.append(2)
print(queue.popleft()) # 3
print(queue.popleft()) # 2

Operations

  • Access in the beginning/end: O(1) by pointers
  • Enqueue (insertion): O(1) by accessing via pointer and updating it
  • Dequeue (deletion): O(1) by accessing via pointer and updating it

Hash Map

It’s a key-value pair collection, where each key is unique. It provides operation efficiency (all operations are O(1)) through memory overhead. Uses hashing function to convert a key into an index and maintains an array where each index corresponds to a bucket that can hold multiple pairs.

Set

It’s a unordered collection of distinct elements. Internally uses hash maps and all operations are O(1).

Binary Tree

It’s a hierarchical data structure in which each node has at most two children and the topmost node is called root.

Traversal

def preorder_print(self, start, traversal):
    """Root->Left->Right"""
    if start:
        traversal += (str(start.value) + "-")
        traversal = self.preorder_print(start.left, traversal)
        traversal = self.preorder_print(start.right, traversal)
    return traversal
def inorder_print(self, start, traversal):
    """Left->Root->Right"""
    if start:
        traversal = self.inorder_print(start.left, traversal)
        traversal += (str(start.value) + "-")
        traversal = self.inorder_print(start.right, traversal)
    return traversal
def postorder_print(self, start, traversal):
    """Left->Right->Root"""
    if start:
        traversal = self.postorder_print(start.left, traversal)
        traversal = self.postorder_print(start.right, traversal)
        traversal += (str(start.value) + "-")
    return traversal

Heap

Is a specialized tree-based data structure that satisfies the heap property (for max and min heaps) and all operations are O(log n). It’s used to implement priority queues, for heap sort and graph algorithms like Dijkstra’s.

import heapq

min_heap = []
heapq.heappush(min_heap, 4)
heapq.heappush(min_heap, 1)
heapq.heappush(min_heap, 3)
min_element = heapq.heappop(min_heap)
print(min_element) # 1

max_heap = []
heapq.heappush(max_heap, -1)
heapq.heappush(max_heap, -4)
heapq.heappush(max_heap, -3)
max_element = -heapq.heappop(max_heap)
print(max_element) # 4