Introduction to Data Structures
Why Data Structures Matter
Data structures are specialized formats for organizing, processing, retrieving, and storing data. They form the backbone of efficient algorithms and are essential for solving complex computational problems. The right data structure can:
- Significantly improve application performance
- Reduce memory consumption
- Simplify code and improve readability
- Enable elegant solutions to otherwise difficult problems
As a developer, understanding data structures is not just an academic exercise—it's a practical skill that directly impacts your ability to write efficient and maintainable code.
Types of Data Structures
Data structures can be broadly categorized into two types:
1. Primitive Data Structures
These are the basic data types provided by programming languages:
- Numbers (integers, floating-point)
- Strings
- Booleans
- Null/Undefined values
2. Non-Primitive (Composite) Data Structures
These are more complex structures built using primitive types and other composite structures. They can be further categorized as:
Linear Data Structures
Elements are arranged in sequential order:
- Arrays: Collection of elements identified by index
- Linked Lists: Collection of nodes connected via references
- Stacks: Last-In-First-Out (LIFO) collection
- Queues: First-In-First-Out (FIFO) collection
Non-Linear Data Structures
Elements are not arranged sequentially:
- Trees: Hierarchical structure with a root node and child nodes
- Graphs: Collection of nodes (vertices) connected by edges
- Hash Tables: Key-value pairs with efficient lookups
- Heaps: Specialized tree-based structure for priority operations
Key Operations and Complexity
For each data structure, we analyze performance based on common operations:
- Access: Retrieving an element (by index, key, etc.)
- Search: Finding an element
- Insertion: Adding a new element
- Deletion: Removing an element
- Traversal: Visiting all elements
The efficiency of these operations is measured using Big O notation, which describes how the performance scales with input size.
Arrays
Arrays are among the most fundamental data structures, offering direct access to elements via indices.
Implementation
// Creating arrays in JavaScript
const fruits = ['apple', 'banana', 'orange'];
const numbers = new Array(1, 2, 3, 4, 5);
const matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; // 2D array
Key Operations
// Access - O(1)
const firstFruit = fruits[0]; // apple
// Search - O(n)
const hasApple = fruits.includes('apple'); // true
// Insertion - O(1) at the end, O(n) elsewhere
fruits.push('grape'); // Add to end
fruits.unshift('pear'); // Add to beginning
fruits.splice(2, 0, 'kiwi'); // Add at specific position
// Deletion - O(1) at the end, O(n) elsewhere
fruits.pop(); // Remove from end
fruits.shift(); // Remove from beginning
fruits.splice(1, 1); // Remove at specific position
// Traversal - O(n)
fruits.forEach(fruit => console.log(fruit));
Time Complexity Analysis
Operation | Average Case | Worst Case |
---|---|---|
Access | O(1) | O(1) |
Search | O(n) | O(n) |
Insertion | O(n) | O(n) |
Deletion | O(n) | O(n) |
Arrays offer constant-time access but linear-time insertions and deletions (except at the end).
Linked Lists
A linked list consists of nodes, each containing data and a reference to the next node.
Implementation
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0;
}
// Add a node to the end of the list
append(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++;
}
// Add a node to the beginning of the list
prepend(data) {
const newNode = new Node(data);
newNode.next = this.head;
this.head = newNode;
this.size++;
}
// Insert at a specific position
insertAt(data, index) {
if (index < 0 || index > this.size) {
return false;
}
if (index === 0) {
this.prepend(data);
return true;
}
const newNode = new Node(data);
let current = this.head;
let previous;
let count = 0;
while (count < index) {
previous = current;
current = current.next;
count++;
}
newNode.next = current;
previous.next = newNode;
this.size++;
return true;
}
// Remove a node at a specific position
removeAt(index) {
if (index < 0 || index >= this.size) {
return null;
}
let current = this.head;
let previous;
let count = 0;
if (index === 0) {
this.head = current.next;
} else {
while (count < index) {
previous = current;
current = current.next;
count++;
}
previous.next = current.next;
}
this.size--;
return current.data;
}
// Find a node by its data
find(data) {
let current = this.head;
let index = 0;
while (current) {
if (current.data === data) {
return index;
}
current = current.next;
index++;
}
return -1;
}
// Print the linked list
printList() {
let current = this.head;
const values = [];
while (current) {
values.push(current.data);
current = current.next;
}
return values.join(' -> ');
}
// Get the size of the linked list
getSize() {
return this.size;
}
// Check if the linked list is empty
isEmpty() {
return this.size === 0;
}
}
// Usage example
const list = new LinkedList();
list.append(10);
list.append(20);
list.prepend(5);
list.insertAt(15, 2);
console.log(list.printList()); // Output: 5 -> 10 -> 15 -> 20
console.log(list.find(15)); // Output: 2
list.removeAt(1);
console.log(list.printList()); // Output: 5 -> 15 -> 20
Time Complexity Analysis
Operation | Average Case | Worst Case |
---|---|---|
Access | O(n) | O(n) |
Search | O(n) | O(n) |
Insertion | O(1) or O(n) | O(n) |
Deletion | O(1) or O(n) | O(n) |
Linked lists excel at insertions and deletions (when the position is known) but have slower access times compared to arrays.
Stacks
Stacks follow the Last-In-First-Out (LIFO) principle, similar to a stack of plates.
Implementation
class Stack {
constructor() {
this.items = [];
}
// Add an element to the top of the stack
push(element) {
this.items.push(element);
}
// Remove and return the top element
pop() {
if (this.isEmpty()) {
return "Underflow";
}
return this.items.pop();
}
// Return the top element without removing it
peek() {
if (this.isEmpty()) {
return "Stack is empty";
}
return this.items[this.items.length - 1];
}
// Check if the stack is empty
isEmpty() {
return this.items.length === 0;
}
// Return the size of the stack
size() {
return this.items.length;
}
// Clear the stack
clear() {
this.items = [];
}
// Print the stack elements
print() {
return this.items.join(' ');
}
}
// Usage example
const stack = new Stack();
stack.push(10);
stack.push(20);
stack.push(30);
console.log(stack.print()); // Output: 10 20 30
console.log(stack.peek()); // Output: 30
console.log(stack.pop()); // Output: 30
console.log(stack.print()); // Output: 10 20
Applications of Stacks
- Function call management (call stack)
- Expression evaluation and syntax parsing
- Undo mechanisms in applications
- Browser history (back button implementation)
- Depth-first traversal of trees and graphs
Time Complexity Analysis
Operation | Time Complexity |
---|---|
Push | O(1) |
Pop | O(1) |
Peek | O(1) |
Search | O(n) |
Size | O(1) |
Queues
Queues follow the First-In-First-Out (FIFO) principle, similar to a line of people.
Implementation
class Queue {
constructor() {
this.items = {};
this.frontIndex = 0;
this.backIndex = 0;
}
// Add an element to the back of the queue
enqueue(element) {
this.items[this.backIndex] = element;
this.backIndex++;
}
// Remove and return the front element
dequeue() {
if (this.isEmpty()) {
return "Underflow";
}
const item = this.items[this.frontIndex];
delete this.items[this.frontIndex];
this.frontIndex++;
return item;
}
// Return the front element without removing it
front() {
if (this.isEmpty()) {
return "Queue is empty";
}
return this.items[this.frontIndex];
}
// Check if the queue is empty
isEmpty() {
return this.frontIndex === this.backIndex;
}
// Return the size of the queue
size() {
return this.backIndex - this.frontIndex;
}
// Clear the queue
clear() {
this.items = {};
this.frontIndex = 0;
this.backIndex = 0;
}
// Print the queue elements
print() {
const values = [];
for (let i = this.frontIndex; i < this.backIndex; i++) {
values.push(this.items[i]);
}
return values.join(' ');
}
}
// Usage example
const queue = new Queue();
queue.enqueue('A');
queue.enqueue('B');
queue.enqueue('C');
console.log(queue.print()); // Output: A B C
console.log(queue.front()); // Output: A
console.log(queue.dequeue()); // Output: A
console.log(queue.print()); // Output: B C
Applications of Queues
- Task scheduling in operating systems
- Handling of requests on a single shared resource (printer, CPU)
- Buffering for data streams
- Breadth-first traversal of trees and graphs
- Message queues in distributed systems
Time Complexity Analysis
Operation | Time Complexity |
---|---|
Enqueue | O(1) |
Dequeue | O(1) |
Front | O(1) |
Search | O(n) |
Size | O(1) |
Hash Tables
Hash tables store key-value pairs and provide efficient lookups using a hash function.
Implementation
class HashTable {
constructor(size = 53) {
this.table = new Array(size);
this.size = size;
}
// Simple hash function
_hash(key) {
let total = 0;
let prime = 31;
// Use only the first 100 characters of the key to limit computation
for (let i = 0; i < Math.min(key.length, 100); i++) {
const char = key[i];
const value = char.charCodeAt(0) - 96; // 'a' is 1, 'b' is 2, etc.
total = (total * prime + value) % this.size;
}
return total;
}
// Set a key-value pair
set(key, value) {
const index = this._hash(key);
if (!this.table[index]) {
this.table[index] = [];
}
// Check if key exists and update
for (let i = 0; i < this.table[index].length; i++) {
if (this.table[index][i][0] === key) {
this.table[index][i][1] = value;
return;
}
}
// If key doesn't exist, add new key-value pair
this.table[index].push([key, value]);
}
// Get a value by key
get(key) {
const index = this._hash(key);
if (!this.table[index]) {
return undefined;
}
for (let i = 0; i < this.table[index].length; i++) {
if (this.table[index][i][0] === key) {
return this.table[index][i][1];
}
}
return undefined;
}
// Remove a key-value pair
remove(key) {
const index = this._hash(key);
if (!this.table[index]) {
return false;
}
for (let i = 0; i < this.table[index].length; i++) {
if (this.table[index][i][0] === key) {
this.table[index].splice(i, 1);
return true;
}
}
return false;
}
// Get all keys
keys() {
const keysArr = [];
for (let i = 0; i < this.table.length; i++) {
if this.table[i]) {
for (let j = 0; j < this.table[i].length; j++) {
keysArr.push(this.table[i][j][0]);
}
}
}
return keysArr;
}
// Get all values
values() {
const valuesArr = [];
for (let i = 0; i < this.table.length; i++) {
if (this.table[i]) {
for (let j = 0; j < this.table[i].length; j++) {
valuesArr.push(this.table[i][j][1]);
}
}
}
return valuesArr;
}
}
// Usage example
const ht = new HashTable(17);
ht.set("maroon", "#800000");
ht.set("yellow", "#FFFF00");
ht.set("olive", "#808000");
ht.set("salmon", "#FA8072");
console.log(ht.get("maroon")); // Output: #800000
console.log(ht.get("yellow")); // Output: #FFFF00
console.log(ht.keys()); // Output: ['maroon', 'yellow', 'olive', 'salmon']
ht.remove("olive");
console.log(ht.keys()); // Output: ['maroon', 'yellow', 'salmon']
Time Complexity Analysis
Operation | Average Case | Worst Case |
---|---|---|
Insert | O(1) | O(n) |
Delete | O(1) | O(n) |
Search | O(1) | O(n) |
Hash tables provide near-constant time operations on average, but performance degrades with many collisions.
Trees
Trees are hierarchical data structures with a root node and child nodes.
Binary Search Tree Implementation
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
this.root = null;
}
// Insert a value into the BST
insert(value) {
const newNode = new TreeNode(value);
if (!this.root) {
this.root = newNode;
return this;
}
let current = this.root;
while (true) {
if (value === current.value) return undefined; // Duplicate values not allowed
if (value < current.value) {
if (!current.left) {
current.left = newNode;
return this;
}
current = current.left;
} else {
if (!current.right) {
current.right = newNode;
return this;
}
current = current.right;
}
}
}
// Find a value in the BST
find(value) {
if (!this.root) return false;
let current = this.root;
let found = false;
while (current && !found) {
if (value < current.value) {
current = current.left;
} else if (value > current.value) {
current = current.right;
} else {
found = true;
}
}
if (!found) return false;
return current;
}
// Check if a value exists in the BST
contains(value) {
if (!this.root) return false;
let current = this.root;
while (current) {
if (value < current.value) {
current = current.left;
} else if (value > current.value) {
current = current.right;
} else {
return true;
}
}
return false;
}
// Breadth-First Search (level by level)
bfs() {
const data = [];
const queue = [];
let node = this.root;
if (!node) return data;
queue.push(node);
while (queue.length) {
node = queue.shift();
data.push(node.value);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
return data;
}
// Depth-First Search: Pre-order (node, left, right)
dfsPreOrder() {
const data = [];
function traverse(node) {
data.push(node.value);
if (node.left) traverse(node.left);
if (node.right) traverse(node.right);
}
if (this.root) traverse(this.root);
return data;
}
// Depth-First Search: In-order (left, node, right)
dfsInOrder() {
const data = [];
function traverse(node) {
if (node.left) traverse(node.left);
data.push(node.value);
if (node.right) traverse(node.right);
}
if (this.root) traverse(this.root);
return data;
}
// Depth-First Search: Post-order (left, right, node)
dfsPostOrder() {
const data = [];
function traverse(node) {
if (node.left) traverse(node.left);
if (node.right) traverse(node.right);
data.push(node.value);
}
if (this.root) traverse(this.root);
return data;
}
}
// Usage example
const bst = new BinarySearchTree();
bst.insert(10);
bst.insert(6);
bst.insert(15);
bst.insert(3);
bst.insert(8);
bst.insert(20);
console.log(bst.bfs()); // Output: [10, 6, 15, 3, 8, 20]
console.log(bst.dfsPreOrder()); // Output: [10, 6, 3, 8, 15, 20]
console.log(bst.dfsInOrder()); // Output: [3, 6, 8, 10, 15, 20]
console.log(bst.dfsPostOrder()); // Output: [3, 8, 6, 20, 15, 10]
Time Complexity Analysis
Operation | Average Case | Worst Case |
---|---|---|
Access | O(log n) | O(n) |
Search | O(log n) | O(n) |
Insertion | O(log n) | O(n) |
Deletion | O(log n) | O(n) |
Binary search trees provide efficient operations when balanced, but can degrade to linear time if unbalanced.
Red-Black Trees
Red-Black Trees are self-balancing binary search trees that maintain balance through a set of properties and rebalancing operations, ensuring O(log n) worst-case time complexity for basic operations.
Key Properties
- Every node is either red or black
- The root is always black
- No red node has a red child (red nodes can only have black children)
- Every path from the root to any leaf contains the same number of black nodes
- All NULL pointers (leaves) are considered black
Implementation
class RedBlackNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
this.parent = null;
this.color = "RED"; // New nodes are always red
}
}
class RedBlackTree {
constructor() {
this.NIL = new RedBlackNode(null); // Sentinel node
this.NIL.color = "BLACK";
this.NIL.left = null;
this.NIL.right = null;
this.root = this.NIL;
}
// Left rotation for balancing
leftRotate(x) {
const y = x.right;
x.right = y.left;
if (y.left !== this.NIL) {
y.left.parent = x;
}
y.parent = x.parent;
if (x.parent === null) {
this.root = y;
} else if (x === x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
y.left = x;
x.parent = y;
}
// Right rotation for balancing
rightRotate(y) {
const x = y.left;
y.left = x.right;
if (x.right !== this.NIL) {
x.right.parent = y;
}
x.parent = y.parent;
if (y.parent === null) {
this.root = x;
} else if (y === y.parent.left) {
y.parent.left = x;
} else {
y.parent.right = x;
}
x.right = y;
y.parent = x;
}
// Insert a new value
insert(value) {
const newNode = new RedBlackNode(value);
newNode.left = this.NIL;
newNode.right = this.NIL;
let y = null;
let x = this.root;
// Find the position to insert
while (x !== this.NIL) {
y = x;
if (newNode.value < x.value) {
x = x.left;
} else {
x = x.right;
}
}
newNode.parent = y;
if (y === null) {
this.root = newNode; // Tree was empty
} else if (newNode.value < y.value) {
y.left = newNode;
} else {
y.right = newNode;
}
// If tree was empty, color the root black and return
if (newNode.parent === null) {
newNode.color = "BLACK";
return;
}
// If grandparent is null, we're done
if (newNode.parent.parent === null) {
return;
}
// Fix the tree to maintain Red-Black properties
this.fixInsert(newNode);
}
// Fix Red-Black Tree properties after insertion
fixInsert(k) {
let u;
while (k.parent.color === "RED") {
if (k.parent === k.parent.parent.right) {
u = k.parent.parent.left; // Uncle
if (u.color === "RED") {
// Case 1: Uncle is red - recolor
u.color = "BLACK";
k.parent.color = "BLACK";
k.parent.parent.color = "RED";
k = k.parent.parent;
} else {
if (k === k.parent.left) {
// Case 2: Uncle is black and k is a left child
k = k.parent;
this.rightRotate(k);
}
// Case 3: Uncle is black and k is a right child
k.parent.color = "BLACK";
k.parent.parent.color = "RED";
this.leftRotate(k.parent.parent);
}
} else {
u = k.parent.parent.right; // Uncle
if (u.color === "RED") {
// Case 1: Uncle is red - recolor
u.color = "BLACK";
k.parent.color = "BLACK";
k.parent.parent.color = "RED";
k = k.parent.parent;
} else {
if (k === k.parent.right) {
// Case 2: Uncle is black and k is a right child
k = k.parent;
this.leftRotate(k);
}
// Case 3: Uncle is black and k is a left child
k.parent.color = "BLACK";
k.parent.parent.color = "RED";
this.rightRotate(k.parent.parent);
}
}
if (k === this.root) {
break;
}
}
this.root.color = "BLACK";
}
// Search for a value
search(value) {
return this._searchRecursive(this.root, value);
}
_searchRecursive(node, value) {
if (node === this.NIL) return false;
if (value === node.value) {
return true;
}
if (value < node.value) {
return this._searchRecursive(node.left, value);
}
return this._searchRecursive(node.right, value);
}
// Inorder traversal
inOrder() {
const result = [];
this._inOrderRecursive(this.root, result);
return result;
}
_inOrderRecursive(node, result) {
if (node === this.NIL) return;
this._inOrderRecursive(node.left, result);
result.push(`${node.value}(${node.color})`);
this._inOrderRecursive(node.right, result);
}
}
// Usage example
const rbTree = new RedBlackTree();
[7, 3, 18, 10, 22, 8, 11, 26, 2, 6, 13].forEach(val => rbTree.insert(val));
console.log(rbTree.search(8)); // true
console.log(rbTree.search(9)); // false
console.log(rbTree.inOrder()); // Sorted list with color information
Visualization
To understand Red-Black Trees better, let's see what happens when we insert values:
- Initial empty tree: Just the root (BLACK)
- Insert 7: Root is always BLACK
- Insert 3: 3 is RED, as a child of BLACK 7
- Insert 18: 18 is RED, as a right child of BLACK 7
- Insert 10: 10 is RED, but its parent (18) is also RED, violating property 3
- After rebalancing: 7 is BLACK, with children 3 (BLACK) and 18 (BLACK)
- 10 becomes a RED child of 18
Each insertion may trigger rotations and color changes to maintain the Red-Black properties.
Time Complexity Analysis
Operation | Average Case | Worst Case |
---|---|---|
Access | O(log n) | O(log n) |
Search | O(log n) | O(log n) |
Insertion | O(log n) | O(log n) |
Deletion | O(log n) | O(log n) |
Red-Black Trees guarantee O(log n) worst-case performance for basic operations, making them suitable for applications where consistent performance is critical, such as in database indexes and system implementations.
Applications
- Implementing associative arrays in languages like Java (TreeMap) and C++ (std::map)
- Process scheduling in operating systems
- Database indexing for consistent lookup times
- Computational geometry algorithms
- K-means variant algorithms for nearest neighbor search
Graphs
Graphs consist of vertices (nodes) connected by edges, representing relationships between entities.
Implementation: Adjacency List
class Graph {
constructor() {
this.adjacencyList = {};
}
// Add a vertex to the graph
addVertex(vertex) {
if (!this.adjacencyList[vertex]) {
this.adjacencyList[vertex] = [];
}
}
// Add an edge between two vertices
addEdge(vertex1, vertex2) {
if (this.adjacencyList[vertex1] && this.adjacencyList[vertex2]) {
this.adjacencyList[vertex1].push(vertex2);
this.adjacencyList[vertex2].push(vertex1); // For undirected graph
}
}
// Remove an edge between two vertices
removeEdge(vertex1, vertex2) {
if (this.adjacencyList[vertex1] && this.adjacencyList[vertex2]) {
this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter(
v => v !== vertex2
);
this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter(
v => v !== vertex1
);
}
}
// Remove a vertex and all its edges
removeVertex(vertex) {
if (!this.adjacencyList[vertex]) return;
while (this.adjacencyList[vertex].length) {
const adjacentVertex = this.adjacencyList[vertex].pop();
this.removeEdge(vertex, adjacentVertex);
}
delete this.adjacencyList[vertex];
}
// Depth-First Traversal (recursive)
depthFirstRecursive(start) {
const result = [];
const visited = {};
const adjacencyList = this.adjacencyList;
(function dfs(vertex) {
if (!vertex) return null;
visited[vertex] = true;
result.push(vertex);
adjacencyList[vertex].forEach(neighbor => {
if (!visited[neighbor]) {
return dfs(neighbor);
}
});
})(start);
return result;
}
// Depth-First Traversal (iterative)
depthFirstIterative(start) {
const result = [];
const visited = {};
const stack = [start];
visited[start] = true;
while (stack.length) {
const currentVertex = stack.pop();
result.push(currentVertex);
this.adjacencyList[currentVertex].forEach(neighbor => {
if (!visited[neighbor]) {
visited[neighbor] = true;
stack.push(neighbor);
}
});
}
return result;
}
// Breadth-First Traversal
breadthFirst(start) {
const queue = [start];
const result = [];
const visited = {};
visited[start] = true;
while (queue.length) {
const currentVertex = queue.shift();
result.push(currentVertex);
this.adjacencyList[currentVertex].forEach(neighbor => {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.push(neighbor);
}
});
}
return result;
}
}
// Usage example
const g = new Graph();
g.addVertex("A");
g.addVertex("B");
g.addVertex("C");
g.addVertex("D");
g.addVertex("E");
g.addVertex("F");
g.addEdge("A", "B");
g.addEdge("A", "C");
g.addEdge("B", "D");
g.addEdge("C", "E");
g.addEdge("D", "E");
g.addEdge("D", "F");
g.addEdge("E", "F");
console.log(g.depthFirstRecursive("A")); // Output: ['A', 'B', 'D', 'E', 'C', 'F']
console.log(g.breadthFirst("A")); // Output: ['A', 'B', 'C', 'D', 'E', 'F']
Time Complexity Analysis
Operation | Adjacency List | Adjacency Matrix |
---|---|---|
Add Vertex | O(1) | O(V²) |
Add Edge | O(1) | O(1) |
Remove Vertex | O(V + E) | O(V²) |
Remove Edge | O(E) | O(1) |
Query | O(V) | O(1) |
Storage | O(V + E) | O(V²) |
V = number of vertices, E = number of edges
Heaps
Heaps are specialized tree-based data structures that satisfy the heap property.
Implementation: Max Heap
class MaxBinaryHeap {
constructor() {
this.values = [];
}
// Helper methods for index calculations
getParentIndex(index) {
return Math.floor((index - 1) / 2);
}
getLeftChildIndex(index) {
return 2 * index + 1;
}
getRightChildIndex(index) {
return 2 * index + 2;
}
// Insert a value into the heap
insert(value) {
this.values.push(value);
this.bubbleUp();
return this;
}
// Move the last element up to its correct position
bubbleUp() {
let index = this.values.length - 1;
const element = this.values[index];
while (index > 0) {
const parentIndex = this.getParentIndex(index);
const parent = this.values[parentIndex];
if (element <= parent) break;
// Swap with parent
this.values[parentIndex] = element;
this.values[index] = parent;
index = parentIndex;
}
}
// Extract the maximum element
extractMax() {
const max = this.values[0];
const end = this.values.pop();
if (this.values.length > 0) {
this.values[0] = end;
this.sinkDown();
}
return max;
}
// Move the root element down to its correct position
sinkDown() {
let index = 0;
const length = this.values.length;
const element = this.values[0];
while (true) {
const leftChildIndex = this.getLeftChildIndex(index);
const rightChildIndex = this.getRightChildIndex(index);
let leftChild, rightChild;
let swap = null;
if (leftChildIndex < length) {
leftChild = this.values[leftChildIndex];
if (leftChild > element) {
swap = leftChildIndex;
}
}
if (rightChildIndex < length) {
rightChild = this.values[rightChildIndex];
if (
(swap === null && rightChild > element) ||
(swap !== null && rightChild > leftChild)
) {
swap = rightChildIndex;
}
}
if (swap === null) break;
// Swap with child
this.values[index] = this.values[swap];
this.values[swap] = element;
index = swap;
}
}
}
// Usage example
const heap = new MaxBinaryHeap();
heap.insert(41);
heap.insert(39);
heap.insert(33);
heap.insert(18);
heap.insert(27);
heap.insert(12);
heap.insert(55);
console.log(heap.values); // [55, 39, 41, 18, 27, 12, 33]
console.log(heap.extractMax()); // 55
console.log(heap.values); // [41, 39, 33, 18, 27, 12]
Time Complexity Analysis
Operation | Time Complexity |
---|---|
Insertion | O(log n) |
Removal | O(log n) |
Search | O(n) |
Peek | O(1) |
Heapify | O(n) |
Choosing the Right Data Structure
Selecting the appropriate data structure depends on:
- Operations Required: What operations will be performed most frequently?
- Time Complexity: What performance characteristics are needed?
- Memory Constraints: How much memory is available?
- Data Volume: How much data needs to be handled?
- Data Organization: What is the natural structure of the data?
Common Use Cases
Data Structure | Best For |
---|---|
Array | Sequential access, direct indexing |
Linked List | Frequent insertions/deletions at various positions |
Stack | LIFO operations, function calls, undo mechanisms |
Queue | FIFO operations, order processing, BFS algorithms |
Hash Table | Fast lookups, caches, dictionaries |
Binary Search Tree | Ordered data, range queries, moderate insertions |
Heap | Priority processing, scheduling, finding min/max |
Graph | Relationship mapping, networks, pathfinding |
Conclusion
Data structures provide the foundation for efficient algorithms and software. Understanding their properties, operations, and performance characteristics allows you to make informed decisions about which structure to use for a particular problem.
As your applications grow in complexity, the importance of choosing the right data structure becomes more significant. A well-chosen data structure can be the difference between an application that scales gracefully and one that breaks under load.
In the upcoming articles, we'll dive deeper into each data structure, exploring advanced operations, variants, and real-world applications. We'll also examine specialized data structures designed for specific use cases and constraints.