Skip to content

Data Structure

Array

Static Array

In static array, memory is allocated at compile time having a fixed size of it. We can't change the size of the array once it is declared.

  1. In C++, arr[0] is the starting address of the array, and arr[1] is the address of the next element (i.e., arr[0] + sizeof(xx)).

  2. arr is a pointer to the first element of the array, i.e., *arr = arr[0] or arr= &arr[0].

  3. The memory of the array is contiguous.

Time Complexity

  1. Retrieval & Modification: O(1).

  2. Insertion & Deletion (not at the end): O(N).

  3. If full, we can't insert more elements as the size is fixed. A new array will be created and elements will be copied to the new array with time complexity O(N).

Dynamic Array

Dynamic array is based on static array. It automatically handles resizing and other operations with built-in functions.

Linked List

Compared to arrays, linked lists do not store data in contiguous memory locations. Instead, each element points to the next element. Thus, having following pros and cons:

Pros: 1. No need for contiguous memory. 2. Easy insertion and deletion with O(1) time complexity

Cons: 1. Access requires traversing the list with O(N) time complexity.

Hash Table

Hash table uses a hash function to compute an index into an array of buckets, from which the desired value can be found.

Hash collision

When two keys hash to the same index, it is called a hash collision. There are several ways to handle hash collisions:

  1. Chaining: Each bucket contains a linked list of elements, and when a collision occurs, the element is added to the list.

  2. Open Addressing: When a collision occurs, the algorithm searches for the next open slot in the hash table.

Load factor is the ratio of the number of elements to the number of buckets. A high load factor results in more collisions. When the load factor exceeds a threshold, the hash table needs to be resized.

Binary Tree

    1
   / \
  2   3
 /   / \
4   5   6
   /     \
  7       8

Binary Search Tree

In a binary search tree (BST), the left subtree of a node contains only nodes with keys less than the node's key, and the right subtree only contains nodes with keys greater than the node's key:

    7
   / \
  4   9
 / \   \
1   5   10

Note that the following tree is not a BST because the right child of 4 is greater than 7.

    7
   / \
  4   9
 / \   \
1   8   10

Recursive/Level Traversal

Depth-First Search (DFS)

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def traverse(root: TreeNode):
    if root is None:
        return
    # Code for pre-order traversal
    traverse(root.left)
    # Code for in-order traversal
    traverse(root.right)
    # Code for post-order traversal

Note that here the order of traversal is fixed. The pre-order, in-order, and post-order traversals differ in the order of visiting the root node.

Breadth-First Search (BFS)

from collections import deque

def levelOrderTraverse(root):
    if root is None:
        return
    q = deque()
    q.append(root)
    while q:
        cur = q.popleft()
        print(cur.val)

        if cur.left is not None:
            q.append(cur.left)
        if cur.right is not None:
            q.append(cur.right)

DFS vs. BFS

  1. BFS is usually used for finding the shortest path as BFS traverses all nodes at the current depth before moving to the next depth.
  2. DFS is usually used for finding all possible paths as DFS goes as deep as possible before backtracking.

N-ary Tree

An N-ary tree is a tree in which each node has at most N children.

    1
   /|\ 
  3 2 4
 / \
5   6

Forest

Forest is a collection of disjoint trees. A forest can be represented as a list of trees.

DFS on N-ary Tree

class Node:
    def __init__(self, val=None, children=None):
        self.val = val
        self.children = children

def traverse(root: Node):
    if root is None:
        return
    # Code for pre-order traversal
    for child in root.children:
        traverse(child)
    # Code for post-order traversal

BFS on N-ary Tree

from collections import deque

def level_order_traverse(root):
    if root is None:
        return

    q = deque()
    q.append(root)

    while q:
        cur = q.popleft()

        print(cur.val)

        for child in cur.children:
            q.append(child)

Heap

Binary heap is a complete binary tree that is complete and obeys min-heap property.

  • Min-heap: Every node is less than or equal to its children.
  • Complete: Missing nodes are only at the bottom level and to the right.

The following is not a heap as it is not complete:

    5
   / 
  6   
 / \ 
7  8 

Heap Add

To insert an element into a heap, we need:

  1. Add the element to the end of the heap.
  2. Swim up the element (swap with its parent) until the heap property is satisfied.

Heap Delete

To delete the root element from a heap, we need:

  1. Replace the root with the last element.
  2. Sink down the element (swap with its child) until the heap property is satisfied.