Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
DSAJavaScriptAlgorithmsData StructuresRecursionInterview Preparation

JavaScript DSA From Scratch — Implementations & Handwritten Traces

From-scratch JavaScript implementations of math, search, sort, stack, queue, linked list, hash table, graph, and BST — each with a handwritten execution-trace table, complexity, and demo calls.

Sep 3, 202647 min read

Introduction

From-scratch JavaScript is the fastest way to own an algorithm — not a library call, the actual loops, pointers, and recursive frames. This notebook walks every implementation in the code-evolution corpus: math helpers, search and sort, stacks and queues, linked lists, hash tables, graphs, and binary search trees.

Each section has:

  1. The implementation — the code as written, cleaned for reading
  2. Complexity — time and space in a table
  3. Handwritten trace — step-by-step execution tables from the source files
  4. Demo calls — inputs with the output on the right

Related visualizers: Big O Notation · DSA Arrays · 90 Interview Problems

Note

These traces use the same inputs as the original files ([8, 20, -2, 4, -6] for sorts, [-5, 2, 4, 6, 10] for binary search, and so on). Read a row left to right — that is one CPU step.

Quick index

Math

#TopicTimeSpace
1FibonacciO(n)O(n)
2FactorialO(n)O(1)
3Prime checkO(√n)O(1)
4Power of twoO(log n)O(1)
5Power of two (bitwise)O(1)O(1)

Recursion

#TopicTimeSpace
6Recursive FibonacciO(2ⁿ)O(n)
7Recursive factorialO(n)O(n)
8Recursive binary searchO(log n)O(log n)
9Tower of HanoiO(2ⁿ)O(n)

Search & sort

#TopicTimeSpace
10Linear searchO(n)O(1)
11Binary searchO(log n)O(1)
12Bubble sortO(n²)O(1)
13Insertion sortO(n²)O(1)
14Merge sortO(n log n)O(n)
15Quick sortO(n log n) avgO(log n)

Combinatorics & DP

#TopicTimeSpace
16Cartesian productO(m · n)O(m · n)
17Climbing staircaseO(n)O(n)

Stack

#TopicPushPop
18Stack with arrayO(1)O(1)
19Stack with objectO(1)O(1)
20Linked-list stackO(1)O(n)*

Queue

#TopicEnqueueDequeue
21Array queueO(1)O(n)
22Optimized object queueO(1)O(1)
23Circular queueO(1)O(1)
24Linked-list queueO(n)O(1)

Linked list

#TopicHighlight
25Singly linked listinsert / reverse
26Linked list with tailO(1) append
27Doubly linked listO(1) pop from tail

Hash, graph, tree

#TopicAvg lookup
28Hash tableO(k)
29Undirected graphO(1) hasEdge
30Binary search treeO(log n)

Math

1. Fibonacci

Time: O(n) · Space: O(n)

Build the first n Fibonacci numbers into an array. Each term is the sum of the previous two.

js
function fibonacci(n) {
  const fib = [0, 1]
  if (n === undefined) return []
  if (n === 1) return [0]
  if (n === 2) return fib
 
  for (let i = 2; i < n; i++) {
    fib[i] = fib[i - 1] + fib[i - 2]
  }
  return fib
}
CaseTimeSpaceWhy
Loop 2..n-1O(n)O(n)Array stores n numbers
n is 0/1/2O(1)O(1)Early return

Handwritten trace — fibonacci(7)

StepiOperationArray state
0—Initialize fib = [0, 1][0, 1]
1—n === undefined? no[0, 1]
2—n === 1? no[0, 1]
3—n === 2? no[0, 1]
42fib[2] = 1 + 0 = 1[0, 1, 1]
53fib[3] = 1 + 1 = 2[0, 1, 1, 2]
64fib[4] = 2 + 1 = 3[0, 1, 1, 2, 3]
75fib[5] = 3 + 2 = 5[0, 1, 1, 2, 3, 5]
86fib[6] = 5 + 3 = 8[0, 1, 1, 2, 3, 5, 8]
977 < 7 is false — loop ends[0, 1, 1, 2, 3, 5, 8]

Final result: [0, 1, 1, 2, 3, 5, 8]

Function calls with output

js
fibonacci() // → []
fibonacci(1) // → [0]
fibonacci(2) // → [0, 1]
fibonacci(3) // → [0, 1, 1]
fibonacci(7) // → [0, 1, 1, 2, 3, 5, 8]

Back to index


2. Factorial

Time: O(n) · Space: O(1)

n! is the product 1 × 2 × … × n. 0! and 1! are 1.

js
function factorial(n) {
  let result = 1
  if (n === 0) return 1
  if (n === 1) return 1
 
  for (let i = 2; i <= n; i++) {
    result = result * i
  }
  return result
}
CaseTimeSpaceWhy
Loop 2..nO(n)O(1)One accumulator
n is 0 or 1O(1)O(1)Immediate return

Handwritten trace — factorial(5)

StepiOperationResult
0—result = 11
1—n === 0? no1
2—n === 1? no1
321 * 22
432 * 36
546 * 424
6524 * 5120
766 <= 5 is false120

Final result: 120

Function calls with output

js
factorial(0) // → 1
factorial(1) // → 1
factorial(5) // → 120

Back to index


3. Prime check

Time: O(√n) · Space: O(1)

A prime is greater than 1 with no divisor other than 1 and itself. If n = a × b and a > √n, then b < √n — so checking up to √n is enough.

js
function isPrime(n) {
  if (n < 2) return false
 
  for (let i = 2; i <= Math.sqrt(n); i++) {
    if (n % i === 0) return false
  }
  return true
}
InputBound √nChecks without optChecks with √n
10~3.1681 (hits 2)
5~2.2431
1000010099981 (hits 2)

