Month 6, Week 2

Algorithms & Data Structures II

The Advanced Toolkit

Module 1: Hash Tables

The Engine of Instant Lookups

The Problem: Finding Data is Slow

In an array, finding an element by its value is an O(n) operation. If you have a million users, finding one by their email address could take a million comparisons.

How can we do better? How can we achieve O(1), or constant-time, lookups?

The Solution: Hash Tables

A Hash Table is a data structure that maps keys to values for highly efficient lookups. It uses a hash function to compute an index into an array of buckets or slots.

Analogy: A library with a magical filing system. To find a book, you tell the librarian the title (the key). The librarian performs a magic spell (the hash function) on the title, which instantly tells them exactly which shelf and slot (the index) the book is on.

Hash Tables in JavaScript

You have been using hash tables all along!

  • JavaScript's Object literal is a general-purpose hash table implementation.
  • The modern `Map` object is a more specialized and often superior hash table implementation.

The Problem: Collisions

What happens if the hash function produces the same index for two different keys? This is called a collision.

A common solution is called Separate Chaining. Instead of storing the value directly in the array slot, we store a linked list of key-value pairs at that index. If a collision occurs, we just add the new pair to the linked list.

Hash Table Performance

The time complexity depends on the quality of the hash function.

  • Average Case (good hash function, few collisions):
    • Insertion: O(1)
    • Deletion: O(1)
    • Search: O(1)
  • Worst Case (bad hash function, all keys collide):
    • All operations degrade to O(n) because the hash table effectively becomes a single linked list.

Module 2: Trees

Hierarchical Data Structures

What is a Tree?

A tree is a non-linear data structure that consists of nodes in a parent-child relationship. It's used to represent hierarchical data.

Examples: A file system, an organization chart, the DOM.

Binary Search Trees (BST)

A Binary Tree is a tree where each node has at most two children, referred to as the left and right child.

A Binary Search Tree is a special type of binary tree with a strict ordering property:

  • All values in a node's left subtree are less than the node's value.
  • All values in a node's right subtree are greater than the node's value.

BST Performance

This ordering property is a superpower. It allows for incredibly fast operations by repeatedly discarding half the tree.

For a balanced BST (where the height is kept to a minimum):

  • Insertion: O(log n)
  • Search: O(log n)
  • Deletion: O(log n)

This is why databases use tree-like structures (B-Trees) for their indexes!

Implementing a BST Node


                        class TreeNode {
                            public value: number;
                            public left: TreeNode | null = null;
                            public right: TreeNode | null = null;

                            constructor(value: number) {
                                this.value = value;
                            }
                        }

                        class BinarySearchTree {
                            public root: TreeNode | null = null;

                            insert(value: number) {
                                // ... logic to traverse and insert
                            }

                            find(value: number): TreeNode | null {
                                // ... logic to traverse and find
                            }
                        }
                    

Module 3: Graphs (Conceptual)

The Ultimate Network Structure

What is a Graph?

A graph is a collection of vertices (or nodes) and edges that connect them. It is the most general-purpose data structure, capable of modeling any kind of relationship.

Examples: Social networks (users are vertices, friendships are edges), the internet (web pages are vertices, links are edges), maps (cities are vertices, roads are edges).

Mid-Lecture Knowledge Check

Module 4: Fundamental Algorithms

Searching and Sorting

Searching Algorithms

  • Linear Search: The simplest search. You iterate through a collection one by one until you find your target.
    • Time Complexity: O(n)
  • Binary Search: The "divide and conquer" search. Requires the collection to be sorted. You check the middle element, and if it's not your target, you discard the half where your target cannot be.
    • Time Complexity: O(log n)

Sorting Algorithms: The Inefficient Ones

These are simple to understand but too slow for most real-world applications with large datasets.

Bubble Sort

Repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order. The largest elements "bubble" to the end.

Time Complexity: O(n²)

Insertion Sort

Builds the final sorted array one item at a time. It iterates through the input elements and inserts each element into its correct position in the sorted part of the array.

Time Complexity: O(n²)

Sorting Algorithms: The Efficient Ones

These use the "divide and conquer" strategy and are much faster.

Merge Sort

It divides the unsorted list into n sublists, each containing one element, and then repeatedly merges sublists to produce new sorted sublists until there is only one sublist remaining.

Time Complexity: O(n log n)

Quick Sort

It picks an element as a "pivot" and partitions the given array around the picked pivot. All smaller elements go to the left, all larger elements go to the right. It then recursively applies this to the sub-arrays.

Time Complexity: Average Case O(n log n), Worst Case O(n²)

In-Class Practical Exercise

Implementing Binary Search

Your task is to implement the binary search algorithm in TypeScript. This is a foundational "divide and conquer" algorithm that every engineer must know.

  1. Create a file `search.ts`.
  2. Write a function `binarySearch(arr: number[], target: number): number`. It should return the index of the target if found, or `-1` if not.
  3. The input array `arr` will always be sorted.
  4. Initialize two pointers, `left` (at index 0) and `right` (at index `arr.length - 1`).
  5. Use a `while` loop that continues as long as `left <= right`.
  6. Inside the loop:
    • Calculate the `middle` index.
    • Compare the element at the `middle` index with the `target`.
    • If they match, you've found it! Return the `middle` index.
    • If the middle element is less than the target, it means the target must be in the right half. Move the `left` pointer to `middle + 1`.
    • If the middle element is greater than the target, discard the right half by moving the `right` pointer to `middle - 1`.
  7. If the loop finishes without finding the target, return `-1`.

Final Knowledge Check