Complete Data Structures and Algorithms Guide
Introduction
This comprehensive guide covers fundamental data structures and algorithms with detailed explanations and implementations in both C++ and Python. Each section includes theoretical concepts, complexity analysis, advantages, disadvantages, and complete working code.
Linear Data Structures
Array
Detailed Explanation: Arrays are the most fundamental data structure where elements of the same type are stored in consecutive memory locations. Each element can be accessed directly using its index, making random access very efficient. The index typically starts from 0 in most programming languages.
C++ Implementation
#include <iostream>
#include <vector>
using namespace std;
class Array {
private:
<int> arr;
vectorint capacity;
public:
(int size) : capacity(size) {
Array.reserve(size);
arr}
void insert(int element) {
if (arr.size() < capacity) {
.push_back(element);
arr}
}
void remove(int index) {
if (index >= 0 && index < arr.size()) {
.erase(arr.begin() + index);
arr}
}
int get(int index) {
if (index >= 0 && index < arr.size()) {
return arr[index];
}
return -1;
}
void display() {
for (int val : arr) {
<< val << " ";
cout }
<< endl;
cout }
};
Python Implementation
class Array:
def __init__(self, capacity):
self.arr = []
self.capacity = capacity
def insert(self, element):
if len(self.arr) < self.capacity:
self.arr.append(element)
def remove(self, index):
if 0 <= index < len(self.arr):
self.arr.pop(index)
def get(self, index):
if 0 <= index < len(self.arr):
return self.arr[index]
return None
def display(self):
print(self.arr)
Linked List
Detailed Explanation: A linked list consists of nodes where each node contains data and a pointer/reference to the next node in the sequence. Unlike arrays, linked list elements are not stored in contiguous memory locations. The list is accessed through the head node, and traversal happens by following the links from one node to the next.
C++ Implementation
#include <iostream>
using namespace std;
struct Node {
int data;
* next;
Node
(int value) : data(value), next(nullptr) {}
Node};
class LinkedList {
private:
* head;
Node
public:
() : head(nullptr) {}
LinkedList
void insertAtHead(int data) {
* newNode = new Node(data);
Node->next = head;
newNode= newNode;
head }
void insertAtTail(int data) {
* newNode = new Node(data);
Nodeif (!head) {
= newNode;
head return;
}
* current = head;
Nodewhile (current->next) {
= current->next;
current }
->next = newNode;
current}
void deleteNode(int data) {
if (!head) return;
if (head->data == data) {
* temp = head;
Node= head->next;
head delete temp;
return;
}
* current = head;
Nodewhile (current->next && current->next->data != data) {
= current->next;
current }
if (current->next) {
* temp = current->next;
Node->next = current->next->next;
currentdelete temp;
}
}
void display() {
* current = head;
Nodewhile (current) {
<< current->data << " -> ";
cout = current->next;
current }
<< "NULL" << endl;
cout }
~LinkedList() {
while (head) {
* temp = head;
Node= head->next;
head delete temp;
}
}
};
Python Implementation
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def insert_at_head(self, data):
= Node(data)
new_node next = self.head
new_node.self.head = new_node
def insert_at_tail(self, data):
= Node(data)
new_node if not self.head:
self.head = new_node
return
= self.head
current while current.next:
= current.next
current next = new_node
current.
def delete_node(self, data):
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
return
= self.head
current while current.next and current.next.data != data:
= current.next
current
if current.next:
next = current.next.next
current.
def display(self):
= self.head
current while current:
print(current.data, end=" -> ")
= current.next
current print("None")
Stack
Detailed Explanation: A stack is a linear data structure that follows the LIFO principle, meaning the last element added is the first one to be removed. Think of it like a stack of plates - you can only add or remove plates from the top. The stack has a “top” pointer that indicates the position of the most recently added element.
C++ Implementation
#include <iostream>
#include <vector>
using namespace std;
class Stack {
private:
<int> stack;
vector
public:
void push(int data) {
.push_back(data);
stack}
void pop() {
if (!isEmpty()) {
.pop_back();
stack}
}
int top() {
if (!isEmpty()) {
return stack.back();
}
return -1;
}
bool isEmpty() {
return stack.empty();
}
int size() {
return stack.size();
}
void display() {
for (int i = stack.size() - 1; i >= 0; i--) {
<< stack[i] << endl;
cout }
}
};
Python Implementation
class Stack:
def __init__(self):
self.stack = []
def push(self, data):
self.stack.append(data)
def pop(self):
if not self.is_empty():
return self.stack.pop()
return None
def top(self):
if not self.is_empty():
return self.stack[-1]
return None
def is_empty(self):
return len(self.stack) == 0
def size(self):
return len(self.stack)
def display(self):
for item in reversed(self.stack):
print(item)
Queue
Detailed Explanation: A queue is a linear data structure that follows the FIFO principle, similar to a real-world queue or line. Elements are added (enqueued) at the rear/back and removed (dequeued) from the front. It maintains two pointers: front (for removal) and rear (for insertion).
C++ Implementation
#include <iostream>
using namespace std;
class Queue {
private:
int* arr;
int front, rear, capacity, size;
public:
(int cap) : capacity(cap), front(0), rear(-1), size(0) {
Queue= new int[capacity];
arr }
void enqueue(int data) {
if (size == capacity) return;
= (rear + 1) % capacity;
rear [rear] = data;
arr++;
size}
void dequeue() {
if (isEmpty()) return;
= (front + 1) % capacity;
front --;
size}
int getFront() {
if (!isEmpty()) {
return arr[front];
}
return -1;
}
bool isEmpty() {
return size == 0;
}
bool isFull() {
return size == capacity;
}
void display() {
if (isEmpty()) return;
for (int i = 0; i < size; i++) {
<< arr[(front + i) % capacity] << " ";
cout }
<< endl;
cout }
~Queue() {
delete[] arr;
}
};
Python Implementation
from collections import deque
class Queue:
def __init__(self):
self.queue = deque()
def enqueue(self, data):
self.queue.append(data)
def dequeue(self):
if not self.is_empty():
return self.queue.popleft()
return None
def front(self):
if not self.is_empty():
return self.queue[0]
return None
def is_empty(self):
return len(self.queue) == 0
def size(self):
return len(self.queue)
def display(self):
print(list(self.queue))
Non-Linear Data Structures
Binary Tree
Detailed Explanation: A binary tree is a hierarchical data structure where each node has at most two children, referred to as left child and right child. The topmost node is called the root, and nodes with no children are called leaves. Each node contains data and pointers to its left and right children.
C++ Implementation
#include <iostream>
#include <queue>
using namespace std;
struct TreeNode {
int data;
* left;
TreeNode* right;
TreeNode
(int value) : data(value), left(nullptr), right(nullptr) {}
TreeNode};
class BinaryTree {
private:
* root;
TreeNode
void inorderHelper(TreeNode* node) {
if (node) {
(node->left);
inorderHelper<< node->data << " ";
cout (node->right);
inorderHelper}
}
void preorderHelper(TreeNode* node) {
if (node) {
<< node->data << " ";
cout (node->left);
preorderHelper(node->right);
preorderHelper}
}
void postorderHelper(TreeNode* node) {
if (node) {
(node->left);
postorderHelper(node->right);
postorderHelper<< node->data << " ";
cout }
}
public:
() : root(nullptr) {}
BinaryTree
void insert(int data) {
* newNode = new TreeNode(data);
TreeNodeif (!root) {
= newNode;
root return;
}
<TreeNode*> q;
queue.push(root);
q
while (!q.empty()) {
* current = q.front();
TreeNode.pop();
q
if (!current->left) {
->left = newNode;
currentreturn;
} else if (!current->right) {
->right = newNode;
currentreturn;
} else {
.push(current->left);
q.push(current->right);
q}
}
}
void inorder() {
(root);
inorderHelper<< endl;
cout }
void preorder() {
(root);
preorderHelper<< endl;
cout }
void postorder() {
(root);
postorderHelper<< endl;
cout }
};
Python Implementation
from collections import deque
class TreeNode:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
class BinaryTree:
def __init__(self):
self.root = None
def insert(self, data):
= TreeNode(data)
new_node if not self.root:
self.root = new_node
return
= deque([self.root])
queue while queue:
= queue.popleft()
current
if not current.left:
= new_node
current.left return
elif not current.right:
= new_node
current.right return
else:
queue.append(current.left)
queue.append(current.right)
def inorder(self, node=None):
if node is None:
= self.root
node
if node:
self.inorder(node.left)
print(node.data, end=" ")
self.inorder(node.right)
def preorder(self, node=None):
if node is None:
= self.root
node
if node:
print(node.data, end=" ")
self.preorder(node.left)
self.preorder(node.right)
def postorder(self, node=None):
if node is None:
= self.root
node
if node:
self.postorder(node.left)
self.postorder(node.right)
print(node.data, end=" ")
Binary Search Tree (BST)
Detailed Explanation: A Binary Search Tree is a special type of binary tree that maintains the BST property: for every node, all values in the left subtree are smaller than the node’s value, and all values in the right subtree are greater. This property enables efficient searching, insertion, and deletion operations.
C++ Implementation
#include <iostream>
using namespace std;
struct BSTNode {
int data;
* left;
BSTNode* right;
BSTNode
(int value) : data(value), left(nullptr), right(nullptr) {}
BSTNode};
class BST {
private:
* root;
BSTNode
* insertHelper(BSTNode* node, int data) {
BSTNodeif (!node) {
return new BSTNode(data);
}
if (data < node->data) {
->left = insertHelper(node->left, data);
node} else if (data > node->data) {
->right = insertHelper(node->right, data);
node}
return node;
}
* searchHelper(BSTNode* node, int data) {
BSTNodeif (!node || node->data == data) {
return node;
}
if (data < node->data) {
return searchHelper(node->left, data);
}
return searchHelper(node->right, data);
}
* findMin(BSTNode* node) {
BSTNodewhile (node && node->left) {
= node->left;
node }
return node;
}
* deleteHelper(BSTNode* node, int data) {
BSTNodeif (!node) return node;
if (data < node->data) {
->left = deleteHelper(node->left, data);
node} else if (data > node->data) {
->right = deleteHelper(node->right, data);
node} else {
if (!node->left) {
* temp = node->right;
BSTNodedelete node;
return temp;
} else if (!node->right) {
* temp = node->left;
BSTNodedelete node;
return temp;
}
* temp = findMin(node->right);
BSTNode->data = temp->data;
node->right = deleteHelper(node->right, temp->data);
node}
return node;
}
void inorderHelper(BSTNode* node) {
if (node) {
(node->left);
inorderHelper<< node->data << " ";
cout (node->right);
inorderHelper}
}
public:
() : root(nullptr) {}
BST
void insert(int data) {
= insertHelper(root, data);
root }
bool search(int data) {
return searchHelper(root, data) != nullptr;
}
void remove(int data) {
= deleteHelper(root, data);
root }
void inorder() {
(root);
inorderHelper<< endl;
cout }
};
Python Implementation
class BSTNode:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
class BST:
def __init__(self):
self.root = None
def insert(self, data):
self.root = self._insert_helper(self.root, data)
def _insert_helper(self, node, data):
if not node:
return BSTNode(data)
if data < node.data:
= self._insert_helper(node.left, data)
node.left elif data > node.data:
= self._insert_helper(node.right, data)
node.right
return node
def search(self, data):
return self._search_helper(self.root, data)
def _search_helper(self, node, data):
if not node or node.data == data:
return node is not None
if data < node.data:
return self._search_helper(node.left, data)
return self._search_helper(node.right, data)
def delete(self, data):
self.root = self._delete_helper(self.root, data)
def _delete_helper(self, node, data):
if not node:
return node
if data < node.data:
= self._delete_helper(node.left, data)
node.left elif data > node.data:
= self._delete_helper(node.right, data)
node.right else:
if not node.left:
return node.right
elif not node.right:
return node.left
= self._find_min(node.right)
temp = temp.data
node.data = self._delete_helper(node.right, temp.data)
node.right
return node
def _find_min(self, node):
while node and node.left:
= node.left
node return node
def inorder(self, node=None):
if node is None:
= self.root
node
if node:
self.inorder(node.left)
print(node.data, end=" ")
self.inorder(node.right)
Hash Table
Detailed Explanation: A hash table (hash map) is a data structure that uses a hash function to map keys to values. It provides very fast average-case performance for insertion, deletion, and lookup operations. The hash function converts keys into array indices, allowing direct access to stored values.
C++ Implementation
#include <iostream>
#include <vector>
#include <list>
using namespace std;
class HashTable {
private:
<list<pair<int, int>>> table;
vectorint capacity;
int hashFunction(int key) {
return key % capacity;
}
public:
(int cap) : capacity(cap) {
HashTable.resize(capacity);
table}
void insert(int key, int value) {
int index = hashFunction(key);
for (auto& pair : table[index]) {
if (pair.first == key) {
.second = value;
pairreturn;
}
}
[index].push_back({key, value});
table}
bool search(int key, int& value) {
int index = hashFunction(key);
for (const auto& pair : table[index]) {
if (pair.first == key) {
= pair.second;
value return true;
}
}
return false;
}
void remove(int key) {
int index = hashFunction(key);
[index].remove_if([key](const pair<int, int>& p) {
tablereturn p.first == key;
});
}
void display() {
for (int i = 0; i < capacity; i++) {
<< "Bucket " << i << ": ";
cout for (const auto& pair : table[i]) {
<< "(" << pair.first << ", " << pair.second << ") ";
cout }
<< endl;
cout }
}
};
Python Implementation
class HashTable:
def __init__(self, capacity=10):
self.capacity = capacity
self.table = [[] for _ in range(capacity)]
def _hash_function(self, key):
return key % self.capacity
def insert(self, key, value):
= self._hash_function(key)
index
for i, (k, v) in enumerate(self.table[index]):
if k == key:
self.table[index][i] = (key, value)
return
self.table[index].append((key, value))
def search(self, key):
= self._hash_function(key)
index
for k, v in self.table[index]:
if k == key:
return v
return None
def remove(self, key):
= self._hash_function(key)
index
for i, (k, v) in enumerate(self.table[index]):
if k == key:
del self.table[index][i]
return True
return False
def display(self):
for i, bucket in enumerate(self.table):
print(f"Bucket {i}: {bucket}")
Graph
Detailed Explanation: A graph is a non-linear data structure consisting of vertices (nodes) and edges that connect these vertices. Graphs are used to represent relationships between entities and are fundamental in computer science for modeling networks, social connections, transportation systems, and many other real-world scenarios.
C++ Implementation (Adjacency List)
#include <iostream>
#include <vector>
#include <queue>
#include <stack>
using namespace std;
class Graph {
private:
int vertices;
<vector<int>> adjList;
vector
public:
(int v) : vertices(v) {
Graph.resize(v);
adjList}
void addEdge(int src, int dest) {
[src].push_back(dest);
adjList[dest].push_back(src); // For undirected graph
adjList}
void BFS(int start) {
<bool> visited(vertices, false);
vector<int> q;
queue
[start] = true;
visited.push(start);
q
while (!q.empty()) {
int vertex = q.front();
.pop();
q<< vertex << " ";
cout
for (int neighbor : adjList[vertex]) {
if (!visited[neighbor]) {
[neighbor] = true;
visited.push(neighbor);
q}
}
}
<< endl;
cout }
void DFS(int start) {
<bool> visited(vertices, false);
vector<int> s;
stack
.push(start);
s
while (!s.empty()) {
int vertex = s.top();
.pop();
s
if (!visited[vertex]) {
[vertex] = true;
visited<< vertex << " ";
cout
for (int neighbor : adjList[vertex]) {
if (!visited[neighbor]) {
.push(neighbor);
s}
}
}
}
<< endl;
cout }
void display() {
for (int i = 0; i < vertices; i++) {
<< i << ": ";
cout for (int neighbor : adjList[i]) {
<< neighbor << " ";
cout }
<< endl;
cout }
}
};
Python Implementation
from collections import deque, defaultdict
class Graph:
def __init__(self, vertices):
self.vertices = vertices
self.adj_list = defaultdict(list)
def add_edge(self, src, dest):
self.adj_list[src].append(dest)
self.adj_list[dest].append(src) # For undirected graph
def bfs(self, start):
= [False] * self.vertices
visited = deque([start])
queue = True
visited[start]
while queue:
= queue.popleft()
vertex print(vertex, end=" ")
for neighbor in self.adj_list[vertex]:
if not visited[neighbor]:
= True
visited[neighbor]
queue.append(neighbor)print()
def dfs(self, start):
= [False] * self.vertices
visited = [start]
stack
while stack:
= stack.pop()
vertex
if not visited[vertex]:
= True
visited[vertex] print(vertex, end=" ")
for neighbor in self.adj_list[vertex]:
if not visited[neighbor]:
stack.append(neighbor)print()
def display(self):
for vertex in range(self.vertices):
print(f"{vertex}: {self.adj_list[vertex]}")
Sorting Algorithms
Bubble Sort
Detailed Explanation: Bubble Sort is the simplest sorting algorithm that works by repeatedly stepping through the list, comparing adjacent elements and swapping them if they are in the wrong order. The pass through the list is repeated until the list is sorted. The algorithm gets its name because smaller elements “bubble” to the top of the list.
C++ Implementation
#include <iostream>
#include <vector>
using namespace std;
void bubbleSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
bool swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
(arr[j], arr[j + 1]);
swap= true;
swapped }
}
if (!swapped) break;
}
}
Python Implementation
def bubble_sort(arr):
= len(arr)
n for i in range(n - 1):
= False
swapped for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
+ 1] = arr[j + 1], arr[j]
arr[j], arr[j = True
swapped if not swapped:
break
return arr
Selection Sort
Detailed Explanation: Selection Sort divides the input list into two parts: a sorted portion at the left end and an unsorted portion at the right end. Initially, the sorted portion is empty and the unsorted portion is the entire list. The algorithm proceeds by finding the smallest element in the unsorted portion and swapping it with the first element of the unsorted portion.
C++ Implementation
void selectionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
= j;
minIdx }
}
if (minIdx != i) {
(arr[i], arr[minIdx]);
swap}
}
}
Python Implementation
def selection_sort(arr):
= len(arr)
n for i in range(n - 1):
= i
min_idx for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
= j
min_idx if min_idx != i:
= arr[min_idx], arr[i]
arr[i], arr[min_idx] return arr
Insertion Sort
Detailed Explanation: Insertion Sort builds the final sorted array one element at a time. It works by taking elements from the unsorted portion and inserting them into their correct position in the sorted portion. It’s similar to how you might sort playing cards in your hands - pick up cards one by one and insert each into its proper position among the cards you’ve already sorted.
C++ Implementation
void insertionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
[j + 1] = arr[j];
arr--;
j}
[j + 1] = key;
arr}
}
Python Implementation
def insertion_sort(arr):
for i in range(1, len(arr)):
= arr[i]
key = i - 1
j
while j >= 0 and arr[j] > key:
+ 1] = arr[j]
arr[j -= 1
j + 1] = key
arr[j return arr
Merge Sort
Detailed Explanation: Merge Sort is a divide-and-conquer algorithm that divides the input array into two halves, recursively sorts both halves, and then merges the two sorted halves. The key operation is the merge process, which combines two sorted arrays into one sorted array.
C++ Implementation
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
<int> leftArr(n1), rightArr(n2);
vector
for (int i = 0; i < n1; i++)
[i] = arr[left + i];
leftArrfor (int j = 0; j < n2; j++)
[j] = arr[mid + 1 + j];
rightArr
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
[k++] = leftArr[i++];
arr} else {
[k++] = rightArr[j++];
arr}
}
while (i < n1) arr[k++] = leftArr[i++];
while (j < n2) arr[k++] = rightArr[j++];
}
void mergeSort(vector<int>& arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
(arr, left, mid);
mergeSort(arr, mid + 1, right);
mergeSort(arr, left, mid, right);
merge}
}
Python Implementation
def merge_sort(arr):
if len(arr) <= 1:
return arr
= len(arr) // 2
mid = merge_sort(arr[:mid])
left = merge_sort(arr[mid:])
right
return merge(left, right)
def merge(left, right):
= []
result = j = 0
i
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])+= 1
i else:
result.append(right[j])+= 1
j
result.extend(left[i:])
result.extend(right[j:])return result
Quick Sort
Detailed Explanation: Quick Sort is a divide-and-conquer algorithm that picks a pivot element and partitions the array around the pivot such that elements smaller than the pivot are on the left and elements greater than the pivot are on the right. It then recursively applies the same process to the sub-arrays.
C++ Implementation
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
++;
i(arr[i], arr[j]);
swap}
}
(arr[i + 1], arr[high]);
swapreturn i + 1;
}
void quickSort(vector<int>& arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
quickSort}
}
Python Implementation
def quick_sort(arr, low=0, high=None):
if high is None:
= len(arr) - 1
high
if low < high:
= partition(arr, low, high)
pi - 1)
quick_sort(arr, low, pi + 1, high)
quick_sort(arr, pi return arr
def partition(arr, low, high):
= arr[high]
pivot = low - 1
i
for j in range(low, high):
if arr[j] < pivot:
+= 1
i = arr[j], arr[i]
arr[i], arr[j]
+ 1], arr[high] = arr[high], arr[i + 1]
arr[i return i + 1
Heap Sort
Detailed Explanation: Heap Sort is a comparison-based sorting algorithm that uses a binary heap data structure. It works by building a max heap from the input data, then repeatedly extracting the maximum element from the heap and placing it at the end of the sorted array. The heap property ensures that the largest element is always at the root.
C++ Implementation
void heapify(vector<int>& arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest])
= left;
largest
if (right < n && arr[right] > arr[largest])
= right;
largest
if (largest != i) {
(arr[i], arr[largest]);
swap(arr, n, largest);
heapify}
}
void heapSort(vector<int>& arr) {
int n = arr.size();
for (int i = n / 2 - 1; i >= 0; i--)
(arr, n, i);
heapify
for (int i = n - 1; i > 0; i--) {
(arr[0], arr[i]);
swap(arr, i, 0);
heapify}
}
Python Implementation
def heapify(arr, n, i):
= i
largest = 2 * i + 1
left = 2 * i + 2
right
if left < n and arr[left] > arr[largest]:
= left
largest
if right < n and arr[right] > arr[largest]:
= right
largest
if largest != i:
= arr[largest], arr[i]
arr[i], arr[largest]
heapify(arr, n, largest)
def heap_sort(arr):
= len(arr)
n
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
for i in range(n - 1, 0, -1):
0], arr[i] = arr[i], arr[0]
arr[0)
heapify(arr, i,
return arr
Searching Algorithms
Linear Search
Detailed Explanation: Linear Search, also known as sequential search, is the simplest searching algorithm that checks every element in the list sequentially until the target element is found or the end of the list is reached. It works on both sorted and unsorted arrays and doesn’t require any special data structure.
C++ Implementation
int linearSearch(const vector<int>& arr, int target) {
for (int i = 0; i < arr.size(); i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
Python Implementation
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i
return -1
Binary Search
Detailed Explanation: Binary Search is an efficient algorithm for searching a sorted array by repeatedly dividing the search interval in half. It compares the target value to the middle element of the array and eliminates half of the remaining elements based on the comparison result. This divide-and-conquer approach makes it much faster than linear search.
C++ Implementation
int binarySearch(const vector<int>& arr, int target) {
int left = 0, right = arr.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
= mid + 1;
left } else {
= mid - 1;
right }
}
return -1;
}
// Recursive version
int binarySearchRecursive(const vector<int>& arr, int target, int left, int right) {
if (left > right) return -1;
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) return binarySearchRecursive(arr, target, mid + 1, right);
else return binarySearchRecursive(arr, target, left, mid - 1);
}
Python Implementation
def binary_search(arr, target):
= 0, len(arr) - 1
left, right
while left <= right:
= (left + right) // 2
mid
if arr[mid] == target:
return mid
elif arr[mid] < target:
= mid + 1
left else:
= mid - 1
right
return -1
# Recursive version
def binary_search_recursive(arr, target, left=0, right=None):
if right is None:
= len(arr) - 1
right
if left > right:
return -1
= (left + right) // 2
mid
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, right)
else:
return binary_search_recursive(arr, target, left, mid - 1)
Graph Algorithms
Breadth-First Search (BFS)
Detailed Explanation: BFS is a graph traversal algorithm that systematically explores vertices of a graph in breadth-first manner. It starts from a source vertex and explores all vertices at distance 1, then all vertices at distance 2, and so on. BFS uses a queue data structure to keep track of vertices to be explored.
C++ Implementation
void BFS(vector<vector<int>>& graph, int start) {
int n = graph.size();
<bool> visited(n, false);
vector<int> q;
queue
[start] = true;
visited.push(start);
q
while (!q.empty()) {
int vertex = q.front();
.pop();
q<< vertex << " ";
cout
for (int neighbor : graph[vertex]) {
if (!visited[neighbor]) {
[neighbor] = true;
visited.push(neighbor);
q}
}
}
<< endl;
cout }
Python Implementation
from collections import deque
def bfs(graph, start):
= set()
visited = deque([start])
queue
visited.add(start)
while queue:
= queue.popleft()
vertex print(vertex, end=" ")
for neighbor in graph[vertex]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)print()
Depth-First Search (DFS)
Detailed Explanation: DFS is a graph traversal algorithm that explores vertices by going as deep as possible along each branch before backtracking. It uses a stack (either explicit or through recursion) to remember vertices to backtrack to when a dead end is reached.
C++ Implementation
void DFSUtil(vector<vector<int>>& graph, int vertex, vector<bool>& visited) {
[vertex] = true;
visited<< vertex << " ";
cout
for (int neighbor : graph[vertex]) {
if (!visited[neighbor]) {
(graph, neighbor, visited);
DFSUtil}
}
}
void DFS(vector<vector<int>>& graph, int start) {
int n = graph.size();
<bool> visited(n, false);
vector(graph, start, visited);
DFSUtil<< endl;
cout }
// Iterative version
void DFSIterative(vector<vector<int>>& graph, int start) {
int n = graph.size();
<bool> visited(n, false);
vector<int> s;
stack
.push(start);
s
while (!s.empty()) {
int vertex = s.top();
.pop();
s
if (!visited[vertex]) {
[vertex] = true;
visited<< vertex << " ";
cout
for (int neighbor : graph[vertex]) {
if (!visited[neighbor]) {
.push(neighbor);
s}
}
}
}
<< endl;
cout }
Python Implementation
def dfs_recursive(graph, start, visited=None):
if visited is None:
= set()
visited
visited.add(start)print(start, end=" ")
for neighbor in graph[start]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
def dfs_iterative(graph, start):
= set()
visited = [start]
stack
while stack:
= stack.pop()
vertex
if vertex not in visited:
visited.add(vertex)print(vertex, end=" ")
for neighbor in graph[vertex]:
if neighbor not in visited:
stack.append(neighbor)print()
Dijkstra’s Algorithm
Detailed Explanation: Dijkstra’s algorithm solves the single-source shortest path problem for graphs with non-negative edge weights. It maintains a set of vertices whose shortest distance from source is known and gradually expands this set until all vertices are included.
C++ Implementation
#include <vector>
#include <queue>
#include <limits>
using namespace std;
<int> dijkstra(vector<vector<pair<int, int>>>& graph, int start) {
vectorint n = graph.size();
<int> dist(n, INT_MAX);
vector<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
priority_queue
[start] = 0;
dist.push({0, start});
pq
while (!pq.empty()) {
int u = pq.top().second;
.pop();
pq
for (auto& edge : graph[u]) {
int v = edge.first;
int weight = edge.second;
if (dist[u] + weight < dist[v]) {
[v] = dist[u] + weight;
dist.push({dist[v], v});
pq}
}
}
return dist;
}
Python Implementation
import heapq
def dijkstra(graph, start):
= {vertex: float('infinity') for vertex in graph}
distances = 0
distances[start] = [(0, start)]
pq
while pq:
= heapq.heappop(pq)
current_distance, current_vertex
if current_distance > distances[current_vertex]:
continue
for neighbor, weight in graph[current_vertex]:
= current_distance + weight
distance
if distance < distances[neighbor]:
= distance
distances[neighbor]
heapq.heappush(pq, (distance, neighbor))
return distances
Dynamic Programming
Dynamic Programming Fundamentals
Detailed Explanation: Dynamic Programming (DP) is an optimization technique that solves complex problems by breaking them down into simpler subproblems. It stores the results of subproblems to avoid computing the same results again. DP is applicable when the problem has optimal substructure and overlapping subproblems.
Fibonacci with DP
C++ Implementation
// Memoization (Top-down)
int fibMemo(int n, vector<int>& memo) {
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo);
memoreturn memo[n];
}
// Tabulation (Bottom-up)
int fibDP(int n) {
if (n <= 1) return n;
<int> dp(n+1);
vector[0] = 0; dp[1] = 1;
dpfor (int i = 2; i <= n; i++) {
[i] = dp[i-1] + dp[i-2];
dp}
return dp[n];
}
// Space-optimized
int fibOptimized(int n) {
if (n <= 1) return n;
int prev2 = 0, prev1 = 1, current;
for (int i = 2; i <= n; i++) {
= prev1 + prev2;
current = prev1;
prev2 = current;
prev1 }
return current;
}
Python Implementation
# Memoization
def fib_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
= fib_memo(n-1, memo) + fib_memo(n-2, memo)
memo[n] return memo[n]
# Tabulation
def fib_dp(n):
if n <= 1:
return n
= [0] * (n + 1)
dp 1] = 1
dp[for i in range(2, n + 1):
= dp[i-1] + dp[i-2]
dp[i] return dp[n]
# Space-optimized
def fib_optimized(n):
if n <= 1:
return n
= 0, 1
prev2, prev1 for i in range(2, n + 1):
= prev1 + prev2
current = prev1, current
prev2, prev1 return prev1
String Algorithms
KMP Algorithm (Pattern Matching)
Detailed Explanation: The Knuth-Morris-Pratt (KMP) algorithm is an efficient string matching algorithm that finds occurrences of a pattern within a text. It preprocesses the pattern to create a failure function that helps skip characters during matching, avoiding redundant comparisons.
C++ Implementation
<int> computeLPS(string pattern) {
vectorint m = pattern.length();
<int> lps(m, 0);
vectorint len = 0;
int i = 1;
while (i < m) {
if (pattern[i] == pattern[len]) {
++;
len[i] = len;
lps++;
i} else {
if (len != 0) {
= lps[len - 1];
len } else {
[i] = 0;
lps++;
i}
}
}
return lps;
}
<int> KMPSearch(string text, string pattern) {
vector<int> result;
vectorint n = text.length();
int m = pattern.length();
<int> lps = computeLPS(pattern);
vector
int i = 0; // index for text
int j = 0; // index for pattern
while (i < n) {
if (pattern[j] == text[i]) {
++;
i++;
j}
if (j == m) {
.push_back(i - j);
result= lps[j - 1];
j } else if (i < n && pattern[j] != text[i]) {
if (j != 0) {
= lps[j - 1];
j } else {
++;
i}
}
}
return result;
}
Python Implementation
def compute_lps(pattern):
= len(pattern)
m = [0] * m
lps = 0
length = 1
i
while i < m:
if pattern[i] == pattern[length]:
+= 1
length = length
lps[i] += 1
i else:
if length != 0:
= lps[length - 1]
length else:
= 0
lps[i] += 1
i return lps
def kmp_search(text, pattern):
= len(text)
n = len(pattern)
m
= compute_lps(pattern)
lps = []
result
= j = 0
i
while i < n:
if pattern[j] == text[i]:
+= 1
i += 1
j
if j == m:
- j)
result.append(i = lps[j - 1]
j elif i < n and pattern[j] != text[i]:
if j != 0:
= lps[j - 1]
j else:
+= 1
i
return result
Advanced Data Structures
Trie (Prefix Tree)
Detailed Explanation: A Trie is a tree data structure used for storing and searching strings efficiently. Each node represents a character, and paths from root to leaves represent complete strings. It’s particularly useful for prefix-based operations like autocomplete and spell checkers.
C++ Implementation
class TrieNode {
public:
* children[26];
TrieNodebool isEndOfWord;
() {
TrieNode= false;
isEndOfWord for (int i = 0; i < 26; i++) {
[i] = nullptr;
children}
}
};
class Trie {
private:
* root;
TrieNode
public:
() {
Trie= new TrieNode();
root }
void insert(string word) {
* current = root;
TrieNodefor (char c : word) {
int index = c - 'a';
if (current->children[index] == nullptr) {
->children[index] = new TrieNode();
current}
= current->children[index];
current }
->isEndOfWord = true;
current}
bool search(string word) {
* current = root;
TrieNodefor (char c : word) {
int index = c - 'a';
if (current->children[index] == nullptr) {
return false;
}
= current->children[index];
current }
return current->isEndOfWord;
}
bool startsWith(string prefix) {
* current = root;
TrieNodefor (char c : prefix) {
int index = c - 'a';
if (current->children[index] == nullptr) {
return false;
}
= current->children[index];
current }
return true;
}
};
Python Implementation
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
= self.root
current for char in word:
if char not in current.children:
= TrieNode()
current.children[char] = current.children[char]
current = True
current.is_end_of_word
def search(self, word):
= self.root
current for char in word:
if char not in current.children:
return False
= current.children[char]
current return current.is_end_of_word
def starts_with(self, prefix):
= self.root
current for char in prefix:
if char not in current.children:
return False
= current.children[char]
current return True
Segment Tree
Detailed Explanation: A Segment Tree is a binary tree used for range queries on arrays. Each node stores information about a segment of the array, allowing efficient range queries and updates. It’s particularly useful when you need to perform many range sum, minimum, maximum, or other associative operations.
C++ Implementation
class SegmentTree {
private:
<int> tree;
vectorint n;
void build(vector<int>& arr, int node, int start, int end) {
if (start == end) {
[node] = arr[start];
tree} else {
int mid = (start + end) / 2;
(arr, 2*node, start, mid);
build(arr, 2*node+1, mid+1, end);
build[node] = tree[2*node] + tree[2*node+1];
tree}
}
void updateHelper(int node, int start, int end, int idx, int val) {
if (start == end) {
[node] = val;
tree} else {
int mid = (start + end) / 2;
if (idx <= mid) {
(2*node, start, mid, idx, val);
updateHelper} else {
(2*node+1, mid+1, end, idx, val);
updateHelper}
[node] = tree[2*node] + tree[2*node+1];
tree}
}
int queryHelper(int node, int start, int end, int l, int r) {
if (r < start || end < l) {
return 0;
}
if (l <= start && end <= r) {
return tree[node];
}
int mid = (start + end) / 2;
int left_sum = queryHelper(2*node, start, mid, l, r);
int right_sum = queryHelper(2*node+1, mid+1, end, l, r);
return left_sum + right_sum;
}
public:
(vector<int>& arr) {
SegmentTree= arr.size();
n .resize(4 * n);
tree(arr, 1, 0, n-1);
build}
void update(int idx, int val) {
(1, 0, n-1, idx, val);
updateHelper}
int query(int l, int r) {
return queryHelper(1, 0, n-1, l, r);
}
};
Python Implementation
class SegmentTree:
def __init__(self, arr):
self.n = len(arr)
self.tree = [0] * (4 * self.n)
self.build(arr, 1, 0, self.n - 1)
def build(self, arr, node, start, end):
if start == end:
self.tree[node] = arr[start]
else:
= (start + end) // 2
mid self.build(arr, 2 * node, start, mid)
self.build(arr, 2 * node + 1, mid + 1, end)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def update(self, idx, val):
self._update_helper(1, 0, self.n - 1, idx, val)
def _update_helper(self, node, start, end, idx, val):
if start == end:
self.tree[node] = val
else:
= (start + end) // 2
mid if idx <= mid:
self._update_helper(2 * node, start, mid, idx, val)
else:
self._update_helper(2 * node + 1, mid + 1, end, idx, val)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def query(self, l, r):
return self._query_helper(1, 0, self.n - 1, l, r)
def _query_helper(self, node, start, end, l, r):
if r < start or end < l:
return 0
if l <= start and end <= r:
return self.tree[node]
= (start + end) // 2
mid = self._query_helper(2 * node, start, mid, l, r)
left_sum = self._query_helper(2 * node + 1, mid + 1, end, l, r)
right_sum return left_sum + right_sum
Conclusion
This comprehensive guide covers the fundamental data structures and algorithms essential for computer science and software development. Each data structure and algorithm has been explained with:
- Detailed conceptual explanations
- Advantages and disadvantages
- Time and space complexity analysis
- Complete implementations in both C++ and Python
- Key characteristics and properties
Understanding these concepts is crucial for: - Technical interviews - Algorithm design - Performance optimization - Problem-solving skills - Software architecture decisions
The choice of data structure or algorithm depends on the specific requirements of your problem, including time constraints, space limitations, and the operations you need to perform most frequently.