Handwritten trace — isPrime(10)

√10 ≈ 3.16 so the loop would check i = 2, 3.

StepiOperationResult
0—10 < 2?false, continue
1210 % 2 === 0?true → return false immediately

Final result: false (checked 1 iteration instead of 8)

Handwritten trace — isPrime(5)

√5 ≈ 2.24 so the loop checks i = 2 only.

StepiOperationResult
0—5 < 2?false
125 % 2 === 0?false (5 % 2 = 1)
233 <= 2.24?false, loop ends
3—return true5 is prime

Final result: true

Function calls with output

js
isPrime(1) // → false
isPrime(3) // → true
isPrime(4) // → false
isPrime(5) // → true
isPrime(10) // → false
isPrime(10000) // → false
isPrime(235) // → false  (5 × 47)

Back to index


4. Power of two

Time: O(log n) · Space: O(1)

Keep dividing by 2. If you hit an odd number before 1, it is not a power of two.

js
function isPowerOfTwo(n) {
  if (n < 1) return false
 
  while (n > 1) {
    if (n % 2 !== 0) return false
    n = n / 2
  }
  return true
}

Handwritten trace — isPowerOfTwo(5)

StepnCheckAction
05n < 1?false
15n > 1enter loop
255 % 2 !== 0true
35return falsestop

Final result: false

Handwritten trace — isPowerOfTwo(8)

StepnCheckAction
08n < 1?false
18n > 1enter
288 % 2 !== 0false
38n = 8 / 2n becomes 4
44n > 1enter
544 % 2 !== 0false
64n = 4 / 2n becomes 2
72n > 1enter
822 % 2 !== 0false
92n = 2 / 2n becomes 1
101n > 1exit loop
111return true8 = 2³

Final result: true

Function calls with output

js
isPowerOfTwo(1) // → true
isPowerOfTwo(2) // → true
isPowerOfTwo(4) // → true
isPowerOfTwo(6) // → false
isPowerOfTwo(8) // → true

Back to index


5. Power of two (bitwise)

Time: O(1) · Space: O(1)

A positive power of two has exactly one bit set. n & (n - 1) clears that bit and must be 0.

js
function isPowerOfTwoBitwise(n) {
  if (n < 1) return false
  return (n & (n - 1)) === 0
}

Handwritten trace — isPowerOfTwoBitwise(8)

StepExpressionBinaryResult
1n10008
2n - 101117
38 AND 700000
40 === 0—true

Final result: true

Handwritten trace — isPowerOfTwoBitwise(6)

StepExpressionBinaryResult
1n01106
2n - 101015
36 AND 501004
44 === 0—false

Final result: false

Back to index


Recursion

6. Recursive Fibonacci

Time: O(2ⁿ) · Space: O(n)

Each call branches into two more calls. Overlapping subproblems are recomputed — that is why this is slow.

js
function recursiveFibonacci(n) {
  if (n < 2) return n
  return recursiveFibonacci(n - 1) + recursiveFibonacci(n - 2)
}

Handwritten trace — recursiveFibonacci(4)

Call tree (left child = n-1, right child = n-2):

plaintext
recursiveFibonacci(4)
├─ recursiveFibonacci(3)
│  ├─ recursiveFibonacci(2)
│  │  ├─ recursiveFibonacci(1) → 1
│  │  └─ recursiveFibonacci(0) → 0
│  │  → 1
│  └─ recursiveFibonacci(1) → 1
│  → 2
└─ recursiveFibonacci(2)
   ├─ recursiveFibonacci(1) → 1
   └─ recursiveFibonacci(0) → 0
   → 1
Return frameValue
fib(1) / fib(0)1 / 0
fib(2) = 1 + 01
fib(3) = 1 + 12
fib(4) = 2 + 13

Final result: 3

fib(2) is computed twice. That duplication is the O(2ⁿ) cost. Prefer the iterative version above, or memoize.

Function calls with output

js
recursiveFibonacci(0) // → 0
recursiveFibonacci(1) // → 1
recursiveFibonacci(6) // → 8

Back to index


7. Recursive factorial

Time: O(n) · Space: O(n)

Unwind until 0! = 1, then multiply on the way back.

js
function recursiveFactorial(n) {
  if (n === 0) return 1
  return n * recursiveFactorial(n - 1)
}

Handwritten trace — recursiveFactorial(4)

plaintext
recursiveFactorial(4)
├─ recursiveFactorial(3)
│  ├─ recursiveFactorial(2)
│  │  ├─ recursiveFactorial(1)
│  │  │  └─ recursiveFactorial(0) → 1
│  │  │  → 1 * 1 = 1
│  │  └─ 2 * 1 = 2
│  └─ 3 * 2 = 6
└─ 4 * 6 = 24
CallWaiting onReturns
fact(0)base1
fact(1)1 * fact(0)1
fact(2)2 * fact(1)2
fact(3)3 * fact(2)6
fact(4)4 * fact(3)24

Final result: 24

Function calls with output

js
recursiveFactorial(0) // → 1
recursiveFactorial(1) // → 1
recursiveFactorial(5) // → 120

Back to index


8. Recursive binary search

Time: O(log n) · Space: O(log n)

Same idea as iterative binary search, but each half is a new stack frame. Works only on a sorted array.

js
function recursiveBinarySearch(arr, target) {
  return search(arr, target, 0, arr.length - 1)
}
 
