Data Structures Implementation
Learn how to implement fundamental data structures in JavaScript. Each implementation includes time complexity analysis and practical usage examples.
Stack
LIFO (Last In, First Out) data structure
Insert:
Search:
Space: O(n)
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
if (this.isEmpty()) {
return "Stack is empty";
}
return this.items.pop();
}
peek() {
if (this.isEmpty()) {
return "Stack is empty";
}
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
clear() {
this.items = [];
}
}Queue
FIFO (First In, First Out) data structure
Insert:
Search:
Space: O(n)
class Queue {
constructor() {
this.items = [];
}
enqueue(element) {
this.items.push(element);
}
dequeue() {
if (this.isEmpty()) {
return "Queue is empty";
}
return this.items.shift();
}
front() {
if (this.isEmpty()) {
return "Queue is empty";
}
return this.items[0];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
clear() {
this.items = [];
}
}Linked List
Linear data structure with nodes connected by pointers
Insert: O(1)
Search: O(n)
Space: O(n)
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0;
}
insertAtBeginning(data) {
const newNode = new Node(data);
newNode.next = this.head;
this.head = newNode;
this.size++;
}
insertAtEnd(data) {
const newNode = new Node(data);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.size++;
}
deleteFirst() {
if (!this.head) return null;
const deletedNode = this.head;
this.head = this.head.next;
this.size--;
return deletedNode.data;
}
deleteLast() {
if (!this.head) return null;
if (!this.head.next) {
const deletedNode = this.head;
this.head = null;
this.size--;
return deletedNode.data;
}
let current = this.head;
while (current.next.next) {
current = current.next;
}
const deletedNode = current.next;
current.next = null;
this.size--;
return deletedNode.data;
}
search(data) {
let current = this.head;
while (current) {
if (current.data === data) {
return true;
}
current = current.next;
}
return false;
}
print() {
let current = this.head;
let result = [];
while (current) {
result.push(current.data);
current = current.next;
}
return result.join(' -> ');
}
}Binary Tree
Tree data structure with at most two children per node
Insert: O(log n)
Search: O(log n)
Space: O(n)
class TreeNode {
constructor(data) {
this.data = data;
this.left = null;
this.right = null;
}
}
class BinaryTree {
constructor() {
this.root = null;
}
insert(data) {
const newNode = new TreeNode(data);
if (!this.root) {
this.root = newNode;
return;
}
this.insertNode(this.root, newNode);
}
insertNode(node, newNode) {
if (newNode.data < node.data) {
if (!node.left) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (!node.right) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
search(data) {
return this.searchNode(this.root, data);
}
searchNode(node, data) {
if (!node) return false;
if (node.data === data) return true;
if (data < node.data) {
return this.searchNode(node.left, data);
} else {
return this.searchNode(node.right, data);
}
}
// Inorder traversal (Left -> Root -> Right)
inorderTraversal(node = this.root, result = []) {
if (node) {
this.inorderTraversal(node.left, result);
result.push(node.data);
this.inorderTraversal(node.right, result);
}
return result;
}
// Preorder traversal (Root -> Left -> Right)
preorderTraversal(node = this.root, result = []) {
if (node) {
result.push(node.data);
this.preorderTraversal(node.left, result);
this.preorderTraversal(node.right, result);
}
return result;
}
// Postorder traversal (Left -> Right -> Root)
postorderTraversal(node = this.root, result = []) {
if (node) {
this.postorderTraversal(node.left, result);
this.postorderTraversal(node.right, result);
result.push(node.data);
}
return result;
}
}Hash Table
Data structure that maps keys to values using hash function
Insert: O(1)
Search: O(1)
Space: O(n)
class HashTable {
constructor(size = 53) {
this.keyMap = new Array(size);
}
_hash(key) {
let total = 0;
let WEIRD_PRIME = 31;
for (let i = 0; i < Math.min(key.length, 100); i++) {
let char = key[i];
let value = char.charCodeAt(0) - 96;
total = (total * WEIRD_PRIME + value) % this.keyMap.length;
}
return total;
}
set(key, value) {
let index = this._hash(key);
if (!this.keyMap[index]) {
this.keyMap[index] = [];
}
// Check if key already exists
for (let i = 0; i < this.keyMap[index].length; i++) {
if (this.keyMap[index][i][0] === key) {
this.keyMap[index][i][1] = value;
return;
}
}
this.keyMap[index].push([key, value]);
}
get(key) {
let index = this._hash(key);
if (this.keyMap[index]) {
for (let i = 0; i < this.keyMap[index].length; i++) {
if (this.keyMap[index][i][0] === key) {
return this.keyMap[index][i][1];
}
}
}
return undefined;
}
keys() {
let keysArr = [];
for (let i = 0; i < this.keyMap.length; i++) {
if (this.keyMap[i]) {
for (let j = 0; j < this.keyMap[i].length; j++) {
keysArr.push(this.keyMap[i][j][0]);
}
}
}
return keysArr;
}
values() {
let valuesArr = [];
for (let i = 0; i < this.keyMap.length; i++) {
if (this.keyMap[i]) {
for (let j = 0; j < this.keyMap[i].length; j++) {
valuesArr.push(this.keyMap[i][j][1]);
}
}
}
return valuesArr;
}
}Min/Max Heap
Complete binary tree with heap property
Insert: O(log n)
Search:
Space: O(n)
class MinHeap {
constructor() {
this.heap = [];
}
parentIndex(index) {
return Math.floor((index - 1) / 2);
}
leftChildIndex(index) {
return 2 * index + 1;
}
rightChildIndex(index) {
return 2 * index + 2;
}
hasParent(index) {
return this.parentIndex(index) >= 0;
}
hasLeftChild(index) {
return this.leftChildIndex(index) < this.heap.length;
}
hasRightChild(index) {
return this.rightChildIndex(index) < this.heap.length;
}
parent(index) {
return this.heap[this.parentIndex(index)];
}
leftChild(index) {
return this.heap[this.leftChildIndex(index)];
}
rightChild(index) {
return this.heap[this.rightChildIndex(index)];
}
swap(index1, index2) {
[this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]];
}
peek() {
if (this.heap.length === 0) {
return null;
}
return this.heap[0];
}
insert(value) {
this.heap.push(value);
this.heapifyUp();
}
heapifyUp() {
let index = this.heap.length - 1;
while (this.hasParent(index) && this.parent(index) > this.heap[index]) {
this.swap(this.parentIndex(index), index);
index = this.parentIndex(index);
}
}
remove() {
if (this.heap.length === 0) {
return null;
}
const item = this.heap[0];
this.heap[0] = this.heap[this.heap.length - 1];
this.heap.pop();
this.heapifyDown();
return item;
}
heapifyDown() {
let index = 0;
while (this.hasLeftChild(index)) {
let smallerChildIndex = this.leftChildIndex(index);
if (this.hasRightChild(index) && this.rightChild(index) < this.leftChild(index)) {
smallerChildIndex = this.rightChildIndex(index);
}
if (this.heap[index] < this.heap[smallerChildIndex]) {
break;
} else {
this.swap(index, smallerChildIndex);
}
index = smallerChildIndex;
}
}
size() {
return this.heap.length;
}
isEmpty() {
return this.heap.length === 0;
}
}Key Takeaways
- • Stack: Perfect for undo operations, function call stack, and bracket matching
- • Queue: Ideal for task scheduling, breadth-first search, and print spooling
- • Linked List: Great for dynamic memory allocation and when you need frequent insertions/deletions
- • Binary Tree: Excellent for hierarchical data and efficient searching
- • Hash Table: Best for fast lookups, caching, and removing duplicates
- • Heap: Perfect for priority queues, scheduling algorithms, and finding min/max efficiently