Dutch National Flag Sorting Algorithm
Dutch National Flag Sorting Algorithm
1 BBN
int low = 0, mid = 0, high = [Link]() - 1;
while (mid <= high) {
if (nums[mid] == 0) {
swap(nums[low], nums[mid]);
low++, mid++;
} else if (nums[mid] == 1) {
mid++;
} else {
swap(nums[mid], nums[high]);
high--;
}
}
}
2 BBN
4. Function Objects
Vectors
Vector Operations
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
v.push_back(6); // Add element at end
v.pop_back(); // Remove last element
cout << "First element: " << [Link]() << endl;
cout << "Last element: " << [Link]() << endl;
cout << "Size: " << [Link]() << endl;
return 0;
}
Iterators in Vectors
vector<int>::iterator it = [Link]();
cout << *it; // Access first element using iterator
List Operations
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> l = {10, 20, 30};
l.push_front(5);
l.push_back(40);
3 BBN
l.pop_front();
l.pop_back();
return 0;
}
Deque Example
#include <deque>
#include <iostream>
using namespace std;
int main() {
deque<int> dq;
dq.push_front(1);
dq.push_back(2);
dq.pop_front();
return 0;
}
Pairs
Pair Example
pair<int, string> p = {1, "hello"};
cout << [Link] << " " << [Link];
Stack Operations
#include <stack>
#include <iostream>
using namespace std;
int main() {
stack<int> s;
[Link](10);
[Link](20);
cout << [Link](); // 20
[Link]();
return 0;
}
4 BBN
Queue (FIFO - First In First Out)
Queue Example
#include <queue>
using namespace std;
queue<int> q;
[Link](10);
[Link](20);
cout << [Link](); // 10
[Link]();
Priority Queue
priority_queue<int> pq;
[Link](10);
[Link](30);
[Link](20);
cout << [Link](); // 30 (highest priority)
int main() {
std::map<int, std::string> mp;
mp[1] = "One";
5 BBN
mp[2] = "Two";
mp[3] = "Three";
std::cout << "Value at key 2: " << mp[2] << std::endl;
int main() {
std::set<int> s = {10, 20, 30, 40, 50};
auto lb = s.lower_bound(30);//30
auto ub = s.upper_bound(30);//40
std::cout << "Lower bound of 30: " << *lb << std::endl;
std::cout << "Upper bound of 30: " << *ub << std::endl;
}
int main() {
std::vector<int> v = {4, 1, 3, 5, 2};
std::sort([Link](), [Link]());
for (int x : v) std::cout << x << " ";
}
6 BBN
#include <algorithm>
int main() {
std::vector<std::pair<int, int>> vp = {{1, 3}, {2, 2}, {3, 1}};
std::sort([Link](), [Link](), cmp);
for (auto p : vp)
std::cout << "(" << [Link] << ", " << [Link] << ") ";
}
Permutations and Basic STL Functions
next_permutation() & prev_permutation() for generating sequences.
Basic functions: swap(), min(), max().
Binary search for checking value existence.
Counting set bits in an integer using built-in GCC functions.
Example: Finding Next Permutation
#include <iostream>
#include <algorithm>
int main() {
std::string s = "abc";
std::next_permutation([Link](), [Link]());
std::cout << "Next permutation: " << s << std::endl;
}
Built-in Functions
popcount() for counting set bits in integers.
Overview of STL containers: maps, sets, queues, lists, deques.
Iterators, custom comparators, and time complexities.
Encouragement for hands-on practice!
Example: Using __builtin_popcount()
#include <iostream>
int main() {
int x = 15; // Binary: 1111
std::cout << "Set bit count: " << __builtin_popcount(x) << std::endl;
}
Strings
What is a String?
In programming, a string is a sequence of characters enclosed in double quotes (" ").
Example: "College" is a string.
A string can be a word, phrase, or even a complete sentence.
7 BBN
Character Arrays vs Strings
Before understanding strings, it's important to learn about character arrays.
In C++, a character array is a sequence of characters stored in contiguous memory
locations.
Example of a character array:
char str[] = {'A', 'B', 'C'};
Special Feature of Character Arrays
Character arrays can also store strings.
However, to be considered a valid string, a special character must be added at the end:
null character (\0).
Example:
char str[] = {'A', 'B', 'C', '\0'};
The null character (\0) marks the end of the string.
Without \0, the character array does not behave like a proper string.
Memory Allocation in Character Arrays
Each character in a character array takes 1 byte of memory.
Printing Character Arrays
If we print a normal integer array, it gives a memory address.
But if we print a character array, it prints the entire string stored in it.
Example:
char str[] = "ABC";
cout << str; // Output: ABC
This happens because C++ treats character arrays with a null character (\0) as strings.
Calculating String Length
To find the length of a string, we use the function strlen().
Example:
cout << strlen(str);
Assigning Strings to Character Arrays
Instead of storing individual characters, we can directly assign a string literal.
Example:
char str[] = "Hello";
String literals are constant values that do not change.
Accessing Individual Characters
Just like arrays, we can access individual characters of a string using indexing.
Example:
char str[] = "Hello";
cout << str[1]; // Output: e
If we try to access the index of the null character (\0), nothing is displayed.
Using String Class:
string str = "Apna College";
Advantages of Strings Over Character Arrays
No need to specify size.
Supports dynamic resizing.
Easier to perform operations like concatenation and comparison.
Operations on Strings
Concatenation (Joining Strings):
8 BBN
string s1 = "Bit By ";
string s2 = "Note";
string s3 = s1 + s2; // Result: "Bit By Note"
Comparison:
if (s1 == s2) {
cout << "Strings are equal";
} else {
cout << "Strings are not equal";
}
Taking String Input
To take input for a string, we use getline().
Syntax:
getline(cin, string_name);
This method allows us to take a full sentence as input, including spaces.
Looping Through a String
We can use loops to iterate through a string.
Using a for-loop with an index:
for (int i = 0; i < [Link](); i++) {
cout << str[i] << " ";
}
Using a for-each loop:
for (char c : str) {
cout << c << " ";
}
Functions and
Category Operators Functionality
String Length length() or size() It will return the length of the string.
9 BBN
Functions and
Category Operators Functionality
Permutation in String
Problem Statement:
We need to check if a permutation of string s1 exists as a substring in string s2.
Approach:
1. Store Frequency of s1: Create a frequency array for s1.
2. Sliding Window on s2: Traverse s2 using a window of size [Link], updating the
frequency dynamically.
3. Compare Frequency Arrays: If the window frequency matches s1's frequency, return
true.
4. Edge Case: If no matching window is found, return false.
10 BBN
We create an integer array of size 26 (for lowercase English letters) and store the frequency of
characters in s1.
int[] frequency = {0};
for (int i = 0; i < [Link](); i++) {
frequency[[Link](i) - 'a']++;
}
11 BBN
// Function to check if any permutation of pattern is in text
bool checkPermutationInText(string text, string pattern) {
int n = [Link](), m = [Link]();
if (m > n) return false;
vector<int> textFreq(26, 0), patternFreq(26, 0);
// Fill frequency array for the first 'm' characters
for (int i = 0; i < m; i++) {
textFreq[text[i] - 'a']++;
patternFreq[pattern[i] - 'a']++;
}
// Check for the first window
if (areEqual(textFreq, patternFreq)) return true;
12 BBN
1. Initialize Pointers: Use a pointer i to traverse the array and a pointer index to track the
position in the compressed array.
2. Count Consecutive Characters: Use a loop to count consecutive occurrences of each
character.
3. Modify the Array In-Place:
o Store the character at the current index.
o If the character count > 1, convert the count to a string and store each digit in the
array.
4. Return the New Length: The final value of index gives the new length of the
compressed array.
Implementation in C++
#include <iostream>
#include <vector>
using namespace std;
while (i < n) {
char currentChar = chars[i];
int count = 0;
// Count occurrences of currentChar
while (i < n && chars[i] == currentChar) {
count++;
i++;
}
int main() {
vector<char> chars = {'a', 'a', 'b', 'b', 'c', 'c', 'c'};
int newLength = compress(chars);
13 BBN
// Output compressed characters
for (int i = 0; i < newLength; i++) {
cout << chars[i] << " ";
}
cout << "\nNew Length: " << newLength << endl;
return 0;
}
Explanation of Code
1. We initialize index = 0 to keep track of the compressed array position.
2. The outer while loop iterates through the input array.
3. The inner while loop counts occurrences of a character.
4. The character is stored in chars[index].
5. If the count is greater than 1, we convert it to a string and store each digit separately.
6. Finally, we return index as the new length of the array.
14 BBN
int countPrimes(int n) {
if (n <= 1) return 0;
vector<bool> isPrime(n, true);
isPrime[0] = isPrime[1] = false;
int count = 0;
for (int i = 2; i < n; i++) {
if (isPrime[i]) count++;
}
return count;
}
int main() {
int n;
cout << "Enter n: ";
cin >> n;
cout << "Number of primes less than " << n << " is: " << countPrimes(n) << endl;
return 0;
}
Time Complexity
O(ns log n) due to the Sieve of Eratosthenes, which is efficient for large values of n.
Space Complexity
O(n) for storing the boolean vector isPrime.
What is GCD?
The Greatest Common Divisor (GCD) of two numbers is the largest number that divides both
of them without leaving a remainder.
For example:
GCD(12, 18) = 6 because 6 is the largest number that divides both 12 and 18.
GCD(35, 10) = 5 because 5 is the largest number that divides both 35 and 10.
15 BBN
o If i divides both a and b, update gcd.
4. Print the final value of gcd.
🔹 Example:
Input: a = 12, b = 18
Common divisors: 1, 2, 3, 6
Largest one = 6 → This is the GCD.
C++ Program
Here’s the C++ code implementing both methods:
#include <iostream>
using namespace std;
16 BBN
}
return a; // GCD found
}
int main() {
int a, b;
cout << "Enter two numbers: ";
cin >> a >> b;
cout << "GCD (Brute Force) of " << a << " and " << b << " is: " << gcdBruteForce(a, b) <<
endl;
cout << "GCD (Euclidean) of " << a << " and " << b << " is: " << gcdEuclidean(a, b) <<
endl;
return 0;
}
Reversing an Integer & Checking for Palindrome in
C++
1. Reversing an Integer
To reverse a given integer, we extract digits one by one and adjust the reversed number by
multiplying it by 10 and adding the extracted digit.
Steps to Reverse a Number:
1. Initialize reverseNumber as 0.
2. Extract the last digit of n using n % 10.
3. Update reverseNumber as reverseNumber * 10 + digit.
4. Remove the last digit from n using n / 10.
5. Repeat until n becomes 0.
6. Handle the case where reversing the number causes overflow.
C++ Code for Reversing an Integer:
#include <iostream>
#include <climits>
using namespace std;
int reverseInteger(int n) {
int reverseNumber = 0;
while (n != 0) {
int digit = n % 10;
17 BBN
n /= 10;
}
return reverseNumber;
}
int main() {
int num;
cout << "Enter a number: ";
cin >> num;
return 0;
}
2D Arrays in C++
What is a 2D Array?
A 2D array is like a table with rows and columns, used to store data in a structured format. It
is also called a matrix in programming.
For example, if we have a 4×3 matrix, it means:
4 rows (horizontal lines)
3 columns (vertical lines)
Declaring a 2D Array in C++
We use square brackets [ ] to define the number of rows and columns.
int matrix[4][3]; // 4 rows and 3 columns
Initializing a 2D Array
We can initialize a 2D array with values like this:
int matrix[4][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12}
};
Each { } represents a row.
Accessing Elements in a 2D Array
To access an element, we need row and column indexes (starting from 0).
cout << matrix[2][1]; // Prints 8 (Row index 2, Column index 1)
We can also modify an element:
matrix[2][1] = 18; // Changes value from 8 to 18
Printing a 2D Array using Loops
We use nested loops to print all elements row-wise.
for(int i = 0; i < 4; i++) { // Loop for rows
for(int j = 0; j < 3; j++) { // Loop for columns
18 BBN
cout << matrix[i][j] << " ";
}
cout << endl; // Move to the next row
}
Output:
1 2 3
4 5 6
7 8 9
10 11 12
Passing a 2D Array to a Function
When passing a 2D array to a function, we also pass the number of columns:
void printMatrix(int matrix[][3], int rows) {
for(int i = 0; i < rows; i++) {
for(int j = 0; j < 3; j++) {
cout << matrix[i][j] << " ";
}
cout << endl;
}
}
Call this function with:
printMatrix(matrix, 4);
19 BBN
cout << "Right Sum: " << rightDsum << endl;
}
int main()
{
int nums[4][4] = { {1, 2, 3, -4},
{-5, -6, -7, 8},
{-9, 0, -11, 0},
{-9, 10, -11, 0} };
2D Vectors
Instead of fixed-size 2D arrays, we can use 2D vectors for dynamic resizing.
Advantage: Each row can have a different number of columns.
Defining a 2D Vector
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6, 7}, // Extra elements in this row
{8, 9, 10}
};
20 BBN
Feature 2D Array 2D Vector
Row Size Fixed for all rows Can vary per row
21 BBN
o Use mid = (low + high) / 2 to find the middle element.
o Compare matrix[row][col] with the target:
If equal, return true.
If less, move to the right half (low = mid + 1).
If greater, move to the left half (high = mid - 1).
Code:-
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int row=-1;
int start=0, end=[Link]()-1;
int cols=matrix[0].size()-1;
while(start<=end){
int mid=start+(end-start)/2;
if(target>=matrix[mid][0] && target<=matrix[mid][cols]){
row=mid;
break;
}
else if(target<matrix[mid][0]){
end=mid-1;
}
else{
start=mid+1;
}
}
if(row==-1)return false;
start=0, end=cols;
while(start<=end){
int mid=start+(end-start)/2;
if(target==matrix[row][mid])return true;
else if(target<matrix[row][mid]){
end=mid-1;
}
else{
start=mid+1;
}
}
return false;
}
};
22 BBN
Why Can't We Use the Previous Approach?
In the previous problem, each row’s first element was greater than the last element of the
previous row. However, in this problem, row values overlap in range.
For example, if we search for 5, it could exist in multiple overlapping ranges instead of a single
unique row. This means we cannot apply the previous approach directly.
How to Solve This Problem?
Since the data is sorted, a binary search or its variation should be considered.
Observations:
The smallest value in the matrix will always be at (0,0) (top-left corner).
The largest value will always be at (M-1, N-1) (bottom-right corner).
Choosing a Search Strategy
To apply binary search, we need a search range within which our target value exists.
We define our search space as:
Start from (0, N-1) → Top-right corner
Midpoint selection strategy:
o If the midpoint value = target, we return true.
o If the midpoint value > target, we move left.
o If the midpoint value < target, we move down.
By always reducing search space row-wise or column-wise, we ensure an efficient search.
Why Start from the Top-Right Corner?
If we choose any random middle element, it may lead to multiple search directions.
23 BBN
Corner elements reduce possibilities, helping us eliminate larger portions of the
matrix.
At (0, N-1):
o If target < matrix[0][N-1], move left (reduce column).
o If target > matrix[0][N-1], move down (increase row).
Thus, starting from top-right reduces unnecessary comparisons and makes the search more
efficient.
class Solution {
public:
bool searchMatrix(vector<vector<int>>& mat, int target) {
int row=0,col=mat[0].size()-1;
while(row<[Link]() && col<mat[0].size()){
if(mat[row][col]==target)return true;
else if(mat[row][col]>target)col--;
else row++;
}
return false;
}
};
Spiral Matrix Traversal - C++ Notes
Introduction
The Spiral Matrix is a common 2D array problem frequently asked in interviews.
It involves traversing a given matrix in a spiral order and storing the elements in a
specific sequence.
Example problem: Leetcode #54 - Spiral Matrix.
Understanding the Problem
Given an m × n matrix, traverse it in a spiral pattern:
o Start from the top-left corner.
o Move left to right along the top boundary.
o Move top to bottom along the right boundary.
o Move right to left along the bottom boundary.
o Move bottom to top along the left boundary.
o Repeat for the inner layers until all elements are covered.
Example
Consider the given 4×4 matrix:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
Spiral order traversal:
1 → 2 → 3 → 4 ↓ 8 → 12 → 16 ← 15 ← 14 ← 13 ↑ 9 → 5 → 6 → 7 ↓ 11 → 10
Output: [1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10]
Approach
Use four boundary markers:
o startRow (initially 0)
o endRow (m - 1)
o startCol (0)
24 BBN
oendCol (n - 1)
Use a loop to iterate through boundaries:
1. Top boundary → left to right (fixed row)
2. Right boundary → top to bottom (fixed column)
3. Bottom boundary → right to left (fixed row)
4. Left boundary → bottom to top (fixed column)
5. Shrink boundaries after each traversal.
Code Implementation (C++)
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans;
int srow=0,scol=0;
int erow=[Link]()-1,ecol=matrix[0].size()-1;
while(srow<=erow && scol<=ecol){
//top
for(int col=scol;col<=ecol;col++){
ans.push_back(matrix[srow][col]);
}
//right
for(int row=srow+1;row<=erow;row++){
ans.push_back(matrix[row][ecol]);
}
if(srow !=erow){
//bottom
for(int col=ecol-1;col>=scol;col--){
ans.push_back(matrix[erow][col]);
}
}
if(scol !=ecol){
//left
for(int row=erow-1;row>=srow+1;row--){
ans.push_back(matrix[row][scol]);
}
}
srow++;
erow--;
scol++;
ecol--;
}
return ans;
}
};
Time Complexity
O(m × n) → Each element is visited once.
Space Complexity
O(1) → Uses only extra space for output array.
25 BBN
Finding a Pair with Target Sum using Unordered
Map
Problem Statement
Given an array of integers nums and an integer target, return indices of the two numbers such that
they add up to target.
You may assume that each input would have exactly one solution, and you may not use
the same element twice.
Approach
1. Pick the First Value: We iterate over the array and pick an element as the first value.
2. Calculate the Second Value: The second value can be found using:
Second Value = Target − First Value
3. Check if Second Value Exists:
o If it exists in the remaining array, we return both indices.
o If not, we store the first value in an unordered map.
Why Use Unordered Map?
Unordered maps allow fast searching (average O(1) time complexity).
It helps us quickly check if the second value exists in the stored elements.
26 BBN
};
27 BBN
ans.push_back(b);
return ans;
}
};
Approach:-
Concept of Linked List
A linked list is a collection of nodes where each node points to the next one.
Example: 1 -> 2 -> 3 -> 4 -> 5
If a node points back to a previous node, a cycle is formed.
Reimagining Array as a Linked List
Each index of an array is treated as a node in the linked list.
Each element at that index points to the next node.
Example:
Array: [3, 1, 3, 4, 2]
28 BBN
Index: 0 1 2 3 4
o Index 0 → Index 3 (since arr[0] = 3)
o Index 3 → Index 4 (since arr[3] = 4)
o Index 4 → Index 2 (since arr[4] = 2)
o Index 2 → Index 3 (since arr[2] = 3) → Cycle detected
Slow-Fast Pointer Approach
1. Step 1: Detect Cycle
o Use two pointers: Slow (moves 1 step) & Fast (moves 2 steps).
o If Slow meets Fast, a cycle exists.
2. Step 2: Find Cycle Start
o Move Slow to the start, keep Fast at meeting point.
o Move both one step at a time until they meet again.
Meeting point = Start of the cycle = Duplicate number.
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow=nums[0],fast=nums[0];
do{
slow=nums[slow];
fast=nums[nums[fast]];
}while(slow!=fast);
slow=nums[0];
while(slow!=fast){
slow=nums[slow];
fast=nums[fast];
}
return slow;
}
};
29 BBN
Since this is a variation of Two Sum and Three Sum, we use a Two Pointer approach.
Steps to Solve
1. Sort the array (Two-pointer works
efficiently in sorted arrays).
2. Pick the first number nums[i] using a
loop.
3. Pick the second number nums[j]
using another loop (j = i + 1).
4. Use two pointers (k, l) to find the
remaining two numbers:
o k = j + 1 (start pointer)
o l = n - 1 (end pointer)
5. Check the sum of nums[i] + nums[j]
+ nums[k] + nums[l]:
o If equal to target, store the combination.
o If less than target, move k++ to increase sum.
o If greater than target, move l-- to decrease sum.
6. Avoid duplicates by skipping repeated values.
int k = j + 1, l = n - 1;
while (k < l) {
long sum = (long)nums[i] + nums[j] + nums[k] + nums[l];
if (sum == target) {
result.push_back({nums[i], nums[j], nums[k], nums[l]});
while (k < l && nums[k] == nums[k + 1]) k++; // Skip duplicates
while (k < l && nums[l] == nums[l - 1]) l--; // Skip duplicates
k++; l--;
}
else if (sum < target) k++;
else l--;
}
}
}
return result;
}
30 BBN
Time Complexity Analysis
Sorting the array: O(N log N)
Two nested loops (i and j): O(N²)
Two-pointer search (k and l): O(N)
Overall Complexity: O(N³)
Space Complexity
O(1) (Ignoring output storage)
Step-by-Step Approach
Step 1: Use a HashMap to Store Prefix Sum
Frequencies
unordered_map<int, int> m;
We use an unordered map (m) to store
how many times a particular sum has
appeared before.
Key: sum (prefix sum up to a certain index)
Value: Count of how many times this
sum appeared
31 BBN
Step 2: Initialize Variables
int sum = 0, count = 0;
m[0] = 1;
sum = 0: Keeps track of the running sum (prefix sum).
count = 0: Stores the number of valid subarrays found.
m[0] = 1: Important! It ensures we correctly count subarrays starting from index 0.
32 BBN
Understanding With an Example
Input:
nums = {1, 2, 3}, k=3
Dry Run:
Inde nums[i su sum - k m[sum - k] Exists? coun HashMap (m)
x ] m t
0 1 1 -2 ❌ No 0 {0:1, 1:1}
Output: 2 ✅
(Subarrays: [1,2] and [3])
Recursion
1. What is Recursion?
o Recursion happens when a function calls itself.
o This is different from regular function calls where one function calls another.
o It continues calling itself until it reaches a stopping condition (Base Case).
2. How Does Recursion Work?
o Each function call does a small part of the work.
o The remaining work is passed on to the next function call.
o Eventually, it reaches the simplest problem (Base Case), where it stops.
Step-by-Step Approach
Step 1: Identify the Base Case
The recursion must stop at some point.
Base Case: If n == 1, print 1 and stop further calls.
Step 2: Identify the Recursive Case
If n > 1,
o Print n.
o Call the function again with n-1.
o This will print the next smaller number.
Step 3: Write the Recursive Function
#include <iostream>
using namespace std;
void printNumbers(int n) {
// Base case: stop when n reaches 1
33 BBN
if (n == 1) {
cout << 1 << endl;
return;
}
// Print the current number
cout << n << endl;
// Recursive call with n-1
printNumbers(n - 1);
}
📌 Output:
4
3
2
1
Understanding the Call Stack with Recursion
In C++, when a function is called, its execution details (like local variables and return addresses)
are stored in the Call Stack. Once the function completes, its stack frame is removed, and
execution returns to the previous function call.
How the Call Stack Works Internally?
Step-by-Step Execution:
34 BBN
o Fib(2) = 1 + 0 = 1
o Fib(3) = 1 + 1 = 2
o Fib(4) = 2 + 1 = 3
o Fib(5) = 3 + 2 = 5
Problem Statement
Find the Nth Fibonacci number using recursion.
Example: If n = 6, output should be 8.
Recursion Concept
Recursion: Solving a big problem by breaking it into smaller sub-problems.
Recursive relation: Fib(n) = Fib(n-1) + Fib(n-2)
Recursive calls continue until we reach the base case.
Base Cases
Fib(0) = 0
Fib(1) = 1
Recursive Function in C++
#include <iostream>
using namespace std;
Time Complexity
Recursive function creates a tree of function calls.
Each call branches into two more calls.
Time Complexity: O(2^n) (Exponential, very slow for
large n).
Space Complexity
O(n) due to recursive function call stack.
35 BBN
3. {1, 2} (including both elements)
4. {} (empty subset, containing no elements)
Thus, the total number of subsets for an array of size n is given by:
Total subsets=2n
for a given array.
Using Recursion to Generate Subsets
Recursion is a fundamental concept that helps solve many problems based on subsets.
To generate subsets, we follow this approach:
Each element of the array has a choice:
1. Include it in the subset.
2. Exclude it from the subset.
We apply recursion to solve this problem step by step.
Instead of solving the entire problem at once, we break it into smaller subproblems.
For every element in the array, we take a recursive decision to either include or
exclude it.
Example of Recursive Approach
Let's consider an array {1, 2, 3} and generate its subsets using recursion:
{1, 2, 3}, {1, 2}, {1, 3}, {1}, {2, 3}, {2}, {3}, {}
This gives us 2^3 = 8 subsets, as expected.
Recursive Code to Print Subsets (C++)
class Solution {
public:
void allsets(vector<int>& nums,vector<vector<int>>& ans,vector<int> sub,int i){
if(i==[Link]()){
ans.push_back(sub);
return;
}
sub.push_back(nums[i]);
allsets(nums,ans,sub,i+1);
sub.pop_back();
allsets(nums,ans,sub,i+1);
}
36 BBN
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> ans;
vector<int> sub;
allsets(nums,ans,sub,0);
return ans;
}
};
Explanation of the Code:
1. Base Condition: If we reach the end of the array, print the subset and return.
2. Recursive Steps:
o First, we exclude the current element and move to the next index.
o Then, we include the current element and again move to the next index.
o After both recursive calls, we backtrack to remove the last added element.
3. Printing: Every subset is printed at the base condition when index == [Link]().
Time Complexity Analysis
Since we generate 2^n subsets (each element has two choices), the time complexity of this
approach is O(2^n).
Handling Duplicate Elements in Subset Generation
Problem Understanding
Previously, we dealt with unique elements in the subset problem.
Now, we are considering an array that contains duplicate elements.
Key Question: How do duplicates affect subset generation?
Example
Given an array: {1, 2, 2}
Using the previous subset approach, we generate all subsets.
Challenge: Duplicate subsets appear in the output.
Recursive Approach for Subset Generation
1. Start with an empty subset.
2. At each index (i), choose to either include or exclude the current element.
3. Move to the next index and repeat the process.
4. Generate all subsets and handle duplicates.
37 BBN
Issue with Duplicate Subsets
If we blindly follow inclusion/exclusion for each element, we may generate duplicate
subsets.
The key observation:
o If we exclude a duplicate element at some point, we must continue excluding all
its occurrences in subsequent steps.
Fixing the Duplicate Issue
1. Sort the array first to group identical elements together.
2. Modify the recursive approach:
o If an element is excluded, skip all its consecutive occurrences to avoid
duplicate subsets.
Recursive Implementation Strategy
class Solution {
public:
void allsets(vector<int>& nums,vector<vector<int>>& ans, vector<int> sub,int i){
if(i==[Link]()){
ans.push_back({sub});
return;
}
sub.push_back(nums[i]);
allsets(nums,ans,sub,i+1);
sub.pop_back();
while(i+1<[Link]()&&nums[i+1]==nums[i])i++;
allsets(nums,ans,sub,i+1);
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> ans;
vector<int> sub;
sort([Link](),[Link]());
38 BBN
allsets(nums,ans,sub,0);
return ans;
}
};
Complexity Analysis
The total number of subsets is O(2ⁿ), but removing duplicates reduces redundant
calculations.
Sorting takes O(n log n), and the subset generation process runs in O(2ⁿ) (in worst-case
scenarios).
Permutations of an Array
Introduction
The same logic applies whether the input is an array or a string.
A permutation is a rearrangement of the elements of an array in all possible orders.
Understanding Permutations
Given an array [1, 2, 3], the possible permutations are:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
The total number of permutations of n elements is n! (n factorial).
Mathematical Logic Behind Permutations
Imagine n positions that we need to fill using n elements.
For the first position: We have n choices.
For the second position: n-1 choices remain.
For the third position: n-2 choices remain.
… continues until only one choice is left.
39 BBN
o Swap elements to form new permutations.
o Move to the next position.
C++ Code for Generating Permutations
class Solution {
public:
void permutations(vector<int>& nums,int idx,vector<vector<int>>& ans){
if(idx==[Link]()-1){
ans.push_back(nums);
return;
}
for(int i=idx;i<[Link]();i++){
swap(nums[i],nums[idx]);
permutations(nums,idx+1,ans);
swap(nums[i],nums[idx]);
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;
permutations(nums,0,ans);
return ans;
}
};
Explanation of Code
1. permutation(nums, index, result):
o Generates permutations using recursion and backtracking.
o Swaps elements to create different orders.
2. permute(nums):
o Calls the recursive function to generate all permutations.
3. main():
o Defines an array {1, 2, 3} and prints all permutations.
N-Queens Problem
Introduction
The N-Queens problem is a classic problem in computer science that helps understand
recursion and backtracking. The goal is to place N queens on an N×N chessboard so that no two
queens attack each other.
Understanding the Chessboard and Queen’s Movement
A queen in chess can attack other pieces in the following ways:
Vertically (same column)
Horizontally (same row)
Diagonally (both left and right diagonals)
Thus, our task is to place N queens on the board such that none of them attack each other.
Approach: Recursion and Backtracking
The problem is solved using recursion and backtracking:
1. Place a queen in a row.
2. Move to the next row and try placing the next queen in a valid position.
40 BBN
3. If a row has no valid positions, backtrack and change the previous queen’s position.
4. Repeat until all N queens are successfully placed.
class Solution {
public:
bool isSafe(vector<string>& board, int row, int col, int n){
//row
int i=0;
while(i<n){
if(board[row][i]=='Q')return false;
if(board[i][col]=='Q')return false;
i++;
}
//left diagnol
i=row;
int j=col;
while(i>=0&&j>=0){
if(board[i][j]=='Q')return false;
i--;
j--;
}
41 BBN
//right diagnol
i=row,j=col;
while(i>=0&&j<n){
if(board[i][j]=='Q')return false;
i--;
j++;
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> ans;
vector<string> board(n,string(n,'.'));
nQueens(board,ans,n,0);
return ans;
}
};
Rat Maze
Handling Validity and Base Cases in Matrix Problems using C++
Understanding the Boundary Conditions
When working with matrices, the first step is to ensure that our row and column values are within
valid boundaries. If these values are invalid, we cannot perform valid operations on the matrix.
Thus, in our base case, we define the following conditions:
if (r < 0 || c < 0 || r >= n || c >= n || matrix[r][c] == 0) {
return;
}
Here, n represents the size of the matrix. The conditions ensure that:
r < 0 or c < 0: We are not accessing negative indices.
r >= n or c >= n: We are within the matrix bounds.
matrix[r][c] == 0: We do not proceed if the value in the cell is zero (invalid path).
Handling the Base Case for Finding a Solution
42 BBN
When we reach the bottom-right corner of the matrix (r == n - 1 && c == n - 1), we have found
a valid path:
if (r == n - 1 && c == n - 1) {
answer.push_back(path);
return;
}
This ensures that once we reach our goal, we store the path and return.
Handling Opposite Choices to Prevent Infinite Recursion
If we choose to move downward (r+1, c), we should not immediately move upward (r-1, c) in the
next step, as this creates an infinite loop. Similarly, moving left after moving right leads to the
same issue. To prevent this, we track visited cells.
Using a Visited Matrix to Track Paths
We maintain an n x n visited matrix, initialized with false, to track whether a cell has been
visited:
vector<vector<bool>> visited(n, vector<bool>(n, false));
Before exploring a cell, we mark it as visited:
visited[r][c] = true;
If we encounter a cell that is already visited, we return immediately:
if (visited[r][c]) return;
Once we return from recursion, we reset the cell to false to allow other paths to use it:
visited[r][c] = false;
This technique is crucial for backtracking, ensuring that previously visited cells can be
reconsidered when trying alternate paths.
Final Recursive Function with Backtracking
Here’s how the recursive function implements the above logic:
void findPath(vector<vector<int>>& matrix, vector<vector<bool>>& visited, int r, int c,
string path, vector<string>& answer) {
int n = [Link]();
if (r < 0 || c < 0 || r >= n || c >= n || matrix[r][c] == 0 || visited[r][c]) {
return;
}
if (r == n - 1 && c == n - 1) {
answer.push_back(path);
return;
}
visited[r][c] = true;
// Exploring all possible directions
findPath(matrix, visited, r + 1, c, path + "D", answer); // Down
findPath(matrix, visited, r, c + 1, path + "R", answer); // Right
findPath(matrix, visited, r - 1, c, path + "U", answer); // Up
findPath(matrix, visited, r, c - 1, path + "L", answer); // Left
// Backtracking step
visited[r][c] = false;
}
Palindrome Partitioning
Problem Statement
43 BBN
Given a string s, we need to return a vector of vectors of strings, where each vector contains
possible partitions of s that are all palindromic in nature.
Understanding the Problem
To solve this problem, we need to:
1. Generate all possible partitions of the string.
2. Filter partitions that are palindromic and store them in the answer vector.
Breaking Down the Problem
Partitioning a String: If a string s has a length of n, we can place n - 1 cuts to generate
all possible partitions.
Example: For string "abc":
o Partitions: {"a", "b", "c"}, {"ab", "c"}, {"a", "bc"}, {"abc"}
o We must check which of these partitions contain only palindromic substrings.
Palindrome Checking
A substring is palindromic if it reads the same forward and backward. Example:
"aba" is a palindrome
"abc" is not a palindrome
Recursive Approach for Solution
1. Start from the beginning of the string and try to make cuts at each index.
2. If the left substring (from start
to current index) is a palindrome,
then recursively partition the
remaining right substring.
3. Collect all palindromic partitions
and return them as a vector of
vectors.
Code Implementation
class Solution {
public:
bool isPalindrome(string p){
int start=0;
int end=[Link]()-1;
while(start<end){
if(p[start]!=p[end]){
return false;
}
start++;
end--;
}
return true;
}
void helper(string s, vector<string>& part,vector<vector<string>>& ans){
if([Link]()==0){
ans.push_back(part);
44 BBN
return;
}
for(int i=0;i<[Link]();i++){
string p =[Link](0,i+1);
if(isPalindrome(p)){
part.push_back(p);
helper([Link](i+1),part,ans);
part.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
vector<vector<string>> ans;
vector<string> part;
helper(s,part,ans);
return ans;
}
};
45 BBN
mid = start + (end - start) / 2;
o Recursively call merge sort on both halves:
o mergeSort(arr, start, mid);
mergeSort(arr, mid + 1, end);
2. Base Case
o The recursion stops when the array contains only one element:
if (start >= end) return;
3. Merging Process
o Merge the two sorted halves into a single sorted array.
o Use two pointers, one for each half, and compare elements.
o Place the smaller element into a temporary array and continue merging.
o Copy the merged elements back to the original array.
C++ Implementation
#include <iostream>
#include <vector>
using namespace std;
void Merge(vector<int> &nums, int start, int mid, int end)
{
int start1 = start;
int start2 = mid + 1;
vector<int> temp;
while (start <= mid && start2 <= end)
{
if (nums[start] < nums[start2])
{
temp.push_back(nums[start++]);
}
else
{
temp.push_back(nums[start2++]);
}
}
while (start <= mid)
{
temp.push_back(nums[start++]);
}
for (int i = 0; i < [Link](); i++)
{
nums[i + start1] = temp[i];
}
}
void MS(vector<int> &nums, int start, int end)
{
if (start == end)
return;
int mid = start + (end - start) / 2;
// left
MS(nums, start, mid);
46 BBN
// right
MS(nums, mid + 1, end);
Merge(nums, start, mid, end);
}
int main()
{
vector<int> nums = {12, 9, 1, 7, -6, 5};
MS(nums, 0, [Link]() - 1);
for (int dig : nums)
{
cout << dig << " ";
}
return 0;
}
Time Complexity Analysis
Dividing the array: O(log N) because we repeatedly split it into halves.
Merging process: O(N) for merging two halves.
Overall complexity: O(N log N), making it efficient for large datasets
QuickSort Algorithm
Introduction:
QuickSort is a divide and conquer sorting algorithm that sorts an array efficiently. It is based
on the pivot and partition approach.
By the end of this note, you will understand:
The approach of QuickSort
The time and space complexity
The dry run of QuickSort on an array
Steps of QuickSort:
1. Choose a Pivot: Select a pivot element from the array. We will use the last element as
the pivot in our implementation.
2. Partition the Array: Rearrange the array so that:
o Elements smaller than the pivot go to the left.
o Elements greater than the pivot go to the right.
3. Recursively Apply QuickSort on the left and right subarrays.
Implementation of QuickSort in C++:
#include <iostream>
using namespace std;
47 BBN
}
}
swap(arr[i + 1], arr[high]);
return i + 1; // Return pivot index
}
// QuickSort function
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1); // Sort left half
quickSort(arr, pivotIndex + 1, high); // Sort right half
}
}
Dry Run Example:
Given Array: {3, 6, 1, 5, 2, 4}
1. Choose pivot = 4
2. Partitioning results in: {3, 1, 2, 4, 6, 5}
3. Recursively apply QuickSort on {3, 1, 2} and {6, 5}
4. Continue partitioning and sorting until fully sorted.
Time and Space Complexity:
Worst Case: O(n²) → Unbalanced partitioning (already sorted/reverse sorted array
without optimization)
Average Case: O(n log n)
Space Complexity: O(log n) (Recursive stack space)
Inversion Count
Definition
An inversion is a pair (i, j) in an array arr such that:
i < j (i.e., the first element appears before the second in the array)
arr[i] > arr[j] (i.e., the first element is greater than the second)
The inversion count of an array is the total number of such pairs.
48 BBN
Key Idea
In Merge Step, while merging two sorted halves, if arr[i] > arr[j], then all elements
after arr[i] in the left half will also be greater than arr[j], forming multiple inversions at
once.
Algorithm
1. Divide: Recursively divide the array into two halves.
2. Count left and right inversions using recursion.
3. Merge: While merging the two sorted halves, count cross inversions.
4. Return total count as sum of left, right, and cross inversions.
C++ Implementation
#include <iostream>
using namespace std;
int mergeAndCount(int arr[], int temp[], int left, int mid, int right) {
int i = left, j = mid, k = left, invCount = 0;
49 BBN
int countInversionsMergeSort(int arr[], int n) {
int temp[n];
return mergeSortAndCount(arr, temp, 0, n - 1);
}
Complexity Analysis
Merge Sort runs in O(N log N)
Space Complexity = O(N) due to temporary array
Comparison of Approaches
Approach Time Complexity Space Complexity
Brute Force O(N^2) O(1)
Merge Sort O(N log N) O(N)
50 BBN
The algorithm uses backtracking to explore possible paths recursively. The main steps are:
1. Start at an initial position (0,0).
2. Mark the current square as visited.
3. Try all possible knight moves.
4. If a move leads to a solution, return true.
5. If not, backtrack and try a different move.
6. If all moves are exhausted and the tour is incomplete, return false.
Recursive Function
class Solution {
public:
bool isValid(vector<vector<int>>& grid,int r,int c,int expV,int n){
if(r<0||c<0||r>=n||c>=n||expV!=grid[r][c])return false;
if(expV==n*n-1)return true;
return isValid(grid,r+2,c+1,expV+1,n)||isValid(grid,r+2,c-1,expV+1,n)||
isValid(grid,r-2,c-1,expV+1,n)||isValid(grid,r-2,c+1,expV+1,n)||
isValid(grid,r+1,c+2,expV+1,n)||isValid(grid,r-1,c+2,expV+1,n)||isValid(grid,r-1,c-
2,expV+1,n)||isValid(grid,r+1,c-2,expV+1,n);
}
bool checkValidGrid(vector<vector<int>>& grid) {
return isValid(grid,0,0,0,[Link]());
}
};
51 BBN
Implement methods (functions) like changeDepartment() or calculateTax(), which modify
or retrieve information about teachers.
Use objects to store details of individual teachers.
class Teacher {
public:
string name;
string department;
string subject;
double salary;
double calculateTax() {
return salary * 0.1; // Example: 10% tax
}
};
Here, Teacher is the class, while individual teacher records will be objects created from this
class.
3. Implementation of OOPs in C++ STL
Many C++ libraries, such as vector, string, and stack, are implemented using OOPs concepts.
Learning how to create and use classes will help us understand and utilize these libraries
efficiently.
Access Modifiers
When studying classes and objects, defining properties and member functions is not
enough. We also need to specify their accessibility using access modifiers.
What are Access Modifiers?
Access modifiers are special keywords in C++ that define the accessibility of class members
(data and methods). There are three types of access modifiers:
1. Private
2. Public
3. Protected
1. Private Members
Any data members (properties) or methods declared as private are only accessible
within the class.
By default, all members of a class in C++ are private.
Private members cannot be accessed outside the class, including in the main()
function.
If we try to access a private member outside the class, it results in an error.
Example:
class Teacher {
private:
string name;
};
52 BBN
2. Public Members
Any data members or methods declared as public are accessible both inside and
outside the class.
Public members can be accessed in the main function or other classes.
Example:
class Teacher {
public:
string name;
};
Here, salary is protected and can only be accessed inside this class and its derived classes.
Why Use Private Members?
In real-world applications, some data should not be accessible outside the class.
For example, in a college system, details like name, department, and subject can be public,
but salary should be private and accessible only by authorized users (e.g., the accounts
team).
Getter and Setter Functions
Since private members cannot be accessed directly, we use getter and setter functions to
access them.
Setter function → Used to set (modify) a private variable.
Getter function → Used to retrieve (get) a private variable.
Example:
class Teacher {
private:
double salary;
public:
void setSalary(double s) {
salary = s;
}
double getSalary() {
return salary;
}
};
53 BBN
Here, setSalary() is setting the salary value, and getSalary() is returning the value.
Encapsulation in OOP
Encapsulation means binding data and methods together while restricting direct
access to the data.
Private and protected access modifiers help achieve encapsulation.
In interviews, encapsulation is one of the four key pillars of Object-Oriented
Programming (OOP):
1. Encapsulation
2. Abstraction
3. Inheritance
4. Polymorphism
Constructors in C++
When working with classes and objects, one important concept is the constructor. A
constructor is a special method that is automatically invoked whenever an object is created. It
is simple but plays a key role in initializing objects.
How Constructors Work
When an object is created, the constructor is called automatically.
It helps in allocating memory and initializing values to data members.
If a programmer does not define a constructor, C++ automatically provides a default
constructor.
Example:
class Teacher {
public:
Teacher() {
cout << "Hi, I am a constructor" << endl;
}
};
int main() {
Teacher t1; // Constructor is called automatically
Teacher t2; // Constructor is called again
}
Output:
Hi, I am a constructor
Hi, I am a constructor
Every time an object (t1 or t2) is created, the constructor is invoked.
Why Use Constructors?
Automatic Initialization: Suppose all teachers in a school belong to the Computer
Science department. Instead of manually assigning the department to each teacher, we
can initialize it inside the constructor.
Example:
class Teacher {
public:
string department;
54 BBN
Teacher() {
department = "Computer Science";
}
};
int main() {
Teacher t1;
cout << [Link]; // Output: Computer Science
}
Constructor Properties
1. Same Name as Class: The constructor function name must be identical to the class
name.
2. No Return Type: Constructors do not have a return type (not even void).
3. Automatically Invoked: They execute only once when an object is created.
Memory Allocation and Constructors
Memory for an object is allocated only when the constructor is called.
Example:
class A {
int x;
};
o Memory is not allocated when a class is defined.
o Memory is allocated only when an object is created.
A obj1, obj2;
o obj1 and obj2 will get separate memory spaces for their variables.
Types of Constructors in C++
1. Default (Non-Parameterized) Constructor
o Does not take any arguments.
Example:
class A {
public:
A() {
cout << "Default Constructor Called" << endl;
}
};
2. Parameterized Constructor
o Takes arguments to initialize objects with custom values.
Example:
class Teacher {
public:
string name;
string department;
Teacher(string n, string d) {
name = n;
department = d;
55 BBN
}
};
int main() {
Teacher t1("John", "Mathematics");
cout << [Link] << " - " << [Link];
}
Output:
John - Mathematics
3. Copy Constructor
o Creates a new object as a copy of an existing object.
Example:
class Teacher {
public:
string name;
Teacher(string n) {
name = n;
}
// Copy Constructor
Teacher(const Teacher &t) {
name = [Link];
}
};
int main() {
Teacher t1("Alice");
Teacher t2 = t1; // Copy constructor is called
cout << [Link]; // Output: Alice
}
56 BBN
Using this ensures that the object's properties are properly assigned values from
the parameters.
How this Pointer Works
Whenever an object is created, this stores the memory address of that object.
Example: If an object t1 is created at memory location 100, this will store 100.
this is an automatically created pointer that always points to the calling object.
Understanding Pointers for this
int x = 10;
int* ptr = &x;
We can access x using either:
o x
o *ptr (dereferencing the pointer)
Similarly, in this pointer:
o *this refers to the entire object.
o To access properties, we can use:
(*this).property
o Instead of this complex syntax, C++ provides a shortcut: this->property.
Copy Constructor in C++
The copy constructor is used to copy properties from one object to another.
Teacher t1("John", "CS", "C++", 50000);
Teacher t2 = t1; // Calls the Copy Constructor
How It Works:
o When t2 is created using t1, C++ automatically invokes the default copy
constructor.
o It copies all properties of t1 into t2.
How Memory Works:
o If t1 has:
Name: John
Subject: C++
Department: CS
Salary: 50,000
o The memory is duplicated for t2, creating an exact copy.
Creating a Custom Copy Constructor
We can manually define our own copy constructor:
class Teacher {
public:
string name, department, subject;
int salary;
// Copy Constructor
Teacher(const Teacher& t) {
name = [Link];
department = [Link];
subject = [Link];
salary = [Link];
}
};
This ensures deep copying instead of relying on the default behavior.
57 BBN
Dynamic Memory Allocation
Dynamic memory allocation allows us to allocate memory at runtime using heap memory. It is
useful when we don’t know the exact size of memory needed beforehand.
Why use it?
The size of memory is determined at runtime.
More efficient use of memory compared to stack allocation.
How to allocate memory dynamically?
We use new in C++ to allocate memory and delete to free it.
Example:
#include <iostream>
using namespace std;
int main() {
int *ptr = new int; // Allocating memory dynamically
*ptr = 10; // Storing value in allocated memory
For Arrays:
int *arr = new int[5]; // Allocating array dynamically
delete[] arr; // Free memory allocated for array
Copy Constructor
A copy constructor is used to create a new object as a copy of an existing object.
Why use it?
It ensures proper copying when an object contains dynamically allocated memory.
Default copy constructor performs shallow copy, which may lead to memory issues.
We define a custom copy constructor for deep copy.
Shallow Copy (Default Copy Constructor)
#include <iostream>
using namespace std;
class Example {
public:
int *ptr;
Example(int val) {
ptr = new int(val);
}
void show() {
cout << "Value: " << *ptr << endl;
}
};
58 BBN
int main() {
Example obj1(10);
Example obj2 = obj1; // Default copy constructor (Shallow Copy)
[Link]();
[Link]();
return 0;
}
class Example {
public:
int *ptr;
Example(int val) {
ptr = new int(val); // Allocating memory dynamically
}
void show() {
cout << "Value: " << *ptr << endl;
}
~Example() {
delete ptr; // Freeing memory (Problem occurs here)
}
};
int main() {
Example obj1(10);
Example obj2 = obj1; // Shallow Copy
[Link]();
[Link]();
59 BBN
[Link](); // Accessing deleted memory -> **Undefined Behavior (Crash)**
return 0;
}
Deep Copy (User-Defined Copy Constructor)
To avoid issues, we define a custom copy constructor that allocates new memory.
class Example {
public:
int *ptr;
Example(int val) {
ptr = new int(val);
}
void show() {
cout << "Value: " << *ptr << endl;
}
~Example() {
delete ptr; // Free memory
}
};
int main() {
Example obj1(10);
Example obj2 = obj1; // Deep copy prevents shared memory issue
[Link]();
[Link]();
return 0;
}
Key Differences:
Feature Shallow Copy Deep Copy
Memory Allocation Same memory shared New memory allocated
Changes in One Affects Yes No
Another?
Custom Copy Constructor No Yes
Needed?
60 BBN
A destructor is a special function that frees (deallocates) memory when an object is
no longer needed.
If you don’t define a destructor, the compiler creates one automatically.
The default destructor only frees statically allocated memory, not dynamically
allocated memory.
Dynamic Memory & Destructor Problem:
If an object contains dynamically allocated memory (using new), the default destructor
does not free it.
This can cause a memory leak, where memory is wasted and not reused.
Deleting Dynamic Memory:
In C++, new is used to allocate memory, and delete is used to free it.
Example:
int* ptr = new int(55); // Allocating memory
delete ptr; // Freeing memory
delete ptr does not delete the pointer; it deletes the memory that the pointer was
pointing to.
Example of Destructor:
#include <iostream>
using namespace std;
class Student {
public:
int* cgpa; // Pointer for dynamic memory
Student() {
cgpa = new int(10); // Allocating memory dynamically
cout << "Constructor: Memory allocated\n";
}
~Student() { // Destructor
delete cgpa; // Freeing allocated memory
cout << "Destructor: Memory freed\n";
}
};
int main() {
Student s1;
} // Destructor automatically called here
Output:
Constructor: Memory allocated
Destructor: Memory freed
The constructor allocates memory dynamically.
The destructor ensures that memory is properly freed.
Why Destructors Are Important?
In small programs, memory is automatically cleared when the program exits.
61 BBN
In real-world applications (like company projects), properly freeing memory can cause
memory leaks and slow down the system.
Memory Leak Example:
If we forget to delete dynamically allocated memory:
void memoryLeak() {
int* num = new int(5); // Memory allocated
// No delete statement here → Memory leak!
}
After calling memoryLeak(), the allocated memory remains occupied, wasting resources.
Inheritance
Inheritance in C++ is a way to pass properties from one class (parent class) to another class
(child class). There are three main types of inheritance modes:
1. Private Mode: If a class inherits another class in private mode, all public and protected
members of the parent class become private in the child class.
2. Protected Mode: If inheritance happens in protected mode, all public and protected
members of the parent class become protected in the child class.
3. Public Mode: In public mode, all public members of the parent class remain public in
the child class, and protected members remain protected.
Important Rules:
Private members of the parent class are never inherited.
The mode of inheritance decides how inherited properties behave inside the
child class.
Types of Inheritance:
1. Single Inheritance
A child class inherits from a single parent class.
class Person {
public:
string name;
int age;
};
62 BBN
};
2. Multi-level Inheritance
A class is inherited by another class, which is further inherited by a third class.
class Person {
public:
string name;
int age;
};
3. Multiple Inheritance
A child class inherits from multiple parent classes.
class Student {
public:
string name;
int rollNumber;
};
class Teacher {
public:
string subject;
double salary;
};
4. Hierarchical Inheritance
Multiple child classes inherit from a single parent class.
class Person {
public:
string name;
int age;
};
63 BBN
class Teacher : public Person {
public:
string subject;
};
Polymorphism
What is Polymorphism?
Polymorphism is an important concept in Object-Oriented Programming (OOP). The word
"Polymorphism" comes from two Greek words:
Poly means "many"
Morph means "forms"
So, Polymorphism means the ability of an object to take multiple forms. It allows objects
to behave differently based on the context.
Example: Constructor Overloading
Constructor Overloading is a simple example of Polymorphism. It means defining multiple
constructors in the same class with different parameters.
Example Code:
#include<iostream>
using namespace std;
class Student {
public:
string name;
// Non-parameterized constructor
Student() {
cout << "Non-Parameterized Constructor Called" << endl;
}
// Parameterized constructor
Student(string n) {
name = n;
cout << "Parameterized Constructor Called" << endl;
}
};
int main() {
Student s1; // Calls Non-Parameterized Constructor
Student s2("Tony"); // Calls Parameterized Constructor
return 0;
}
Explanation:
If no parameters are given → Non-Parameterized Constructor is called.
If a name is provided → Parameterized Constructor is called.
Types of Polymorphism
1. Compile-time Polymorphism (Static Polymorphism)
64 BBN
2. Run-time Polymorphism (Dynamic Polymorphism)
1. Compile-time Polymorphism
In Compile-time Polymorphism, the method to be executed is decided at compile-time.
Examples:
Function Overloading
Constructor Overloading
Operator Overloading
Example: Function Overloading
Function Overloading means defining multiple functions with the same name but different
parameters.
Example Code:
#include<iostream>
using namespace std;
class Print {
public:
void show(int x) {
cout << "Integer: " << x << endl;
}
void show(char c) {
cout << "Character: " << c << endl;
}
};
int main() {
Print p;
[Link](10); // Calls show(int)
[Link]('A'); // Calls show(char)
return 0;
}
Explanation:
The function show() is overloaded.
If an integer is passed → show(int) is called.
If a character is passed → show(char) is called.
Operator Overloading (Self-study)
Operators like +, =, * can be overloaded in C++ to work with user-defined data types.
65 BBN
class Parent {
public:
virtual void show() {
cout << "Parent class show()" << endl;
}
};
int main() {
Parent* p;
Child c;
p = &c;
p->show(); // Calls Child class show() due to dynamic binding
return 0;
}
Explanation:
The show() function in the parent class is declared as virtual, allowing dynamic binding.
A pointer of type Parent points to an object of Child.
At runtime, show() from Child is executed instead of Parent's show().
Key Differences Between Compile-time and Run-time Polymorphism
Feature Compile-time Polymorphism Run-time Polymorphism
66 BBN
Access Modifiers (like public, private, protected) help in implementing abstraction by
controlling visibility.
2. Abstract Classes
If we prefix a class with abstract, it becomes an abstract class.
Abstract class: A class that serves as a blueprint but cannot be instantiated.
A normal class is used to create objects, whereas an abstract class is meant for
inheritance.
Purpose of an Abstract Class:
o Defines a structure for other classes.
o Specifies which functions must be implemented in derived classes.
Example:
o A Shape class cannot be drawn because we don’t know what shape it is (circle,
square, etc.).
o The Shape class will have a draw() function, but we won’t implement it inside
Shape.
o Instead, Circle, Square, and Rectangle classes will inherit from Shape and
implement the draw() function accordingly.
3. Pure Virtual Functions
A function that has no implementation in the base class and must be implemented in
derived classes.
Syntax:
class Shape {
public:
virtual void draw() = 0; // Pure virtual function
};
A class with at least one pure virtual function is an abstract class.
Key Property:
o Abstract classes cannot have objects.
o Only derived classes can be instantiated.
4. Static Keyword
The static keyword can be used with:
o Variables inside functions.
o Class members (variables and functions).
Static Variable in a Function:
o Retains its value between function calls.
Example:
void fun() {
static int x = 0;
cout << x << endl;
x++;
}
If fun() is called three times, output will be:
0
1
2
67 BBN
Unlike normal variables, a static variable is created only once and persists throughout the
program.
5. Static Members in a Class
Static Variables in a Class:
o Shared by all objects of the class.
o Example:
class Example {
static int count;
};
Call it using:
Example::show();
Friend Function
What is a Friend Function?
A friend function is a function that is not a member of a class but has access to the private
and protected members of that class.
Why Use Friend Function?
It helps access private data of a class without using member functions.
It allows two or more classes to share data without inheritance.
It is useful when overloading operators.
How to Declare a Friend Function?
Use the keyword friend inside the class.
The function definition is outside the class.
Example: Adding Two Numbers Using Friend Function
#include <iostream>
using namespace std;
class A {
private:
int num;
public:
A(int value) { num = value; }
68 BBN
friend int add(A obj1, A obj2);
};
int main() {
A obj1(5), obj2(10);
cout << "Sum: " << add(obj1, obj2) << endl;
return 0;
}
Output:
Sum: 15
Linked List
Introduction to Linked List
A Linked List is a dynamic data structure used for storing a collection of elements. Unlike
arrays, linked lists allow efficient insertions and deletions without requiring memory reallocation.
Advantages of Linked List:
Dynamic size: No need to define size beforehand.
Efficient Insertions/Deletions: Operations like insertions and deletions are faster than
arrays as elements are linked using pointers.
Memory Utilization: No memory wastage due to pre-allocation.
Types of Linked List:
1. Singly Linked List: Each node points to the next node in the list.
2. Doubly Linked List: Each node contains pointers to both the next and previous nodes.
3. Circular Linked List: The last node points back to the first node.
// Node structure
class Node {
public:
int data;
Node* next;
Node(int val) {
data = val;
69 BBN
next = nullptr;
}
};
70 BBN
}
cout << "NULL" << endl;
}
};
Main Function
int main() {
LinkedList list;
[Link](10);
[Link](20);
[Link](30);
[Link](20);
cout << "After deletion: ";
[Link]();
return 0;
}
Explanation of Operations
1. Insertion
Creates a new node.
If the list is empty, sets the new node as head.
Otherwise, traverses to the last node and appends the new node.
2. Deletion
If the node to be deleted is the head, moves head to the next node and deletes the
original head.
Otherwise, finds the node before the target node, updates its next pointer, and deletes the
target node.
3. Traversal (Display)
Starts from head and moves through each node, printing its data until reaching nullptr.
Time Complexity
Operation Time Complexity
Deletion O(n)
Traversal O(n)
72 BBN
while(next!=NULL){
head->next=prev;
prev=head;
head=next;
next=head->next;
}
head->next=prev;
return head;
}
};
Example Cases
Example 1 (Even-sized list)
📌 Linked List: 1 → 2 → 3 → 4 → 5 → 6
✅ Middle Node: 4 (since 4 is the second middle node in an even-sized list)
Example 2 (Odd-sized list)
📌 Linked List: 1 → 2 → 3 → 4 → 5
✅ Middle Node: 3 (since 3 is exactly in the middle)
73 BBN
Step Slow Pointer Fast Pointer
Move 3 4 null
✅ Middle Node Found: 4
Odd-sized List (1 → 2 → 3 → 4 → 5)
Step Slow Pointer Fast Pointer
Start 1 1
Move 1 2 3
Move 2 3 5
Move 3 null null
✅ Middle Node Found: 3
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow=head;
ListNode* fast=head;
while(fast!=NULL&&fast->next!=NULL){
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
};
74 BBN
To solve this problem, we will use recursion. Many Linked List problems are easier to solve
using recursion rather than an iterative approach. Recursive solutions are often simpler and
more intuitive in such cases.
Understanding Recursion
Whenever we write a recursive function, we do not focus on solving the entire problem at once.
Instead, we break the larger problem into a smaller, simpler problem.
The big problem in this problem is merging two sorted linked lists. However, the small
problem is determining the head node for the merged list at each recursion level.
Steps to Solve Recursively:
1. If both lists are non-empty, compare the head nodes (h1 and h2).
2. The smaller node will become the head of the new merged list.
3. The remaining part of the linked list will be merged recursively.
4. Return the merged list.
Recursive Logic
At each step, we compare h1 and h2:
1. Case 1: If h1->val <= h2->val, h1 becomes the head.
o Recursively merge h1->next with h2.
o Set h1->next to the result of the recursive call.
o Return h1.
2. Case 2: If h1->val > h2->val, h2 becomes the head.
o Recursively merge h1 with h2->next.
o Set h2->next to the result of the recursive call.
o Return h2.
Base Condition
If either list is empty, return the non-empty list as the merged result.
Implementation
class Solution {
public:
}
};
Complexity Analysis
Time Complexity: O(N + M), where N and M are the lengths of the two lists.
Space Complexity: O(N + M) due to recursive stack space.
75 BBN
Deep Copy of a Linked List with Random Pointers
Understanding the Problem
A linked list of length n is given such that each node contains an additional random pointer, which
could point to any node in the list, or null.
Construct a deep copy of the list. The deep copy should consist of exactly n brand new nodes,
where each new node has its value set to the value of its corresponding original node. Both
the next and random pointer of the new nodes should point to new nodes in the copied list such that
the pointers in the original list and copied list represent the same list state. None of the pointers in
the new list should point to nodes in the original list.
For example, if there are two nodes X and Y in the original list, where [Link] --> Y, then for the
corresponding two nodes x and y in the copied list, [Link] --> y.
The linked list is represented in the input/output as a list of n nodes. Each node is represented as a
pair of [val, random_index] where:
Your code will only be given the head of the original linked list.
Example Representation
76 BBN
Problem Breakdown
This is LeetCode problem 138.
We need to implement a function that receives the head of the given linked list and
returns the head of its deep copy.
Approach to Solve the Problem
We solve this problem in two steps:
Step 1: Create a Simple Copy of the Linked List
Ignore the random pointers for now.
Simply copy each node and maintain the next connections as in the original list.
The copied linked list will follow the same sequence as the original, but without any
random pointer connections.
Step 2: Copy the Random Pointer Connections
Iterate through the original list again.
Assign the corresponding random pointers from the original list to the copied list.
Step-by-Step Execution
1. Creating a Simple Copy
o Initialize old_temp to point to the head of the original linked list.
o Create a new_head which is a copy of the first node.
o Maintain two pointers:
old_temp: Traverses the original list.
new_temp: Traverses the newly created list.
o For each node in the original list:
Create a copy node with the same value.
Set new_temp.next = copy_node.
Move old_temp and new_temp to their respective next nodes.
77 BBN
o Repeat until old_temp reaches NULL.
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head==nullptr)return nullptr;
Node* newHead=new Node(head->val);
unordered_map<Node*,Node*> m;
Node* oldtemp=head->next;
Node* newtemp=newHead;
m[head]=newHead;
while(oldtemp!=nullptr){
Node* newNode=new Node(oldtemp->val);
m[oldtemp]=newNode;
newtemp->next=newNode;
newtemp=newtemp->next;
oldtemp=oldtemp->next;
}
newtemp->next=nullptr;
newtemp=newHead;
oldtemp=head;
while(oldtemp!=nullptr){
newtemp->random=m[oldtemp->random];
oldtemp=oldtemp->next;
newtemp=newtemp->next;
}
return newHead;
}
};
Final Output
The copied list should have nodes with the same values and connections as the original.
The next and random pointers should correctly replicate those of the original list.
The function returns the head of the copied linked list.
78 BBN
Structure of a Doubly Linked List Node
Each node in a DLL contains:
1. Data (stores information)
2. Next pointer (points to the next node)
79 BBN
Contains:
o head (starting node)
o tail (last node)
Constructor initializes:
o head = NULL
o tail = NULL
class Doubly
{
Node *head;
Node *tail;
public:
Doubly()
{
head = tail = nullptr;
}
80 BBN
{
if (head == nullptr)
{
cout << "Empty list" << endl;
return;
}
Node *temp = head;
head = head->next;
if (head == nullptr)
{
tail = nullptr;
}
else
{
head->prev = nullptr;
}
delete temp;
}
void pop_back()
{
if (head == nullptr)
{
cout << "Empty list." << endl;
return;
}
Node *temp = tail;
tail = tail->prev;
if (tail == nullptr)
{
head = nullptr;
}
else
{
tail->next = nullptr;
}
delete temp;
}
81 BBN
else
{
cout << temp->data << " <---> ";
}
temp = temp->next;
}
cout << " ---> NULL" << endl;
}
82 BBN
o A next pointer pointing to the next node.
A Circular List class contains:
o A head pointer (optional).
o A tail pointer (important for easy insertion).
7. Creating a Circular Linked List in C++
First, define a Node class with:
o A constructor initializing data and next.
Define a CircularLinkedList class with:
o Head and tail pointers initialized to NULL.
#include <iostream>
using namespace std;
class Node
{
public:
int val;
Node *next;
Node(int val)
{
this->val = val;
next = nullptr;
}
};
class Circular
{
Node *head;
Node *tail;
public:
Circular()
{
head = tail = nullptr;
}
void push_front(int val)
{
Node *newNode = new Node(val);
if (head == nullptr)
{
head = tail = newNode;
}
else
{
newNode->next = head;
head = newNode;
}
tail->next = head;
}
83 BBN
{
Node *newNode = new Node(val);
if (head == nullptr)
{
head = tail = newNode;
}
else
{
tail->next = newNode;
tail = newNode;
}
tail->next = head;
}
void pop_front()
{
if (head == nullptr)
{
cout << "Empty list.";
return;
}
Node *temp = head;
if (head == tail)
{
head = tail = nullptr;
}
else
{
head = head->next;
tail->next = head;
}
delete temp;
}
void pop_back()
{
if (head == nullptr)
{
cout << "Empty list.";
return;
}
Node *temp = head;
if (head == tail)
{
delete temp;
head = tail = nullptr;
return;
}
84 BBN
temp = temp->next;
}
delete tail;
tail = temp;
tail->next = head;
}
void print()
{
if (head == nullptr)
return;
Node *temp = head;
do
{
cout << temp->val << " ---> ";
temp = temp->next;
} while (temp != head);
cout << head->val << endl;
}
};
int main()
{
Circular cl;
cl.push_front(2);
// cl.push_back(4);
// cl.push_back(6);
[Link]();
cl.pop_back();
// cl.pop_back();
[Link]();
return 0;
}
85 BBN
Call the recursive function on the child node, which will return the flattened child
o
list’s head.
o Store current->next in a temporary pointer (nextPointer) before modifying any
connections.
o Update current->next to point to the flattened child list.
4. Maintain Double-Linking:
o Since it's a doubly linked list, update the previous pointer of the first node in the
flattened child list to point back to current.
o Connect the last node of the flattened child list to nextPointer (if it exists).
5. Repeat Until Fully Flattened:
o Move current to the next node and repeat the process until the entire linked list is
flattened.
Recursive Thought Process
Recursion works on breaking the problem into smaller parts and solving each part independently.
If recursion can flatten a large linked list, it can also flatten a smaller child list.
We trust the recursive function to flatten the child list for us.
Once we receive the flattened child list’s head, we merge it into the main list.
Final Steps
1. Store current->next in nextPointer.
2. Call the recursive function on the child list.
3. Update current->next to point to the flattened child list.
4. Ensure the previous pointer is updated in the doubly linked list.
5. Finally, connect the last node of the flattened child list to nextPointer.
class Solution {
public:
Node* flatten(Node* head) {
if(head==nullptr)return nullptr;
Node* curr=head;
while(curr!=nullptr){
if(curr->child!=nullptr){
Node* next=curr->next;
curr->next=flatten(curr->child);
curr->next->prev=curr;
curr->child=nullptr;
while(curr->next!=nullptr){
curr=curr->next;
}
if(next!=nullptr){
curr->next=next;
next->prev=curr;
}
}
curr=curr->next;
}
return head;
}
86 BBN
};
87 BBN
Example: If we have 1 -> 2 -> 3 -> 4 -> 5 with K=3, after reversing the first K-group, it
becomes:
3 -> 2 -> 1 -> 4 -> 5
Then, we connect this to the reversed rest of the linked list.
Implementation Logic
1. Create a function reverseKGroup(head, K).
2. Check if at least K nodes exist.
3. Reverse the first K nodes.
4. Call reverseKGroup() on the remaining linked list.
5. Connect the reversed group to the reversed remainder.
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
if (head == nullptr) return nullptr;
Complexity Analysis
Time Complexity: O(N) as each node is visited once and reversed once.
Space Complexity: O(N/K) due to recursive calls (stack space).
88 BBN
Example Execution
Input: 1 -> 2 -> 3 -> 4 -> 5 -> 6, K=3
Output: 3 -> 2 -> 1 -> 6 -> 5 -> 4
Stacks
What is a Stack?
A stack is a data structure that follows the LIFO (Last In, First Out) rule. This means the last
item you add is the first one you remove. Think of a stack of plates—when you add a new plate,
it goes on top, and when you take one, you remove the top plate first.
Operations in a Stack
1. Push → Add an element to the top of the stack.
2. Pop → Remove the top element from the stack.
3. Peek (or Top) → View the top element without removing it.
4. isEmpty → Check if the stack is empty.
5. Size → Get the number of elements in the stack.
Example of a Stack
Imagine you have a stack of books:
You add books one by one (Push).
You remove the top book first (Pop).
You check which book is on top without
removing it (Peek).
Implementation of a Stack
Stacks can be implemented in:
Vectors (dynamic size, faster access)
#include <iostream>
#include <vector>
using namespace std;
class Stack {
private:
vector<int> v;
public:
void push(int val) { v.push_back(val); } // Add element
void pop() { if (![Link]()) v.pop_back(); } // Remove top
int top() { return [Link]() ? -1 : [Link](); } // Return top element
bool empty() { return [Link](); } // Check if empty
};
int main() {
Stack s;
[Link](10);
[Link](20);
[Link](30);
while (![Link]()) {
cout << [Link]() << " "; // Print top
[Link](); // Remove top
89 BBN
}
return 0;
}
📝 Output
30 20 10
Linked Lists (dynamic size, uses more memory)
#include <iostream>
using namespace std;
class Node {
public:
int data;
Node* next;
Node(int val) { data = val; next = NULL; }
};
class Stack {
private:
Node* head;
public:
Stack() { head = NULL; }
void pop() {
if (head != NULL) {
Node* temp = head;
head = head->next;
delete temp;
}
}
int top() {
return head == NULL ? -1 : head->data;
}
bool empty() {
return head == NULL;
}
};
int main() {
Stack s;
[Link](10);
[Link](20);
90 BBN
[Link](30);
while (![Link]()) {
cout << [Link]() << " ";
[Link]();
}
return 0;
}
📝 Output
30 20 10
Uses dynamic array (more memory needed Uses nodes (extra memory for
Memory Usage
for resizing) pointers)
Push
O(1) (most cases), O(N) if resizing is needed O(1) always
Complexity
91 BBN
What is an Invalid Parentheses String?
A string is invalid when:
A closing bracket appears without a matching opening bracket.
The order of brackets is incorrect.
Extra opening brackets exist without a closing pair.
Examples:
❌ "{[}]" → The { does not close correctly before ].
❌ "({[})]" → The { is closed before the [ is closed, which is incorrect.
❌ "(((" → There are three opening brackets but no closing brackets.
92 BBN
When Does a String Become Invalid?
Let's check "({[}])" step by step.
Characte Stack
Stack Action
r Content
( Push to stack (
{ Push to stack ({
[ Push to stack ({[
} ❌ Mismatch! Expected ], found { Invalid
❌ Stack mismatch occurs → The string is invalid.
bool isValid(string s) {
stack<char> st;
for (char c : s) {
if (c == '(' || c == '{' || c == '[') {
[Link](c); // Push opening brackets
} else {
if ([Link]()) return false; // No matching opening bracket
int main() {
string test = "({[]})";
cout << (isValid(test) ? "Valid" : "Invalid") << endl;
return 0;
}
93 BBN
Stock Span Problem?
The Stock Span Problem is a financial problem where we calculate how many consecutive
days before today had a price lower than or equal to today’s price.
For example, if today’s stock price is 70, we count how many previous days had prices
lower than or equal to 70.
✅ Example:
Stock Prices: [100, 80, 60, 70, 60, 75, 85]
Stock Span: [1, 1, 1, 2, 1, 4, 6]
Day 0 (100) → No previous lower prices → Span = 1
Day 1 (80) → No previous lower prices → Span = 1
Day 2 (60) → No previous lower prices → Span = 1
Day 3 (70) → 60 is lower → Span = 2 (includes itself)
Day 4 (60) → No previous lower prices → Span = 1
Day 5 (75) → 60, 70, and 60 are lower → Span = 4
Day 6 (85) → 75, 60, 70, 60 are lower → Span = 6
1 80 [0, 1] i - [Link]() = 1 1
94 BBN
Day Price Stack (Indexes) Span Calculation Final Span
2 60 [0, 1, 2] i - [Link]() = 1 1
4 60 [0, 1, 4] i - [Link]() = 1 1
if ([Link]()) {
span[i] = i + 1; // No previous greater element
} else {
span[i] = i - [Link](); // Distance from nearest greater element
}
int main() {
vector<int> nums = {100, 80, 60, 70, 60, 75, 85};
vector<int> span = StockSpan(nums);
95 BBN
Stack Approach: Each element is pushed & popped at most once → O(N)
✅ Stack ensures efficient lookup of previous greater elements.
You are given two distinct 0-indexed integer arrays nums1 and nums2, where nums1 is a subset
of nums2.
For each 0 <= i < [Link], find the index j such that nums1[i] == nums2[j] and determine the next
greater element of nums2[j] in nums2. If there is no next greater element, then the answer for this
query is -1.
Return an array ans of length [Link] such that ans[i] is the next greater element as described
above.
Example 1:
Output: [-1,3,-1]
Explanation: The next greater element for each value of nums1 is as follows:
- 4 is underlined in nums2 = [1,3,4,2]. There is no next greater element, so the answer is -1.
- 2 is underlined in nums2 = [1,3,4,2]. There is no next greater element, so the answer is -1.
96 BBN
We store elements in a stack because we need to maintain the order of the next greater
elements in reverse order.
The stack helps us track the immediate greater element efficiently.
Since a stack follows LIFO (Last In, First Out), the most recent element will always be
on top, making it easy to access.
Implementation Strategy
1. Start from the rightmost element and move toward the left.
2. Use a stack to store potential Next Greater Elements.
3. For each element:
o Pop elements from the stack until we find a greater element.
o If no greater element exists, store -1.
o Push the current element onto the stack so it can be used for future elements.
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
//find next Greater Element for nums2
unordered_map<int,int> m;
stack<int> s;
for(int i=[Link]()-1;i>=0;i--){
while(![Link]() && [Link]()<=nums2[i]){
[Link]();
}
if([Link]()){
m[nums2[i]]=-1;
}
else{
m[nums2[i]]=[Link]();
}
[Link](nums2[i]);
}
// setup them into nums1 order wise
for(int i=0;i<[Link]();i++){
nums1[i]=m[nums1[i]];
}
return nums1;
}
};
Example Walkthrough
Let’s consider an array:
[6, 8, 0, 1, 3]
1. Start from the last element (3)
o Since it’s the last, its Next Greater Element = -1
o Push 3 onto the stack.
2. Move to 1
o The top of the stack is 3, which is greater than 1.
o So, Next Greater Element for 1 = 3.
o Push 1 onto the stack.
97 BBN
3. Move to 0
o The top of the stack is 1, which is greater than 0.
o So, Next Greater Element for 0 = 1.
o Push 0 onto the stack.
4. Move to 8
o The stack contains [3, 1, 0], but all these values are smaller than 8.
o Pop all smaller values.
o Since the stack is empty now, Next Greater Element for 8 = -1.
o Push 8 onto the stack.
5. Move to 6
o The stack’s top is 8, which is greater than 6.
o So, Next Greater Element for 6 = 8.
o Push 6 onto the stack.
Final Output
For [6, 8, 0, 1, 3], the Next Greater Elements are:
[8, -1, 1, 3, -1]
Understanding Rectangles in a
Histogram
Each rectangle in the histogram is formed by using one
or more bars. The area of any rectangle is calculated
using the formula:
Area=Height×Width
Our goal is to maximize this area.
Approach to Solve the Problem
There are two main approaches:
1. Brute Force Approach (Basic method)
2. Optimal Approach (Efficient method using stacks)
98 BBN
2. Optimal Approach using Stack (Fast)
Instead of checking all possible rectangles, we use a stack to keep track of bars efficiently.
Steps for the Optimal Approach:
1. Use a stack to store the indices of histogram bars.
2. Process each bar:
o If the stack is empty or the current bar is taller than the top of the stack, push it.
o If a smaller bar is found, pop elements from the stack and compute areas using the
popped height.
3. Calculate width using the nearest smaller bars on the left and right.
4. Store the maximum area.
Time complexity: O(N) (Efficient).
finding the Nearest Smaller Element (Left Smaller)
The Nearest Smaller Element means finding the closest smaller value to the left of each
element in an array. This is also known as Previous Smaller Element.
Approach
1. Use a Forward Loop
o Since we need to check left-side elements, we traverse from left to right (i = 0 to
n).
2. Using a Stack
o We use a stack to efficiently find the left smaller element.
o For each element, check whether a smaller element exists in the stack.
Step-by-Step Process
1. Start iterating from left to right.
2. If the stack is empty, there is no smaller element → Answer is -1.
3. If the top of the stack is smaller than the current element, that is our answer.
4. If the top is greater, pop elements until we find a smaller value or the stack becomes
empty.
5. Push the current element’s index into the stack.
6. Repeat the process for all elements.
Example
Array: [2, 1, 5, 6, 2, 3]
Elemen Stack Left Stack
t (Before) Smaller (After)
2 Empty -1 [2]
1 [2] -1 (pop 2) [1]
5 [1] 1 [1, 5]
6 [1, 5] 5 [1, 5, 6]
2 [1, 5, 6] 1 (pop 6, 5) [1, 2]
3 [1, 2] 2 [1, 2, 3]
Implementation (C++)
class Solution {
99 BBN
public:
int largestRectangleArea(vector<int>& h) {
int n=[Link]();
stack<int> s;
vector<int> RSmall(n,n);
vector<int> LSmall(n,-1);
for(int i=n-1;i>=0;i--){
while(![Link]() && h[i]<=h[[Link]()]){
[Link]();
}
if(![Link]()){
RSmall[i]=[Link]();
}
[Link](i);
}
while(![Link]()){
[Link]();
}
for(int i=0;i<n;i++){
while(![Link]() && h[i]<=h[[Link]()]){
[Link]();
}
if(![Link]()){
LSmall[i]=[Link]();
}
[Link](i);
}
int maxArea=0;
for(int i=0;i<n;i++){
int width=RSmall[i]-LSmall[i]-1;
maxArea=max(maxArea,h[i]*width);
}
return maxArea;
}
};
Time Complexity
Each element is pushed once and popped once from the stack.
O(N) time complexity (efficient solution).
100 BBN
Find how much rainwater is trapped between the bars.
🔸 What we use:
left pointer → starts from 0
right pointer → starts from n - 1
leftMax → maximum height from the left
rightMax → maximum height from the right
ans → to store total water
🔸 Logic:
1. Run a loop while left < right.
2. At each step, update leftMax and rightMax.
3. If leftMax < rightMax:
→ Water trapped = leftMax - height[left]
→ Add to ans
→ Move left++
4. Else:
→ Water trapped = rightMax - height[right]
→ Add to ans
→ Move right—
class Solution {
public:
int trap(vector<int>& h) {
int l=0,r=[Link]()-1;
int lmax=h[0],rmax=h[r];
int totalWater=0;
while(l<r){
lmax=max(lmax,h[l]);
rmax=max(rmax,h[r]);
if(lmax<rmax){
totalWater+=lmax-h[l];
l++;
}
else{
totalWater+=rmax-h[r];
101 BBN
r--;
}
}
return totalWater;
}
};
🔸 Example:
For bars [4, 0, ..., 10]
Left boundary = 4
Right boundary = 10
Water trapped over 0 = 4 - 0 = 4
Even if there's a big bar like 12 on right → doesn’t matter
because left boundary (4) is still the limiting factor.
102 BBN
Person 2
And you get this matrix:
arr = [
[0, 1, 0], # Person 0 knows person 1
[0, 0, 0], # Person 1 knows no one
[0, 1, 0] # Person 2 knows person 1
]
Now let's check:
Person 0 knows person 1
Person 1 knows no one ✅
Person 2 knows person 1
So, everyone knows Person 1 and Person 1 knows no one → 🎉 Person 1 is the celebrity!
🔸 More Examples:
✅ Example 1:
arr = [
[0, 1, 1],
[0, 0, 1],
[0, 0, 0]
]
Check person 2:
Person 0 knows 2 ✅
Person 1 knows 2 ✅
Person 2 knows no one ✅
So, person 2 is celebrity → ✅ Answer: 2
❌ Example 2:
arr = [
[0, 1, 0],
[0, 0, 1],
[1, 0, 0]
]
Check all:
No one is known by all
And no one fulfills the condition
So → ❌ No celebrity → Answer: -1
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
103 BBN
stack<int> s;
if (knows(M, a, b)) {
// a cannot be celebrity
[Link](b);
} else {
// b cannot be celebrity
[Link](a);
}
}
return candidate;
}
int main() {
vector<vector<int>> arr = {
{0, 1, 0},
{0, 0, 0},
{0, 1, 0}
};
return 0;
}
class Solution {
104 BBN
public:
int trap(vector<int>& height) {
}
};
Functions to Implement
You will implement a class called LRUCache with:
1. Constructor(capacity):
o Takes the max size of the cache.
2. get(key):
o Returns the value of the given key.
o If key is not found, return -1.
o Also updates usage priority.
3. put(key, value):
o Inserts or updates the key with the value.
o If cache is full, it removes the least recently used item before inserting.
106 BBN
🔹 Steps for put() Function:
1. If the key already exists:
o Delete the existing node.
o Remove from the map.
2. If cache is full (size == capacity):
o Remove LRU (tail's previous node).
o Delete from map.
3. Create a new node and insert it using addNode().
4. Add it to the map.
🔹 deleteNode(node) Function
Reconnect:
o [Link] → [Link]
o [Link] → [Link]
🔹 Time Complexity:
addNode() → Constant time (O(1))
deleteNode() → Constant time (O(1))
put() → Constant time (O(1))
🔹 get(key) Function:
If key exists:
o Move that node to the most recently used (MRU) position using:
deleteNode()
addNode()
o Return value.
If not, return -1.
✅ What is Queue?
Queue is a First-In-First-Out (FIFO) data structure.
This means the element which goes first, comes out first.
Queue has two ends:
o Front: from where elements are removed.
o Rear: where new elements are added.
107 BBN
o Adding element to the rear of queue.
o Example: If we push 1, then 2, then 3 → They are added at the rear (last).
2. Pop (Dequeue)
o Removing element from the front of queue.
o Example: If we pop now → 1 is removed first, then 2, then 3.
3. Front
o Checking the front element (the element to be removed next).
o In queue, front element is most important like:
Top in Stack.
Head in Linked List.
✅ FIFO vs LIFO
Stack = LIFO (Last In, First Out)
Queue = FIFO (First In, First Out)
Example:
We push: 1, 2, 3 → Pop will remove: 1, then 2, then 3.
✅ Queue Implementation
We can implement queue in many ways, here we use Linked List.
Important Functions to Implement:
1. Front() – Return front element (linked list head's data).
2. Push() – Insert at rear (linked list's tail).
3. Pop() – Delete from front (delete linked list's head).
All work in O(1) time.
108 BBN
o Each node has data and next pointer.
2. Create Queue class
o It has two pointers: head and tail.
3. Constructor
o Initialize head and tail as NULL.
4. Push function
o Add new node at tail.
5. Pop function
o Remove node from head.
6. Front function
o Return data of head node.
#include <iostream>
using namespace std;
class Node
{
public:
int data;
Node *next;
Node(int d)
{
data = d;
next = nullptr;
}
};
class Queue
{
Node *head;
Node *tail;
public:
Queue()
{
head = tail = nullptr;
}
void push(int val)
{
Node *newNode = new Node(val);
if (empty())
{
head = tail = newNode;
return;
}
else
tail->next = newNode;
tail = newNode;
}
void pop()
{
109 BBN
if (empty())
return;
if (head->next == nullptr)
{
head = tail = nullptr;
return;
}
Node *temp = head;
if (head->next == nullptr)
{
head = tail = nullptr;
delete temp;
return;
}
else
{
head = head->next;
delete temp;
return;
}
}
int front()
{
if (empty())
return -1;
return head->data;
}
bool empty()
{
return head == nullptr;
}
};
int main()
{
Queue q;
[Link](4);
[Link](8);
[Link](0);
[Link](-1);
while (![Link]())
{
cout << [Link]() << " ";
[Link]();
}
[Link]();
return 0;
}
110 BBN
Difference between Queue and Deque:
Feature Queue Deque (Double Ended Queue)
Meaning FIFO – First In First Out Insert and delete from both ends
Deletion Only from front end Both front and rear ends
Example Line of people at a counter Browser history (back and forward stack)
#include <iostream>
#include <queue>
#include <deque>
using namespace std;
int main()
{
queue<int> q;
[Link](1);
[Link](2);
[Link](3);
[Link](4);
while (![Link]())
{
cout << [Link]() << " ";
[Link]();
cout << endl;
}
deque<int> dq;
dq.push_back(1);
dq.push_back(2);
dq.push_front(3);
cout << [Link]() << " " << [Link]() << endl;
return 0;
}
111 BBN
In a normal queue, we insert (push) elements from the rear and remove (pop) them from the front.
🌀 Circular Queue
🧠 Why Circular?
In a normal queue, once the array is full, we can't use the empty space at the start.
In Circular Queue, we reuse that empty space by wrapping around.
112 BBN
📌 Push Operation (Insert)
💡 Example
Push 1 → rear = 0
Push 2 → rear = 1
Push 3 → rear = 2
Now pop:
#include <iostream>
using namespace std;
class CQueue
{
public:
int cap, currSize = 0;
int *arr;
CQueue(int val)
113 BBN
{
cap = val;
arr = new int[cap];
}
int f = 0;
int r = -1;
int front()
{
if (isEmpty())
{
cout << "Queue is Empty" << endl;
return -1;
}
return arr[f];
}
~CQueue()
{
delete[] arr;
}
114 BBN
};
int main()
{
CQueue qq(3);
[Link](1);
[Link](3);
[Link](5);
[Link]();
[Link](2);
while (![Link]())
{
cout << [Link]() << " ";
[Link]();
}
}
💡 Problem Statement
We need to implement a Stack (LIFO - Last In First Out) using two Queues (FIFO - First In First Out).
115 BBN
1. Move all elements from q1 to q2.
2. Push the new element into q1.
3. Move all elements back from q2 to q1.
📌 After this, q1 will have the latest pushed element at the front.
Simply pop the front element of q1, which represents the top of the stack.
Just return the front of q1, as it holds the top of the stack.
💻 C++ Code
class MyStack {
public:
queue<int> q1, q2;
MyStack() {
// Constructor: no special initialization needed
}
void push(int x) {
// Step 1: Move all elements from q1 to q2
while (![Link]()) {
[Link]([Link]());
[Link]();
}
int pop() {
116 BBN
int val = [Link]();
[Link]();
return val;
}
int top() {
return [Link]();
}
bool empty() {
return [Link]();
}
};
🔍 Time Complexity
🔶 Problem Statement
Given a string of characters (like a stream), you have to find the first non-repeating (unique) character at
every point in time.
🔶 Example
1. a → First non-repeating = a
2. aa → All are repeating → -1
3. aab → b is first non-repeating
4. aabc → b is still first non-repeating
Output: a -1 b b
✅ 1. Frequency Map
cpp
117 BBN
CopyEdit
unordered_map<char, int> freq;
✅ 2. Queue
To keep characters in the order they arrive. It will help us know which character came first.
cpp
CopyEdit
queue<char> q;
🔶 Logic
Step-by-step:
1. 'a' → freq[a]=1 → push 'a' → front is 'a' → output 'a'
2. 'a' → freq[a]=2 → queue has 'a' → freq >1 → pop 'a' → queue empty → output '-1'
3. 'b' → freq[b]=1 → push 'b' → front is 'b' → output 'b'
4. 'c' → freq[c]=1 → queue is [b,c] → front 'b' is non-repeating → output 'b'
🔶 Final Output:
a -1 b b
🔶 C++ Code
cpp
CopyEdit
#include <iostream>
#include <unordered_map>
#include <queue>
using namespace std;
string firstNonRepeating(string s) {
118 BBN
unordered_map<char, int> freq;
queue<char> q;
string result = "";
for (char ch : s) {
freq[ch]++;
[Link](ch);
if (![Link]()) {
result += [Link]();
} else {
result += '#'; // Or use "-1" if you want
}
}
return result;
}
Time: O(N)
Space: O(1) → Since only 26 lowercase letters exist
🔶 What is Given?
An array of numbers
A value k (the size of the sliding window)
We need to find the maximum number in every window of size k as we slide the window from left to right.
📌 Example:
119 BBN
🧾 Step-by-Step Solutions:
✅ Simple Idea:
cpp
CopyEdit
for i = 0 to n-k:
max = find maximum in window [i to i+k-1]
store max in answer
1. Remove elements from front of deque if they are out of the current window
2. Remove smaller elements from the back of deque because they can never be max if a bigger value
comes after them
3. Add the current element at the back
1. At the end of the loop, we push the last element (7) inside the deque dq.
2. After that, the loop is finished, meaning we start analyzing the next window.
3. The maximum element of the last window will always be at the front of the deque, which in our
example is 7.
4. So, after the full loop completes, we add the front of dq to our result list.
We need to remove elements from dq that do not belong to the current window.
For this, instead of storing the actual elements, we will now store their indices in the deque.
120 BBN
If we know an index, we can track the position of that element and decide if it's inside the current
window or not.
✅ Example:
if ([Link]() <= i - k)
dq.pop_front();
i = current index
k = size of window
If the index at the front of the deque is less than or equal to (i - k), it means that element has gone out
of the window, so we remove it.
🎯 For example:
If i = 4 and k = 3, then:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
vector<int> res;
//step 1
for(int i=0;i<k;i++){
121 BBN
while([Link]()>0 && nums[[Link]()]<=nums[i]){
dq.pop_back();
}
dq.push_back(i);
}
//step 2
for(int i=k;i<[Link]();i++){
res.push_back(nums[[Link]()]);
while([Link]()>0 && [Link]()<=i-k){
dq.pop_front();
}
while([Link]()>0 && nums[[Link]()]<=nums[i]){
dq.pop_back();
}
dq.push_back(i);
}
res.push_back(nums[[Link]()]);
return res;
}
};
🎯 Goal
We have to find the starting station index from where we can complete the full circular tour.
If it's not possible to complete the tour from any station, we return -1.
Let’s say:
gas = [1, 2, 4]
cost = [3, 4, 1]
122 BBN
Fuel = 2, cost = 4 → Not enough
Try from index 2:
Fuel = 4, cost = 1 → ✅ Possible
o Remaining = 3 → Add gas at next station... continue...
Let’s say:
gas = [2, 3, 4]
cost = [3, 4, 3]
▶️Since total gas < total cost, it’s impossible to complete the tour.
🔁 So, return -1.
Once Total Gas ≥ Total Cost, a valid solution will always exist.
But we need to find the unique starting index where the tour is possible.
text
CopyEdit
1. Start from index 0
2. Keep a variable `fuel = 0`
3. Traverse all stations:
a. Add `gas[i] - cost[i]` to `fuel`
b. If `fuel` < 0 → we cannot start from previous start, so update `start = i + 1` and
reset `fuel = 0`
4. At the end, return the final `start` index
123 BBN
If at any point your fuel goes below 0, it means the tour can’t start from that or any previous station.
So, we shift the starting point forward.
Let:
gas = [5, 1, 2, 3, 4]
cost = [4, 4, 1, 5, 1]
Total gas = 15, total cost = 15 ✅ So solution exists.
Use the above algorithm and find the correct starting index (It will be 4).
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int totgas=0,totcost=0;
int start=0,currCost=0;
for(int i=0;i<[Link]();i++){
totgas+=gas[i];
totcost+=cost[i];
currCost+=gas[i]-cost[i];
if(currCost<0){
start=i+1;
currCost=0;
}
}
return totgas < totcost ? -1 : start;
}
};
Until now, we studied Linear Data Structures like Arrays, Vectors, and Linked Lists, which store data in a
straight line.
Real-life Examples:
1. Folders in a computer: There are main folders, subfolders, and files inside them.
2. Family Tree: Grandparents → Parents → You.
We use Tree Data Structures to store this kind of data. In computer science, when we have to store data in
multiple levels, we use Trees.
124 BBN
A Tree is made of Nodes.
Each node stores data, just like in a Linked List.
The top node in a tree is called the Root Node.
From the root, there are branches that connect to other nodes. This continues down in levels.
In DSA, we draw trees starting from the root at the top, not like real trees where the root is at the bottom.
Important Terms
Example:
If node 1 connects to nodes 2, 3, and 4, then:
1 is the parent
2, 3, and 4 are its children.
A Binary Tree is a special kind of tree where each node can have at most 2 children.
These children are usually called:
Left Child
Right Child
Example:
125 BBN
Preorder = Root -> Left Subtree -> Right Subtree
So if we go in order:
This function:
Let’s say:
preorder = [1, 2, -1, -1, 3, -1, -1]
➡️Final Tree:
126 BBN
1
/ \
2 3
🔁 Summary of Rules:
✅ Final Output:
CopyEdit
1 2 3 4 5
Time Complexity
We visit each node once, so time complexity is O(n), where n = number of nodes.
🌳 Level Order Traversal (with new line for each level) – Easy English Notes
128 BBN
If the popped node is NULL:
o It means one level is completed.
o Print a new line.
o Check if the queue still has nodes left:
If yes, push another NULL to mark the next level.
💡 Key Points:
Every node is inserted and removed only once from the queue.
So the Time Complexity is O(n), where n = number of nodes.
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
class Node
{
public:
int data;
Node *left;
Node *right;
Node(int val)
{
data = val;
left = right = nullptr;
}
};
static int idx = -1;
Node *buildTree(vector<int> Order)
{
idx++;
if (Order[idx] == -1)
return nullptr;
Node *root = new Node(Order[idx]);
root->left = buildTree(Order);
root->right = buildTree(Order);
return root;
}
void preOrder(Node *root)
{
129 BBN
if (root == nullptr)
return;
cout << root->data << " ";
preOrder(root->left);
preOrder(root->right);
}
void inOrder(Node *root)
{
if (root == nullptr)
return;
inOrder(root->left);
cout << root->data << " ";
inOrder(root->right);
}
postOrder(root->left);
postOrder(root->right);
cout << root->data << " ";
}
130 BBN
cout << temp->data << " ";
if (temp->left != nullptr)
{
[Link](temp->left);
}
if (temp->right != nullptr)
{
[Link](temp->right);
}
}
}
int main()
{
vector<int> Order = {1, 2, -1, -1, 3, 4, -1, -1, 5, -1, -1};
Node *root = buildTree(Order);
levelOrder(root);
return 0;
}
🧠 What is given?
You are given a Binary Tree.
You also have access to its root node.
You need to complete a function named height() that returns the height of the binary tree.
💡 Example:
If a path from root to a leaf node has 3 nodes → then height = 3.
👉 In recursion:
131 BBN
Calculate the height of the left subtree
Calculate the height of the right subtree
Take the maximum of both, and add 1 (for the current root node)
📌 Formula:
Suppose root is 1
Left subtree height = 1
Right subtree height = 2
So, overall height = 1 + max(1, 2) = 3
if root is None:
return 0
💡 Goal:
🧠 Logic:
👉 Formula:
Total Count = Left Count + Right Count + 1
132 BBN
🔁 How it works:
Suppose:
🔧 Steps (Function):
cpp
CopyEdit
int count(TreeNode* root) {
if (root == NULL) return 0; // base case
int left = count(root->left); // count left subtree
int right = count(root->right); // count right subtree
return left + right + 1; // add root node
}
🧱 Base Case:
markdown
CopyEdit
1
/ \
2 3
/ \
4 5
Step-by-step:
133 BBN
o Total = 1 + 3 + 1 = 5
🧮 Time Complexity:
In a CBT:
o All levels are completely filled except the last level.
o The last level is filled from left to right.
We imagine missing nodes in a CBT while calculating the width. This helps us count the "gap" caused by
missing nodes too.
134 BBN
📘 Example Breakdown
markdown
CopyEdit
1
/ \
3 2
/ \
5 9
👉 Imagine it as CBT:
arduino
CopyEdit
Level 0: 1 → width = 1
Level 1: 3 2 → width = 2
Level 2: 5 _ _ 9 → width = 4 (includes missing nodes)
📘 Another Example
Assume a tree with 4 levels, and after imagining missing nodes using CBT:
Level 1 → width = 1
Level 2 → width = 2
Level 3 → width = 4
Level 4 → width = 7 (after including imaginary nodes)
class Solution {
public:
int widthOfBinaryTree(TreeNode* root) {
queue<pair<TreeNode*,unsigned long long >> q;
int maxWidth=0;
135 BBN
[Link]({root,0});
while([Link]()>0){
int levsize=[Link]();
unsigned long long stridx=[Link]().second;
unsigned long long endidx=[Link]().second;
maxWidth=max(maxWidth,(int)(endidx-stridx+1));
for(int i=0;i<levsize;i++){
auto node=[Link]();
[Link]();
if([Link]->left){
[Link]({[Link]->left,2*[Link]+1});
}
if([Link]->right){
[Link]({[Link]->right,2*[Link]+2});
}
}
return maxWidth;
}
};
In in-order traversal, we visit nodes in this order: left subtree → root → right subtree.
For example, for a tree with the nodes 2, 1, 4, 3, 5, the in-order traversal sequence will be 2, 1, 4, 3, 5.
We know that recursion is an easy way to do in-order traversal. However, today we will learn a non-recursive
approach that doesn’t use a stack. Instead, we will create temporary threads in the tree to help with the
traversal.
Before diving into the Morris In-order Traversal, we must understand the in-order predecessor of a node. This
is the node that comes before the current node in the in-order sequence.
For example:
The in-order predecessor of node 3 is node 4 (because 4 comes before 3 in the in-order sequence).
The in-order predecessor of node 1 is node 2 (because 2 comes before 1).
136 BBN
In a typical recursive approach, we can easily backtrack to the root after visiting the left or right subtrees.
However, in the iterative method, once we go to the left subtree, we don't have a direct way to come back to the
root.
Morris In-order Traversal solves this problem by creating temporary connections between nodes:
When we move to the left subtree, we create a temporary link from the in-order predecessor to the
current node.
After finishing the left subtree, we use this connection to return to the root.
In Morris In-order Traversal, the in-order predecessor is the rightmost node in the left subtree of a node.
For example:
For node 1, the in-order predecessor is node 4 (the rightmost node in the left subtree of node 1).
Example Walkthrough
markdown
CopyEdit
1
/ \
2 3
/ \
4 5
Start at node 1.
137 BBN
Move to node 2 (left child). Since node 2 has a left child (node 4), we find the in-order predecessor of
node 2, which is node 4.
Create a temporary thread from node 4 to node 2, then move to node 4.
Since node 4 has no left child, print 4 and move back to node 2 using the temporary thread.
Print node 2 and move to its right child, node 5.
Repeat the same process until we finish traversing the entire tree.
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
TreeNode* curr=root;
while(curr!=nullptr){
if(curr->left==nullptr){
ans.push_back(curr->val);
curr=curr->right;
}
else{
//Find Inorder Predecessor (IP)
TreeNode* IP =curr->left;
while(IP->right!=nullptr && IP->right!=curr){
IP=IP->right;
}
//Create IP
if(IP->right==NULL){
IP->right=curr;
curr=curr->left;
}
//Delete IP
else{
IP->right=nullptr;
ans.push_back(curr->val);
curr=curr->right;
}
}
}
return ans;
}
};
Time Complexity
Morris In-order Traversal runs in O(n) time, where n is the number of nodes in the tree.
This time complexity is the same as the recursive approach, but Morris traversal doesn’t require a stack
or recursion.
138 BBN
139 BBN