function search(arr, target, leftIndex, rightIndex) {
  if (leftIndex > rightIndex) return -1
 
  const middleIndex = Math.floor((leftIndex + rightIndex) / 2)
 
  if (target === arr[middleIndex]) return middleIndex
  if (target < arr[middleIndex]) {
    return search(arr, target, leftIndex, middleIndex - 1)
  }
  return search(arr, target, middleIndex + 1, rightIndex)
}

Visualizer: Binary Search

Handwritten trace — recursiveBinarySearch([-5, 2, 4, 6, 10], 6)

StepCallMiddlearr[mid]CompareNext
1search(arr, 6, 0, 4)244 < 6search right side
2search(arr, 6, 3, 4)366 === 6return 3

Final result: 3

Function calls with output

js
recursiveBinarySearch([-5, 2, 4, 6, 10], 10) // → 4
recursiveBinarySearch([-5, 2, 4, 6, 10], 6) // → 3
recursiveBinarySearch([-5, 2, 4, 6, 10], 20) // → -1

Back to index


9. Tower of Hanoi

Time: O(2ⁿ) · Space: O(n)

Move n disks from A to C using B. Never put a larger disk on a smaller one. Total moves = 2ⁿ − 1.

js
function towerOfHanoi(n, fromRod, toRod, usingRod) {
  if (n === 1) {
    console.log(`Move disk 1 from ${fromRod} to ${toRod}`)
    return
  }
 
  towerOfHanoi(n - 1, fromRod, usingRod, toRod)
  console.log(`Move disk ${n} from ${fromRod} to ${toRod}`)
  towerOfHanoi(n - 1, usingRod, toRod, fromRod)
}

Handwritten trace — towerOfHanoi(3, 'A', 'C', 'B')

StepCallAction
1hanoi(3, A, C, B)move 2 disks A → B using C
2hanoi(2, A, B, C)move 1 disk A → C using B
3hanoi(1, A, C, B)move disk 1 from A to C
4—move disk 2 from A to B
5hanoi(1, C, B, A)move disk 1 from C to B
6—move disk 3 from A to C
7hanoi(2, B, C, A)move 2 disks B → C using A
8hanoi(1, B, A, C)move disk 1 from B to A
9—move disk 2 from B to C
10hanoi(1, A, C, B)move disk 1 from A to C

Final move list: A→C, A→B, C→B, A→C, B→A, B→C, A→C

Back to index


Search & sort

10. Linear search

Time: O(n) · Space: O(1)

Walk left to right. Return the index on match, otherwise -1. Works on unsorted data.

Visualizer: Linear Search

js
function linearSearch(arr, target) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) return i
  }
  return -1
}

Handwritten trace — linearSearch([-5, 2, 10, 4, 6], 10)

Stepiarr[i]CompareResult
10-5-5 === 10? falsecontinue
2122 === 10? falsecontinue
321010 === 10? truereturn 2

Final result: 2

Function calls with output

js
linearSearch([-5, 2, 10, 4, 6], 10) // → 2
linearSearch([-5, 2, 10, 4, 6], 6) // → 4
linearSearch([-5, 2, 10, 4, 6], 20) // → -1

Back to index


11. Binary search

Time: O(log n) · Space: O(1)

Sorted array only. Each step halves the remaining window.

Visualizer: Binary Search

js
function binarySearch(arr, target) {
  let leftIndex = 0
  let rightIndex = arr.length - 1
 
  while (leftIndex <= rightIndex) {
    const middleIndex = Math.floor((leftIndex + rightIndex) / 2)
 
    if (arr[middleIndex] === target) return middleIndex
    if (target < arr[middleIndex]) {
      rightIndex = middleIndex - 1
    } else {
      leftIndex = middleIndex + 1
    }
  }
  return -1
}

Handwritten trace — binarySearch([-5, 2, 4, 6, 10], 6)

Stepleftrightmidarr[mid]ComparisonAction
104244 < 6move left to 3
234366 === 6return 3

Final result: 3

Function calls with output

js
binarySearch([-5, 2, 4, 6, 10], 10) // → 4
binarySearch([-5, 2, 4, 6, 10], 6) // → 3
binarySearch([-5, 2, 4, 6, 10], 20) // → -1

Back to index


12. Bubble sort

Time: O(n²) · Space: O(1)

Swap adjacent pairs until a full pass makes no swaps. Largest value bubbles to the end each pass.

Visualizer: Bubble Sort

js
function bubbleSort(arr) {
  let swapped
  do {
    swapped = false
    for (let i = 0; i < arr.length - 1; i++) {
      if (arr[i] > arr[i + 1]) {
        const temp = arr[i]
        arr[i] = arr[i + 1]
        arr[i + 1] = temp
        swapped = true
      }
    }
  } while (swapped)
}

Handwritten trace — bubbleSort([8, 20, -2, 4, -6])

PassArray beforeComparisonArray after
1[8, 20, -2, 4, -6]8 > 20? no[8, 20, -2, 4, -6]
20 > -2? yes[8, -2, 20, 4, -6]
20 > 4? yes[8, -2, 4, 20, -6]
20 > -6? yes[8, -2, 4, -6, 20]
2[8, -2, 4, -6, 20]8 > -2? yes[-2, 8, 4, -6, 20]
8 > 4? yes[-2, 4, 8, -6, 20]
8 > -6? yes[-2, 4, -6, 8, 20]
3[-2, 4, -6, 8, 20]-2 > 4? no[-2, 4, -6, 8, 20]
4 > -6? yes[-2, -6, 4, 8, 20]
4[-2, -6, 4, 8, 20]-2 > -6? yes[-6, -2, 4, 8, 20]

