The Problem Solver Guide To Coding
The Problem Solver Guide To Coding
February, 2024
The Problem Solver’s Guide To Coding
First edition. February, 2024.
ISBN 9788797517413 (PDF)
Copyright © 2024 Nhut Nguyen.
All rights reserved.
www.nhutnguyen.com
To my dearest mother, Nguyen Thi Kim Sa.
iii
iv
PREFACE
v
Overview of the book
The Problem Solver’s Guide to Coding presents challenges covering fundamental data
structures, algorithms, and mathematical problems. Challenges are grouped in top-
ics, starting with the simplest data structure - Array. Most are arranged in order of
increasing difficulty, but you can pick any chapter or any challenge to start since I
write each independently to the other.
Challenges in this book are curated from LeetCode.com, focusing on those that are
not difficult but provide valuable learning experiences. You might encounter some
simple challenges I go directly to the code without saying much about the idea
(intuition) since their solution is straightforward.
I also keep the problems’ original constraints (inputs’ size, limits, etc.) as the code
in this book is the ones I submitted on Leetcode.com. It explains why I usually focus
on the core algorithm and do not consider/handle corner cases or invalid inputs.
The problems in each chapter comes with a detailed solution, explaining the logic
behind the solution and how to implement it in C++, my strongest programming
language.
At the end of some problems, I also provide similar problems on leetcode.com for
you to solve on your own, as practicing is essential for reinforcing understanding
and mastery of the concepts presented in the book. By engaging in problem-solving
exercises, you can apply what you have learned, develop your problem-solving skills,
and gain confidence in your ability to tackle real-world challenges.
In this book, I focus on readable code rather than optimal one, as most of you are
at the beginner level. Some of my solutions might need to be in better runtime or
memory. But I keep my code in my style or coding convention, where readability is
vital.
Moreover, my weekly sharing of articles with various developer communities has re-
fined the content and established a connection with a diverse group of programming
enthusiasts.
vi
Who is this book for?
This book is tailored to benefit a wide audience, from students beginning their
programming journey to experienced developers looking to enhance their skills.
Regardless of your experience level, whether you’re preparing for coding inter-
views or simply seeking to improve your problem-solving abilities, this book is
designed to meet your needs.
As a minimum requirement, you are supposed to have some basic background in
C++ programming language, data structures and algorithms like a second-year
undergraduate in Computer Science.
What sets this book apart is its focus on practicality. The challenges presented here
are not just exercises; they mirror real coding interviews from top companies like
FAANG.
As you work through the coding challenges in this book, you’ll learn new skills, im-
prove your problem-solving abilities, and develop your confidence as a programmer.
Acknowledgement
vii
Students and developers! By immersing yourself in the challenges and insights
shared in this book, you will not only prepare for coding interviews but also cultivate
a mindset beyond the scope of a job interview. You will become a problem solver, a
strategic thinker, and a proficient C++ programmer.
As you embark on this journey, remember that every challenge you encounter is an
opportunity for growth. Embrace the complexities, learn from each solution, and
let the knowledge you gain propel you to new heights in your programming career.
Thank you for joining me on this expedition.
May your code be elegant, your algorithms efficient, and your programming
journey genuinely transformative.
Happy coding!
Copenhagen, February 2024.
Nhut Nguyen, Ph.D.
viii
CONTENTS
1 Introduction 1
1.1 Why LeetCode? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 A brief about algorithm complexity . . . . . . . . . . . . . . . . . . . 2
1.3 Why readable code? . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Array 7
2.1 Transpose Matrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2 Valid Mountain Array . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 Shift 2D Grid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4 Find All Numbers Disappeared in an Array . . . . . . . . . . . . . . . 19
2.5 Rotate Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.6 Spiral Matrix II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.7 Daily Temperatures . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3 Linked List 39
3.1 Merge Two Sorted Lists . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.2 Remove Linked List Elements . . . . . . . . . . . . . . . . . . . . . . 44
3.3 Intersection of Two Linked Lists . . . . . . . . . . . . . . . . . . . . . 51
3.4 Swap Nodes in Pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
3.5 Add Two Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4 Hash Table 71
4.1 Roman to Integer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.2 Maximum Erasure Value . . . . . . . . . . . . . . . . . . . . . . . . . 75
4.3 Find and Replace Pattern . . . . . . . . . . . . . . . . . . . . . . . . . 79
ix
5 String 83
5.1 Valid Anagram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
5.2 Detect Capital . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.3 Unique Morse Code Words . . . . . . . . . . . . . . . . . . . . . . . . 92
5.4 Unique Email Addresses . . . . . . . . . . . . . . . . . . . . . . . . . 95
5.5 Longest Substring Without Repeating Characters . . . . . . . . . . . 101
5.6 Compare Version Numbers . . . . . . . . . . . . . . . . . . . . . . . . 105
6 Stack 111
6.1 Baseball Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.2 Valid Parentheses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
6.3 Backspace String Compare . . . . . . . . . . . . . . . . . . . . . . . . 119
6.4 Remove All Adjacent Duplicates in String II . . . . . . . . . . . . . . 123
9 Sorting 171
9.1 Majority Element . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
9.2 Merge Sorted Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
9.3 Remove Covered Intervals . . . . . . . . . . . . . . . . . . . . . . . . 182
9.4 My Calendar I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
9.5 Remove Duplicates from Sorted Array II . . . . . . . . . . . . . . . . 192
x
11 Dynamic Programming 219
11.1 Fibonacci Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
11.2 Unique Paths . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
11.3 Largest Divisible Subset . . . . . . . . . . . . . . . . . . . . . . . . . 232
11.4 Triangle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
11.5 Unique Paths II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
12 Counting 249
12.1 Single Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
12.2 First Unique Character in a String . . . . . . . . . . . . . . . . . . . . 254
12.3 Max Number of K-Sum Pairs . . . . . . . . . . . . . . . . . . . . . . . 258
15 Mathematics 321
15.1 Excel Sheet Column Number . . . . . . . . . . . . . . . . . . . . . . 322
15.2 Power of Three . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
15.3 Best Time to Buy and Sell Stock . . . . . . . . . . . . . . . . . . . . . 329
15.4 Subsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335
15.5 Minimum Moves to Equal Array Elements II . . . . . . . . . . . . . . 338
15.6 Array Nesting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
15.7 Count Sorted Vowel Strings . . . . . . . . . . . . . . . . . . . . . . . 347
15.8 Concatenation of Consecutive Binary Numbers . . . . . . . . . . . . . 353
15.9 Perfect Squares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
16 Conclusion 365
xi
A Coding challenge best practices 367
A.1 Read the problem carefully . . . . . . . . . . . . . . . . . . . . . . . 367
A.2 Plan and pseudocode . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
A.3 Test your code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
A.4 Optimize for time and space complexity . . . . . . . . . . . . . . . . 368
A.5 Write clean, readable code . . . . . . . . . . . . . . . . . . . . . . . . 368
A.6 Submit your code and learn from feedback . . . . . . . . . . . . . . . 368
A.7 Keep practicing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368
xii CONTENTS
CHAPTER
ONE
INTRODUCTION
1
LeetCode also provides premium services like mock interviews with real-world com-
panies, career coaching, and job postings. These premium services are designed to
help you prepare for technical interviews, sharpen your skills, and advance your
careers.
LeetCode has become a popular resource for technical interview preparation, as
many companies use similar problems to screen and evaluate potential candidates.
The platform has helped many users to secure job offers from top companies in the
technology industry, including Google, Microsoft, and Facebook.
In summary, LeetCode is a valuable resource for programmers and software engi-
neers looking to improve their coding skills, prepare for technical interviews, and
advance their careers. Its extensive collection of coding challenges, community dis-
cussion forums, and premium services make it an all-in-one platform for coding
practice and skills enhancement.
2 Chapter 1. Introduction
an algorithm.
4 Chapter 1. Introduction
I hope this book is an enjoyable and educational experience that will chal-
lenge and inspire you. Whether you want to enhance your skills, prepare
for a technical interview, or just have fun, this book has something for
you. So, get ready to put your coding skills to the test and embark on a
challenging and rewarding journey through the world of coding challenges!
TWO
ARRAY
This chapter will explore the basics of arrays - collections of elements organized
in a sequence. While they may seem simple, you can learn many concepts and
techniques from arrays to improve your coding skills. We’ll cover topics like index-
ing, iteration, and manipulation, as well as dynamic arrays (std::vector) and
time/space complexity.
Along the way, we’ll tackle challenging problems like searching, sorting, and sub-
array problems, using a structured approach to break down complex tasks into
manageable steps.
What this chapter covers:
1. Fundamentals of Arrays: Gain a solid understanding of arrays, their proper-
ties, and how to access and manipulate elements efficiently.
2. Array Operations: Learn essential array operations like insertion, deletion,
and updating elements, and understand their trade-offs.
3. Dynamic Arrays: Explore dynamic arrays, their advantages over static arrays,
and the mechanics of resizing.
4. Time and Space Complexity: Grasp the importance of analyzing the effi-
ciency of algorithms and how to evaluate the time and space complexity of
array-related operations.
5. Common Array Algorithms: Discover classic algorithms such as searching,
sorting, and various techniques for tackling subarray problems.
6. Problem-Solving Strategies: Develop systematic strategies to approach
7
array-related challenges, including how to break down problems, devise al-
gorithms, and validate solutions.
1 You are given a 2D integer array matrix, and your objective is to find the transpose
of the given matrix.
The transpose of a matrix involves flipping the matrix over its main diagonal, effec-
tively swapping its row and column indices.
⎡ ⎤ ⎡ ⎤
1 2 3 1 4 7
⎣4 5 6⎦ −→ ⎣2 5 8⎦
7 8 9 3 6 9
Example 1
Example 2
8 Chapter 2. Array
Constraints
• m == matrix.length.
• n == matrix[i].length.
• 1 <= m, n <= 1000.
• 1 <= m * n <= 10^5.
• -10^9 <= matrix[i][j] <= 10^9.
2.1.2 Solution
Code
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> transpose(const vector<vector<int>>& matrix) {
// declare the transposed matrix mt of desired size, i.e.
// mt's number of rows = matrix's number of columns
// mt's number of columns = matrix's number of rows
vector<vector<int>> mt(matrix[0].size(),
vector<int>(matrix.size()));
for (int i = 0; i < mt.size(); i++) {
for (int j = 0; j < mt[i].size(); j++) {
mt[i][j] = matrix[j][i];
}
}
return mt;
}
void printResult(const vector<vector<int>>& matrix) {
cout << "[";
for (auto& row : matrix) {
cout << "[";
for (int m : row) {
cout << m << ",";
(continues on next page)
Output:
[[1,4,7,][2,5,8,][3,6,9,]]
[[1,4,][2,5,][3,6,]]
Complexity
Note that the matrix might not be square, you cannot just swap the elements using
for example the function std::swap.
10 Chapter 2. Array
2.2 Valid Mountain Array
1 You are given an array of integers arr, and your task is to determine whether it is
a valid mountain array.
A valid mountain array must meet the following conditions:
1. The length of arr should be greater than or equal to 3.
2. There should exist an index i such that 0 < i < arr.length - 1, and the
elements up to i (arr[0] to arr[i]) should be in strictly ascending order,
while the elements starting from i (arr[i] to arr[arr.length-1]) should be
in strictly descending order.
Example 1
Example 3
Constraints
2.2.2 Solution
Code
#include <vector>
#include <iostream>
using namespace std;
bool validMountainArray(const vector<int>& arr) {
if (arr.size() < 3) {
return false;
}
const int N = arr.size() - 1;
int i = 0;
// find the top of the mountain
while (i < N && arr[i] < arr[i + 1]) {
i++;
(continues on next page)
12 Chapter 2. Array
(continued from previous page)
}
// condition: 0 < i < N - 1
if (i == 0 || i == N) {
return false;
}
// going from the top to the bottom
while (i < N && arr[i] > arr[i + 1]) {
i++;
}
return i == N;
}
int main() {
vector<int> arr{2,1};
cout << validMountainArray(arr) << endl;
arr = {3,5,5};
cout << validMountainArray(arr) << endl;
arr = {0,3,2,1};
cout << validMountainArray(arr) << endl;
arr = {9,8,7,6,5,4,3,2,1,0};
cout << validMountainArray(arr) << endl;
}
Output:
0
0
1
0
This solution iteratively checks for the two slopes of a mountain array, ensuring
that the elements to the left are strictly increasing and the elements to the right are
strictly decreasing. If both conditions are met, the function returns true, indicating
that the input array is a valid mountain array; otherwise, it returns false.
Breaking down the problem into distinct stages, like finding the peak of the moun-
tain and then traversing down from there, can simplify the logic and improve code
readability. This approach facilitates a clear understanding of the algorithm’s pro-
gression and helps in handling complex conditions effectively.
2.2.4 Exercise
• Beautiful Towers I
1 Youare given a 2D grid with dimension mxn and an integer k. Your task is to
perform k shift operations on the grid.
In each shift operation:
• The element at grid[i][j] moves to grid[i][j+1].
• The element at grid[i][n-1] moves to grid[i+1][0].
• The element at grid[m-1][n-1] moves to grid[0][0].
After performing k shift operations, return the updated 2D grid.
1
https://leetcode.com/problems/shift-2d-grid/
14 Chapter 2. Array
Example 1
⎡ ⎤ ⎡ ⎤
1 2 3 9 1 2
⎣4 5 6⎦ −→ ⎣3 4 5⎦
7 8 9 6 7 8
Example 2
⎡ ⎤ ⎡ ⎤ ⎡ ⎤
3 8 1 9 13 3 8 1 21 13 3 8
⎢19 7 2 5⎥ ⎥ ⎢ 9 19 7 2 ⎥
⎥ ⎢ 1 9 19 7⎥
⎢
⎣4 →⎢ →⎢ ⎥
6 11 10⎦ ⎣5 4 6 11⎦ ⎣2 5 4 6⎦
12 0 21 13 10 12 0 21 11 10 12 0
⎡ ⎤ ⎡ ⎤
0 21 13 3 12 0 21 13
⎢8 1 9 19⎥ ⎢3 8 1 9⎥
→⎢⎣7 2 5
⎥→⎢ ⎥
4⎦ ⎣19 7 2 5⎦
6 11 10 12 4 6 11 10
Example 3
Constraints
You can convert the 2D grid into a 1D vector v to perform the shifting easier. One
way of doing this is concatenating the rows of the matrix.
• If you shift the grid k = i*N times where N = v.size() and i is any non-
negative integer, you go back to the original grid; i.e. you did not shift it.
• If you shift the grid k times with 0 < k < N, the first element of the result starts
from v[N-k].
• In general, the first element of the result starts from v[N - k%N].
Example 1
Code
#include <vector>
#include <iostream>
using namespace std;
vector<vector<int>> shiftGrid(vector<vector<int>>& grid, int k) {
vector<int> v;
// store the 2D grid values into a 1D vector v
for (auto& r : grid) {
v.insert(v.end(), r.begin(), r.end());
}
const int N = v.size();
(continues on next page)
16 Chapter 2. Array
(continued from previous page)
// number of rows
const int m = grid.size();
// number of columns
const int n = grid[0].size();
Output:
[[9,1,2,][3,4,5,][6,7,8,]]
[[12,0,21,13,][3,8,1,9,][19,7,2,5,][4,6,11,10,]]
[[1,2,3,][4,5,6,][7,8,9,]]
This solution flattens the 2D grid into a 1D vector v, representing the grid’s ele-
ments in a linear sequence. Then, by calculating the new position for each element
after the shift operation, it reconstructs the grid by placing the elements back into
their respective positions based on the calculated indices. This approach avoids un-
necessary copying or shifting of elements within the grid, optimizing both memory
and time complexity.
Complexity
1. To convert a 2D matrix into a 1D vector, you can use the std::vector’s func-
tion insert().
2. The modulo operator % is usually used to ensure the index is inbound.
18 Chapter 2. Array
2.4 Find All Numbers Disappeared in an Array
1 You are given an array nums of n integers, where each integer nums[i] falls within
the range [1, n]. Your task is to find and return an array containing all the integers
in the range [1, n] that are not present in the given array nums.
Example 1
Example 2
Constraints
• n == nums.length.
• 1 <= n <= 10^5.
• 1 <= nums[i] <= n.
Follow up
Can you solve the problem without using additional memory and achieve a linear
runtime complexity? You can assume that the list you return does not count as extra
space.
1
https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/
You can use a vector of bool to mark which value appeared in the array.
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> findDisappearedNumbers(const vector<int>& nums) {
20 Chapter 2. Array
(continued from previous page)
print(result);
}
Output:
[5,6,]
[2,]
This code declares a vector named exist of type bool and initializes all of its values
to false. Its size is declared as n + 1 where n = nums.size() so it can mark the
values ranged from 1 to n.
Then it performs the marking of all nums’s elements to true. The ones that are false
will belong to the result.
Complexity
You could use the indices of the array nums to mark the appearances of its elements
because they are just a shift ([1, n] vs. [0, n-1]).
One way of marking the appearance of a value j (1 <= j <= n) is making the
element nums[j-1] to be negative. Then the indices j’s whose nums[j-1] are still
positive are the ones that do not appear in nums.
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> findDisappearedNumbers(vector<int>& nums) {
const int n = nums.size();
int j;
for (int i{0}; i < n; i++) {
// make sure j is positive since nums[i] might be
// changed to be negative in previous steps
j = abs(nums.at(i));
22 Chapter 2. Array
(continued from previous page)
result.push_back(j);
}
}
return result;
}
void print(const vector<int>& nums) {
cout << "[";
for (auto& a : nums) {
cout << a << ",";
}
cout << "]\n";
}
int main() {
vector<int> nums = {4,3,2,7,8,2,3,1};
auto result = findDisappearedNumbers(nums);
print(result);
nums = {1,1};
result = findDisappearedNumbers(nums);
print(result);
}
Output:
[5,6,]
[2,]
The key to this solution is that it utilizes the array to mark the presence of numbers.
Negating the value at the index corresponding to each number found in the input
array effectively marks that number as present. Then, by iterating through the
modified array, it identifies the missing numbers by checking which indices still
hold positive values.
2.4.5 Exercise
24 Chapter 2. Array
Example 1
⎡ ⎤ ⎡ ⎤
1 2 3 7 4 1
⎣4 5 6⎦ −→ ⎣8 5 2⎦
7 8 9 9 6 3
Example 2
⎡ ⎤ ⎡ ⎤
5 1 9 11 15 13 2 5
⎢ 2 4 8 10⎥ ⎢14 3 4 1 ⎥
⎣13 3 6 7 ⎦ −→ ⎣12 6 8 9 ⎦
⎢ ⎥ ⎢ ⎥
15 14 12 16 16 7 10 11
Constraints
• n == matrix.length == matrix[i].length.
• 1 <= n <= 20.
• -1000 <= matrix[i][j] <= 1000.
For any square matrix, the rotation 90 degrees clockwise can be performed in two
steps:
1. Transpose the matrix.
2. Mirror the matrix vertically.
#include <iostream>
#include <vector>
using namespace std;
void rotate(vector<vector<int>>& matrix) {
const int n = matrix.size();
// transpose
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
// vertical mirror
for (int i = 0; i < n; i++) {
for (int j = 0; j < n / 2; j++ ) {
swap(matrix[i][j], matrix[i][n - 1 - j]);
}
}
}
void printMatrix(const vector<vector<int>>& matrix) {
cout << "[";
for (auto& row: matrix) {
cout << "[";
for (auto& a: row) {
cout << a << ",";
}
cout << "],";
}
cout << "]\n";
}
int main() {
vector<vector<int>> matrix{{1,2,3},{4,5,6},{7,8,9}};
rotate(matrix);
printMatrix(matrix);
matrix = {{5,1,9,11},{2,4,8,10},{13,3,6,7},{15,14,12,16}};
rotate(matrix);
(continues on next page)
26 Chapter 2. Array
(continued from previous page)
printMatrix(matrix);
}
Output:
[[7,4,1,],[8,5,2,],[9,6,3,],]
[[15,13,2,5,],[14,3,4,1,],[12,6,8,9,],[16,7,10,11,],]
Complexity
2.5.4 Exercise
Input: n = 3
Output: [[1,2,3],[8,9,4],[7,6,5]]
Example 2
Input: n = 1
Output: [[1]]
Constraints
2.6.2 Solution
28 Chapter 2. Array
Code
#include <vector>
#include <iostream>
using namespace std;
enum Direction {RIGHT, DOWN, LEFT, UP};
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> m(n, vector<int>(n));
int bottom = n - 1;
int right = n - 1;
int top = 0;
int left = 0;
int row = 0;
int col = 0;
Direction d = RIGHT;
int a = 1;
while (top <= bottom && left <= right) {
m[row][col] = a++;
switch (d) {
case RIGHT: if (col == right) {
top++;
d = DOWN;
row++;
} else {
col++;
}
break;
case DOWN: if (row == bottom) {
right--;
d = LEFT;
col--;
} else {
row++;
}
break;
case LEFT: if (col == left) {
bottom--;
(continues on next page)
30 Chapter 2. Array
Output:
[[1,2,3,][8,9,4,][7,6,5,]]
[[1,]]
This solution uses a Direction enum and boundary variables to iteratively fill the
matrix in a spiral pattern. Updating the direction of movement based on the cur-
rent position and boundaries efficiently populates the matrix with sequential values,
traversing in a clockwise direction from the outer layer to the inner layer.
Complexity
Enumerating directions with an enum (like Direction) can enhance code readability
and maintainability, especially in algorithms involving traversal or movement. It
aids in clearly defining and referencing the possible directions within the problem
domain.
2.6.4 Exercise
• Spiral Matrix
1 Youare given an array of integers temperatures, which represents the daily tem-
peratures. Your task is to create an array answer such that answer[i] represents the
1
https://leetcode.com/problems/daily-temperatures/
Example 1
Example 2
Example 3
Constraints
For each temperatures[i], find the closest temperatures[j] with j > i such that
temperatures[j] > temperatures[i], then answer[i] = j - i. If not found,
answer[i] = 0.
32 Chapter 2. Array
Example 1
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> dailyTemperatures(const vector<int>& temperatures) {
vector<int> answer(temperatures.size());
for (int i = 0; i < temperatures.size(); i++) {
answer[i] = 0;
for (int j = i + 1; j < temperatures.size(); j++) {
if (temperatures[j] > temperatures[i]) {
answer[i] = j - i;
break;
}
}
}
return answer;
}
void print(const vector<int>& answer) {
cout << "[";
for (auto& v : answer ) {
cout << v << ",";
}
cout << "]\n";
}
int main() {
vector<int> temperatures{73,74,75,71,69,72,76,73};
(continues on next page)
Output:
[1,1,4,2,1,1,0,0,]
[1,1,1,0,]
[1,1,0,]
This solution iterates through the temperatures array and, for each temperature,
iterates through the remaining temperatures to find the next higher temperature.
Storing the time difference between the current day and the next higher tempera-
ture day constructs the resulting array representing the number of days until warmer
temperatures.
Complexity
34 Chapter 2. Array
• The value answer[i] = 0 tells you that there is no warmer temperature than
temperatures[i].
When computing answer[i] in the reversed order, you can use that knowledge more
efficiently.
Suppose you already know the future values answer[j]. To compute an older
value answer[i] with i < j, you need only to compare temperatures[i] with
temperatures[i + 1] and its chain of warmer temperatures.
Example 1
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> dailyTemperatures(const vector<int>& temperatures) {
vector<int> answer(temperatures.size(), 0);
for (int i = temperatures.size() - 2; i >= 0 ; i--) {
int j = i + 1;
(continues on next page)
Output:
(continues on next page)
36 Chapter 2. Array
(continued from previous page)
[1,1,4,2,1,1,0,0,]
[1,1,1,0,]
[1,1,0,]
The key to this solution lies in its optimized approach to finding the next higher
temperature. It utilizes a while loop to traverse the temperatures array efficiently,
skipping elements if they are not potential candidates for a higher temperature.
Updating the index based on previously calculated values stored in the answer array
avoids unnecessary iterations, resulting in improved performance compared to the
straightforward nested loop approach.
This improved solution reduces the time complexity to O(N) as it iterates through the
temperatures vector only once, resulting in a more efficient algorithm for finding
the waiting periods for each day.
Complexity
Worse cases for the while loop are when most temperatures[j] in their chain are
cooler than temperatures[i].
In these cases, the resulting answer[i] will be either 0 or a big value j - i. Those
extreme values give you a huge knowledge when computing answer[i] for other
older days i.
The value 0 would help the while loop terminates very soon. On the other hand,
the big value j - i would help the while loop skips the days j very quickly.
• Runtime: O(N), where N = temperatures.length.
• Extra space: O(1).
2.7.4 Tips
In some computations, you could improve the performance by using the knowledge
of the results you have computed.
In this particular problem, it can be achieved by doing it in the reversed order.
38 Chapter 2. Array
CHAPTER
THREE
LINKED LIST
In this chapter, we’ll learn about linked list - a unique and dynamic data structure
that challenges our understanding of sequential data.
Unlike arrays, linked lists do not impose a fixed size or continuous memory block.
Rather, they consist of nodes that contain data and a reference to the next node.
This seemingly simple concept unlocks many possibilities, from creating efficient
insertions and deletions to creatively solving problems that may seem specifically
designed for linked list manipulation.
Our exploration of linked lists will encompass a variety of variations and intricacies,
including singly linked lists. By delving into these lists, you’ll discover how they
empower us to tackle problems that may initially appear complicated.
What this chapter covers:
1. Introduction to Linked Lists: Gain a comprehensive understanding of linked
lists, their advantages, and their role in problem-solving.
2. Singly Linked Lists: Explore the mechanics of singly linked lists, mastering
the art of traversal, insertion, and deletion.
3. Advanced Linked List Concepts: Learn about sentinel nodes, dummy nodes,
and techniques to handle common challenges like detecting cycles and revers-
ing lists.
4. Problem-Solving Strategies: Develop strategies to approach linked list prob-
lems systematically, including strategies for merging lists, detecting intersec-
tions, and more.
39
3.1 Merge Two Sorted Lists
the starting nodes of two sorted linked lists, list1 and list2, your task is to
1 Given
Example 1
Example 2
Constraints
For each pair of nodes between the two lists, pick the node having smaller value to
append to the new list.
Code
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
ListNode zero(0);
auto z = mergeTwoLists(nullptr, &zero);
printResult(z);
}
Output:
[1,1,2,3,4,4,]
[]
[0,]
Complexity
3.1.3 Conclusion
This solution merges two sorted linked lists efficiently without using extra space.
It identifies the head of the merged list by comparing the values of the first nodes of
the input lists. Then, it iterates through both lists, linking nodes in ascending order
until one list is exhausted.
1 You are given the starting node, head, of a linked list, and an integer val. Your task
is to eliminate all nodes from the linked list that have a value equal to val. After
removing these nodes, return the new starting node of the modified linked list.
Example 1
Example 2
Constraints
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
Removing a node A in a linked list means instead of connecting the previous node
A.pre to A, you connect A.pre to A.next.
Code
#include <iostream>
struct ListNode {
int val;
ListNode *next;
(continues on next page)
Output:
[1,2,3,4,5,]
[]
[]
This solution efficiently removes nodes with a specified value val from a linked
list by using two pointers (head and pre) to traverse the list and update the next
pointers to bypass nodes with the specified value.
Complexity
head has no pre. You can create a dummy node for head.pre whose values is out of
the contraints.
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
ListNode* removeElements(ListNode* head, int val) {
ListNode seven4(7);
ListNode seven3(7, &seven4);
ListNode seven2(7, &seven3);
ListNode seven1(7, &seven2);
newHead = removeElements(&seven1, 7);
print(newHead);
}
Output:
[1,2,3,4,5,]
[]
[]
This solution creates a preHead node with a value of 2023 (an arbitrary value larger
than 50) and sets its next pointer to point to the original head of the linked list.
The purpose of this preHead node is to serve as the dummy or sentinel node at the
beginning of the linked list. Having a preHead node simplifies the code because it
eliminates the need to handle the special case of removing nodes from the beginning
of the list separately.
The remaining code is the same.
Attention!
Depending on your real situation, in practice, you might need to deallocate memory
for the removed nodes; especially when they were allocated by the new operator.
• In some linked list problems where head needs to be treated as a special case,
you can create a previous dummy node for it to adapt the general algorithm.
• Be careful with memory leak when removing nodes of the linked list contain-
ing pointers.
1 You are provided with the starting nodes of two singly linked lists, headA and headB.
Your task is to find the node where these two lists intersect. If there is no point of
intersection, return null.
For example, the following two linked lists begin to intersect at node c1:
Note that the linked lists do not have any cycles, and you must ensure that the
original structure of the linked lists remains unchanged after solving this problem.
Example 1
1
https://leetcode.com/problems/intersection-of-two-linked-lists/
Example 2
Example 3
Follow up
• Could you write a solution that runs in O(m + n) time and use only O(1)
memory?
You can store all nodes of listA then iterate listB to determine which node is the
intersection. If none is found, the two lists have no intersection.
Example 1
Code
#include <iostream>
#include <unordered_map>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
(continues on next page)
ListNode one1(1);
one1.next = &eight;
ListNode four1(4);
four1.next = &one1;
ListNode one2(1);
one2.next = &eight;
ListNode six2(6);
six2.next = &one2;
ListNode five2(5);
(continues on next page)
ListNode one12(1);
one12.next = &two;
ListNode nine1(9);
nine1.next = &one12;
ListNode one11(1);
one11.next = &nine1;
ListNode three2(3);
three2.next = &two;
cout << (getIntersectionNode(&one11, &three2) == &two) << endl;
}
{ // Example 3
ListNode four(4);
ListNode six(6);
six.next = &four;
ListNode two(2);
two.next = &six;
ListNode five(5);
ListNode one(1);
one.next = &five;
cout << (getIntersectionNode(&two, &one) == nullptr) << endl;
}
}
Output:
1
1
(continues on next page)
This code uses an unordered map to store the nodes of headA while traversing it.
Then, it traverses headB and checks if each node in headB exists in the map of nodes
from headA. If a common node is found, it returns that node as the intersection
point; otherwise, it returns nullptr to indicate no intersection.
Complexity
• Runtime: O(m + n), where m, n are the number of nodes of listA and listB.
• Extra space: O(m).
If the two lists do not share the same tail, they have no intersection. Otherwise,
they must intersect at some node.
After iterating to find the tail node, you know the length of the two lists. That
information gives you a hint of how to reiterate to find the intersection node.
Example 1
• After iterating listA = [4,1,8,4,5], you find the tail node is '5' and listA.
length = 5.
• After iterating listB = [5,6,1,8,4,5], you find the tail node is the last '5'
and listB.length = 6.
• The two lists share the same tail. They must intersect at some node.
• To find that intersection node, you have to reiterate the two lists.
• Since listB.length = 6 > 5 = listA.length, you can start iterating listB
first until the number of its remaining nodes is the same as listA. In this case,
it is the node '6' of listB.
• Now you can iterate them at the same time to find which node is shared.
Code
#include <iostream>
#include <unordered_map>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
int lengthA = 0;
ListNode *nodeA = headA;
while (nodeA->next != nullptr) {
lengthA++;
nodeA = nodeA->next;
}
int lengthB = 0;
ListNode *nodeB = headB;
while (nodeB->next != nullptr) {
lengthB++;
nodeB = nodeB->next;
}
// not the same tail -> no intersection
if (nodeA != nodeB) {
return nullptr;
}
nodeA = headA;
nodeB = headB;
// find the nodeA in listA and nodeB in listB
// that make two lists have the same length
while (lengthA > lengthB) {
nodeA = nodeA->next;
lengthA--;
(continues on next page)
ListNode one1(1);
one1.next = &eight;
ListNode four1(4);
four1.next = &one1;
ListNode one2(1);
one2.next = &eight;
ListNode six2(6);
six2.next = &one2;
ListNode five2(5);
five2.next = &six2;
cout << (getIntersectionNode(&four1, &five2) == &eight) << endl;
}
{ // Example 2
ListNode four(4);
ListNode two(2);
(continues on next page)
ListNode one12(1);
one12.next = &two;
ListNode nine1(9);
nine1.next = &one12;
ListNode one11(1);
one11.next = &nine1;
ListNode three2(3);
three2.next = &two;
cout << (getIntersectionNode(&one11, &three2) == &two) << endl;
}
{ // Example 3
ListNode four(4);
ListNode six(6);
six.next = &four;
ListNode two(2);
two.next = &six;
ListNode five(5);
ListNode one(1);
one.next = &five;
cout << (getIntersectionNode(&two, &one) == nullptr) << endl;
}
}
Output:
1
1
1
This improved solution finds the intersection of two linked lists by first determining
their lengths and adjusting the pointers so that they start from the same relative
position to the intersection point. Then, it iterates through both linked lists until it
finds the common intersection node.
• Runtime: O(m + n), where m, n are the number of nodes of listA and listB.
• Extra space: O(1).
3.3.5 Exercise
1 Youare provided with a linked list. Your goal is to exchange every two adjacent
nodes in the list and then return the head of the modified list.
You must solve this problem without altering the values within the nodes; you
should only modify the arrangement of the nodes themselves.
1
https://leetcode.com/problems/swap-nodes-in-pairs/
Example 2
Input: head = []
Output: []
Example 3
Constraints
Draw a picture of the swapping to identify the correct order of the update.
Denote (cur, next) the pair of nodes you want to swap and prev be the previous
node that links to cur. Here are the steps you need to perform for the swapping.
1. Update the links between nodes.
2. Go to the next pair.
Code
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
ListNode* swapPairs(ListNode* head) {
// the list does not have enough nodes to swap
if (head == nullptr || head->next == nullptr) {
(continues on next page)
Output:
[2,1,4,3,]
[]
[5,]
Complexity
3.4.3 Conclusion
This solution swaps pairs of nodes in a linked list by adjusting the pointers accord-
ingly.
It initializes pointers to the current node (curNode), its next node (nextNode), and
the previous node (preNode). Then, it iterates through the list, swapping pairs of
nodes by adjusting their next pointers and updating the preNode pointer.
This approach efficiently swaps adjacent nodes in the list without requiring addi-
tional space, effectively transforming the list by rearranging pointers.
1 You
have two linked lists that represent non-negative integers. The digits of these
numbers are stored in reverse order, with each node containing a single digit.
Your task is to add the two numbers represented by these linked lists and return the
result as a new linked list.
You can assume that the two numbers don’t have leading zeros, except for the num-
ber 0 itself.
Example 1
Example 3
Constraints
• The number of nodes in each linked list is in the range [1, 100].
• 0 <= Node.val <= 9.
• It is guaranteed that the list represents a number that does not have leading
zeros.
Perform the school addition calculation and store the result in one of the lists.
Without loss of generality, let us store the result in l1. Then you might need to
extend it when l2 is longer than l1 and when the result requires one additional
node (Example 3).
Code
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
(continues on next page)
Output:
[7,0,8,]
[0,]
[8,9,9,9,0,0,0,1,]
Complexity
3.5.3 Conclusion
This solution leverages a dummy node (prehead) to simplify the handling of edge
cases and to hook the head of the resulting list.
By iterating through both input lists simultaneously and performing addition digit
by digit while keeping track of carry, it efficiently computes the sum without the
need for additional checks for the head of the resulting list.
This approach streamlines the addition process, resulting in a concise and straight-
forward implementation.
3.5.4 Exercise
FOUR
HASH TABLE
71
Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
For example, 2 is denoted as II, which is essentially two ones added together. Simi-
larly, 12 is represented as XII, indicating X + II. The number 27 is written as XXVII,
which stands for XX + V + II.
Roman numerals are generally written from the largest value to the smallest value,
moving from left to right. However, there are exceptions to this pattern. For in-
stance, the numeral for 4 is IV instead of IIII, where I is placed before V to subtract
1 from 5. Similarly, 9 is IX, representing the subtraction of 1 from 10. There are six
such subtraction instances:
• I before V (5) or X (10) forms 4 and 9.
• X before L (50) or C (100) forms 40 and 90.
• C before D (500) or M (1000) forms 400 and 900.
Your task is to convert a given Roman numeral into its equivalent integer value.
Example 1
Input: s = "III"
Output: 3
Explanation: III = 3.
Input: s = "LVIII"
Output: 58
Explanation: L = 50, V= 5, III = 3.
Example 3
Input: s = "MCMXCIV"
Output: 1994
Explanation: M = 1000, CM = 900, XC = 90 and IV = 4.
Constraints
To treat the subtraction cases easier you can iterate the string s backward.
Code
#include <iostream>
#include <unordered_map>
using namespace std;
const unordered_map<char, int> value = {
{'I', 1}, {'V', 5},
{'X', 10}, {'L', 50},
{'C', 100}, {'D', 500},
{'M', 1000}
(continues on next page)
Output:
3
58
1994
Complexity
This problem can be solved using a map to store the values of each Roman numeral
character. This solution iterates through the string from right to left, accumulating
the integer value based on the corresponding Roman numeral characters.
By comparing the current character’s value with the previous one, the solution han-
dles cases of subtraction (e.g., IV, IX, etc.) by subtracting the value if it’s smaller
and adding it otherwise.
4.1.4 Exercise
• Integer to Roman
1 You have an array of positive integers called nums, and you wish to remove a sub-
array from it that consists of distinct elements. The score you achieve by removing
this subarray is the sum of its elements.
Your goal is to determine the highest possible score attainable by erasing exactly
one subarray from the provided array.
A subarray, denoted as b, is considered part of another array, a, if it appears consec-
utively within a, i.e., if it is equivalent to a[l], a[l+1], ..., a[r] for some indices
(l, r).
1
https://leetcode.com/problems/maximum-erasure-value/
Example 2
Constraints
You can use a map to store the position of the elements of nums. Then when iterating
nums you can identify if an element has been visited before. That helps you to decide
if a subarray contains unique elements.
Code
#include <iostream>
#include <unordered_map>
#include <vector>
using namespace std;
int maximumUniqueSubarray(const vector<int>& nums) {
// sum stores the running sum of nums
// i.e., sum[i] = nums[0] + ... + nums[i]
vector<int> sum(nums.size(), 0);
(continues on next page)
Output:
17
8
Complexity
4.2.3 Conclusion
This solution computes the maximum sum of a subarray containing unique ele-
ments.
It uses a sliding window approach to maintain a running sum of the elements en-
countered so far and a hashmap to keep track of the positions of previously seen
elements. By updating the starting index of the window when a repeated element
is encountered, it ensures that the current subarray contains only unique elements.
This approach optimizes the computation of the maximum sum by handling the
sliding window and updating the sum accordingly, resulting in an overall efficient
solution.
1 Youare provided with a list of strings named words and a string named pattern.
Your task is to find the strings from words that match the given pattern. The order
in which you return the answers does not matter.
A word is considered to match the pattern if there is a mapping p of the letters such
that, when each letter x in the pattern is replaced with p(x), the word is formed.
Keep in mind that a permutation of letters is a one-to-one correspondence from
letters to letters, where each letter is mapped to a distinct letter, and no two letters
are mapped to the same letter.
Example 1
"ccc" does not match the pattern because {a -> c, b -> c, ...} is not a␣
˓→permutation, since a and b map to the same letter.
Example 2
Code
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
vector<string> findAndReplacePattern(const vector<string>& words, const␣
˓→string& pattern) {
vector<string> result;
// need two maps for the bijection
unordered_map<char,char> w_to_p, p_to_w;
int i;
for (auto& w : words) {
w_to_p.clear();
p_to_w.clear();
i = 0;
while (i < w.length()) {
if (w_to_p.find(w[i]) != w_to_p.end()) {
// w[i] was mapped to some letter x
// but x != pattern[i]
if (w_to_p[w[i]] != pattern[i]) {
break;
}
} else {
if (p_to_w.find(pattern[i]) != p_to_w.end()) {
// w[i] was not mapped to any letter yet
(continues on next page)
Output:
[mee,aqq,]
[a,b,c,]
4.3.3 Conclusion
This solution efficiently finds and returns words from a vector of strings that match
a given pattern in terms of character bijection. It uses two unordered maps to
establish and maintain the bijection while iterating through the characters of the
words and the pattern.
FIVE
STRING
In this chapter, we’ll learn about the importance of strings in programming. Strings
help us work with text and are essential for many tasks, from processing data to
creating better communication between programs and people. By understanding
strings, you’ll be better equipped to solve problems and make things easier for users.
What this chapter covers:
1. Understanding Strings: Lay the groundwork by comprehending the nature
of strings, character encoding schemes, and the basics of representing and
storing textual data.
2. String Manipulation: Explore the art of string manipulation, covering opera-
tions like concatenation, slicing, reversing, and converting cases.
3. String Searching and Pattern Matching: Delve into strategies for finding
substrings, detecting patterns, and performing advanced search operations
within strings.
4. Anagrams and Palindromes: Tackle challenges related to anagrams and
palindromes, honing your ability to discern permutations and symmetric con-
structs.
5. Problem-Solving with Strings: Learn how to approach coding problems that
involve string manipulation, from simple tasks to intricate algorithms.
83
5.1 Valid Anagram
Example 1
Example 2
Constraints
Follow up
• What if the inputs contain Unicode characters? How would you adapt your
solution to such a case?
1
https://leetcode.com/problems/valid-anagram/
84 Chapter 5. String
5.1.2 Solution 1: Rearrange both s and t into a sorted string
Code
#include <iostream>
#include <algorithm>
using namespace std;
bool isAnagram(string& s, string& t) {
// anagrams must have the same length
if (s.length() != t.length()) {
return false;
}
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
int main() {
cout << isAnagram("anagram", "nagaram") << endl;
cout << isAnagram("rat", "car") << endl;
}
Output:
1
0
This solution determines if two strings are anagrams by comparing their sorted ver-
sions. If the sorted versions are equal, the original strings are anagrams, and the
function returns true. Otherwise, it returns false.
Complexity
Code
#include <iostream>
using namespace std;
bool isAnagram(const string& s, const string& t) {
if (s.length() != t.length()) {
return false;
}
// s and t consist of only lowercase English letters
// you can encode 0: 'a', 1: 'b', .., 25: 'z'.
int alphabet[26];
for (int i = 0; i < 26; i++) {
alphabet[i] = 0;
}
// count the frequency of each letter in s
for (auto& c : s) {
alphabet[c - 'a']++;
}
for (auto& c : t) {
alphabet[c - 'a']--;
// if s and t have the same length but are not anagrams,
// there must be some letter in t having higher frequency than␣
˓→s
Output:
(continues on next page)
86 Chapter 5. String
(continued from previous page)
1
0
This solution efficiently determines if two strings are anagrams by counting the fre-
quency of each character in both strings using an array. If the character frequencies
match for both strings, they are anagrams.
Complexity
Code
#include <iostream>
#include <unordered_map>
using namespace std;
bool isAnagram(const string& s, const string& t) {
if (s.length() != t.length()) {
return false;
}
// this alphabet can store all UTF-8 characters
unordered_map<char, int> alphabet;
for (auto& c : s) {
alphabet[c]++;
}
for (auto& c : t) {
alphabet[c]--;
if (alphabet[c] < 0) {
return false;
(continues on next page)
Output:
1
0
Complexity
88 Chapter 5. String
5.1.6 Exercise
1 The task is to determine if the usage of capital letters in a given string, word, is
correct according to the following rules:
1. All letters in the word are capital, like “USA”.
2. All letters in the word are not capital, like “leetcode”.
3. Only the first letter in the word is capital, like “Google”.
If the capitalization in the given word adheres to these rules, the function should
return true; otherwise, it should return false.
Example 1
Example 2
5.2.2 Solution
Only when the first two characters of the word are uppercase, the rest must be the
same. Otherwise, the rest is always lowercase.
Code
#include <string>
#include <iostream>
using namespace std;
//! @return true if (c is lowercase and isLower is true)
//! or (c is uppercase and isLower is false).
//! false, otherwise.
bool isValidCase(const char& c, const bool isLower) {
if (isLower) {
return 'a' <= c && c <= 'z';
}
return 'A' <= c && c <= 'Z';
}
bool detectCapitalUse(const string& word) {
if (word.length() == 1) {
return true;
}
bool isLower = true;
90 Chapter 5. String
(continued from previous page)
for (int i = 1; i < word.length(); i++) {
if (!isValidCase(word[i], isLower)) {
return false;
}
}
return true;
}
int main() {
cout << detectCapitalUse("USA") << endl;
cout << detectCapitalUse("FlaG") << endl;
cout << detectCapitalUse("leetcode") << endl;
cout << detectCapitalUse("Google") << endl;
}
Output:
1
0
1
1
Complexity
5.2.3 Conclusion
This solution efficiently checks whether a given word follows one of the specified
capitalization rules by iterating through the characters of the word and using the
isValidCase function to validate each character’s capitalization based on the current
capitalization type (isLower). If no violations are found, the word is considered
valid, and the function returns true.
1 Theproblem involves the International Morse Code, which defines a standard way
to encode letters with dots and dashes. Each English letter corresponds to a specific
sequence in Morse Code, and a full table mapping each letter is provided.
For instance, 'a' is encoded as ".-", 'b' as "-...", and so on.
The full table for the 26 letters of the English alphabet is given below:
You are given an array of strings named words, where each word can be represented
as a concatenation of the Morse code for each of its letters. For example, the word
"cab" can be represented as "-.-..--...", which is the concatenation of "-.-.",
".-", and "-...". This concatenated Morse code representation is referred to as the
“transformation” of a word.
Your task is to count the number of different transformations that can be obtained
from all the words in the given array.
1
https://leetcode.com/problems/unique-morse-code-words/
92 Chapter 5. String
Example 1
Example 2
Constraints
Code
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
const vector<string> morse{
".-", "-...", "-.-.", "-..", ".", "..-.", "--.",
"....", "..", ".---", "-.-", ".-..", "--", "-.",
(continues on next page)
Output:
2
1
Complexity
94 Chapter 5. String
5.3.3 Conclusion
This solution converts each word into Morse code based on a predefined mapping
and uses an unordered set to keep track of unique representations. By inserting
each representation into the set, it automatically filters out duplicates. The final
result is the size of the set, which represents the number of unique Morse code
representations among the input words.
1 Eachvalid email address is composed of a local name and a domain name, sepa-
rated by the '@' sign. The local name may contain lowercase letters, one or more
'.' characters, and a plus '+' sign. However, the rules for dots and the plus sign do
not apply to the domain name.
For example, in the email "[email protected]", "alice" is the local name, and
"leetcode.com" is the domain name.
If you insert periods '.' between certain characters in the local name, the email will
still be forwarded to the same address without the dots in the local name. This rule
does not apply to the domain name.
For example, "[email protected]" and "[email protected]" both forward
to the same email address.
If you include a plus '+' sign in the local name, everything after the first plus sign
is ignored, allowing for email filtering. This rule also does not apply to the domain
name.
For example, "[email protected]" will be forwarded to "[email protected]".
It is possible to use both of these rules at the same time.
Given an array of strings emails, where each element is an email address to which
an email is sent, your task is to determine the number of different addresses that
will actually receive the emails after applying the rules described above.
1
https://leetcode.com/problems/unique-email-addresses/
Output: 2
Explanation: "[email protected]" and "[email protected]"␣
˓→actually receive mails.
Example 2
Constraints
96 Chapter 5. String
Code
#include<string>
#include<iostream>
#include<vector>
#include <unordered_set>
using namespace std;
int numUniqueEmails(const vector<string>& emails) {
unordered_set<string> s;
for (auto& e: emails) {
auto apos = e.find('@');
Output:
2
3
2
This solution parses a list of email addresses, normalizes each email address by
removing periods and ignoring characters after the plus sign in the local name, and
then counts the number of unique email addresses. The use of an unordered set
ensures that only unique email addresses are counted.
Complexity
Code
#include<string>
#include<iostream>
#include<vector>
#include <unordered_set>
using namespace std;
(continues on next page)
98 Chapter 5. String
(continued from previous page)
int numUniqueEmails(const vector<string>& emails) {
unordered_set<string> s;
for (auto& e: emails) {
string address;
int i = 0;
// the local name ends here
while (e[i] != '@' && e[i] != '+') {
// ignore each '.' found
if (e[i++] == '.') {
continue;
}
// add valid characters to local name
address += e[i++];
}
// combine local name with domain one
address += e.substr(e.find('@', i));
s.insert(address);
}
return s.size();
}
int main() {
vector<string> emails{"[email protected]",
"[email protected]",
"[email protected]"};
cout << numUniqueEmails(emails) << endl;
emails = {"[email protected]","[email protected]","[email protected]"};
cout << numUniqueEmails(emails) << endl;
emails = {"[email protected]","test.email.leet+alex@code.
˓→com"};
Output:
2
3
2
• string::find(char, pos=0) returns the position of the first char which appears
in the string string starting from pos.
1 Given a string s, your task is to determine the length of the longest substring within
s that does not contain any repeating characters.
Example 1
Input: s = "abcabcbb"
Output: 3
Explanation: The answer is "abc", with a length of 3.
Example 2
Input: s = "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.
Example 3
Input: s = "pwwkew"
Output: 3
Explanation: The answer is "wke", with a length of 3.
Notice that the answer must be a substring, "pwke" is a subsequence and␣
˓→not a substring.
1
https://leetcode.com/problems/longest-substring-without-repeating-characters/
Whenever you meet a visited character s[i] == s[j] for some 0 <= i < j <
s.length, the substring "s[i]...s[j - 1]" might be valid, i.e., it consists of only
nonrepeating characters.
But in case you meet another visited character s[x] == s[y] where x < i < j
< y, the substring "s[x]...s[y - 1]" is not valid because it consists of repeated
character s[i] == s[j].
That shows the substring "s[i]...s[j - 1]" is not always a valid one. You might
need to find the right starting position start >= i for the valid substring "s[start].
..s[j - 1]".
Example 4
#include <iostream>
#include <unordered_map>
using namespace std;
int lengthOfLongestSubstring(const string& s) {
// keep track latest index of a character in s
unordered_map<char, int> position;
Output:
3
1
3
Complexity
5.5.3 Conclusion
This solution utilizes a sliding window approach to track the starting index of the
current substring and an unordered map to store the position of the characters en-
countered so far. By updating the starting index when a repeating character is
encountered, it ensures that the current substring contains only unique characters.
This approach optimizes the computation of the length of the longest substring by
handling the sliding window and updating the length accordingly, resulting in an
overall efficient solution.
5.5.4 Exercise
1 Given two version numbers, version1 and version2, your task is to compare them.
Version numbers consist of one or more revisions joined by a dot '.'. Each revision
is composed of digits and may contain leading zeros. Each revision has at least one
character. Revisions are indexed from left to right, with the leftmost revision being
revision 0, the next revision being revision 1, and so on.
For instance, 2.5.33 and 0.1 are valid version numbers.
To compare version numbers, you should compare their revisions in left-to-right
order. Revisions are compared using their integer value, ignoring any leading zeros.
This means that revisions 1 and 001 are considered equal. If a version number does
not specify a revision at a particular index, treat that revision as 0. For example,
version 1.0 is less than version 1.1 because their revision 0s are the same, but their
revision 1s are 0 and 1 respectively, and 0 is less than 1.
The function should return the following:
• If version1 is less than version2, return -1.
• If version1 is greater than version2, return 1.
• If version1 and version2 are equal, return 0.
Example 1
1
https://leetcode.com/problems/compare-version-numbers/
Example 3
Constraints
5.6.2 Solution
version = revisions[0].revisions[1].revisions[2]....
Code
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
using namespace std;
//! @return the vector of revisions of the version
//! @example if version = "1.02.11", return {1,2,11}
vector<int> toVector(const string& version) {
vector<int> revisions;
string revision;
for (auto& c : version) {
if (c != '.') {
// continue to build current revision
revision += c;
} else {
// current revision completes
// uses stoi() to ignore leading zeros
revisions.push_back(stoi(revision));
int i = 0;
// perform the comparison on the revisions
while (i < r1.size() && i < r2.size()) {
if (r1[i] < r2[i]) {
return -1;
} else if (r1[i] > r2[i]) {
return 1;
}
i++;
}
if (i == r1.size()) {
// if version1 is not longer than version2
// and version2 still has some valid revisions remain
if (accumulate(r2.begin() + i, r2.end(), 0) > 0) {
return -1;
}
} else if (accumulate(r1.begin() + i, r1.end(), 0) > 0) {
// if version2 is not longer than version1
// and version1 still has some valid revisions remain
return 1;
}
return 0;
}
int main() {
cout << compareVersion("1.01", "1.001") << endl;
cout << compareVersion("1.0", "1.0.0") << endl;
cout << compareVersion("0.1", "1.1") << endl;
}
Output:
0
0
-1
5.6.3 Conclusion
This solution first converts the version strings into vectors of integers represent-
ing the individual components of the version numbers. This conversion is done by
iterating through each character of the version string, accumulating digits until en-
countering a dot, at which point the accumulated integer is added to the revisions
vector.
Once both version strings are converted into vectors, the function iterates through
the vectors, comparing corresponding elements to determine the relationship be-
tween the versions. Additionally, it accounts for any remaining digits in the longer
version string after the common components by summing them up and comparing
the totals.
This approach simplifies the comparison process by breaking down the version
strings into easily comparable components.
C++ Notes
2
https://en.cppreference.com/w/cpp/string/basic_string/stol
3
https://en.cppreference.com/w/cpp/algorithm/accumulate
SIX
STACK
This chapter explores the stack data structure, a useful tool for managing data in a
Last-In-First-Out (LIFO) way. We’ll investigate the basics of stacks and examine how
they work using C++’s std::stack` and std::vector from the Standard Template
Library (STL).
Stacks in programming are like a stack of books where you add and remove books
from the top. They provide a structured way to manage data, making them ideal
for handling temporary information, tracking function calls, and solving various
algorithmic challenges.
What this chapter covers:
1. Introduction to Stacks: Begin by understanding the core principles of stacks,
their fundamental operations, and their real-world applications.
2. Leveraging `std::stack`: Dive into the STL’s powerful std::stack container,
mastering its usage and versatility for stack-based operations.
3. Exploring `std::vector`: Discover the capabilities of std::vector in context
with stacks, exploiting its dynamic array nature to create flexible stack struc-
tures.
4. Stack Operations: Explore operations such as push and pop, understanding
their impact on the stack’s state and memory usage.
5. Balancing Parentheses: Tackle the classic problem of parentheses balancing
using stacks, a prime example of their utility in parsing and validation.
As you progress through this chapter, you’ll learn about the importance of stacks
and how std::stack and std::vector can help you solve problems more efficiently. By
111
the end of the chapter, you’ll thoroughly understand the stack data structure’s Last-
In-First-Out (LIFO) principle and how you can leverage std::stack and std::vector to
manage data effectively. Let’s embark on this enlightening journey through stacks
and uncover their potential for simplifying complex operations and algorithmic
problems!
1 Youare responsible for keeping score in a unique baseball game with special rules.
The game involves multiple rounds where the scores of previous rounds can influ-
ence the scores of future rounds.
At the beginning of the game, your record is empty. You are given a list of operations
called ops, where each ops[i] is one of the following:
1. An integer x - This represents recording a new score of x.
2. "+" - This represents recording a new score that is the sum of the previous two
scores. It is guaranteed that there will always be two previous scores.
3. "D" - This represents recording a new score that is double the previous score.
It is guaranteed that there will always be a previous score.
4. "C" - This represents invalidating the previous score, removing it from the
record. It is guaranteed that there will always be a previous score.
Your task is to calculate and return the sum of all the scores in the record after
performing all the operations.
1
https://leetcode.com/problems/baseball-game/
Example 2
"D" - Add 2 * -2 = -4 to the record; the record is now [5, -2, -4].
"9" - Add 9 to the record; the record is now [5, -2, -4, 9].
"+" - Add -4 + 9 = 5 to the record, record is now [5, -2, -4, 9, 5].
"+" - Add 9 + 5 = 14 to the record, record is now [5, -2, -4, 9, 5, 14].
The total sum is 5 + -2 + -4 + 9 + 5 + 14 = 27.
Example 3
6.1.2 Solution
Code
#include <vector>
#include <iostream>
#include <string>
#include <numeric>
using namespace std;
int calPoints(const vector<string>& ops) {
vector<int> stk;
for (auto& s : ops) {
if (s == "C") {
stk.pop_back();
} else if (s == "D") {
stk.push_back(stk.back()*2);
} else if (s == "+") {
stk.push_back(stk[stk.size() - 1] + stk[stk.size() - 2]);
} else { // s is an integer
stk.push_back(stoi(s));
}
}
// compute the sum
return accumulate(stk.begin(), stk.end(), 0);
}
(continues on next page)
Output:
30
27
This solution simulates the baseball game by processing each round’s operation and
maintaining a stack of valid points. It accurately calculates the final sum of valid
points based on the given operations.
Complexity
1. The data structure stk you might need to solve this problem is a stack. But
here are the reasons you had better use std::vector:
• std::vector has also methods push_back(value) and pop_back() like
the ones in stack.
• On the other hand, a stack does not give easy access to the second last
element for the operator "+" in this problem.
2. accumulate(stk.begin(), stk.end(), 0) computes the sum of the vector
stk.
1 Youare given a string s containing only the characters '(', ')', '{', '}', '[', and
']'. Your task is to check if the input string is valid.
A string is considered valid if the following conditions are satisfied:
1. Opening brackets must be closed by the same type of brackets.
2. Opening brackets must be closed in the correct order, meaning that the inner-
most opening bracket should be closed before its surrounding brackets.
Example 1
Input: s = "()"
Output: true
Example 2
Input: s = "()[]{}"
Output: true
1
https://leetcode.com/problems/valid-parentheses/
Input: s = "(]"
Output: false
Constraints
Code
#include <iostream>
#include <stack>
using namespace std;
bool isValid(const string& s) {
stack<char> stk;
for (auto& c : s) {
if (c == '(' || c == '[' || c == '{') {
stk.push(c);
} else if (stk.empty()) {
(continues on next page)
Output:
1
1
0
0
Complexity:
6.2.4 Exercise
1 You are provided with two strings, s and t. Your task is to determine if these two
strings are equal when typed into an empty text editor, where the character '#'
represents a backspace action.
Note that applying a backspace action to an empty text does not change the text;
it remains empty. Your function should return true if the two strings become equal
after considering the backspace actions, otherwise return false.
Example 1
Example 3
Constraints
Follow up
6.3.2 Solution: Build and clean the string using the stack’s behaviors
Code
#include <iostream>
#include <vector>
using namespace std;
string cleanString(const string &s) {
vector<char> v;
for (int i = 0; i < s.length(); i++) {
if (s[i] != '#') {
(continues on next page)
Output:
1
1
0
This solution effectively handles backspace characters ('#') in input strings s and
t by constructing cleaned versions of the strings and then comparing the cleaned
strings for equality.
You can use the methods push and pop of the data structure stack to build and clean
the strings.
But vector has also such methods: push_back and pop_back.
On the other hand, using vector it is easier to construct a string by constructor
than using stack after cleaning.
6.3.4 Exercise
1 You are given a string s and an integer k. A k duplicate removal operation involves
selecting k adjacent and identical letters from s and removing them, causing the
remaining portions on the left and right of the removed substring to join together.
You need to perform the k duplicate removal operation on s repeatedly until it is no
longer possible. After completing all such operations, return the resulting string. It
is guaranteed that the answer will be unique.
Example 1
Input: s = "abcd", k = 2
Output: "abcd"
Explanation: There is nothing to delete.
Example 2
Input: s = "deeedbbcccbdaa", k = 3
Output: "aa"
Explanation:
First delete "eee" and "ccc", get "ddbbbdaa"
Then delete "bbb", get "dddaa"
Finally delete "ddd", get "aa"
1
https://leetcode.com/problems/remove-all-adjacent-duplicates-in-string-ii/
Input: s = "pbbcggttciiippooaais", k = 2
Output: "ps"
Constraints
Construct a stack of strings that has adjacent equal letters and perform the removal
during building those strings.
Example 2
#include <iostream>
#include <vector>
using namespace std;
string removeDuplicates(string& s, int k) {
// stk is used as a stack
// all letters in each string a of stk are equal
// every a's length is less than k
vector<string> stk;
int i = 0;
while (i < s.length()) {
// a represents the current string with duplicate letters
string a;
Output:
abcd
aa
ps
Complexity
• The data structure stk you might need to solve this problem is a stack. But
here are the reasons you had better use std::vector:
• std::vector also has methods push_back(value) and pop_back() like the
ones in a stack.
• On the other hand, it is faster for a vector to perform the string concatenation
at the end.
6.4.4 Exercise
SEVEN
This chapter explores priority queues (or heaps), the fascinating data structures
designed to manage elements with distinct levels of importance. In this chapter,
we’ll focus on harnessing the capabilities of C++’s std::priority_queue from the
Standard Template Library (STL).
Think of a priority queue as a line at a theme park, where individuals with priority
passes are served before others. Similarly, a priority queue ensures that elements
with higher priority are processed ahead of those with lower priority, enabling us to
address a wide range of problems that involve ordering and selection.
What this chapter covers:
1. Understanding Priority Queues: Begin by grasping the essence of priority
queues, their underlying mechanisms, and the significance of their unique
ordering.
2. Leveraging std::priority_queue: Dive into the versatile
std::priority_queue container provided by the STL, mastering its us-
age for managing priorities effectively.
3. Operations and Methods: Explore the operations available in
std::priority_queue, including insertion, and extraction while maintaining
optimal order.
4. Custom Comparators: Customize the behavior of your priority queue by uti-
lizing custom comparators, tailoring it to handle diverse data types and prior-
ity criteria.
5. Problem-Solving with Priority Queues: Learn strategies for tackling prob-
lems where prioritization is key, from scheduling tasks to efficient data re-
129
trieval.
1 Youare given an array of integers called stones, where each stones[i] represents
the weight of the i-th stone.
A game is played with these stones as follows: In each turn, we choose the two
heaviest stones and smash them together. Let us say the weights of the two heaviest
stones are x and y, where x <= y. The outcome of this smash operation is:
1. If x is equal to y, both stones are destroyed.
2. If x is not equal to y, the stone with weight x is destroyed, and the stone with
weight y now has a new weight of y - x.
The game continues until there is at most one stone left. Your task is to determine
the smallest possible weight of the remaining stone after the game ends. If there are
no stones left, return 0.
Example 1
1
https://leetcode.com/problems/last-stone-weight/
Constraints
The only things you want at any time are the two heaviest stones. One way of
keeping this condition is by using std::priority_queue.
Code
#include <vector>
#include <iostream>
#include <queue>
using namespace std;
int lastStoneWeight(vector<int>& stones) {
priority_queue<int> q(stones.begin(), stones.end());
while (q.size() >= 2) {
int y = q.top();
q.pop();
int x = q.top();
q.pop();
// compare two heaviest stones
if (y != x) {
q.push(y - x);
}
}
return q.empty() ? 0 : q.top();
(continues on next page)
Output:
1
1
Complexity
7.1.3 Conclusion
This solution efficiently simulates the process of smashing stones and finding the last
remaining stone by using a max-heap (priority queue) to always select the heaviest
stones to smash together.
1 Create a class that can find the k-th largest element in a stream of integers. This
is the k-th largest element when the elements are arranged in sorted order, not the
k-th distinct element.
1
https://leetcode.com/problems/kth-largest-element-in-a-stream/
Example 1
Input
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
Output
[null, 4, 5, 5, 8, 8]
Explanation
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8
Constraints
Sort the stream when initialization. And keep it sorted whenever you append a new
value.
Example 1
Code
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
class KthLargest {
vector<int> _nums;
int _k;
public:
KthLargest(int k, vector<int>& nums) : _nums(nums), _k(k) {
// sort the nums when constructed
sort(_nums.begin(), _nums.end(), std::greater());
}
Output:
4
5
5
8
8
This solution maintains a sorted vector _nums in non-ascending order upon initial-
ization, which stores the elements. When adding a new element val, it inserts it
into _nums while maintaining the sorted order.
Since _nums is sorted in non-ascending order, the k-th largest element is always at
index _k - 1. Thus, upon adding a new element, it returns the value at index _k -
1 as the k-th largest element in the collection.
This approach optimizes the add operation by leveraging the sorted nature of the
data structure, resulting in efficient retrieval of the k-th largest element.
• Runtime: for the constructor O(N*logN), where N = nums.length. For the add
method, O(N).
• Extra space: O(1).
There is a data structure that has the property you want in this problem.
It is std::priority_queue, which keeps its top element is always the largest one
according to the comparison you define for the queue.
By default, the “less than” comparison is used for std::priority_queue (heap) and
the top one is always the biggest element.
If you want the top one is always the smallest element, you can use the comparison
“greater than” for your heap.
Code
#include <vector>
#include <queue>
#include <iostream>
using namespace std;
class KthLargest {
priority_queue<int, vector<int>, greater<int>> _q;
int _k;
public:
KthLargest(int k, vector<int>& nums)
// create the heap when constructed
: _q(nums.begin(), nums.end()), _k(k) {
Output:
4
5
5
8
8
Complexity
• Runtime: for the constructor, O(N*logN), where N = nums.length. For the add
method, O(logN).
• Extra space: O(1).
The key insight of Solution 2 is utilizing a min-heap (priority queue with the greater
comparator) to find the kth largest element in a collection.
Upon initialization, the constructor populates the priority queue with the elements
from the input vector nums. When adding a new element val, it inserts it into the
priority queue and then removes elements until the size of the priority queue is
reduced to _k, ensuring that only the k largest elements are retained in the queue.
Finally, it returns the top element of the priority queue, which represents the kth
largest element. This approach leverages the properties of a min-heap to track the
kth largest element in the collection, resulting in an overall efficient solution.
7.2.5 Exercise
1 Youare given an n x n matrix where each row and column is sorted in ascending
order. Your task is to find the k-th smallest element in this matrix.
Please note that we are looking for the k-th smallest element based on its position
in the sorted order, and not counting distinct elements.
Additionally, it is required to find a solution with a memory complexity better than
O(n^2).
1
https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/
Example 2
Constraints
• n == matrix.length == matrix[i].length.
• 1 <= n <= 300.
• -10^9 <= matrix[i][j] <= 10^9.
• All the rows and columns of matrix are guaranteed to be sorted in non-
decreasing order.
• 1 <= k <= n^2.
Follow up
• Could you solve the problem with a constant memory (i.e., O(1) memory com-
plexity)?
• Could you solve the problem in O(n) time complexity? The solution may be
too advanced for an interview but you may find reading this paper fun.
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int kthSmallest(const vector<vector<int>>& matrix, int k) {
vector<int> m;
// transform the 2D matrix into a 1D array m
for (auto& row : matrix) {
m.insert(m.end(), row.begin(), row.end());
}
// sort the array m
sort(m.begin(), m.end());
return m.at(k - 1);
}
int main() {
vector<vector<int>> matrix{{1,5,9},{10,11,13},{12,13,15}};
cout << kthSmallest(matrix, 8) << endl;
matrix = {{-5}};
cout << kthSmallest(matrix, 1) << endl;
}
Output:
13
-5
The core idea behind this solution is to transform the 2D matrix into a 1D sorted
array, making it easier to find the k-th smallest element efficiently. The time com-
plexity of this solution is dominated by the sorting step, which is O(N*logN), where
N is the total number of elements in the matrix.
Instead of sorting after building the vector in Solution 1, you can do the other way
around. It means building up the vector from scratch and keeping it sorted.
Since you need only the k-th smallest element, std::priority_queue can be used for
this purpose.
Code
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int kthSmallest(const vector<vector<int>>& matrix, int k) {
priority_queue<int> q;
for (int row = 0; row < matrix.size(); row++) {
for (int col = 0; col < matrix[row].size(); col++) {
q.push(matrix[row][col]);
// maintain q's size does not exceed k
if (q.size() > k) {
q.pop();
}
}
}
return q.top();
}
int main() {
vector<vector<int>> matrix{{1,5,9},{10,11,13},{12,13,15}};
cout << kthSmallest(matrix, 8) << endl;
(continues on next page)
Output:
13
-5
Complexity
7.3.4 Conclusion
7.3.5 Exercise
1 You are provided with an array of integers called target with n elements. You start
with another array, arr, consisting of n elements, all initialized to 1. You have the
ability to perform the following operation:
1. Calculate the sum of all elements in your current array arr, let’s call it x.
2. Choose an index i where 0 <= i < n, and update the value at index i in arr
to be x.
You can repeat this operation as many times as needed. Your task is to determine
whether it’s possible to transform the initial array arr into the given target array
using this operation. If it’s possible, return true; otherwise, return false.
Example 1
Example 2
Constraints
• n == target.length.
• 1 <= n <= 5 * 10^4.
• 1 <= target[i] <= 10^9.
If you start from arr = [1,1,...,1] and follow the required procedure, the new
element x you get for the next state is always the max element of arr.
To solve this problem, you can start from the max element of the given target to
compute its previous state until you get the arr = [1,1,...,1].
Example 1
• If target = [m,1] or target = [1,m] for any m >= 1, you can always turn it
to arr = [1,1].
• If the changed value after the subtraction is still the max element of the previ-
ous state, you need to redo the subtraction at the same position. In this case,
the modulo might be used instead of subtraction.
Code
#include <iostream>
#include <numeric>
#include <algorithm>
#include <vector>
using namespace std;
bool isPossible(const vector<int>& target) {
// compute sum of all target's elements
unsigned long sum = accumulate(target.begin(),
target.end(),
(unsigned long) 0);
// find the max element in the target
// pmax is the pointer to the max element,
// *pmax is the value that pointer points to
auto pmax = max_element(target.begin(), target.end());
while (*pmax > 1) {
// compute the remaining sum
sum -= *pmax;
if (sum == 1) {
// This is the case target = [m,1],
// which you can always turn it to [1,1].
return true;
}
if (*pmax <= sum) {
// the next subtraction leads to non-positive values
return false;
}
(continues on next page)
Output:
1
0
1
This solution iteratively reduces the maximum element in the target array while
keeping track of the total sum. It checks various conditions to determine whether
it’s possible to reach an array consisting of only 1s. If all conditions are satisfied, it
returns true; otherwise, it returns false.
In the solution above, the position of the max element in each state is not so impor-
tant as long as you update exactly it, not the other ones.
That might lead to the usage of the std::priority_queue.
Code
#include <iostream>
#include <numeric>
#include <queue>
#include <vector>
using namespace std;
bool isPossible(const vector<int>& target) {
// create a heap from the target
priority_queue<int> q(target.begin(), target.end());
// compute the sum of all elements
unsigned long sum = accumulate(target.begin(),
target.end(),
(unsigned long) 0);
while (q.top() > 1) {
// compute the remaining sum
sum -= q.top();
if (sum == 1) {
return true;
}
if (q.top() <= sum) {
return false;
}
if (sum == 0) {
(continues on next page)
Output:
1
0
1
7.4.4 Conclusion
Solution 2 uses a max heap (priority_queue) to efficiently find and process the
maximum element in the target array while keeping track of the total sum. It
checks various conditions to determine whether it’s possible to reach an array con-
sisting of only 1s.
7.4.5 Exercise
EIGHT
BIT MANIPULATION
In this chapter, we’re diving deep into Bit Manipulation, a fascinating computer
science and programming area that manipulates individual bits within data.
Bit Manipulation is crucial in various programming tasks, from optimizing algo-
rithms to working with hardware-level data. Whether you’re a seasoned program-
mer looking to expand your skill set or a newcomer eager to delve into the intricacies
of bits and bytes, this chapter has something valuable for you.
Here’s what you can expect to explore in this chapter:
1. Understanding the Basics: We’ll start by demystifying bits and binary num-
bers, ensuring you’re comfortable with the fundamentals. You’ll learn to con-
vert between decimal and binary, perform basic bit operations, and understand
two’s complement representation.
2. Bitwise Operators: We’ll delve into the world of bitwise operators in pro-
gramming languages like C++. You’ll get hands-on experience with AND,
OR, XOR, and other essential operators, seeing how they can be applied to
practical coding problems.
3. Bit Hacks: Discover the art of Bit Hacks – clever and often elegant tricks
programmers use to solve specific problems efficiently. You’ll learn to perform
tasks like counting bits, finding the rightmost set bit, and swapping values
without temporary variables.
4. Bit Manipulation Techniques: We’ll explore techniques and patterns for com-
mon bit manipulation tasks, such as setting, clearing, or toggling specific bits,
checking if a number is a power of two, or extracting subsets of bits from a
larger number.
151
By the end of this chapter, you’ll have a solid foundation in Bit Manipulation and
the ability to harness the power of bits to optimize your code and tackle complex
problems. So, let’s embark on this exciting journey into the realm of Bit Manipula-
tion and discover how the smallest data units can have a massive impact on your
coding skills and efficiency!
1 The Hamming distance between two integers is the number of positions at which
the corresponding bits are different.
Given two integers x and y, return the Hamming distance between them.
Example 1
Input: x = 1, y = 4
Output: 2
Explanation:
1 (0 0 0 1)
4 (0 1 0 0)
^ ^
The above arrows point to positions where the corresponding bits are␣
˓→different.
Example 2
Input: x = 3, y = 1
Output: 1
1
https://leetcode.com/problems/hamming-distance/
You could use bitwise XOR (^) to get the bit positions where x and y are different.
Then use bitwise AND operator (&) at each position to count them.
Example 1
x = 1 (0 0 0 1)
y = 4 (0 1 0 0)
z = x^y (0 1 0 1)
Code
#include <iostream>
int hammingDistance(int x, int y) {
// compute the bit difference
int z = x ^ y;
int count = 0;
while (z) {
count += z & 1; // e.g. '0101' & '0001'
// shift z to the right one position
z = z >> 1; // e.g. z = '0101' >> '0010'
}
return count;
}
int main() {
std::cout << hammingDistance(1,4) << std::endl; // 2
std::cout << hammingDistance(1,3) << std::endl; // 1
}
Complexity
8.1.3 Conclusion
Utilizing bitwise operations, such as XOR (^) and bitwise AND (&), allows for ef-
ficient computation of the Hamming distance between two integers. This approach
provides a straightforward and efficient method for calculating the Hamming dis-
tance without the need for complex logic or additional data structures.
8.1.4 Exercise
• Number of 1 Bits
Input: n = 16
Output: true
Example 2
Input: n = 5
Output: false
Example 3
Input: n = 1
Output: true
Constraints
Follow up
Code
#include <iostream>
using namespace std;
bool isPowerOfFour(int n) {
// perform the divison by 4 repeatedly
while (n % 4 == 0 && n > 0) {
(continues on next page)
Output:
1
0
1
This solution repeatedly divides the given number n by 4 until n becomes either 1 or
a number that is not divisible by 4. If n becomes 1 after this process, it means that n
was originally a power of 4.
Complexity
• Runtime: O(logn).
• Extra space: O(1).
You can write down the binary representation of the powers of four to find the
pattern.
1 : 1
4 : 100
16 : 10000
(continues on next page)
You might notice the patterns are n is a positive integer having only one bit 1 in
its binary representation and it is located at the odd positions (starting from the
right).
How can you formulate those conditions?
If n has only one bit 1 in its binary representation 10...0, then n - 1 has the
complete opposite binary representation 01...1.
You can use the bit operator AND to formulate this condition
n & (n - 1) == 0
Let A be the number whose binary representation has only bits 1 at all odd positions,
then n & A is never 0.
In this problem, A < 2^31. You can chooseA = 0x55555555, the hexadecimal of 0101
0101 0101 0101 0101 0101 0101 0101.
Code
#include <iostream>
using namespace std;
bool isPowerOfFour(int n) {
// the condition of the pattern "n is a positive integer
// having only one bit 1 in its binary representation and
// it is located at the odd positions"
return n > 0 && (n & (n - 1)) == 0 && (n & 0x55555555) != 0;
}
int main() {
cout << isPowerOfFour(16) << endl;
cout << isPowerOfFour(5) << endl;
cout << isPowerOfFour(1) << endl;
}
Complexity
• Runtime: O(1).
• Extra space: O(1).
8.2.4 Conclusion
Recognizing the unique properties of powers of four, such as their binary represen-
tation, can lead to efficient solutions. Solution 2 leverages bitwise operations to
check if a number meets the criteria of being a power of four.
By examining the binary representation and ensuring that the only set bit is located
at an odd position, Solution 2 effectively determines whether the number is a power
of four in constant time complexity, without the need for division operations.
But in term of readable code, Solution 2 is not easy to understand like Solution 1,
where complexity of O(logn) is not too bad.
8.2.5 Exercise
• Power of Two
1 You have an array of integers called nums that contains n + 1 integers. Each integer
in the array falls within the range [1, n] inclusive.
Within this array, there is only one number that appears more than once. Your task
is to find and return this repeated number.
Importantly, you must solve this problem without making any modifications to the
original array nums, and you are only allowed to use a constant amount of extra
space.
Example 1
Example 2
Constraints
• How can we prove that at least one duplicate number must exist in nums?
• Can you solve the problem in linear runtime complexity?
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int findDuplicate(vector<int>& nums) {
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i] == nums[i + 1]) {
return nums[i];
}
}
return 0;
}
int main() {
vector<int> nums{1,3,4,2,2};
cout << findDuplicate(nums) << endl;
nums = {3,1,3,4,2};
cout << findDuplicate(nums) << endl;
}
Output:
2
3
The code relies on sorting to bring duplicate elements together, making it easy to
identify them during the linear pass.
8.3.3 Follow up
How can we prove that at least one duplicate number must exist in nums?
Code
#include <vector>
#include <iostream>
using namespace std;
int findDuplicate(const vector<int>& nums) {
// initialize n + 1 elements false
vector<bool> visited(nums.size());
for (auto& a : nums) {
if (visited.at(a)) {
return a;
}
visited[a] = true;
}
return 0;
(continues on next page)
Output:
2
3
Complexity
• Runtime: O(n).
• Extra space: much less than O(n). std::vector<bool> is optimized for space-
efficient.
Since n <= 10^5, you can use this size for a std::bitset to do the marking.
#include <vector>
#include <iostream>
#include <bitset>
using namespace std;
int findDuplicate(const vector<int>& nums) {
// initialize visited = '000..0' with 100001 bits 0
bitset<100001> visited;
for (auto& a : nums) {
if (visited[a]) {
return a;
}
// set a-th bit to 1
visited[a] = 1;
}
return 0;
}
int main() {
vector<int> nums{1,3,4,2,2};
cout << findDuplicate(nums) << endl;
nums = {3,1,3,4,2};
cout << findDuplicate(nums) << endl;
}
Output:
2
3
This code uses a bitset to keep track of visited elements and quickly detects any
duplicate element encountered during the iteration.
• Runtime: O(n).
• Extra space: O(1).
8.3.7 Exercise
• Missing Number
Example 2
Example 3
Constraints
For each words[i], for all words[j] with j > i, check if they do not share common
letters and compute the product of their lengths.
#include <vector>
#include <iostream>
using namespace std;
int maxProduct(const vector<string>& words) {
int maxP = 0;
for (int i = 0; i < words.size(); i++) {
// visited marks all letters that appear in words[i]
// words[i] consists of only 26 lowercase English letters
vector<bool> visited(26, false);
for (auto& c : words[i]) {
// map 'a'->0, 'b'->1, .., 'z'->25
visited[c - 'a'] = true;
}
// compare with all other words[j]
for (int j = i + 1; j < words.size(); j++) {
bool found = false;
for (auto& c : words[j]) {
if (visited[c - 'a']) {
// this words[j] has common letter with words[i]
found = true;
break;
}
}
// if words[j] disjoints words[i]
if (!found) {
// compute and update max product of their lengths
maxP = max(maxP, (int) (words[i].length() * words[j].
˓→length()));
}
}
}
return maxP;
}
int main() {
vector<string> words{"abcw","baz","foo","bar","xtfn","abcdef"};
(continues on next page)
Output:
16
4
0
This solution checks for common characters between pairs of words to determine
their product of lengths.
It iterates through each pair of words in the input vector words, maintaining a
boolean array visited to mark the presence of characters in each word. By compar-
ing the characters of each pair of words, it identifies whether there are any common
characters. If no common characters are found, it computes the product of the
lengths of the two words and updates the maximum product accordingly.
This approach optimizes the computation of the maximum product by efficiently
checking for common characters between pairs of words without requiring addi-
tional space proportional to the length of the words.
Complexity
You can map a words[i] to the bit representation of an integer n by their characters
like the following:
• If the word words[i] contains the letter 'a', the bit at position 0 of n is 1.
• If the word words[i] contains the letter 'b', the bit at position 1 of n is 1.
• ...
• If the word words[i] contains the letter 'z', the bit at position 25 of n is 1.
Then to check if two words have common letters, you just perform the bitwise op-
erator AND on them.
Example 1:
Code
#include <vector>
#include <iostream>
using namespace std;
int maxProduct(const vector<string>& words) {
int maxP = 0;
// initialize all elements of mask to 0
vector<int> mask(words.size());
for (int i = 0; i < words.size(); i++) {
// mark all characters of word[i]
for (auto& c : words[i]) {
// map 'a'->0, 'b'->1, .., 'z'->25
(continues on next page)
}
}
}
return maxP;
}
int main() {
vector<string> words{"abcw","baz","foo","bar","xtfn","abcdef"};
cout << maxProduct(words) << endl;
words = {"a","ab","abc","d","cd","bcd","abcd"};
cout << maxProduct(words) << endl;
words = {"a","aa","aaa","aaaa"};
cout << maxProduct(words) << endl;
}
Output:
16
4
0
This solution represents each word in the input vector words as a bitmask, where
each bit represents the presence or absence of a character in the word.
By iterating through the words and constructing their corresponding bitmasks, it
encodes the character information. Then, by comparing the bitmasks of pairs of
words, it identifies whether there are any common characters between them. If no
common characters are found, it computes the product of the lengths of the two
words and updates the maximum product accordingly.
This approach optimizes the computation of the maximum product by using bit-
wise operations to efficiently check for common characters between pairs of words
Complexity
8.4.4 Tips
NINE
SORTING
The arrangement of the elements in an array can hold the key to improved efficiency
and a deeper understanding of your code, which is explored in this chapter as it
delves into the usage of sorting algorithms.
Sorting is similar to putting puzzle pieces to reveal the overall structure. Rearrang-
ing elements makes it possible to retrieve data more quickly, conduct searches more
quickly, and even discover patterns and linkages that might otherwise go unnoticed.
What this chapter covers:
1. Introduction to Sorting: Establish a strong foundation by understanding the
significance of sorting, its impact on algorithmic performance, and the role of
ordering in data analysis.
2. Stability and Uniqueness: Learn about the concepts of stability and unique-
ness in sorting and how they can impact the integrity and usefulness of sorted
data.
3. Insights through Sorting: Discover scenarios where sorted data provides
valuable insights, such as identifying trends, anomalies, or patterns that in-
form decision-making.
171
9.1 Majority Element
given an array nums with a total of n elements. Your task is to find and return
1 You’re
Example 1
Example 2
Constraints
• n == nums.length.
• 1 <= n <= 5 * 10^4.
• -2^31 <= nums[i] <= 2^31 - 1.
1
https://leetcode.com/problems/majority-element/
Could you solve the problem in linear time and in O(1) space?
Code
#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;
int majorityElement(const vector<int>& nums) {
unordered_map<int,int> freq;
const int HALF = nums.size() / 2;
for (auto& a : nums) {
// count a's occurrences
freq[a]++;
if (freq[a] > HALF) {
return a;
}
}
return nums[0];
}
int main() {
vector<int> nums{3,2,3};
cout << majorityElement(nums) << endl;
nums = {2,2,1,1,1,2,2};
cout << majorityElement(nums) << endl;
}
Output:
3
2
The code effectively counts the occurrences of each integer in the array and checks
if any integer appears more than n/2 times. If so, it returns that integer as the
Complexity
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int majorityElement(vector<int>& nums) {
sort(nums.begin(), nums.end());
return nums[nums.size()/2];
}
int main() {
vector<int> nums{3,2,3};
cout << majorityElement(nums) << endl;
nums = {2,2,1,1,1,2,2};
cout << majorityElement(nums) << endl;
}
Output:
3
2
This code leverages the property of a majority element, which guarantees that it
occupies the middle position in the sorted list of elements. Sorting the array allows
us to easily access this middle element.
Since you are interested in only the middle element after sorting, the partial sorting
algorithm std::nth_element can be used in this case to reduce the cost of the full
sorting.
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int majorityElement(vector<int>& nums) {
const int mid = nums.size() / 2;
// rearrange nums such that all elements less than or equal␣
˓→nums[mid]
Output:
3
2
Complexity
In the code of Solution 3, the partial sorting algorithm std::nth_element will make
sure for all indices i and j that satisfy 0 <= i <= mid <= j < nums.length,
In other words, nums[mid] divides the array nums into two groups: all elements that
are less than or equal to nums[mid] and the ones that are greater than or equal to
nums[mid].
Those two groups are unsorted. That is why the algorithm is called partial sorting.
9.1.6 Exercise
given two integer arrays, nums1 and nums2, both sorted in non-decreasing
1 You’re
order. Additionally, you have two integers, m and n, representing the number of
1
https://leetcode.com/problems/merge-sorted-array/
Example 1
Example 2
Constraints
• nums1.length == m + n.
• nums2.length == n.
• 0 <= m, n <= 200.
• 1 <= m + n <= 200.
• -10^9 <= nums1[i], nums2[j] <= 10^9.
Follow up
Code
#include <iostream>
#include <vector>
using namespace std;
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n)
{
vector<int> result;
int i = 0;
(continues on next page)
Output:
[1,2,2,3,5,6,]
[1,]
[1,]
This solution merges two sorted arrays nums1 and nums2 into nums1 while maintain-
ing sorted order. It iterates through both arrays, comparing elements and adding
them to a temporary result vector. After the merging is complete, it replaces the
contents of nums1 with the merged result.
Complexity
Code
#include <iostream>
#include <vector>
using namespace std;
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n)
{
int k = m + n - 1;
int i = m - 1;
int j = n - 1;
while (k >= 0) {
if (j < 0) {
// nums2 is done
nums1[k--] = nums1[i--];
} else if (i < 0) {
(continues on next page)
Output:
[1,2,2,3,5,6,]
[1,]
[1,]
9.2.4 Conclusion
Solution 2 efficiently merges two sorted arrays, nums1 and nums2, into nums1 while
preserving the sorted order. It uses three pointers (k, i, and j) to perform the merge
in reverse order, which helps avoid the need for additional space.
9.2.5 Exercise
Example 2
Constraints
For each interval i, find if any other interval j such that j covers i or i covers j then
remove the smaller one from intervals.
Example 1
Code
#include <vector>
#include <iostream>
using namespace std;
//! @return true if the interval i is covered by j
inline bool isCovered(const vector<int>& i, const vector<int>& j) {
return j[0] <= i[0] && i[1] <= j[1];
}
int removeCoveredIntervals(vector<vector<int>>& intervals) {
int i = 0;
while (i < intervals.size() - 1) {
int j = i + 1;
bool erase_i = false;
while (j < intervals.size()) {
if (isCovered(intervals[i], intervals[j])) {
// remove intervals[i] from intervals
intervals.erase(intervals.begin() + i);
erase_i = true;
break;
} else if (isCovered(intervals[j], intervals[i])) {
// remove intervals[j] from intervals
intervals.erase(intervals.begin() + j);
} else {
j++;
}
}
if (!erase_i) {
i++;
}
}
return intervals.size();
}
int main() {
vector<vector<int>> intervals{{1,4},{3,6},{2,8}};
(continues on next page)
Output:
2
1
This solution effectively removes covered intervals and retains only those that do not
have others covering them. The time complexity of this solution is O(N^3), where
N is the number of intervals, as it involves nested loops and potential removal of
intervals from the list.
Complexity
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int removeCoveredIntervals(vector<vector<int>>& intervals) {
// sort the intervals using dictionary order
sort(intervals.begin(), intervals.end());
// count the intervals to be removed
int count = 0;
// keep track max right bound of all previous intervals
int maxRight = -1;
// log the left bound of the previous interval
int preLeft = -1;
for (auto& i : intervals) {
if (i[1] <= maxRight) {
// i's right bound is less than some previous one's
count++;
} else if (i[0] == preLeft) {
(continues on next page)
Output:
2
1
Complexity
1 You’re
creating a program to use as your calendar. You can add new events to the
calendar, but only if adding the event will not lead to a double booking.
A double booking occurs when two events have some time overlap, meaning there’s
a shared time period between them.
An event is represented as a pair of integers: start and end, which represent the
booking on a half-open interval [start, end). This interval includes all real num-
bers x such that start <= x < end.
You need to implement the MyCalendar class, which has the following functions:
1. MyCalendar(): Initializes the calendar object.
2. boolean book(int start, int end): This function checks if the event with
the given start and end can be added to the calendar without causing a dou-
ble booking. If it’s possible to add the event without a double booking, the
function returns true. Otherwise, it returns false, and the event is not added
to the calendar.
Example 1
Input
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]
Output
[null, true, false, true]
Explanation
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False. It can not be booked because␣
˓→time 15 is already booked by another event.
(continues on next page)
1
https://leetcode.com/problems/my-calendar-i/
Constraints
You can store the booked events in a vector and check the intersection condition
whenever you add a new event.
Code
#include <iostream>
#include <vector>
using namespace std;
class MyCalendar {
private:
vector<pair<int,int>> _events;
public:
MyCalendar() {}
bool book(int start, int end) {
for (auto& e : _events) {
// check for overlap
if (!(e.second <= start || end <= e.first)) {
return false;
}
}
_events.push_back({start, end});
return true;
}
(continues on next page)
Output:
1
0
1
This code essentially maintains a list of events and checks for overlaps when booking
a new event. If no overlaps are found, it adds the new event to the list and allows
the booking.
Complexity
Since the events have no intersection, they can be sorted. You can also consider two
events to be the same if they intersect.
With that in mind, you can use std::set to store the sorted unique events.
#include <iostream>
#include <set>
using namespace std;
using Event = pair<int,int>;
struct EventCmp {
bool operator()(const Event& lhs, const Event& rhs) const {
return lhs.second <= rhs.first;
}
};
class MyCalendar {
private:
// declare a set with custom comparison operator
set<Event, EventCmp> _events;
public:
MyCalendar() {}
bool book(int start, int end) {
auto result = _events.insert({start, end});
// result.second stores a bool indicating
// if the insertion was actually performed
return result.second;
}
};
int main() {
MyCalendar c;
std::cout << c.book(10, 20) << std::endl;
std::cout << c.book(15, 25) << std::endl;
std::cout << c.book(20, 30) << std::endl;
}
Output:
1
0
1
9.4.5 Exercise
1 Givenan integer array nums already sorted in non-decreasing order, you must re-
move duplicates so that each unique element appears at most twice. The relative
order of the elements should remain unchanged.
Since changing the array’s length in some programming languages is impossible,
you must place the result in the first part of the nums array. In other words, if there
are k elements after removing the duplicates, the first k elements of nums should
contain the final result. Anything beyond the first k elements is not important.
You should return the value of k after placing the final result in the first k slots of
the nums array.
1
https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/
Example 1
What you leave does not matter beyond the returned k (hence, they are␣
˓→underscores).
Example 2
What you leave does not matter beyond the returned k (hence, they are␣
˓→underscores).
Constraints
In order for each unique element to appear at most twice, you have to erase the
further appearances if they exist.
Since the array nums is sorted, you can determine that existence by checking if
nums[i] == nums[i-2] for 2 <= i < nums.length.
Code
#include <vector>
#include <iostream>
using namespace std;
int removeDuplicates(vector<int>& nums) {
int i = 2;
while (i < nums.size()) {
// find the element appearing more than twice
if (nums[i] == nums[i-2]) {
int j = i;
// find all duplicates
while (j < nums.size() && nums[j] == nums[i]) {
j++;
}
// keep nums[i-2] and nums[i-1] remove all later duplicates
nums.erase(nums.begin() + i, nums.begin() + j);
} else {
i++;
}
}
return nums.size();
}
void printResult(const int k, const vector<int>& nums) {
cout << k << ", [";
for (int i = 0; i < k ; i++) {
cout << nums[i] << ",";
}
cout << "]\n";
(continues on next page)
Output:
5, [1,1,2,2,3,]
7, [0,0,1,1,2,3,3,]
This solution efficiently removes duplicates from the sorted array by checking for
duplicates and erasing the excess occurrences while preserving two instances of
each unique element. It then returns the length of the modified array.
Complexity
• Runtime:
– Worst case: O(N^2/3), where N = nums.size(). The complexity of the
erase() method is linear in N. The worst case is when erase() is called
maximum N/3 times.
You might need to avoid the erase() method in the solution above to reduce the
complexity. Moreover, after removing the duplicates, the problem only cares about
the first k elements of the array nums.
If you look at the final result after removing duplication, the expected nums satisfies
You can use this invariant to reassign the array nums only the satisfied elements.
Code
#include <vector>
#include <iostream>
using namespace std;
int removeDuplicates(vector<int>& nums) {
if (nums.size() <= 2) {
return nums.size();
}
int k = 2;
int i = 2;
while (i < nums.size()) {
if (nums[i] > nums[k - 2]) {
// make sure nums[k] != nums[k-2]
nums[k++] = nums[i];
}
i++;
}
return k;
}
void printResult(const int k, const vector<int>& nums) {
cout << k << ", [";
for (int i = 0; i < k ; i++) {
cout << nums[i] << ",";
}
(continues on next page)
Output:
Output:
5, [1,1,2,2,3,]
7, [0,0,1,1,2,3,3,]
Complexity
9.5.4 Conclusion
Solution 2 effectively modifies the input array in-place, removing duplicates that
occur more than twice while maintaining the desired order of unique elements. It
does so in a single pass through the array, resulting in a time complexity of O(N),
where N is the number of elements in the array.
9.5.5 Exercise
TEN
GREEDY ALGORITHM
This chapter will explore a fascinating and highly practical problem-solving ap-
proach known as greedy algorithms. Greedy algorithms are powerful tools for
making decisions at each step of an optimization problem, often leading to efficient
and near-optimal solutions.
In this chapter, we’ll dive deep into the world of greedy algorithms, learning how
to apply them to a wide range of real-world scenarios. Here’s what you can look
forward to:
1. Understanding Greedy Algorithms: We’ll begin by establishing a solid foun-
dation in greedy algorithms. You’ll understand this approach’s key principles,
advantages, and limitations.
2. The Greedy Choice Property: Discover the core characteristic of greedy algo-
rithms—the greedy choice property. Learn how it guides us in making locally
optimal decisions at each step.
3. Greedy for Searching: Greedy algorithms can also be applied to search prob-
lems. We’ll delve into graph traversal algorithms and heuristic search meth-
ods.
4. Exercises and Problems: Reinforce your understanding of greedy algorithms
with exercises and LeetCode problems covering a wide range of greedy-based
challenges. Practice is essential for mastering this problem-solving technique.
By the end of this chapter, you’ll have a comprehensive understanding of greedy
algorithms and the ability to apply them to a wide range of problems, from opti-
mization to search. Greedy algorithms are valuable tools in your problem-solving
toolkit, and this chapter will equip you with the skills needed to confidently tackle
199
complex optimization challenges. So, let’s dive in and explore the world of greedy
algorithms!
1 You are presented with a long flowerbed containing plots, some of which are
planted with flowers (denoted by 1) and some are empty (denoted by 0). Flow-
ers cannot be planted in adjacent plots. You are given an integer array flowerbed
representing the layout of the flowerbed, and an integer n representing the number
of new flowers you want to plant.
Your task is to determine if it is possible to plant n new flowers in the flowerbed
without violating the rule of no-adjacent-flowers. If it is possible, return true; oth-
erwise, return false.
Example 1
Example 2
Code
#include <iostream>
#include <vector>
using namespace std;
bool canPlaceFlowers(vector<int>& flowerbed, int n) {
if (n == 0) {
return true;
}
flowerbed.insert(flowerbed.begin(), 0);
flowerbed.push_back(0);
int i = 1;
while (i < flowerbed.size() - 1) {
if (flowerbed[i - 1] == 0 && flowerbed[i] == 0 && flowerbed[i +␣
˓→1] == 0) {
Output:
1
0
This solution efficiently iterates through the flowerbed, planting flowers wherever
possible while adhering to the constraints. It returns true if it’s possible to plant all
the required flowers and false otherwise.
Complexity
• In this implementation, you could insert element 0 to the front and the back
of vector flowerbed to avoid writing extra code for checking the no-adjacent-
flowers rule at i = 0 and i = flowerbed.size() - 1.
• There are a few ways to insert an element to a vector. Here you can see an
example of using the methods insert and push_back of a std::vector.
• Teemo Attacking
1A string s is considered “good” if there are no two different characters in the string
that have the same frequency, meaning each character appears a unique number of
times.
You’re given a string s, and your task is to determine the minimum number of
characters you need to delete from s to make it a “good” string.
The frequency of a character in a string is the count of times that character appears
in the string. For instance, in the string "aab", the frequency of 'a' is 2, and the
frequency of 'b' is 1.
Example 1
Input: s = "aab"
Output: 0
Explanation: s is already good.
Example 2
Input: s = "aaabbbcc"
Output: 2
Explanation: You can delete two 'b's resulting in the good string
(continues on next page)
1
https://leetcode.com/problems/minimum-deletions-to-make-character-frequencies-unique/
Example 3
Input: s = "ceabaacb"
Output: 2
Explanation: You can delete both 'c's resulting in the good string
˓→"eabaab".
Note that we only care about characters that are still in the string at␣
˓→the end (i.e. frequency of 0 is ignored).
Constraints
Example 4
Code
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int minDeletions(string& s) {
// map 'a'->0, 'b'->1, ..,'z'->25
vector<int> freq(26, 0);
for (char& c: s) {
// count the frequency of character c
freq[c - 'a']++;
}
// sort freq in descending order
sort(freq.begin(), freq.end(), greater<int>());
int deletion = 0;
int currentFreq = freq.at(0); // start with the max frequency
for (int i = 1; i < freq.size() && freq.at(i) > 0; i++) {
if (currentFreq == 0) {
// delete all remaining characters
deletion += freq.at(i);
} else if (freq[i] >= currentFreq) {
// delete just enough to make the freq[i] < currentFreq
deletion += freq.at(i) - currentFreq + 1;
currentFreq--;
} else {
// do not delete on freq[i] < currentFreq
currentFreq = freq.at(i);
}
}
(continues on next page)
Output:
0
2
2
Complexity
10.2.3 Conclusion
Example 1
1
https://leetcode.com/problems/wiggle-subsequence/
Example 3
Constraints
Follow up
First, if you pick all local extrema (minima and maxima) of nums to form a subse-
quence e, then it is wiggle. Let us call it an extrema subsequence.
Code
#include <iostream>
#include <vector>
using namespace std;
int wiggleMaxLength(const vector<int>& nums) {
// nums[0] is always the first extremum
// start to find the second extremum
int i = 1;
while (i < nums.size() && nums[i] == nums[i - 1]) {
i++;
}
if (i == nums.size()) {
// all nums[i] are equal
return 1;
}
int sign = nums[i] > nums[i - 1] ? 1 : -1;
int count = 2;
i++;
while (i < nums.size()) {
if ((nums[i] - nums[i - 1]) * sign < 0) {
// nums[i] is an extremum
count++;
sign = -sign;
(continues on next page)
Output:
6
7
2
Complexity
10.3.3 Conclusion
The problem of finding the length of the longest wiggle subsequence can be effi-
ciently solved using a greedy approach. The solution iterates through the input
array, identifying alternating extremums (peaks and valleys) to form the wiggle
subsequence.
By keeping track of the current trend direction (increasing or decreasing), the so-
lution efficiently identifies extremums and increments the count accordingly. This
greedy approach ensures that each extremum contributes to increasing the length
of the wiggle subsequence, maximizing its overall length.
Example 1
Input: n = "32"
Output: 3
Explanation: 10 + 11 + 11 = 32
Example 2
Input: n = "82734"
Output: 8
Example 3
Input: n = "27346209830709182346"
Output: 9
1
https://leetcode.com/problems/partitioning-into-minimum-number-of-deci-binary-numbers/
Example 2
82734
= 11111
+ 11111
+ 10111
+ 10101
+ 10100
+ 10100
+ 10100
+ 10000
Code
#include <iostream>
using namespace std;
int minPartitions(const string& n) {
char maxDigit = '0';
for (auto& d : n) {
maxDigit = max(maxDigit, d);
(continues on next page)
Output:
3
8
9
Complexity
10.4.3 Conclusion
This problem can be efficiently solved by identifying the maximum digit in the
string. Since each deci-binary number can only contain digits from 0 to 9, the
maximum digit determines the minimum number of deci-binary numbers needed.
By finding the maximum digit in the string and converting it to an integer, the solu-
tion effectively determines the minimum number of deci-binary numbers required.
1 You are assigned to put some amount of boxes onto one truck. You
are given a 2D array boxTypes, where boxTypes[i] = [numberOfBoxes_i,
numberOfUnitsPerBox_i]:
• numberOfBoxes_i is the number of boxes of type i.
• numberOfUnitsPerBox_i is the number of units in each box of the type i.
You are also given an integer truckSize, which is the maximum number of boxes
that can be put on the truck. You can choose any boxes to put on the truck as long
as the number of boxes does not exceed truckSize.
Return the maximum total number of units that can be put on the truck.
Example 1
Constraints
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int maximumUnits(vector<vector<int>>& boxTypes, int truckSize) {
// sort for the boxes based on their number of units
sort(boxTypes.begin(), boxTypes.end(),
[](const vector<int>& a, const vector<int>& b) {
return a[1] > b[1];
});
int maxUnits = 0;
int i = 0;
while (truckSize > 0 && i < boxTypes.size()) {
if (boxTypes[i][0] <= truckSize) {
// put all boxTypes[i] if there is still room
(continues on next page)
Output:
8
91
This solution optimally loads boxes onto a truck to maximize the total number of
units that can be transported, considering both the number of boxes available and
their units per box.
Complexity
Note that two vectors can be compared. That is why you can sort them.
But in this case you want to sort them based on the number of units. That is why
you need to define the comparison function like the code above. Otherwise, the
std::sort algorithm will use the dictionary order to sort them by default.
10.5.4 Exercise
ELEVEN
DYNAMIC PROGRAMMING
This chapter explains dynamic programming, a method for solving complex prob-
lems with strategic optimization. Elegant and efficient solutions can be found by
breaking down problems into smaller subproblems and using memorization and re-
cursion. It’s like solving a puzzle by solving smaller pieces and putting them together
to form the larger picture.
What this chapter covers:
1. Introduction to Dynamic Programming: Establish a solid foundation by un-
derstanding the core principles of dynamic programming, its advantages, and
the problems it best suits.
2. Overlapping Subproblems and Optimal Substructure: Delve into the key
concepts that underlie dynamic programming, namely identifying overlapping
subproblems and exploiting optimal substructure.
3. Fibonacci Series and Beyond: Begin with classic examples like solving the
Fibonacci series and gradually progress to more intricate problems that involve
complex optimization.
4. Efficiency and Trade-offs: Understand the trade-offs involved in dynamic
programming, including the balance between time and space complexity.
5. Problem-Solving Strategies: Develop systematic strategies for approaching
dynamic programming problems, from identifying subproblems to deriving
recurrence relations.
219
11.1 Fibonacci Number
F(0) = 0, F(1) = 1
F(n) = F(n - 1) + F(n - 2), for n > 1.
Example 1
Input: n = 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.
Example 2
Input: n = 3
Output: 2
Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.
Example 3
Input: n = 4
Output: 3
Explanation: F(4) = F(3) + F(2) = 2 + 1 = 3.
1
https://leetcode.com/problems/fibonacci-number/
Code
#include <iostream>
int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
int main() {
std::cout << fib(2) << std::endl;
std::cout << fib(3) << std::endl;
std::cout << fib(4) << std::endl;
}
Output:
1
2
3
This solution computes the nth Fibonacci number using a recursive approach.
Complexity
The time complexity of this solution is exponential, specifically O(2^n). This is be-
cause it repeatedly makes two recursive calls for each n, resulting in an exponential
number of function calls and calculations. As n grows larger, the execution time
increases significantly.
The space complexity of the given recursive Fibonacci solution is O(n). This space
complexity arises from the function call stack when making recursive calls.
#include <iostream>
#include <vector>
int fib(int n) {
if (n <= 1) {
return n;
}
// store all computed Fibonacci numbers
std::vector<int> f(n + 1);
f[0] = 0;
f[1] = 1;
for (int i = 2; i <= n; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
int main() {
std::cout << fib(2) << std::endl;
std::cout << fib(3) << std::endl;
std::cout << fib(4) << std::endl;
}
Output:
1
(continues on next page)
Complexity
• Runtime: O(n).
• Extra space: O(n).
Code
#include <iostream>
int fib(int n) {
if (n <= 1) {
return n;
}
// store only two previous Fibonacci numbers
int f0 = 0;
int f1 = 1;
for (int i = 2; i <= n; i++) {
int f2 = f1 + f0;
// update for next round
f0 = f1;
f1 = f2;
}
return f1;
}
int main() {
std::cout << fib(2) << std::endl;
std::cout << fib(3) << std::endl;
(continues on next page)
Output:
1
2
3
This solution calculates the nth Fibonacci number iteratively using two variables to
keep track of the last two Fibonacci numbers.
Complexity
• Runtime: O(n).
• Extra space: O(1).
11.1.5 Conclusion
The Fibonacci sequence can be efficiently computed using various techniques, in-
cluding recursion with memoization, bottom-up dynamic programming, or even
optimizing space usage by storing only the necessary previous Fibonacci numbers.
Solutions 2 and 3 demonstrate dynamic programming approaches, where Fibonacci
numbers are computed iteratively while storing intermediate results to avoid redun-
dant computations.
Solution 3 further optimizes space usage by only storing the necessary previous Fi-
bonacci numbers, resulting in a space complexity of O(1). Understanding these dif-
ferent approaches and their trade-offs is essential for selecting the most appropriate
solution based on the problem constraints and requirements.
1A robot starts at the top-left corner of a grid with dimensions m x n. It can move
either down or right at each step. The robot’s goal is to reach the bottom-right
corner of the grid.
The problem is to determine the number of unique paths the robot can take to reach
the bottom-right corner.
Example 1
Input: m = 3, n = 7
Output: 28
1
https://leetcode.com/problems/unique-paths/
Input: m = 3, n = 2
Output: 3
Explanation:
From the top-left corner, there are a total of 3 ways to reach the␣
˓→bottom-right corner:
Example 3
Input: m = 7, n = 3
Output: 28
Example 4
Input: m = 3, n = 3
Output: 6
Constraints
At each point, the robot has two ways of moving: right or down. Let P(m,n) is the
wanted result. Then you have a recursive relationship:
P(1, n) = P(m, 1) = 1.
Code
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n) {
if (m == 1 || n == 1) {
return 1;
}
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
}
int main() {
std::cout << uniquePaths(3,7) << std::endl;
std::cout << uniquePaths(7,3) << std::endl;
std::cout << uniquePaths(3,2) << std::endl;
std::cout << uniquePaths(3,3) << std::endl;
}
Output:
28
28
3
6
This is a recursive solution that breaks down the problem into two subproblems:
• uniquePaths(m-1, n)
• uniquePaths(m, n-1)
Each recursive call reduces either the m or n value by 1.
The base case is when m == 1 or n == 1, where there is only 1 unique path.
Code
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n) {
(continues on next page)
Output:
28
28
3
6
You can rephrase the relationship inside the loop like this:
“new value” = “old value” + “previous value”;
Then you do not have to store all values of all rows.
Code
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n) {
// store the number of unique paths for each column in each row
vector<int> dp(n, 1);
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j - 1];
}
}
return dp[n - 1];
}
int main() {
std::cout << uniquePaths(3,7) << std::endl;
std::cout << uniquePaths(7,3) << std::endl;
std::cout << uniquePaths(3,2) << std::endl;
std::cout << uniquePaths(3,3) << std::endl;
}
Complexity
11.2.5 Conclusion
Solution 3 uses only a 1D vector dp of size n to store the number of unique paths for
each column.
First, it initializes all elements of dp to 1, as there’s exactly one way to reach any cell
in the first row or first column.
Then, it iterates through the grid, starting from the second row and second column
(i.e., indices (1, 1)). For each cell, it updates the value in dp by adding the value
from the cell directly above it and the value from the cell to the left of it. This step
efficiently accumulates the number of unique paths to reach the current cell.
Finally, the value at dp[n-1] contains the total number of unique paths to reach the
bottom-right corner of the grid, which is returned as the result.
A bit of wonder
1 You have a collection of positive integers called nums, where each integer is distinct.
Your task is to find the largest subset answer from this collection, such that for every
pair of elements (answer[i], answer[j]) within this subset:
• Either answer[i] is a multiple of answer[j] (i.e., answer[i] % answer[j] ==
0), or
• answer[j] is a multiple of answer[i] (i.e., answer[j] % answer[i] == 0).
If there are multiple possible solutions, you can return any of them.
Example 1
Example 2
Example 3
Note that for a sorted nums, if nums[i] | nums[j] for some i < j, then maxSubset[j]
is a subset of maxSubset[i].
For example, maxSubset[2] is a subset of maxSubset[0] in Example 3 because
nums[0] = 2 | 4 = nums[2].
Code
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
Output:
[2,1,]
[8,4,2,1,]
This solution uses dynamic programming with memoization to find the largest di-
visible subset of a given set of numbers.
The largestDivisibleSubsetOf function recursively computes the largest divisible
subset starting from a particular index i in the sorted array nums. It memoizes
the computed subsets in an unordered map _map to avoid redundant computa-
tions. By iteratively calling largestDivisibleSubsetOf for each index i in the
sorted array and updating the answer with the largest subset found so far, the
largestDivisibleSubset function computes the largest divisible subset of the in-
put array nums.
This approach optimizes the computation by avoiding repeated calculations and
leveraging dynamic programming techniques to efficiently explore the solution
space.
Complexity
In the brute-force solution above, you used a big map to log all maxSubset[i] though
you need only the largest one at the end.
One way to save memory (and eventually improve performance) is just storing
the representative of the chain relationship between the values nums[i] of the
maxSubset through their indices mapping.
That means if maxSubset[i] = [nums[i0] | nums[i1] | ... | nums[iN1]] |
nums[iN]], you could log pre[iN] = iN1, . . . , prev[i1] = i0.
Example 3
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> largestDivisibleSubset(vector<int>& nums) {
if (nums.size() <= 1) {
return nums;
}
sort(nums.begin(), nums.end());
// the size of the resulting subset
int maxSize = 0;
Output:
[2,1,]
[8,4,2,1,]
This solution finds the largest divisible subset of a given set of numbers by dynam-
ically updating the size of the subsets and maintaining the previous index of each
element in their largest subset.
It iterates through the sorted array of numbers, updating the size of the largest
subset that ends with each element by considering the previous elements that are
factors of the current element. By keeping track of the maximum subset size and the
index of the largest element in the subset, it constructs the largest divisible subset.
This approach optimizes the computation by avoiding redundant calculations and
leveraging dynamic programming techniques to efficiently explore the solution
space.
Complexity
11.4 Triangle
1 You’reprovided with a triangle array. Your goal is to find the smallest possible
sum of a path from the top of the triangle to the bottom.
At each step, you have the option to move to an adjacent number in the row below.
Specifically, if you’re at index i in the current row, you can move to either index i
or index i + 1 in the next row.
Example 1
1
https://leetcode.com/problems/triangle/
Constraints
Follow up
• Could you do this using only O(n) extra space, where n is the total number of
rows in the triangle?
You can store all minimum paths at every positions (i,j) so you can compute the
next ones with this relationship.
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minimumTotal(const vector<vector<int>>& triangle) {
(continues on next page)
}
// right most number
minPath[i][N-1] = triangle[i][N-1] + minPath[i-1][N-2];
}
// pick the min path among the ones (begin -> end)
// go to the bottom (n-1)
return *min_element(minPath[n-1].begin(), minPath[n-1].end());
}
int main() {
vector<vector<int>> triangle{{2},{3,4},{6,5,7},{4,1,8,3}};
cout << minimumTotal(triangle) << endl;
triangle = {{-10}};
cout << minimumTotal(triangle) << endl;
}
Output:
11
-10
This solution finds the minimum path sum from the top to the bottom of a triangle,
represented as a vector of vectors. It uses dynamic programming to calculate the
minimum path sum.
The algorithm initializes a minPath vector of vectors to store the minimum path sum
for each element in the triangle. It starts by setting the first row of minPath to be
Complexity
You do not need to store all paths for all rows. The computation of the next row
only depends on its previous one.
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minimumTotal(const vector<vector<int>>& triangle) {
const int n = triangle.size();
// store only min path for each row
vector<int> minPath(n);
minPath[0] = triangle[0][0];
for (int i = 1; i < n; i++) {
// right most number
minPath[i] = triangle[i][i] + minPath[i - 1];
(continues on next page)
}
// left most number
minPath[0] = triangle[i][0] + minPath[0];
}
return *min_element(minPath.begin(), minPath.end());
}
int main() {
vector<vector<int>> triangle{{2},{3,4},{6,5,7},{4,1,8,3}};
cout << minimumTotal(triangle) << endl;
triangle = {{-10}};
cout << minimumTotal(triangle) << endl;
}
Output:
11
-10
Complexity
Example 1
Constraints
• m == obstacleGrid.length.
• n == obstacleGrid[i].length.
• 1 <= m, n <= 100.
• obstacleGrid[i][j] is 0 or 1.
#include <vector>
#include <iostream>
using namespace std;
int uniquePathsWithObstacles(const vector<vector<int>>& obstacleGrid) {
const int row = obstacleGrid.size();
const int col = obstacleGrid[0].size();
vector<vector<int>> np(row, vector<int>(col, 0));
for (int i = 0; i < row && obstacleGrid[i][0] == 0; i++) {
// can move as long as there is no obstacle
np[i][0] = 1;
}
for (int j = 0; j < col && obstacleGrid[0][j] == 0; j++) {
// can move as long as there is no obstacle
np[0][j] = 1;
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (obstacleGrid[i][j] == 0) {
// can move since there is obstacle
np[i][j] = np[i - 1][j] + np[i][j - 1];
}
}
}
return np[row - 1][col - 1];
}
int main() {
vector<vector<int>> obstacleGrid = {{0,0,0},{0,1,0},{0,0,0}};
cout << uniquePathsWithObstacles(obstacleGrid) << endl;
obstacleGrid = {{0,1},{0,0}};
cout << uniquePathsWithObstacles(obstacleGrid) << endl;
}
Output:
2
1
11.5.3 Conclusion
This solution computes the number of unique paths in an m x n grid with obsta-
cles using dynamic programming. It initializes a 2D vector np of the same size as
obstacleGrid to store the number of unique paths for each cell.
First, it initializes the top row and left column of np. If there are no obstacles in the
top row or left column of obstacleGrid, it sets the corresponding cells in np to 1
because there’s only one way to reach any cell in the top row or left column.
Then, it iterates through the grid starting from the second row and second column
(i.e., indices (1, 1)). For each cell, if there’s no obstacle (obstacleGrid[i][j] == 0),
it updates the value in np by summing up the values from the cell directly above it
and the cell to the left of it. This step efficiently accumulates the number of unique
paths while avoiding obstacles.
Finally, the value at np[row-1][col-1] contains the total number of unique paths to
reach the bottom-right corner of the grid, which is returned as the result.
11.5.4 Exercise
TWELVE
COUNTING
In this chapter, we will explore the benefits of counting elements and how it can
enhance the efficiency of different algorithms and operations. By tallying occur-
rences, you can gain valuable insights that simplify computations and give you a
better understanding of your data.
Counting elements is like organizing a messy room. When you categorize items, it
becomes easier to access and make decisions. In algorithms, counting allows you
to optimize processes by identifying the most frequent elements or solving complex
problems more efficiently.
What this chapter covers:
1. Introduction to Counting: Lay the foundation by understanding the signif-
icance of counting elements, its role in performance enhancement, and the
various scenarios where counting is crucial.
2. Frequency Counting: Explore the technique of tallying element occurrences,
enabling you to identify the most frequent items within a dataset quickly.
3. Counting Sort: Delve into the world of counting sort, a specialized sorting
algorithm that capitalizes on the power of element counting to achieve excep-
tional performance.
4. Problem-Solving with Counts: Develop approaches to solve problems that
benefit from element counting, from optimizing search operations to identify-
ing anomalies.
249
12.1 Single Number
1 You’reprovided with a non-empty array of integers called nums. In this array, every
element occurs twice except for one element that appears only once. Your task is to
identify and find that unique element.
To solve this problem, your solution needs to have a linear runtime complexity and
utilize only a constant amount of extra space.
Example 1
Example 2
Example 3
Count how many times each element appears in the array. Then return the one
appearing only once.
Code
#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;
int singleNumber(const vector<int>& nums) {
unordered_map<int, int> count;
for (auto& n : nums) {
count[n]++;
}
int single;
for (auto& pair : count) {
if (pair.second == 1) {
single = pair.first;
break;
}
}
return single;
}
int main() {
vector<int> nums{2,2,1};
cout << singleNumber(nums) << endl;
(continues on next page)
Output:
1
4
1
This solution effectively finds the single number by counting the occurrences of each
element in the array and selecting the one with a count of 1.
Complexity
• Runtime: O(N).
• Extra space: O(N).
You can also use the bitwise XOR operator to cancel out the duplicated elements in
the array. The remain element is the single one.
a XOR a = 0.
a XOR 0 = a.
Code
#include <vector>
#include <iostream>
using namespace std;
int singleNumber(const vector<int>& nums) {
(continues on next page)
Output:
1
4
1
Complexity
• Runtime: O(N).
• Extra space: O(1).
12.1.4 Conclusion
Leveraging bitwise XOR (^) operations offers an efficient solution to find the single
number in an array. Solution 2 utilizes the property of XOR where XORing a number
with itself results in 0.
By XORing all the numbers in the array, Solution 2 effectively cancels out pairs of
identical numbers, leaving only the single number behind. This approach achieves
a linear time complexity without the need for additional data structures, providing
a concise and efficient solution.
• Missing Number
1 Youhave a string called s. Your objective is to locate the index of the first character
in the string that does not repeat anywhere else in the string. If such a character
doesn’t exist, return -1.
Example 1
Input: s = "leetcode"
Output: 0
Example 2
Input: s = "loveleetcode"
Output: 2
Example 3
Input: s = "aabb"
Output: -1
1
https://leetcode.com/problems/first-unique-character-in-a-string/
Code
#include <iostream>
#include <unordered_map>
using namespace std;
int firstUniqChar(const string& s) {
unordered_map<char, int> count;
for (auto& c : s) {
count[c]++;
}
for (int i = 0; i < s.length(); i++) {
if (count[s[i]] == 1) {
return i;
}
}
return -1;
}
int main() {
cout << firstUniqChar("leetcode") << endl;
cout << firstUniqChar("loveleetcode") << endl;
cout << firstUniqChar("aabb") << endl;
}
Output:
0
2
-1
This solution finds the index of the first non-repeating character in a string by using
an unordered map to count the occurrences of each character.
Complexity
From the constraints “s consists of only lowercase English letters”, you can use an
array of 26 elements to store the counts.
Code
#include <iostream>
#include <vector>
using namespace std;
int firstUniqChar(const string& s) {
// map 'a'->0, 'b'->1, .., 'z'->25
// initializes an array of 26 elements, all set to zero
std::array<int, 26> count{};
for (auto& c : s) {
count[c - 'a']++;
}
for (int i = 0; i < s.length(); i++) {
if (count[s[i] - 'a'] == 1) {
return i;
}
(continues on next page)
Output:
0
2
-1
Complexity
12.2.4 Conclusion
Utilizing hash maps or arrays to count the frequency of characters in a string pro-
vides an efficient way to identify the first unique character. Both solutions use this
approach to iterate through the string and count the occurrences of each character.
By storing the counts in a data structure indexed by the character value, the so-
lutions achieve a linear time complexity proportional to the length of the string.
Solution 2 further optimizes memory usage by employing an array with a fixed size
corresponding to the lowercase English alphabet, avoiding the overhead associated
with hash maps.
1 You’reprovided with an array of integers called nums and an integer k. Each oper-
ation involves selecting two numbers from the array whose sum is equal to k, and
then removing them from the array. Your goal is to determine the maximum count
of such operations you can perform on the array.
Example 1
Example 2
You can use a map to count the appearances of the elements of nums.
Example 2
Code
#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;
(continues on next page)
Output:
2
1
Complexity
This solution utilizes an unordered map to store the frequency of each element
encountered while iterating through nums.
By examining each element a in nums, it checks if k - a exists in the map and if its
frequency is greater than 0. If so, it increments the count of pairs and decrements
the frequency of both a and k - a, ensuring that each pair is counted only once.
This approach optimizes the computation by efficiently tracking the frequencies of
elements and identifying valid pairs whose sum equals the target value without
requiring additional space proportional to the size of the array.
12.3.4 Exercise
• Two Sum
THIRTEEN
PREFIX SUMS
This chapter will introduce you to a technique called prefix sums. This technique
can make calculations much faster and more efficient. The chapter will explain how
cumulative aggregation works and can help optimize your operations.
Prefix sums are like building blocks that can create many different algorithms. They
make it easier to handle cumulative values and allow you to solve complex problems
much more efficiently than before.
What this chapter covers:
1. Introduction to Prefix Sums: Establish the groundwork by understanding
the essence of prefix sums, their role in performance enhancement, and the
scenarios where they shine.
2. Prefix Sum Array Construction: Dive into the mechanics of constructing
a prefix sum array, unlocking the potential to access cumulative values effi-
ciently.
3. Range Sum Queries: Explore how prefix sums revolutionize calculating sums
within a given range, enabling quick and consistent results.
4. Subarray Sum Queries: Delve into the technique’s application in efficiently
determining the sum of elements within any subarray of an array.
5. Prefix Sum Variants: Discover the versatility of prefix sums in solving prob-
lems related to averages, running maximum/minimum values, and more.
6. Problem-Solving with Prefix Sums: Develop strategies for solving diverse
problems by incorporating prefix sums, from optimizing sequence operations
to speeding up specific algorithms.
263
13.1 Running Sum of 1d Array
1 Given an array called nums, calculate the running sum of its elements and return
the resulting array. The running sum at index i is the sum of elements from index 0
to i in the nums array.
Example 1
Example 2
Example 3
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> runningSum(const vector<int>& nums) {
vector<int> rs;
int s = 0;
for (auto& n : nums) {
s += n;
rs.push_back(s);
}
return rs;
}
void printResult(const vector<int>& sums) {
cout << "[";
for (auto& s: sums) {
cout << s << ",";
}
cout << "]\n";
}
int main() {
vector<int> nums{1,2,3,4};
auto rs = runningSum(nums);
printResult(rs);
nums = {1,1,1,1,1};
rs = runningSum(nums);
printResult(rs);
nums = {3,1,2,10,1};
(continues on next page)
Output:
[1,3,6,10,]
[1,2,3,4,5,]
[3,4,6,16,17,]
This solution iterates through the input array nums, calculates the running sum at
each step, and appends the running sums to a result vector. This approach efficiently
computes the running sums in a single pass through the array.
Complexity
If nums is allowed to be changed, you could use it to store the result directly.
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> runningSum(vector<int>& nums) {
for (int i = 1; i < nums.size(); i++) {
nums[i] += nums[i - 1];
}
return nums;
}
(continues on next page)
Output:
[1,3,6,10,]
[1,2,3,4,5,]
[3,4,6,16,17,]
Complexity
Solution 2 directly modifies the input array nums to store the running sums by iter-
atively updating each element with the cumulative sum of the previous elements.
This approach efficiently calculates the running sums in a single pass through the
array.
1 You’re provided with an array of integers called nums. Your task is to identify a
subarray (a consecutive sequence of numbers) that has the highest sum. Once you
find this subarray, return the sum of its elements.
Example 1
Example 2
Constraints
13.2.2 Solution
The subarrays you want to find should not have negative prefix sums. A negative
prefix sum would make the sum of the subarray smaller.
Example 1
Code
#include <vector>
#include <iostream>
using namespace std;
int maxSubArray(const vector<int>& nums) {
int maxSum = -10000; // just chose some negative number to start
int currSum = 0; // sum of current subarray
for (auto& num : nums) {
if (currSum < 0) {
// start a new subarray from this num
currSum = num;
(continues on next page)
Output:
6
1
23
Complexity
13.2.3 Conclusion
This solution is the Kadane’s algorithm to find the maximum sum of a contiguous
subarray in the given array nums.
It iterates through the elements of the array, updating currSum to either the current
element or the sum of the current element and the previous currSum, whichever
is greater. By considering whether adding the current element improves the overall
13.2.4 Exercise
1 Given an integer array nums, return an array answer such that answer[i] is equal
to the product of all the elements of nums except nums[i].
The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.
You must write an algorithm that runs in O(n) time and without using the division
operation.
Example 1
Constraints
Follow up
• Can you solve the problem in O(1) extra space complexity? (The output array
does not count as extra space for space complexity analysis.)
To avoid division operation, you can compute the prefix product and the suffix one
of nums[i].
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> productExceptSelf(const vector<int>& nums) {
const int n = nums.size();
vector<int> prefix(n);
prefix[0] = 1;
// compute all prefix products nums[0]*nums[1]*..*nums[i-1]
for (int i = 1; i < n; i++) {
(continues on next page)
Output:
24 12 8 6
0 0 9 0 0
This solution computes the product of all elements in an array except for the current
element.
It accomplishes this by first computing two arrays: prefix and suffix. The prefix
Complexity
13.3.3 Solution 2: Use directly vector answer to store the prefix product
In the solution above you can use directly vector answer for prefix and merge the
last two loops into one.
Code
#include <vector>
#include <iostream>
using namespace std;
vector<int> productExceptSelf(const vector<int>& nums) {
const int n = nums.size();
vector<int> answer(n);
answer[0] = 1;
// compute all prefix products nums[0]*nums[1]*..*nums[i-1]
for (int i = 1; i < n; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}
int suffix = 1;
for (int i = n - 2; i >= 0; i--) {
(continues on next page)
Output:
24 12 8 6
0 0 9 0 0
This code efficiently calculates the products of all elements in the nums vector except
for the element at each index using two passes through the array. The first pass cal-
culates products to the left of each element, and the second pass calculates products
to the right of each element.
13.3.4 Conclusion
The problem of computing the product of all elements in an array except the element
at the current index can be efficiently solved using different approaches. Solution
1 utilizes two separate passes through the array to compute prefix and suffix prod-
ucts independently. By first computing prefix products from left to right and then
suffix products from right to left, this solution efficiently calculates the product of
all elements except the one at the current index.
Solution 2 offers a more concise approach by combining the computation of prefix
and suffix products into a single pass through the array. By iteratively updating a
variable to compute suffix products while simultaneously updating the elements of
the answer array, this solution achieves the desired result more efficiently with only
one pass through the array.
13.3.5 Exercise
1 Youhave an array of integers called nums and an integer k. Your task is to determine
the count of contiguous subarrays within this array, where the sum of elements in
each subarray is equal to the value of k.
1
https://leetcode.com/problems/subarray-sum-equals-k/
Example 2
Constraints
For each element, for all subarrays starting from it, choose the satisfied ones.
Example 3
For nums = [1, -1, 0] and k = 0, you get 3 subarrays for the result:
• There are three subarrays starting from 1, which are [1], [1, -1], and [1,
-1, 0]. Only the last two are satisfied.
• There are two subarrays starting from -1, which are [-1] and [-1, 0]. None
is satisfied.
• Only [0] is the subarray starting from 0. It is satisfied.
#include <iostream>
#include <vector>
using namespace std;
int subarraySum(const vector<int>& nums, int k) {
int count = 0;
for (int i = 0; i < nums.size(); i++) {
int sum = 0;
for (int j = i; j < nums.size(); j++) {
sum += nums[j];
if (sum == k) {
count++;
}
}
}
return count;
}
int main() {
vector<int> nums{1,1,1};
cout << subarraySum(nums, 2) << endl;
nums = {1,2,3};
cout << subarraySum(nums, 3) << endl;
nums = {1,-1,0};
cout << subarraySum(nums, 0) << endl;
}
Output:
2
2
3
In the solution above, many sums can be deducted from the previous ones.
Example 4
For nums = [1, 2, 3, 4]. Assume the sum of the subarrays [1], [1, 2], [1,
2, 3], [1, 2, 3, 4] were computed in the first loop. Then the sum of any other
subarray can be deducted from those values.
• sum([2, 3]) = sum([1, 2, 3]) - sum([1]).
• sum([2, 3, 4]) = sum([1, 2, 3, 4]) - sum([1]).
• sum([3, 4]) = sum(1, 2, 3, 4) - sum(1, 2).
In general, assume you have computed the sum sum[i] for the subarray [nums[0],
nums[1], ..., nums[i]] for all 0 <= i < nums.length. Then the sum of the subarray
[nums[j+1], nums[j+2], ..., nums[i]] for any 0 <= j <= i can be computed as
sum[i] - sum[j].
Code
#include <iostream>
#include <vector>
using namespace std;
int subarraySum(const vector<int>& nums, int k) {
const int n = nums.size();
vector<int> sum(n);
sum[0] = nums[0];
// compute all prefix sums nums[0] + .. + nums[i]
for (int i = 1; i < n; i++) {
(continues on next page)
Output:
2
2
3
This solution uses the concept of prefix sum to efficiently calculate the sum of sub-
arrays. It then iterates through the array to find subarrays with a sum equal to k,
and the nested loop helps in calculating the sum of various subarray ranges. The
time complexity of this solution is improved compared to the brute-force approach.
You can rewrite the condition sum[i] - sum[j] == k in the inner loop of the Solution
2 to sum[i] - k == sum[j].
Then that loop can rephrase to “checking if sum[i] - k was already a value of some
computed sum[j]”.
Now you can use an unordered_map to store the sums as indices for the fast lookup.
Code
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int subarraySum(const vector<int>& nums, int k) {
int count = 0;
// count the frequency of all subarrays' sums
unordered_map<int, int> sums;
int sumi = 0;
for (int i = 0; i < nums.size(); i++) {
sumi += nums[i];
if (sumi == k) {
count++;
}
auto it = sums.find(sumi - k);
if (it != sums.end()) {
// it->second is the count of j so far
// having sum[j] = sum[i] - k
count += it->second;
(continues on next page)
Output:
2
2
3
Complexity
13.4.5 Conclusion
FOURTEEN
TWO POINTERS
This chapter will explore the Two Pointers technique, a strategic approach that can
help solve complex problems quickly and effectively. We’ll show you how to use
simultaneous traversal to streamline operations, optimize algorithms, and extract
solutions from complicated scenarios.
The Two Pointers technique is like exploring a cryptic map from both ends to find
the treasure. It can enhance your problem-solving skills and help you tackle intricate
challenges with a broader perspective.
What this chapter covers:
1. Introduction to Two Pointers: Lay the foundation by understanding the
essence of the Two Pointers technique, its adaptability, and its role in unravel-
ing complex problems.
2. Two Pointers Approach: Dive into the mechanics of the technique, explor-
ing scenarios where two pointers traverse a sequence to locate solutions or
patterns.
3. Collision and Separation: Discover the duality of the technique, where point-
ers can converge to solve particular problems or diverge to address different
aspects of a challenge.
4. Optimal Window Management: Explore how the Two Pointers technique
optimizes sliding window problems, facilitating efficient substring or subarray
analysis.
5. Intersection and Union: Uncover the technique’s versatility in solving prob-
lems that involve intersecting or uniting elements within different sequences.
285
6. Problem-Solving with Two Pointers: Develop strategies to address diverse
problems through the Two Pointers technique, from array manipulation to
string analysis.
1 Given the head of a singly linked list, return the middle node of the linked list.
If there are two middle nodes, return the second middle node.
Example 1
Example 2
1
https://leetcode.com/problems/middle-of-the-linked-list/
Code
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
ListNode* middleNode(ListNode* head) {
ListNode *node = head;
int count = 0;
while (node) {
count++;
node = node->next;
}
int i = 1;
node = head;
while (i <= count/2) {
node = node->next;
i++;
}
return node;
}
void print(const ListNode *head) {
ListNode *node = head;
std::cout << "[";
while (node) {
(continues on next page)
ListNode six(6);
five.next = &six;
result = middleNode(&one);
print(result);
}
Output:
[3,4,5,]
[4,5,6,]
This solution first counts the total number of nodes in the linked list, and then it
iterates to the middle node using the count variable.
Code
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
ListNode* middleNode(ListNode* head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
void print(const ListNode *head) {
ListNode *node = head;
std::cout << "[";
while (node) {
std::cout << node->val << ",";
(continues on next page)
ListNode six(6);
five.next = &six;
result = middleNode(&one);
print(result);
}
Output:
[3,4,5,]
[4,5,6,]
This solution uses two pointers, a slow pointer and a fast pointer, to find the middle
node of a linked list. Both pointers start from the head of the list, and in each
iteration, the slow pointer moves one step forward while the fast pointer moves two
steps forward. This ensures that the slow pointer reaches the middle node of the list
when the fast pointer reaches the end.
By advancing the pointers at different speeds, the algorithm identifies the middle
node of the linked list. If the list has an odd number of nodes, the slow pointer will
be positioned at the middle node. If the list has an even number of nodes, the slow
pointer will be positioned at the node closer to the middle of the list.
Finally, the algorithm returns the slow pointer, which points to the middle node of
the linked list.
This approach optimizes the computation by traversing the linked list only once and
Complexity
14.1.4 OBS!
• The approach using slow and fast pointers looks very nice and faster. But it
is not suitable to generalize this problem to any relative position (one-third,
a quarter, etc.). Moreover, long expressions like fast->next->...->next are
not recommended.
• Though the counting nodes approach does not seem optimized, it is more
readable, scalable and maintainable.
14.1.5 Exercise
1 Given head, the head of a linked list, determine if the linked list has a cycle in it.
Return true if there is a cycle in the linked list. Otherwise, return false.
1
https://leetcode.com/problems/linked-list-cycle/
Example 2
Example 3
• The number of the nodes in the list is in the range [0, 10^4].
• -10^5 <= Node.val <= 10^5.
Follow up
Code
#include <unordered_map>
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
bool hasCycle(ListNode *head) {
std::unordered_map<ListNode*, bool> m;
while (head) {
if (m[head]) {
// found this node marked in the map
return true;
}
m[head] = true; // mark this node visited
head = head->next;
}
return false;
}
int main() {
{
ListNode three(3);
(continues on next page)
Output:
1
1
0
This solution uses a hash map to track visited nodes while traversing the linked list.
By iterating through the linked list and marking pointers to visited nodes in the hash
map, it detects cycles in the linked list. If a node is found marked true in the map,
it indicates the presence of a cycle, and the function returns true. Otherwise, if the
end of the linked list is reached without finding any node marked, it confirms the
absence of a cycle, and the function returns false.
This approach optimizes the computation by leveraging the hash map to efficiently
detect cycles in the linked list without requiring additional space proportional to the
length of the list.
Imagine there are two runners both start to run along the linked list from the head.
One runs twice faster than the other.
If the linked list has a cycle in it, they will meet at some point. Otherwise, they
never meet each other.
Example 1
Example 2
The slower runs [1,2,1,2,...] while the faster runs [1,1,1,...]. They meet each
other at node 1 after two steps.
Code
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
bool hasCycle(ListNode *head) {
if (head == nullptr) {
return false;
}
(continues on next page)
Complexity
14.2.4 Conclusion
Solution 2 uses two pointers, a fast pointer and a slow pointer, to detect cycles in a
linked list.
Both pointers start from the head of the list, and the fast pointer moves two steps
forward while the slow pointer moves one step forward in each iteration. By com-
paring the positions of the fast and slow pointers, the algorithm detects cycles in the
linked list.
If the fast pointer catches up with the slow pointer at any point during traversal, it
indicates the presence of a cycle, and the function returns true. Otherwise, if the
fast pointer reaches the end of the list without intersecting with the slow pointer, it
confirms the absence of a cycle, and the function returns false.
This approach optimizes the computation by simultaneously advancing two pointers
at different speeds to efficiently detect cycles in the linked list.
14.2.5 Exercise
1 Given an array of integers nums, half of the integers in nums are odd, and the other
half are even.
Sort the array so that whenever nums[i] is odd, i is odd, and whenever nums[i] is
even, i is even.
Return any answer array that satisfies this condition.
Example 1
Example 2
Constraints:
For each 0 <= i < nums.length, if nums[i] has the same parity with i, you do
nothing. Otherwise you need to find another nums[j] that has the same parity with
i to swap with nums[i].
Example 1
Code
#include<vector>
#include<iostream>
using namespace std;
vector<int> sortArrayByParityII(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++) {
if (i % 2 != nums[i] % 2) {
// find suitable nums[j] to swap
for (int j = i + 1; j < nums.size(); j++) {
if (nums[j] % 2 == i % 2) {
swap(nums[i], nums[j]);
break;
}
}
}
}
return nums;
(continues on next page)
Output:
4 5 2 7
0 1 8 3 2 9 4 5 2 1 4 7
4 3
648 831 560 997 192 829 986 897 424 843
This solution iteratively scans through the array and swap elements to ensure that
the parity (even or odd) of each element matches its index modulo 2.
The algorithm iterates over each index of the array. For each index i, if the parity
of the element at index i does not match i % 2, it implies that the element is in the
wrong position. In such cases, the algorithm searches for the next element with the
correct parity (i.e., even or odd) starting from index i + 1. Once found, it swaps
the elements at indices i and j, where j is the index of the next element with the
correct parity.
Complexity
In the Bubble Sort approach, you do not make use of the constraint that half of the
integers in nums are even. Because of that, these are unnecessary things:
1. The loops scan through full nums.
2. The loops are nested. That increases the complexity.
3. The swap(nums[i], nums[j]) happens even when nums[j] was already in
place, i.e. nums[j] had the same parity with j (Why to move it?).
Here is a two-pointer approach which takes the important constraint into account.
Code
#include<vector>
#include<iostream>
#include <algorithm>
using namespace std;
vector<int> sortArrayByParityII(vector<int>& nums) {
int N = nums.size();
int evenPos = 0;
int oddPos = N - 1;
while (evenPos < N) {
(continues on next page)
Output:
4 5 2 7
0 1 8 3 2 9 4 5 2 1 4 7
4 3
648 831 560 997 192 829 986 897 424 843
Complexity
14.3.4 Conclusion
Solution 2 uses two pointers, one starting from the beginning of the array (evenPos)
and the other starting from the end (oddPos), to efficiently identify misplaced ele-
ments.
By incrementing evenPos by 2 until an odd element is found and decrementing
oddPos by 2 until an even element is found, the algorithm can swap these elements
to ensure that even-indexed elements contain even values and odd-indexed ele-
ments contain odd values. This process iterates until all even and odd elements are
correctly positioned.
14.3.5 Exercise
1 Youare given an integer array height of length n. There are n vertical lines drawn
such that the two endpoints of the i-th line are (i, 0) and (i, height[i]).
Find two lines that together with the x-axis form a container, such that the container
contains the most water.
Return the maximum amount of water a container can store.
Notice that you may not slant the container.
Example 1
1
https://leetcode.com/problems/container-with-most-water/
Constraints
• n == height.length.
• 2 <= n <= 10^5.
• 0 <= height[i] <= 10^4.
For each line i, find the line j > i such that it gives the maximum amount of water
the container (i, j) can store.
Code
#include <iostream>
#include <vector>
using namespace std;
int maxArea(const vector<int>& height) {
int maxA = 0;
for (int i = 0; i < height.size() - 1; i++) {
for (int j = i + 1; j < height.size(); j++) {
maxA = max(maxA, min(height[i], height[j]) * (j - i));
}
}
return maxA;
}
int main() {
vector<int> height{1,8,6,2,5,4,8,3,7};
cout << maxArea(height) << endl;
(continues on next page)
Output:
49
1
This solution computes the maximum area of water that can be trapped between
two vertical lines by iterating through all possible pairs of lines. By considering all
combinations of lines and calculating the area using the formula (min(height[i],
height[j]) * (j - i)), where height[i] and height[j] represent the heights
of the two lines and (j - i) represents the width between them, it effectively
evaluates the area formed by each pair and updates maxA with the maximum area
encountered.
This approach optimizes the computation by exhaustively considering all possible
pairs of lines and efficiently computing the area without requiring additional space.
Complexity
Any container has left line i and right line j satisfying 0 <= i < j < height.length.
The biggest container you want to find satisfies that condition too.
You can start from the broadest container with the left line i = 0 and the right line j
= height.length - 1. Then by moving i forward and j backward, you can narrow
down the container to find which one will give the maximum amount of water it
can store.
Depending on which line is higher, you can decide which one to move next. Since
you want a bigger container, you should move the shorter line.
maxArea = 8.
Code
#include <iostream>
#include <vector>
using namespace std;
int maxArea(const vector<int>& height) {
int maxA = 0;
int i = 0;
int j = height.size() - 1;
while (i < j) {
if (height[i] < height[j]) {
maxA = max(maxA, height[i] * (j - i) );
i++;
} else {
maxA = max(maxA, height[j] * (j - i) );
j--;
(continues on next page)
Output:
49
1
Complexity
14.4.4 Conclusion
1 Given the head of a linked list, remove the n-th node from the end of the list and
return its head.
Example 1
Example 2
Example 3
Follow up
Code
#include <iostream>
#include <vector>
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
using namespace std;
ListNode* removeNthFromEnd(ListNode* head, int n) {
vector<ListNode*> nodes;
ListNode* node = head;
while (node)
{
nodes.push_back(node);
node = node->next;
}
node = nodes[nodes.size() - n];
(continues on next page)
if (node == head) {
// remove head if n == nodes.size()
head = node->next;
} else {
ListNode* pre = nodes[nodes.size() - n - 1];
pre->next = node->next;
}
return head;
}
void printList(const ListNode *head) {
ListNode* node = head;
cout << "[";
while (node) {
cout << node->val << ",";
node = node->next;
}
cout << "]\n";
}
int main() {
ListNode five(5);
ListNode four(4, &five);
ListNode three(3, &four);
ListNode two(2, &three);
ListNode one(1, &two);
auto head = removeNthFromEnd(&one, 2);
printList(head);
head = removeNthFromEnd(&five, 1);
printList(head);
head = removeNthFromEnd(&four, 1);
printList(head);
}
Output:
[1,2,3,5,]
[]
(continues on next page)
This solution uses a vector to store pointers to all nodes in the linked list, enabling
easy access to the node to be removed and its predecessor.
By iterating through the linked list and storing pointers to each node in the vector,
it constructs a representation of the linked list in an array-like structure. Then, it
retrieves the node to be removed using its index from the end of the vector. Finally,
it handles the removal of the node by updating the next pointer of its predecessor
or updating the head pointer if the node to be removed is the head of the linked list.
This approach optimizes the computation by sacrificing space efficiency for simplic-
ity of implementation and ease of manipulation of linked list elements.
Complexity
The distance between the removed node and the end (nullptr) of the list is always
n.
You can apply the two-pointer technique as follows.
Let the slower runner start after the faster one n nodes. Then when the faster
reaches the end of the list, the slower reaches the node to be removed.
Code
#include <iostream>
#include <vector>
struct ListNode {
int val;
ListNode *next;
(continues on next page)
Output:
[1,2,3,5,]
[]
[4,]
Complexity
14.5.4 Conclusion
Solution 2 uses two pointers, a fast pointer and a slow pointer, to remove the nth
node from the end of a linked list.
Initially, both pointers start from the head of the list. The fast pointer moves n
steps ahead, effectively positioning itself n nodes ahead of the slow pointer. Then,
while the fast pointer is not at the end of the list, both pointers move forward
simultaneously. This ensures that the slow pointer stays n nodes behind the fast
pointer, effectively reaching the node preceding the nth node from the end when
the fast pointer reaches the end of the list. Finally, the nth node from the end is
removed by updating the next pointer of the node preceding it.
This approach optimizes the computation by traversing the linked list only once and
using two pointers to efficiently locate the node to be removed.
1 Givenan integer array nums, you need to find one continuous subarray that if you
only sort this subarray in ascending order, then the whole array will be sorted in
ascending order.
Return the shortest such subarray and output its length.
Example 1
Example 2
Constraints:
Follow up
Example 1
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int findUnsortedSubarray(const vector<int>& nums) {
(continues on next page)
Output:
5
0
0
This solution compares the original array with a sorted version of itself to identify
the unsorted boundaries efficiently.
Complexity
• Runtime: O(N*logN) due to the sorting step, where N is the number of elements
in the nums vector.
• Extra space: O(N).
Assume the subarray A = [nums[0], ..., nums[i - 1]] is sorted. What would
be the wanted right position for the subarray B = [nums[0], ..., nums[i - 1],
nums[i]]?
If nums[i] is smaller than max(A), the longer subarray B is not in ascending order.
You might need to sort it, which means right = i.
Similarly, assume the subarray C = [nums[j + 1], ..., nums[n - 1]] is sorted.
What would be the wanted left position for the subarray D = [nums[j], nums[j +
1], ..., nums[n - 1]]?
If nums[j] is bigger than min(C), the longer subarray D is not in ascending order.
You might need to sort it, which means left = j
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int findUnsortedSubarray(const vector<int>& nums) {
const int n = nums.size();
int right = 0;
int max = nums[0];
for (int i = 0; i < nums.size(); i++) {
if (nums[i] < max) {
right = i;
} else {
max = nums[i];
}
}
int left = n - 1;
int min = nums[n - 1];
for (int j = n - 1; j >= 0; j--) {
if (nums[j] > min) {
left = j;
(continues on next page)
Output:
5
0
0
Complexity
Solution 2 helped you identify the shortest subarray (by the left and right indices)
needed to be sorted in order to sort the whole array.
That means in some cases you can sort an array with complexity O(N + m*logm) <
O(N*logN) where N is the length of the whole array and m is the length of the shortest
subarray.
FIFTEEN
MATHEMATICS
This chapter will explore how mathematics and programming create efficient solu-
tions. We’ll cover mathematical concepts and show how they can be integrated into
coding to enhance problem-solving skills.
Mathematics and programming complement each other and can lead to innovative
outcomes. By applying mathematical principles, you can refine algorithms, identify
patterns, streamline processes, and better understand your code’s underlying logic.
What this chapter covers:
1. Introduction to Mathematics in Coding: Set the stage by understanding
the symbiotic relationship between mathematics and programming and how
mathematical concepts enrich your coding toolkit.
2. Number Theory and Modular Arithmetic: Delve into number theory, under-
standing modular arithmetic and its applications.
3. Combinatorics and Probability: Uncover the power of combinatorial math-
ematics and probability theory in solving problems related to permutations,
combinations, and statistical analysis.
4. Problem-Solving with Mathematics: Develop strategies for leveraging math-
ematical concepts to solve problems efficiently and elegantly, from optimiza-
tion tasks to simulation challenges.
321
15.1 Excel Sheet Column Number
1 Given a string columnTitle that represents the column title as appears in an Excel
sheet, return its corresponding column number.
For example:
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...
Example 1
Example 2
Constraints
Let us write down some other columnTitle strings and its value.
"A" = 1
"Z" = 26
"AA" = 27
"AZ" = 52
"ZZ" = 702
"AAA" = 703
"A" = 1 = 1
"Z" = 26 = 26
"AA" = 27 = 26 + 1
"AZ" = 52 = 26 + 26
"ZZ" = 702 = 26*26 + 26
"AAA" = 703 = 26*26 + 26 + 1
If you map 'A' = 1, ..., 'Z' = 26, the values can be rewritten as
"A" = 1 = 'A'
"Z" = 26 = 'Z'
(continues on next page)
Code
#include <iostream>
using namespace std;
int titleToNumber(const string& columnTitle) {
int column = 0;
for (auto& c : columnTitle) {
// The ASCII value of 'A' is 65.
column = 26*column + (c - 64);
}
return column;
}
int main() {
cout << titleToNumber("A") << endl;
cout << titleToNumber("AB") << endl;
cout << titleToNumber("ZY") << endl;
}
Output:
1
28
701
Complexity
Implementation notes
If you write it as
26*(26*(26*(0 + a) + b) + c) + d,
15.1.3 Exercise
Example 1
Input: n = 27
Output: true
Explanation: 27 = 3^3.
Example 2
Input: n = 0
Output: false
Explanation: There is no x where 3^x = 0.
Example 3
Input: n = -1
Output: false
Explanation: There is no x where 3^x = (-1).
1
https://leetcode.com/problems/power-of-three/
Follow up
Code
#include <iostream>
using namespace std;
bool isPowerOfThree(int n) {
while (n % 3 == 0 && n > 0) {
n /= 3;
}
return n == 1;
}
int main() {
cout << isPowerOfThree(27) << endl;
cout << isPowerOfThree(0) << endl;
cout << isPowerOfThree(-1) << endl;
}
Output:
1
0
0
This solution repeatedly divides the input by 3 until it either becomes 1 (indicating
that it was a power of three) or cannot be divided further by 3.
• Runtime: O(logn).
• Extra space: O(1).
A power of three must divide another bigger one, i.e. 3𝑥 |3𝑦 where 0 ≤ 𝑥 ≤ 𝑦.
Because the constraint of the problem is 𝑛 ≤ 231 − 1, you can choose the biggest
power of three in this range to test the others.
It is 319 = 1162261467. The next power will exceed 231 = 2147483648.
Code
#include <iostream>
using namespace std;
bool isPowerOfThree(int n) {
return n > 0 && 1162261467 % n == 0;
}
int main() {
cout << isPowerOfThree(27) << endl;
cout << isPowerOfThree(0) << endl;
cout << isPowerOfThree(-1) << endl;
}
Output:
1
0
0
• Runtime: O(1).
• Extra space: O(1).
Though Solution 2 offers a direct approach without the need for iteration, it is not
easy to understand like Solution 1, where complexity of O(logn) is not too bad.
15.2.5 Exercise
1 Youare given an array prices where prices[i] is the price of a given stock on the
i-th day.
You want to maximize your profit by choosing a single day to buy one stock and
choosing a different day in the future to sell that stock.
Return the maximum profit you can achieve from this transaction. If you cannot
achieve any profit, return 0.
1
https://leetcode.com/problems/best-time-to-buy-and-sell-stock/
Note that buying on day 2 and selling on day 1 is not allowed because␣
˓→you must buy before you sell.
Example 2
Constraints
For each day i, find the day j > i that gives maximum profit.
Code
#include <vector>
#include <iostream>
using namespace std;
int maxProfit(const vector<int>& prices) {
int maxProfit = 0;
for (int i = 0; i < prices.size(); i++) {
(continues on next page)
Output:
5
0
This solution uses a brute force approach to find the maximum profit. It compares
the profit obtained by buying on each day with selling on all subsequent days and
keeps track of the maximum profit found.
Complexity
Given a past day i, the future day j > i that gives the maximum profit is the day
that has the largest price which is bigger than prices[i].
Conversely, given a future day j, the past day i < j that gives the maximum profit
is the day with the smallest price.
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int maxProfit(const vector<int>& prices) {
int maxProfit = 0;
int i = 0;
while (i < prices.size()) {
// while prices are going down,
// find the bottommost one to start
while (i < prices.size() - 1 && prices[i] >= prices[i + 1]) {
i++;
}
// find the largest price in the future
auto imax = max_element(prices.begin() + i, prices.end());
// find the smallest price in the past
auto imin = min_element(prices.begin() + i, imax);
maxProfit = max(maxProfit, *imax - *imin);
// next iteration starts after the found largest price
i = distance(prices.begin(), imax) + 1;
}
return maxProfit;
}
int main() {
vector<int> prices{7,1,5,3,6,4};
cout << maxProfit(prices) << endl;
prices = {7,6,4,3,1};
cout << maxProfit(prices) << endl;
prices = {2,4,1,7};
cout << maxProfit(prices) << endl;
prices = {2,4,1};
cout << maxProfit(prices) << endl;
}
Output:
(continues on next page)
This solution optimally finds the maximum profit by iterating through the array only
once, avoiding the need for nested loops.
Complexity
Given a future day j, the past day i that gives the maximum profit is the day with
minimum price.
Code
#include <vector>
#include <iostream>
using namespace std;
int maxProfit(const vector<int>& prices) {
int maxProfit = 0;
// keep track the minimum price so fat
int minPrice = prices[0];
for (int i = 1; i < prices.size(); i++) {
// update the minimum price
minPrice = min(minPrice, prices[i]);
maxProfit = max(maxProfit, prices[i] - minPrice);
}
return maxProfit;
}
(continues on next page)
Output:
5
0
6
2
This solution efficiently computes the maximum profit by iterating through the array
only once, maintaining the minimum buying price and updating the maximum profit
accordingly.
Complexity
15.3.5 Conclusion
The problem of finding the maximum profit that can be achieved by buying and
selling a stock can be efficiently solved using different approaches. Solutions 1, 2,
and 3 each offer a different approach to solving the problem, including brute-force
iteration, finding local minima and maxima, and maintaining a running minimum
price.
Solution 3 stands out as the most efficient approach, achieving a linear time com-
plexity by iterating through the prices only once and updating the minimum price
15.3.6 Exercise
15.4 Subsets
1 Givenan integer array nums of unique elements, return all possible subsets (the
power set).
The solution set must not contain duplicate subsets. Return the solution in any
order.
Example 1
Example 2
15.4.2 Solution
You might need to find the relationship between the result of the array nums with
the result of itself without the last element.
Example 3
You can see the powerset of Example 3 was obtained from the one in Example 2 with
additional subsets [2], [1,2]. These new subsets were constructed from subsets [],
[1] of Example 2 appended with the new element 2.
Similarly, the powerset of Example 1 was obtained from the one in Example 3 with
the additional subsets [3], [1,3], [2,3], [1,2,3]. These new subsets were con-
structed from the ones of Example 3 appended with the new element 3.
Code
#include <vector>
#include <iostream>
using namespace std;
vector<vector<int>> subsets(const vector<int>& nums) {
vector<vector<int>> powerset = {{}};
int i = 0;
while (i < nums.size()) {
vector<vector<int>> newSubsets;
(continues on next page)
i++;
}
return powerset;
}
void print(const vector<vector<int>>& powerset) {
for (auto& set : powerset ) {
cout << "[";
for (auto& element : set) {
cout << element << ",";
}
cout << "]";
}
cout << endl;
}
int main() {
vector<int> nums{1,2,3};
auto powerset = subsets(nums);
print(powerset);
nums = {1};
powerset = subsets(nums);
print(powerset);
}
Output:
[][1,][2,][1,2,][3,][1,3,][2,3,][1,2,3,]
[][1,]
15.4.3 Conclusion
This solution generates subsets by iteratively adding each element of nums to the
existing subsets and accumulating the results.
Note that in for (auto subset : powerset) you should not use reference auto&
because we do not want to change the subsets that have been created.
15.4.4 Exercise
• Subsets II
1 Given an integer array nums of size n, return the minimum number of moves re-
quired to make all array elements equal.
In one move, you can increment or decrement an element of the array by 1.
1
https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/
Example 2
Constraints
• n == nums.length.
• 1 <= nums.length <= 10^5.
• -10^9 <= nums[i] <= 10^9.
You are asked to move all elements of an array to the same value M. The problem
can be reduced to identifying what M is.
First, moving elements of an unsorted array and moving a sorted one are the same.
So you can assume nums is sorted in some order. Let us say it is sorted in ascending
order.
Second, M must be in between the minimum element and the maximum one. Ap-
parently!
We will prove that M will be the median of nums, which is nums[n/2] of the sorted
nums.
Example 3
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minMoves2(vector<int>& nums) {
sort(nums.begin(), nums.end());
const int median = nums[nums.size() / 2];
int moves = 0;
for (int& a: nums) {
moves += abs(a - median);
}
return moves;
}
int main() {
vector<int> nums{1,2,3};
(continues on next page)
Output:
2
16
This solution leverages the concept of the median to minimize the total absolute dif-
ferences between each element and the median, resulting in the minimum number
of moves to equalize the array.
Complexity
• Runtime: O(n*logn) due to the sorting step, where n is the number of elements
in the nums array.
• Extra space: O(1).
What you only need in Solution 1 is the median value. Computing the total number
of moves in the for loop does not require the array nums to be fully sorted.
In this case, you can use std::nth_element to reduce the runtime complexity.
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minMoves2(vector<int>& nums) {
const int mid = nums.size() / 2;
(continues on next page)
Output:
2
16
This solution efficiently finds the median of the nums array in linear time using
std::nth_element and then calculates the minimum number of moves to make all
elements equal to this median.
Complexity
In the code of Solution 2, the partial sorting algorithm std::nth_element will make
sure for all indices i and j that satisfy 0 <= i <= mid <= j < nums.length, then
With this property, if mid = nums.length / 2, then the value of nums[mid] is un-
changed no matter how nums is sorted or not.
15.5.5 Exercise
are given an integer array nums of length n where nums is a permutation of the
1 You
Example 2
Constraints:
0 -> 5,
1 -> 4,
2 -> 0,
3 -> 3,
4 -> 1,
(continues on next page)
You can always rearrange the definition of a permutation into groups of cyclic chains
(factors).
The set s[k] in this problem is such a chain. In mathematics, it is called a cycle;
because the chain (0, 5, 6, 2) is considered the same as (5, 6, 2, 0), (6, 2, 0,
5) or (2, 0, 5, 6) in Example 1.
Assume you have used some elements of the array nums to construct some cycles.
To construct another one, you should start with the unused elements.
The problem leads to finding the longest cycle of a given permutation.
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int arrayNesting(const vector<int>& nums) {
int maxLen{0};
vector<bool> visited(nums.size());
for (auto& i : nums) {
if (visited[i]) {
continue;
}
int len{0};
// visit the cycle starting from i
while (!visited[i]) {
visited[i] = true;
i = nums[i];
(continues on next page)
int main() {
vector<int> nums = {5,4,0,3,1,6,2};
cout << arrayNesting(nums) << endl;
nums = {0,1,2};
cout << arrayNesting(nums) << endl;
nums = {0,2,1};
cout << arrayNesting(nums) << endl;
nums = {2,0,1};
cout << arrayNesting(nums) << endl;
}
Output:
4
1
2
3
Complexity
The problem of finding the length of the longest cycle in an array can be efficiently
solved using a cycle detection approach. This solution efficiently detects cycles in
the array by using a boolean array to mark visited elements.
By iterating through each element in the array and visiting the cycle starting from
each unvisited element, the solution identifies the length of each cycle and updates
the maximum length accordingly. This approach ensures that each cycle is visited
only once and maximizes the length of the longest cycle in the array.
1 Givenan integer n, return the number of strings of length n that consist only of
vowels (a, e, i, o, u) and are lexicographically sorted.
A string s is lexicographically sorted if for all valid i, s[i] is the same as or comes
before s[i+1] in the alphabet.
Example 1
Input: n = 1
Output: 5
Explanation: The 5 sorted strings that consist of vowels only are ["a",
˓→"e","i","o","u"].
1
https://leetcode.com/problems/count-sorted-vowel-strings/
Input: n = 2
Output: 15
Explanation: The 15 sorted strings that consist of vowels only are
["aa","ae","ai","ao","au","ee","ei","eo","eu","ii","io","iu","oo","ou",
˓→"uu"].
Note that "ea" is not a valid string since 'e' comes after 'a' in the␣
˓→alphabet.
Example 3
Input: n = 33
Output: 66045
Constraints
Example 3
For n = 3:
• There is (always) only one string starting from u, which is uuu.
• There are 3 strings starting from o: ooo, oou and ouu.
• There are 6 strings starting from i: iii, iio, iiu, ioo, iou, iuu.
• There are 10 strings starting from e: eee, eei, eeo, eeu, eii, eio, eiu, eoo,
eou, euu.
Findings
In Example 3, if you ignore the leading vowel of those strings, then the shorted
strings of the line above all appear in the ones of the line below and the remaining
strings of the line below come from n = 2.
More precisely:
• All the shorted strings oo, ou and uu starting from o appear on the ones starting
from i. The remaining ii, io, iu starting from i come from the strings of
length n = 2 (see Example 2).
• Similarly, all shorted strings ii, io, iu, oo, ou, uu starting from i appear on
the ones starting from e. The remaining ee, ei, eo, eu come from n = 2.
• And so on.
That leads to the following recursive relationship.
Let S(x, n) be the number of strings of length n starting from a vowel x. Then
• S('o', n) = S('o', n - 1) + S('u', n) for all n > 1.
• S('i', n) = S('i', n - 1) + S('o', n) for all n > 1.
• S('e', n) = S('e', n - 1) + S('i', n) for all n > 1.
• S('a', n) = S('a', n - 1) + S('e', n) for all n > 1.
• S(x, 1) = 1 for all vowels x.
• S('u', n) = 1 for all n >= 1.
For this problem, you want to compute
#include <iostream>
using namespace std;
int countVowelStrings(int n) {
int a, e, i, o, u;
a = e = i = o = u = 1;
while (n > 1) {
o += u;
i += o;
e += i;
a += e;
n--;
}
return a + e + i + o + u;
}
int main() {
cout << countVowelStrings(1) << endl;
cout << countVowelStrings(2) << endl;
cout << countVowelStrings(33) << endl;
}
Output:
5
15
66045
This solution efficiently computes the count of vowel strings of length n using dy-
namic programming, updating the counts based on the previous lengths to avoid
redundant calculations.
• Runtime: O(n).
• Extra space: O(1).
The strings of length n you want to count are formed by a number of 'a', then some
number of 'e', then some number of 'i', then some number of 'o' and finally
some number of 'u'.
So it looks like this
s = "aa..aee..eii..ioo..ouu..u".
And you want to count how many possibilities of such strings of length n.
One way to count it is using combinatorics in mathematics.
If you separate the groups of vowels by '|' like this
s = "aa..a|ee..e|ii..i|oo..o|uu..u",
the problem becomes counting how many ways of putting those 4 separators '|' to
form a string of length n + 4.
In combinatorics, the solution is 𝑛+4
(︀ )︀ (︀𝑛)︀
4 , where 𝑘 is the binomial coefficient:
(︂ )︂
𝑛 𝑛!
= .
𝑘 𝑘!(𝑛 − 𝑘)!
#include <iostream>
using namespace std;
int countVowelStrings(int n) {
return (n + 1) * (n + 2) * (n + 3) * (n + 4) / 24;
}
int main() {
cout << countVowelStrings(1) << endl;
cout << countVowelStrings(2) << endl;
cout << countVowelStrings(33) << endl;
}
Output:
5
15
66045
Complexity
• Runtime: O(1).
• Extra space: O(1).
15.7.4 Conclusion
The problem of counting the number of strings of length n that consist of the vowels
‘a’, ‘e’, ‘i’, ‘o’, and ‘u’ in sorted order can be efficiently solved using combinatorial
techniques. Solution 1 uses dynamic programming to iteratively calculate the count
of strings for each length up to n, updating the counts based on the previous counts.
This approach efficiently computes the count of sorted vowel strings for the given
length n without requiring excessive memory usage or computational overhead.
Solution 2 offers a more direct approach by utilizing a combinatorial formula to
calculate the count of sorted vowel strings directly based on the given length n.
By leveraging the combinatorial formula, this solution avoids the need for iterative
calculations and achieves the desired result more efficiently.
1 Given an integer n, return the decimal value of the binary string formed by con-
catenating the binary representations of 1 to n in order, modulo 10^9 + 7.
Example 1
Input: n = 1
Output: 1
Explanation: "1" in binary corresponds to the decimal value 1.
Example 2
Input: n = 3
Output: 27
Explanation: In binary, 1, 2, and 3 corresponds to "1", "10", and "11".
After concatenating them, we have "11011", which corresponds to the␣
˓→decimal value 27.
Example 3
Input: n = 12
Output: 505379714
Explanation: The concatenation results in
˓→"1101110010111011110001001101010111100".
There must be some relationship between the result of n and the result of n - 1.
First, let us list some first values of n.
• For n = 1: the final binary string is "1", its decimal value is 1.
• For n = 2: the final binary string is "110", its decimal value is 6.
• For n = 3: the final binary string is "11011", its decimal value is 27.
Look at n = 3, you can see the relationship between the decimal value of "11011"
and the one of "110" (of n = 2) is:
27 = 6 * 2^2 + 3
Dec("11011") = Dec("110") * 2^num_bits("11") + Dec("11")
Result(3) = Result(2) * 2^num_bits(3) + 3.
6 = 1 * 2^2 + 2
Dec("110") = Dec("1") * 2^num_bits("10") + Dec("10")
Result(2) = Result(1) * 2^num_bits(2) + 2.
Code
#include <cmath>
#include <iostream>
int concatenatedBinary(int n) {
unsigned long long result = 1;
(continues on next page)
Output:
1
27
505379714
Complexity
• Runtime: O(n*logn).
• Extra space: O(1).
15.8.3 Conclusion
1 Given an integer n, return the least number of perfect square numbers that sum to
n.
A perfect square is an integer that is the square of an integer; in other words, it
is the product of some integer with itself. For example, 1, 4, 9, and 16 are perfect
squares while 3 and 11 are not.
Example 1
Input: n = 9
Output: 1
Explanation: 9 is already a perfect square.
Example 2
Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.
Example 3
Input: n = 7
Output: 4
Explanation: 7 = 4 + 1 + 1 + 1.
1
https://leetcode.com/problems/perfect-squares/
Input: n = 12
Output: 3
Explanation: 12 = 4 + 4 + 4.
Constraints
Let us call the function to be computed numSquares(n), which calculates the least
number of perfect squares that sum to n.
Here are the findings.
1. If n is already a perfect square then numSquares(n) = 1.
2. Otherwise, it could be written as n = 1 + (n-1), or n = 4 + (n-4), or n = 9
+ (n-9), etc. which means n is a sum of a perfect square (1, 4 or 9, etc.) and
another number m < n. That leads to the problems numSquares(m) of smaller
values m.
3. If you have gotten the results of the smaller problems numSquares(n-1),
numSquares(n-4), numSquares(n-9), etc. then numSquares(n) = 1 + the
minimum of those results.
Example 4
Code
#include <iostream>
#include <cmath>
#include <unordered_map>
using namespace std;
//! @return the least number of perfect squares that sum to n
//! @param[out] ns a map stores all intermediate results
int nsq(int n, unordered_map<int, int>& ns) {
auto it = ns.find(n);
if (it != ns.end()) {
return it->second;
}
const int sq = sqrt(n);
if (sq * sq == n) {
// n is already a perfect square
ns[n] = 1;
return 1;
}
// if n is written as 1 + 1 + .. + 1,
// maximum of result is n
int result = n;
// finding the minimum nsq(n - i*i) across all i <= sqrt(n)
for (int i = 1; i <= sq; i++) {
//
result = min(result, nsq(n - i*i, ns));
}
// write n as imin^2 + (n - imin^2)
ns[n] = result + 1;
(continues on next page)
Output:
3
2
The key idea of this algorithm is to build the solution incrementally, starting from
the smallest perfect squares, and use memoization to store and retrieve intermediate
results. By doing this, it efficiently finds the minimum number of perfect squares
required to sum up to n.
Complexity
The dynamic programming solution above is good enough. But for those who are
interested in Algorithmic Number Theory, there is a very interesting theorem that
can solve the problem directly without recursion.
It is called Lagrange’s Four-Square Theorem, which states
every natural number can be represented as the sum of four integer squares.
It was proven by Lagrange in 1770.
n = 12 = 4 + 4 + 4 + 0 or 12 = 1 + 1 + 1 + 9.
Applying to our problem, numSquares(n) can only be 1, 2, 3, or 4. Not more.
It turns into the problem of
identifying when numSquares(n) returns 1, 2, 3, or 4.
Here are the cases.
1. If n is a perfect square, numSquares(n) = 1.
2. There is another theorem, Legendre’s Three-Square Theorem, which states
that numSquares(n) cannot be 1, 2, or 3 if n can be expressed as
𝑛 = 4𝑎 (8 · 𝑏 + 7),
where 𝑎, 𝑏 are nonnegative integers.
In other words, numSquares(n) = 4 if n is of this form.
Example 3
Code
#include <iostream>
#include <cmath>
using namespace std;
bool isSquare(int n) {
int sq = sqrt(n);
return sq * sq == n;
}
int numSquares(int n) {
if (isSquare(n)) {
return 1;
}
(continues on next page)
Output:
3
2
This solution finds the minimum number of perfect squares required to sum up to
the given integer n by first applying mathematical properties and Legendre’s three-
square theorem to simplify the problem and then using a loop to find possible com-
binations of two perfect squares.
𝑛 = 4𝑎 · 𝑚.
Code
#include <iostream>
#include <cmath>
using namespace std;
bool isSquare(int n) {
int sq = sqrt(n);
(continues on next page)
Output:
3
2
15.9.5 Conclusion
• The title of this coding challenge (Perfect squares) gives you a hint it is more
about mathematics than coding technique.
• It is amazing from Lagrange’s Four-Square Theorem there are only four possi-
bilities for the answer to the problem. Not many people knowing it.
• You can get an optimal solution to a coding problem when you know some-
thing about the mathematics behind it.
Hope you learn something interesting from this code challenge.
Have fun with coding and mathematics!
15.9.6 Exercise
SIXTEEN
CONCLUSION
Congratulations! You have made it to the end of this book! I hope you have enjoyed
and learned from the coding challenges and solutions presented in this book.
Through these challenges, you have not only improved your coding skills but also
your problem-solving abilities, logical thinking, and creativity. You have been
exposed to different programming techniques and algorithms, which have broad-
ened your understanding of the programming world. These skills and knowledge
will undoubtedly benefit you in your future coding endeavors.
Remember, coding challenges are not only a way to improve your coding skills but
also a fun and engaging way to stay up-to-date with the latest technology trends.
They can also help you to prepare for technical interviews, which are a crucial part
of landing a programming job.
In conclusion, I encourage you to continue exploring the world of coding challenges,
as there is always something new to learn and discover. Keep practicing, keep
learning, and keep challenging yourself. With hard work and dedication, you can
become an expert in coding and a valuable asset to any team.
365
366 Chapter 16. Conclusion
APPENDIX
Here are some best practices to keep in mind when working on coding challenges:
Before jumping into writing code, take the time to read and understand the prob-
lem statement. Make sure you understand the input and output requirements, any
constraints or special cases, and the desired algorithmic approach.
Once you understand the problem, take some time to plan and sketch out a high-
level algorithmic approach. Write pseudocode to help break down the problem into
smaller steps and ensure that your solution covers all cases.
After writing your code, test it thoroughly to make sure it produces the correct
output for a range of test cases. Consider edge cases, large inputs, and unusual
scenarios to make sure your solution is robust.
367
A.4 Optimize for time and space complexity
When possible, optimize your code for time and space complexity. Consider the Big
O notation of your solution and try to reduce it if possible. This can help your code
to run faster and more efficiently.
Make sure your code is easy to read and understand. Use meaningful variable
names, indent properly, and comment your code where necessary. This will make
it easier for other programmers to read and understand your code, and will help
prevent errors and bugs.
Once you have a working solution, submit it for review and feedback. Pay attention
to any feedback you receive and use it to improve your coding skills and approach
for future challenges.
The more coding challenges you complete, the better you will become. Keep prac-
ticing and challenging yourself to learn new techniques and approaches to problem-
solving.
In conclusion, coding challenges are a great way to improve your coding skills and
prepare for technical interviews. By following these best practices, you can ensure
that you approach coding challenges in a structured and efficient manner, producing
clean and readable code that is optimized for time and space complexity.
368
THANK YOU!
Thank you for taking the time to read my first published book. I would love to hear
your thought about this book.
I hope it has been a valuable experience and that you are excited to continue your
coding journey. Best of luck with your coding challenges, and remember to have fun
along the way!
369
370
ABOUT THE AUTHOR
Nhut Nguyen is a seasoned software engineer and career coach with nearly a decade
of experience in the tech industry.
He was born and grew up in Ho Chi Minh City, Vietnam. In 2012, he moved to
Denmark for a Ph.D. in mathematics at the Technical University of Denmark. After
his study, Nhut Nguyen switched to the industry in 2016 and has worked at various
tech companies in Copenhagen, Denmark.
Nhut’s passion for helping aspiring software engineers succeed led him to write
several articles and books where he shares his expertise and practical strategies to
help readers navigate their dream job and work efficiently.
With a strong background in mathematics and computer science and a deep un-
derstanding of the industry, Nhut is dedicated to empowering individuals to unlock
their full potential, land their dream jobs and live happy lives.
Learn more at https://nhutnguyen.com.
371
INDEX
A Morse Code, 92
algorithm complexity, 2
P
B Partial sort, 175
binomial coefficient, 351 permutation, 344
bit masking, 168 power set, 335
bitwise AND, 153
bitwise XOR, 153, 252
R
readable code, 4
C
Coding challenges, 1
S
sliding window, 78, 104
D Sorting, 160
dictionary order, 185 std::accumulate, 109
dummy node, 47 std::bitset, 162
std::nth_element, 176
F std::priority_queue, 131, 136, 141,
Fast and Slow, 289, 295 147
Fibonacci Number, 220 std::set, 190
std::sort, 217
K std::stoi, 109
Kadane's algorithm, 270 std::swap, 27
L
LeetCode, 1
M
memoization, 236
372