Master fundamental concepts with detailed explanations and visual examples
An array is a collection of elements stored at contiguous memory locations. It's one of the most fundamental data structures.
// Declaration and Initialization
int arr[5] = {10, 20, 30, 40, 50};
// Access element: O(1)
int value = arr[2]; // Returns 30
// Update element: O(1)
arr[2] = 35;
// Traverse array: O(n)
for (int i = 0; i < 5; i++) {
cout << arr[i] << " ";
}
// Search element: O(n)
int target = 30;
for (int i = 0; i < 5; i++) {
if (arr[i] == target) {
// Found at index i
}
}
Operation | Time Complexity |
---|---|
Access by Index | O(1) |
Search (unsorted) | O(n) |
Insert at End | O(1) |
Insert at Beginning/Middle | O(n) |
Delete | O(n) |
Strings are sequences of characters. In C++, they can be represented as character arrays or using the string class.
// C++ String Operations
string str = "Hello";
// Length: O(1)
int len = str.length();
// Access character: O(1)
char ch = str[0];
// Concatenation: O(n)
string result = str + " World";
// Substring: O(n)
string sub = str.substr(1, 3); // "ell"
// Find: O(n*m)
int pos = str.find("ll");
// Reverse: O(n)
reverse(str.begin(), str.end());
// Compare: O(n)
if (str1 == str2) { }
A linked list is a linear data structure where elements are stored in nodes. Each node contains data and a reference (pointer) to the next node.
// Node Structure
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
// Insert at Beginning: O(1)
void insertAtBeginning(Node*& head, int value) {
Node* newNode = new Node(value);
newNode->next = head;
head = newNode;
}
// Insert at End: O(n)
void insertAtEnd(Node*& head, int value) {
Node* newNode = new Node(value);
if (!head) {
head = newNode;
return;
}
Node* temp = head;
while (temp->next) {
temp = temp->next;
}
temp->next = newNode;
}
// Delete a Node: O(n)
void deleteNode(Node*& head, int value) {
if (!head) return;
if (head->data == value) {
Node* temp = head;
head = head->next;
delete temp;
return;
}
Node* current = head;
while (current->next && current->next->data != value) {
current = current->next;
}
if (current->next) {
Node* temp = current->next;
current->next = current->next->next;
delete temp;
}
}
// Traverse: O(n)
void printList(Node* head) {
while (head) {
cout << head->data << " -> ";
head = head->next;
}
cout << "NULL" << endl;
}
Feature | Array | Linked List |
---|---|---|
Memory | Contiguous | Non-contiguous |
Size | Fixed | Dynamic |
Access Time | O(1) | O(n) |
Insert at Beginning | O(n) | O(1) |
Memory Overhead | Low | High (pointers) |
A stack is a linear data structure that follows the LIFO principle. Think of it like a stack of plates.
// Using STL Stack
#include <stack>
stack<int> st;
// Push: O(1)
st.push(10);
st.push(20);
st.push(30);
// Pop: O(1)
st.pop(); // Removes 30
// Top: O(1)
int top = st.top(); // Returns 20
// Empty: O(1)
bool isEmpty = st.empty();
// Size: O(1)
int size = st.size();
A queue is a linear data structure that follows the FIFO principle. Think of it like a line at a ticket counter.
Enqueue at Rear, Dequeue from Front
// Using STL Queue
#include <queue>
queue<int> q;
// Enqueue: O(1)
q.push(10);
q.push(20);
q.push(30);
// Dequeue: O(1)
q.pop(); // Removes 10
// Front: O(1)
int front = q.front(); // Returns 20
// Back: O(1)
int back = q.back(); // Returns 30
// Empty: O(1)
bool isEmpty = q.empty();
// Size: O(1)
int size = q.size();
Complexity analysis helps us understand how an algorithm's performance scales with input size. It's crucial for writing efficient code.
Big-O describes the upper bound of time/space an algorithm takes. It focuses on the worst-case scenario.
Notation | Name | Example | n=100 |
---|---|---|---|
O(1) | Constant | Array access, Hash lookup | 1 |
O(log n) | Logarithmic | Binary search | 7 |
O(n) | Linear | Linear search, Array traversal | 100 |
O(n log n) | Linearithmic | Merge sort, Quick sort | 700 |
O(nยฒ) | Quadratic | Bubble sort, Nested loops | 10,000 |
O(2โฟ) | Exponential | Recursive Fibonacci | 1.27 ร 10ยณโฐ |
O(n!) | Factorial | Permutations | 9.3 ร 10ยนโตโท |
// O(1) - Constant Time
int getFirstElement(vector<int>& arr) {
return arr[0]; // Always 1 operation
}
// O(n) - Linear Time
int findMax(vector<int>& arr) {
int max = arr[0];
for (int i = 1; i < arr.size(); i++) { // n iterations
if (arr[i] > max) max = arr[i];
}
return max;
}
// O(log n) - Logarithmic Time
int binarySearch(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;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1; // Divides search space by 2 each time
}
// O(nยฒ) - Quadratic Time
void printPairs(vector<int>& arr) {
for (int i = 0; i < arr.size(); i++) { // n times
for (int j = 0; j < arr.size(); j++) { // n times
cout << arr[i] << ", " << arr[j] << endl;
}
}
}
// O(n log n) - Linearithmic Time
void mergeSort(vector<int>& arr) {
// Divides array log n times (like binary search)
// Each level does n work to merge
// Total: n * log n
}
Space complexity measures the total memory an algorithm uses, including input space and auxiliary space.
// O(1) Space - Constant Space
int sum(int a, int b) {
return a + b; // Only stores result
}
// O(n) Space - Linear Space
vector<int> createCopy(vector<int>& arr) {
vector<int> copy = arr; // Stores n elements
return copy;
}
// O(n) Space - Recursion Stack
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
// Maximum recursion depth is n
}
Bundling data and methods that operate on that data within a single unit (class). Hide internal details and expose only necessary information.
class BankAccount {
private:
double balance; // Hidden from outside
public:
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
double getBalance() {
return balance;
}
};
Hiding complex implementation details and showing only essential features. Focus on "what" rather than "how".
// Abstract class
class Shape {
public:
virtual double area() = 0; // Pure virtual
virtual void draw() = 0;
};
class Circle : public Shape {
private:
double radius;
public:
double area() override {
return 3.14 * radius * radius;
}
void draw() override {
// Drawing implementation
}
};
A mechanism where a new class derives properties and behaviors from an existing class. Promotes code reusability.
class Animal {
protected:
string name;
public:
void eat() {
cout << "Eating..." << endl;
}
};
class Dog : public Animal {
public:
void bark() {
cout << "Woof!" << endl;
}
};
// Dog inherits eat() from Animal
The ability of objects to take multiple forms. Same interface, different implementations.
class Animal {
public:
virtual void sound() {
cout << "Animal sound" << endl;
}
};
class Dog : public Animal {
public:
void sound() override {
cout << "Bark!" << endl;
}
};
class Cat : public Animal {
public:
void sound() override {
cout << "Meow!" << endl;
}
};
// Runtime polymorphism
Animal* animal = new Dog();
animal->sound(); // Outputs: Bark!
Specifier | Same Class | Derived Class | Outside Class |
---|---|---|---|
public | โ | โ | โ |
protected | โ | โ | โ |
private | โ | โ | โ |