Final result: [-6, -2, 4, 8, 20]

Back to index


13. Insertion sort

Time: O(n²) · Space: O(1)

Take one element at a time and insert it into the already-sorted left prefix.

Visualizer: Insertion Sort

js
function insertionSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    const numberToInsert = arr[i]
    let j = i - 1
 
    while (j >= 0 && arr[j] > numberToInsert) {
      arr[j + 1] = arr[j]
      j = j - 1
    }
    arr[j + 1] = numberToInsert
  }
}

Handwritten trace — insertionSort([8, 20, -2, 4, -6])

StepiinsertjConditionAction / array
112008 > 20?no — [8, 20, -2, 4, -6]
22-2120 > -2?shift 20
08 > -2?shift 8
-1stop[-2, 8, 20, 4, -6]
334220 > 4?shift 20
18 > 4?shift 8
0-2 > 4?no — [-2, 4, 8, 20, -6]
44-63..0all largershift all, insert at 0
[-6, -2, 4, 8, 20]

Final result: [-6, -2, 4, 8, 20]

Back to index


14. Merge sort

Time: O(n log n) · Space: O(n)

Split until length < 2, then merge sorted halves by always taking the smaller front value.

Visualizer: Merge Sort

js
function mergeSort(arr) {
  if (arr.length < 2) return arr
 
  const mid = Math.floor(arr.length / 2)
  const leftArr = arr.slice(0, mid)
  const rightArr = arr.slice(mid)
  return merge(mergeSort(leftArr), mergeSort(rightArr))
}
 
function merge(leftArr, rightArr) {
  const sortedArr = []
  while (leftArr.length && rightArr.length) {
    if (leftArr[0] <= rightArr[0]) sortedArr.push(leftArr.shift())
    else sortedArr.push(rightArr.shift())
  }
  return [...sortedArr, ...leftArr, ...rightArr]
}

Handwritten trace — mergeSort([8, 20, -2, 4, -6])

StepCallAction / result
1mergeSort([8,20,-2,4,-6])split [8,20] / [-2,4,-6]
2mergeSort([8,20])split [8] / [20]
3mergeSort([8])base → [8]
4mergeSort([20])base → [20]
5merge([8],[20])[8, 20]
6mergeSort([-2,4,-6])split [-2] / [4,-6]
7mergeSort([-2])base → [-2]
8mergeSort([4,-6])split [4] / [-6]
9merge([4],[-6])[-6, 4]
10merge([-2],[-6,4])[-6, -2, 4]
11merge([8,20],[-6,-2,4])[-6, -2, 4, 8, 20]

Final result: [-6, -2, 4, 8, 20]

Back to index


15. Quick sort

Time: O(n log n) average, O(n²) worst · Space: O(log n) average

Last element is the pivot. Smaller values go left, larger go right, then recurse.

Visualizer: Quick Sort

js
function quickSort(arr) {
  if (arr.length < 2) return arr
 
  const pivot = arr[arr.length - 1]
  const left = []
  const right = []
 
  for (let i = 0; i < arr.length - 1; i++) {
    if (arr[i] < pivot) left.push(arr[i])
    else right.push(arr[i])
  }
  return [...quickSort(left), pivot, ...quickSort(right)]
}

Handwritten trace — quickSort([8, 20, -2, 4, -6])

StepCurrent arrayPivotLeftRightResult piece
1[8, 20, -2, 4, -6]-6[][8, 20, -2, 4]pivot sits first
2[8, 20, -2, 4]4[-2][8, 20]recurse both sides
3[-2]-2[][]base
4[8, 20]20[8][]base on 8
5combine———[-6] + [-2, 4, 8, 20]

Final result: [-6, -2, 4, 8, 20]

Tip

Last-element pivot is simple to trace. Sorted or reverse-sorted input makes it O(n²). Randomized or median-of-three pivot is the usual fix.

Back to index


Combinatorics & DP

16. Cartesian product

Time: O(m · n) · Space: O(m · n)

Every pair from two arrays. Nested loops, one pair per inner iteration.

js
function cartesianProduct(arr1, arr2) {
  const result = []
  for (let i = 0; i < arr1.length; i++) {
    for (let j = 0; j < arr2.length; j++) {
      result.push([arr1[i], arr2[j]])
    }
  }
  return result
}

Handwritten trace — cartesianProduct([1, 2], [3, 4, 5])

StepijPairResult so far
100[1,3][[1,3]]
201[1,4][[1,3],[1,4]]
302[1,5][[1,3],[1,4],[1,5]]
410[2,3][[1,3],[1,4],[1,5],[2,3]]
511[2,4][[1,3],[1,4],[1,5],[2,3],[2,4]]
612[2,5][[1,3],[1,4],[1,5],[2,3],[2,4],[2,5]]

Final result: [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5]]

Back to index


17. Climbing staircase

Time: O(n) · Space: O(n)

Ways to climb n steps taking 1 or 2 at a time. Same recurrence as Fibonacci: ways(i) = ways(i-1) + ways(i-2).

js
function climbingStaircase(n) {
  const noOfWays = [1, 2]
  for (let i = 2; i <= n; i++) {
    noOfWays[i] = noOfWays[i - 1] + noOfWays[i - 2]
  }
  return noOfWays[n - 1]
}

Note

Index n - 1 is the n-th step because the array is 0-based: noOfWays[0] = 1 (1 step), noOfWays[1] = 2 (2 steps).

Handwritten trace — climbingStaircase(5)

StepiBeforeFormulaAfter
12[1, 2]2 = 1 + 1[1, 2, 2]
23[1, 2, 2]3 = 2 + 1[1, 2, 2, 3]
34[1, 2, 2, 3]5 = 3 + 2[1, 2, 2, 3, 5]
45[1, 2, 2, 3, 5]8 = 5 + 3[1, 2, 2, 3, 5, 8]

Return noOfWays[4] → 8

Function calls with output

js
climbingStaircase(1) // → 1
climbingStaircase(2) // → 2
climbingStaircase(3) // → 3
climbingStaircase(4) // → 5
climbingStaircase(5) // → 8

Back to index


Stack

LIFO — last in, first out. Visualizer: Stacks

18. Stack with array

Push / pop / peek: O(1) · Space: O(n)

Array end is the top. Native push / pop are amortized O(1).

js
class Stack {
  constructor() {
    this.arr = []
  }
 
  push(value) {
    this.arr.push(value)
    return this.arr
  }
 
  pop() {
    this.arr.pop()
    return this.arr
  }
 
  lookup() {
    return this.arr[this.arr.length - 1]
  }
}

Handwritten trace

StepOperationStack stateReturn
1new Stack[]created
2push(5)[5][5]
3push(10)[5, 10][5, 10]
4push(15)[5, 10, 15][5, 10, 15]
5push(20)[5, 10, 15, 20]array
6push(25)[5, 10, 15, 20, 25]array
7pop()[5, 10, 15, 20]array
8pop()[5, 10, 15]array
9pop()[5, 10]array
10lookup()[5, 10]10

Back to index


19. Stack with object

Push / pop / peek: O(1) · Space: O(n)

A count cursor is the top key. No array shifting.

js
class Stack {
  constructor() {
    this.database = {}
    this.count = 0
  }
 
  push(value) {
    this.count++
    this.database[this.count] = value
    return this.database
  }
 
  pop() {
    delete this.database[this.count]
    this.count--
    return this.database
  }
 
  peek() {
    return this.database[this.count]
  }
}

Handwritten trace

StepOperationcountdatabaseReturn
1new Stack0emptycreated
2push(5)11: 5object
3push(10)21: 5, 2: 10object
4push(15)31: 5, 2: 10, 3: 15object
5push(20)4keys 1–4object
6push(25)5keys 1–5, top = 25object
7pop()4key 5 deletedobject
8pop()3key 4 deletedobject
9peek()3unchanged15

Back to index


20. Linked-list stack

Push: O(1) · Pop: O(n) in this version · Space: O(n)

push prepends at head (O(1)). pop uses removeFromEnd — that walk is O(n). A proper list stack pops from the front.

js
class LinkedListStack {
  constructor() {
    this.list = new LinkedList() // head + tail
  }
 
  push(value) {
    this.list.prepend(value) // O(1)
  }
 
  pop() {
    return this.list.removeFromEnd() // O(n) — suboptimal
  }
 
  peek() {
    return this.list.head.value
  }
}

Warning

This pop is correct LIFO only if the tail is the oldest node (prepend grows the head). It is still O(n). Use removeFromFront for O(1) pop.

Handwritten trace

StepOperationList (head → tail)sizeReturnTime
1new Stackempty0createdO(1)
2isEmpty()empty0trueO(1)
3push(10)101—O(1)
4push(20)20 → 102—O(1)
5push(30)30 → 20 → 103—O(1)
6push(40)40 → 30 → 20 → 104—O(1)
7pop()40 → 30 → 20310O(n)
8peek()40 → 30 → 20340O(1)

Back to index


Queue

FIFO — first in, first out. Visualizer: Queues

21. Array queue

Enqueue: O(1) · Dequeue: O(n) · Space: O(n)

push at the end is cheap. shift from the front moves every remaining element.

js
class Queue {
  constructor() {
    this.items = []
  }
 
  enqueue(element) {
    this.items.push(element)
  }
 
  dequeue() {
    return this.items.shift()
  }
 
  isEmpty() {
    return this.items.length === 0
  }
 
  peek() {
    return this.isEmpty() ? null : this.items[0]
  }
 
  size() {
    return this.items.length
  }
}

Handwritten trace

StepOperationQueue stateReturn
1new Queue()[]created
2isEmpty()[]true
3enqueue(10)[10]—
4enqueue(20)[10, 20]—
5enqueue(30)[10, 20, 30]—
6size()[10, 20, 30]3
7dequeue()[20, 30]10
8peek()[20, 30]20

dequeue removes the front (FIFO). peek shows the next dequeue without removing it.

Back to index


22. Optimized object queue

Enqueue / dequeue: O(1) · Space: O(n)

front and rear pointers — dequeue increments front instead of shifting.

js
class Queue {
  constructor() {
    this.items = {}
    this.rear = 0
    this.front = 0
  }
 
  enqueue(element) {
    this.items[this.rear] = element
    this.rear++
  }
 
  dequeue() {
    const item = this.items[this.front]
    delete this.items[this.front]
    this.front++
    return item
  }
 
  isEmpty() {
    return this.rear - this.front === 0
  }
 
  peek() {
    return this.items[this.front]
  }
 
  size() {
    return this.rear - this.front
  }
}

Handwritten trace

StepOperationitemsfrontrearReturn
1new Queue()empty00created
2isEmpty()empty00true
3enqueue(10)0: 1001—
4enqueue(20)0: 10, 1: 2002—
5enqueue(30)0: 10, 1: 20, 2: 3003—
6size()same033
7dequeue()1: 20, 2: 301310
8peek()same1320

size() = rear - front = 3 - 1 = 2 after the dequeue.

Back to index


23. Circular queue

Enqueue / dequeue: O(1) · Space: O(capacity)

Fixed buffer. rear = (rear + 1) % capacity wraps into slots freed by dequeue — no wasted holes at the front.

js
class CircularQueue {
  constructor(capacity) {
    this.items = new Array(capacity)
    this.capacity = capacity
    this.currentLength = 0
    this.rear = -1
    this.front = -1
  }
 
  isFull() {
    return this.currentLength === this.capacity
  }
 
  isEmpty() {
    return this.currentLength === 0
  }
 
  enqueue(element) {
    if (this.isFull()) return
    this.rear = (this.rear + 1) % this.capacity
    this.items[this.rear] = element
    this.currentLength += 1
    if (this.front === -1) this.front = this.rear
  }
 
  dequeue() {
    if (this.isEmpty()) return null
    const item = this.items[this.front]
    this.items[this.front] = null
    this.front = (this.front + 1) % this.capacity
    this.currentLength -= 1
    if (this.isEmpty()) {
      this.front = -1
      this.rear = -1
    }
    return item
  }
}

Handwritten trace — CircularQueue(capacity = 5)

StepOperationfrontrearlenitemsReturn
1new CircularQueue(5)-1-10empty slotscreated
2isEmpty()-1-10—true
3enqueue(10)001[10, _, _, _, _]—
4enqueue(20)012[10, 20, _, _, _]—
5enqueue(30)023[10, 20, 30, _, _]—
6enqueue(40)034[10, 20, 30, 40, _]—
7enqueue(50)045[10, 20, 30, 40, 50]—
8isFull()045fulltrue
9dequeue()144[null, 20, 30, 40, 50]10
10peek()144same20
11enqueue(60)105[60, 20, 30, 40, 50]—

Step 11: (4 + 1) % 5 = 0 — rear wraps and reuses index 0.

Back to index


24. Linked-list queue

Enqueue: O(n) with head-only list · Dequeue: O(1) · Space: O(n)

append walks to the tail. removeFromFront is O(1). A tail pointer makes enqueue O(1) too (next section).

js
class LinkedListQueue {
  constructor() {
    this.list = new LinkedList()
  }
 
  enqueue(value) {
    this.list.append(value) // O(n) without tail
  }
 
  dequeue() {
    return this.list.removeFromFront() // O(1)
  }
 
  peek() {
    return this.list.head.value
  }
}

Handwritten trace

StepOperationQueue (head → tail)sizeReturnTime
1new Queue()empty0createdO(1)
2isEmpty()empty0trueO(1)
3enqueue(10)101—O(1)
4enqueue(20)10 → 202—O(n)
5enqueue(30)10 → 20 → 303—O(n)
6dequeue()20 → 30210O(1)
7peek()20 → 30220O(1)
DesignEnqueueDequeue
Array queueO(1)O(n)
This linked-list queueO(n)O(1)
Circular queueO(1)O(1)
List with tailO(1)O(1)

Back to index


Linked list

Visualizer: Linked Lists · Operations

25. Singly linked list

prepend: O(1) · append / insert / remove / search / reverse: O(n) · Space: O(n)

Head-only list. insert at 0 is prepend; otherwise walk to index - 1 and relink.

js
class Node {
  constructor(value) {
    this.value = value
    this.next = null
  }
}
 
class LinkedList {
  constructor() {
    this.head = null
    this.size = 0
  }
 
  prepend(value) {
    const node = new Node(value)
    if (!this.isEmpty()) node.next = this.head
    this.head = node
    this.size++
  }
 
  insert(value, index) {
    if (index < 0 || index > this.size) return
    if (index === 0) return this.prepend(value)
 
    const node = new Node(value)
    let prev = this.head
    for (let i = 0; i < index - 1; i++) prev = prev.next
    node.next = prev.next
    prev.next = node
    this.size++
  }
 
  reverse() {
    let prev = null
    let curr = this.head
    while (curr) {
      const next = curr.next
      curr.next = prev
      prev = curr
      curr = next
    }
    this.head = prev
  }
}
OperationTimeWhy
prependO(1)rewrite head
appendO(n)walk to last node
insert at indexO(n)walk to previous
removeFromO(n)walk to previous
searchO(n)linear scan
reverseO(n)one pointer pass

Handwritten trace — insert sequence

StepOperationListsizeNotes
1new LinkedListempty0head = null
2insert(10, 0)101prepend
3insert(20, 0)20 → 102prepend
4insert(30, 1)20 → 30 → 103prev at 20, splice 30
5insert(40, 2)20 → 30 → 40 → 104prev at 30, splice 40
6reverse()10 → 40 → 30 → 204three-pointer walk

Back to index


26. Linked list with tail

prepend / append / removeFromFront: O(1) · removeFromEnd: O(n) · Space: O(n)

Tail makes append O(1). Removing the last node still needs the second-to-last pointer — that is the singly-linked tax.

js
class LinkedList {
  constructor() {
    this.head = null
    this.tail = null
    this.size = 0
  }
 
  prepend(value) {
    const node = new Node(value)
    if (this.isEmpty()) {
      this.head = node
      this.tail = node
    } else {
      node.next = this.head
      this.head = node
    }
    this.size++
  }
 
  append(value) {
    const node = new Node(value)
    if (this.isEmpty()) {
      this.head = node
      this.tail = node
    } else {
      this.tail.next = node
      this.tail = node
    }
    this.size++
  }
 
  removeFromFront() {
    if (this.isEmpty()) return null
    const value = this.head.value
    this.head = this.head.next
    this.size--
    return value
  }
 
  removeFromEnd() {
    if (this.isEmpty()) return null
    const value = this.tail.value
    if (this.size === 1) {
      this.head = null
      this.tail = null
    } else {
      let prev = this.head
      while (prev.next !== this.tail) prev = prev.next
      prev.next = null
      this.tail = prev
    }
    this.size--
    return value
  }
}

Handwritten trace

StepOperationheadtailListsizeReturn
1new LinkedList()nullnullempty0created
2append(1)1111—
3append(2)121 → 22—
4append(3)131 → 2 → 33—
5prepend(0)030 → 1 → 2 → 34—
6removeFromFront()131 → 2 → 330
7removeFromEnd()121 → 223
VariantappendremoveFromEnd
Head onlyO(n)O(n)
Head + tailO(1)O(n)
Doubly linkedO(1)O(1)

Back to index


27. Doubly linked list

push / pop / unshift / shift: O(1) · Space: O(n)

Each node has prev and next. Tail pop does not scan — it follows tail.prev.

js
class Node {
  constructor(value) {
    this.value = value
    this.next = null
    this.prev = null
  }
}
 
class DoublyLinkedList {
  constructor() {
    this.head = null
    this.tail = null
    this.length = 0
  }
 
  push(value) {
    const newNode = new Node(value)
    if (!this.head) {
      this.head = newNode
      this.tail = newNode
    } else {
      this.tail.next = newNode
      newNode.prev = this.tail
      this.tail = newNode
    }
    this.length++
    return this
  }
 
  pop() {
    if (!this.head) return null
    const popped = this.tail
    if (this.length === 1) {
      this.head = null
      this.tail = null
    } else {
      this.tail = popped.prev
      this.tail.next = null
      popped.prev = null
    }
    this.length--
    return popped
  }
 
  unshift(value) {
    const newNode = new Node(value)
    if (!this.head) {
      this.head = newNode
      this.tail = newNode
    } else {
      newNode.next = this.head
      this.head.prev = newNode
      this.head = newNode
    }
    this.length++
    return newNode
  }
 
  shift() {
    if (!this.head) return null
    const oldHead = this.head
    if (this.length === 1) {
      this.head = null
      this.tail = null
    } else {
      this.head = oldHead.next
      oldHead.next = null
      this.head.prev = null
    }
    this.length--
    return oldHead
  }
}

Handwritten trace — push(10), push(15), push(20), shift(), unshift('world!'), unshift('hello')

StepOperationList (head → tail)lengthReturn
1push(10)101list
2push(15)10 ⇄ 152list
3push(20)10 ⇄ 15 ⇄ 203list
4shift()15 ⇄ 202node 10
5unshift('world!')world! ⇄ 15 ⇄ 203new head
6unshift('hello')hello ⇄ world! ⇄ 15 ⇄ 204new head

showList() → ['hello', 'world!', 15, 20]

Back to index


Hash, graph, tree

28. Hash table

set / get / remove: O(k) average, O(n) worst · Space: O(n + m)

Hash = sum of character codes % table.size. Collisions use separate chaining (an array of [key, value] pairs per bucket).

Visualizer: Hash Tables

js
class HashTable {
  constructor(size) {
    this.table = new Array(size)
    this.size = size
  }
 
  hash(key) {
    let total = 0
    for (let i = 0; i < key.length; i++) total += key.charCodeAt(i)
    return total % this.size
  }
 
  set(key, value) {
    const index = this.hash(key)
    const bucket = this.table[index]
    if (!bucket) {
      this.table[index] = [[key, value]]
      return
    }
    const sameKeyItem = bucket.find((item) => item[0] === key)
    if (sameKeyItem) sameKeyItem[1] = value
    else bucket.push([key, value])
  }
 
  get(key) {
    const bucket = this.table[this.hash(key)]
    const sameKeyItem = bucket?.find((item) => item[0] === key)
    return sameKeyItem ? sameKeyItem[1] : undefined
  }
 
  remove(key) {
    const bucket = this.table[this.hash(key)]
    const sameKeyItem = bucket?.find((item) => item[0] === key)
    if (sameKeyItem) return bucket.splice(bucket.indexOf(sameKeyItem), 1)
  }
}

Hash math for size 50:

KeyCharacter codesSumsum % 50
name110+97+109+10141717
age97+103+1013011
mane109+97+110+10141717

name and mane collide at bucket 17.

Handwritten trace — new HashTable(50)

StepOperationIndexTable stateResult
1set('name', 'Bruce')17table[17] = [['name','Bruce']]—
2set('age', 25)1table[1] = [['age', 25]]—
3display()—prints buckets 1 and 17—
4get('name')17find 'name' in chain'Bruce'
5set('mane', 'Clark')17[['name','Bruce'], ['mane','Clark']]collision
6set('name', 'Diana')17update Bruce → Dianaupdate
7remove('name')17[['mane','Clark']]removed
8display()—1 → age, 17 → mane—
OperationAverageWorstExtra space
hashO(k)O(k)O(1)
setO(k)O(n)O(1)
getO(k)O(n)O(1)
removeO(k)O(n)O(1)

k = key length, n = chain length if every key lands in one bucket.

Back to index


29. Undirected graph

addVertex / addEdge / hasEdge: O(1) average · removeVertex: O(d) · display: O(V + E) · Space: O(V + E)

Adjacency list: each vertex maps to a Set of neighbors. Edges are stored in both directions.

Visualizer: Graphs

js
class Graph {
  constructor() {
    this.adjacencyList = {}
  }
 
  addVertex(vertex) {
    if (!this.adjacencyList[vertex]) this.adjacencyList[vertex] = new Set()
  }
 
  addEdge(vertex1, vertex2) {
    this.addVertex(vertex1)
    this.addVertex(vertex2)
    this.adjacencyList[vertex1].add(vertex2)
    this.adjacencyList[vertex2].add(vertex1)
  }
 
  removeEdge(vertex1, vertex2) {
    this.adjacencyList[vertex1]?.delete(vertex2)
    this.adjacencyList[vertex2]?.delete(vertex1)
  }
 
  removeVertex(vertex) {
    if (!this.adjacencyList[vertex]) return
    for (const adjacentVertex of this.adjacencyList[vertex]) {
      this.removeEdge(vertex, adjacentVertex)
    }
    delete this.adjacencyList[vertex]
  }
 
  hasEdge(vertex1, vertex2) {
    return !!this.adjacencyList[vertex1]?.has(vertex2) && !!this.adjacencyList[vertex2]?.has(vertex1)
  }
}

Handwritten trace

StepOperationInternal changeState
1addVertex(A/B/C)three empty SetsA:[], B:[], C:[]
2addEdge(A, B)A adds B, B adds AA:[B], B:[A]
2baddEdge(B, C)B adds C, C adds BA:[B], B:[A,C], C:[B]
3display()walk listsA→B; B→A,C; C→B
4hasEdge(A, B)both Sets contain each othertrue
5removeVertex(B)drop B–A and B–C, delete BA:[], C:[]; B gone
6display()remaining verticesA→ ; C→

Before removal: A ←→ B ←→ C. After removeVertex(B): A and C are isolated.

OperationTimeSpace
addVertexO(1) avgO(1) vertex
addEdgeO(1) avgO(1) edge
removeEdgeO(1) avgO(1)
removeVertexO(d) avgO(1) extra
hasEdgeO(1) avgO(1)
displayO(V + E)O(1) extra

Back to index


30. Binary search tree

insert / search / delete: O(log n) average, O(n) worst · traversals: O(n) · Space: O(n)

Left child < parent < right child. This walk uses inserts 10, 5, 15, 3 then deletes the leaf 3.

Visualizer: Binary Search Trees · Pre-order

js
class Node {
  constructor(value) {
    this.value = value
    this.left = null
    this.right = null
  }
}
 
class BinarySearchTree {
  constructor() {
    this.root = null
  }
 
  insert(value) {
    const newNode = new Node(value)
    if (!this.root) this.root = newNode
    else this.insertNode(this.root, newNode)
  }
 
  insertNode(root, newNode) {
    if (newNode.value < root.value) {
      if (!root.left) root.left = newNode
      else this.insertNode(root.left, newNode)
    } else {
      if (!root.right) root.right = newNode
      else this.insertNode(root.right, newNode)
    }
  }
 
  search(root, value) {
    if (!root) return false
    if (root.value === value) return true
    if (value < root.value) return this.search(root.left, value)
    return this.search(root.right, value)
  }
 
  min(root) {
    return root.left ? this.min(root.left) : root.value
  }
 
  delete(value) {
    this.root = this.deleteNode(this.root, value)
  }
 
  deleteNode(root, value) {
    if (!root) return root
    if (value < root.value) root.left = this.deleteNode(root.left, value)
    else if (value > root.value) root.right = this.deleteNode(root.right, value)
    else {
      if (!root.left && !root.right) return null
      if (!root.left) return root.right
      if (!root.right) return root.left
      root.value = this.min(root.right)
      root.right = this.deleteNode(root.right, root.value)
    }
    return root
  }
 
  levelOrder() {
    const queue = [this.root]
    while (queue.length) {
      const curr = queue.shift()
      console.log(curr.value)
      if (curr.left) queue.push(curr.left)
      if (curr.right) queue.push(curr.right)
    }
  }
}

Tree after each insert:

plaintext
insert(10)          insert(5)           insert(15)          insert(3)
 
    10                  10                  10                  10
                       /                   /  \                /  \
                      5                   5    15             5    15
                                                             /
                                                            3

Handwritten trace

StepOperationPathResult
1isEmpty()root is nulltrue
2insert(10)empty10 becomes root
3insert(5)5 < 10, left null5 is left of 10
4insert(15)15 > 10, right null15 is right of 10
5insert(3)3 < 10, 3 < 5, left null3 is left of 5
6levelOrder()queue 10 → 5,15 → 3prints 10, 5, 15, 3
7delete(3)3 < 10, 3 < 5, leaf5.left = null
8levelOrder()queue 10 → 5,15prints 10, 5, 15
OperationAverageWorstRecursion stack
insertO(log n)O(n)O(h)
searchO(log n)O(n)O(h)
deleteO(log n)O(n)O(h)
pre/in/postO(n)O(n)O(h)
levelOrderO(n)O(n)O(w)
min / maxO(log n)O(n)O(h)

h = height, w = max width. Sorted inserts degrade the tree into a linked list — that is why AVL / red-black trees rebalance. See AVL Trees.

An iterative addChild variant (no recursion) walks with a while loop and rejects duplicates — same BST rule, different call stack.

Back to index


Summary

This notebook is the code-evolution implementations, indexed the same way as the rest of the DSA series, with the handwritten tracers converted to tables.

FamilyTakeaway
MathLoop when you can; √n and bit tricks matter
RecursionTrace the stack — Fibonacci without memo is O(2ⁿ)
Search / sortBinary search needs sorted input; merge is stable n log n
Stack / queueArray shift is O(n); pointers or a ring buffer fix it
Linked listTail pointer buys O(1) append, not O(1) tail delete
Hash / graph / BSTHash chains collisions; BST height is the real cost

Next reads: Big O Notation · 90 Interview Coding Problems · DSA Arrays

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime