Problem Solvers Coding Techniques
Problem Solvers Coding Techniques
March, 2024
The Problem Solver’s Guide To Coding
First edition. March, 2024.
ISBN 978-87-975174-0-6 (paperback)
ISBN 978-87-975174-1-3 (PDF)
ISBN 978-87-430-5777-2 (hardcover)
Copyright © 2024 Nhut Nguyen.
All rights reserved.
www.nhutnguyen.com
To my dearest mother, Nguyen Thi Kim Sa.
iii
iv
PREFACE
v
The book’s overview
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 (intu-
ition) 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.
This book is tailored to benefit a wide audience, from students beginning their pro-
gramming journey to experienced developers looking to enhance their skills. Re-
gardless of your experience level, whether you’re preparing for coding interviews
or simply seeking to improve your problem-solving abilities, this book is designed
to meet your needs.
vi
As a minimum requirement, you are supposed to have some basic background in
C++ programming language, data structures and algorithms like a second-year un-
dergraduate 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, March 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3 Shift 2D Grid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4 Find All Numbers Disappeared in an Array . . . . . . . . . . . . . . . . 18
2.5 Rotate Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.6 Spiral Matrix II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.7 Daily Temperatures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3 Linked List 37
3.1 Merge Two Sorted Lists . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.2 Remove Linked List Elements . . . . . . . . . . . . . . . . . . . . . . . 42
3.3 Intersection of Two Linked Lists . . . . . . . . . . . . . . . . . . . . . . 48
3.4 Swap Nodes in Pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.5 Add Two Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4 Hash Table 67
4.1 Roman to Integer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.2 Maximum Erasure Value . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.3 Find and Replace Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5 String 79
5.1 Valid Anagram . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
ix
5.2 Detect Capital . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
5.3 Unique Morse Code Words . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.4 Unique Email Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . 90
5.5 Longest Substring Without Repeating Characters . . . . . . . . . . . . 96
5.6 Compare Version Numbers . . . . . . . . . . . . . . . . . . . . . . . . . 100
6 Stack 105
6.1 Baseball Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
6.2 Valid Parentheses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
6.3 Backspace String Compare . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.4 Remove All Adjacent Duplicates in String II . . . . . . . . . . . . . . . 115
9 Sorting 159
9.1 Majority Element . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
9.2 Merge Sorted Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
9.3 Remove Covered Intervals . . . . . . . . . . . . . . . . . . . . . . . . . 169
9.4 My Calendar I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
9.5 Remove Duplicates from Sorted Array II . . . . . . . . . . . . . . . . . 178
x
11.5 Unique Paths II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
12 Counting 231
12.1 Single Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
12.2 First Unique Character in a String . . . . . . . . . . . . . . . . . . . . . 235
12.3 Max Number of K-Sum Pairs . . . . . . . . . . . . . . . . . . . . . . . . 239
15 Mathematics 297
15.1 Excel Sheet Column Number . . . . . . . . . . . . . . . . . . . . . . . 298
15.2 Power of Three . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
15.3 Best Time to Buy and Sell Stock . . . . . . . . . . . . . . . . . . . . . . 304
15.4 Subsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
15.5 Minimum Moves to Equal Array Elements II . . . . . . . . . . . . . . . 313
15.6 Array Nesting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
15.7 Count Sorted Vowel Strings . . . . . . . . . . . . . . . . . . . . . . . . 321
15.8 Concatenation of Consecutive Binary Numbers . . . . . . . . . . . . . . 326
15.9 Perfect Squares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
16 Conclusion 337
CONTENTS xi
xii CONTENTS
CHAPTER
ONE
INTRODUCTION
1
help you prepare for technical interviews, sharpen your skills, and advance your ca-
reers.
LeetCode has become a popular resource for technical interview preparation, as many
companies use similar problems to screen and evaluate potential candidates. The plat-
form 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 engineers
looking to improve their coding skills, prepare for technical interviews, and advance
their careers. Its extensive collection of coding challenges, community discussion
forums, and premium services make it an all-in-one platform for coding practice and
skills enhancement.
2 Chapter 1. Introduction
While it is important to optimize the performance of algorithms, it is also important to
balance this with readability and maintainability. A highly optimized algorithm may
be difficult to understand and maintain, which can lead to problems in the long run.
Therefore, it is important to balance performance and readability when designing and
implementing algorithms.
In summary, algorithm complexity is an essential concept in computer science that
helps programmers evaluate and optimize their algorithms’ performance. By ana-
lyzing an algorithm’s time and space complexity, programmers can identify potential
performance bottlenecks and optimize their code to improve efficiency and scalability.
Readable code is code that is easy to understand, maintain, and modify. It is an essen-
tial aspect of programming, as it ensures that code is accessible to other programmers
and helps to prevent errors and bugs. Readable code is important for several reasons.
Firstly, readable code makes it easier for other programmers to understand and mod-
ify it. This is particularly important in collaborative projects where multiple program-
mers work on the same codebase. If the code is not readable, it can lead to confusion
and errors, making it difficult for others to work on it.
Secondly, readable code helps to prevent bugs and errors. When code is easy to un-
derstand, it is easier to identify and fix potential issues before they become problems.
This is important for ensuring the code is reliable and performs as expected.
Thirdly, readable code can improve the overall quality of the codebase. When code is
easy to understand, it is easier to identify areas for improvement and make changes
to improve the code. This can help improve the codebase’s efficiency and maintain-
ability, leading to a better overall product.
Finally, readable code can save time and money. When code is easy to understand, it
is easier to maintain and modify. This can help reduce the time and resources required
to make changes to the codebase, leading to cost savings in the long run.
In conclusion, readable code is an essential aspect of programming that ensures that
code is accessible, error-free, and efficient. By focusing on readability when designing
and implementing code, programmers can improve the quality and reliability of their
code, leading to a better overall product.
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 indexing, itera-
tion, 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 subar-
ray problems, using a structured approach to break down complex tasks into man-
ageable 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 efficiency
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 array-
related challenges, including how to break down problems, devise algorithms,
and validate solutions.
7
2.1 Transpose Matrix
Example 1
Example 2
Constraints
• m == matrix.length.
• n == matrix[i].length.
• 1 <= m, n <= 1000.
• 1 <= m * n <= 10^5.
• -10^9 <= matrix[i][j] <= 10^9.
1 https://leetcode.com/problems/transpose-matrix/
8 Chapter 2. Array
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 << ",";
}
cout << "]";
}
cout << "]\n";
}
int main() {
vector<vector<int>> matrix = {{1,2,3},{4,5,6},{7,8,9}};
auto result = transpose(matrix);
printResult(result);
matrix = {{1,2,3},{4,5,6}};
result = transpose(matrix);
printResult(result);
}
Output:
(continues on next page)
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
Example 1
Example 2
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++;
}
// 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;
(continues on next page)
12 Chapter 2. Array
(continued from previous page)
}
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.
Complexity
Breaking down the problem into distinct stages, like finding the peak of the mountain
and then traversing down from there, can simplify the logic and improve code read-
ability. This approach facilitates a clear understanding of the algorithm’s progression
and helps in handling complex conditions effectively.
• Beautiful Towers I2 .
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
2 https://leetcode.com/problems/beautiful-towers-i/
1 https://leetcode.com/problems/shift-2d-grid/
14 Chapter 2. Array
⎡ ⎤ ⎡ ⎤
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].
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();
// number of rows
const int m = grid.size();
// number of columns
const int n = grid[0].size();
16 Chapter 2. Array
(continued from previous page)
}
void printResult(const vector<vector<int>>& grid) {
cout << "[";
for (auto& r : grid) {
cout << "[";
for (int a: r) {
cout << a << ",";
}
cout << "]";
}
cout << "]\n";
}
int main() {
vector<vector<int>> grid{{1,2,3},{4,5,6},{7,8,9}};
auto result = shiftGrid(grid, 1);
printResult(result);
grid = {{3,8,1,9},{19,7,2,5},{4,6,11,10},{12,0,21,13}};
result = shiftGrid(grid, 4);
printResult(result);
grid = {{1,2,3},{4,5,6},{7,8,9}};
result = shiftGrid(grid, 9);
printResult(result);
}
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 elements
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 re-
spective positions based on the calculated indices. This approach avoids unnecessary
copying or shifting of elements within the grid, optimizing both memory and time
complexity.
1. To convert a 2D matrix into a 1D vector, you can use the std::vector’s function
insert()2 .
2. The modulo operator % is usually used to ensure the index is inbound.
Example 1
Example 2
2 https://en.cppreference.com/w/cpp/container/vector/insert
1 https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/
18 Chapter 2. Array
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.
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) {
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.
20 Chapter 2. Array
Example 1
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));
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 ar-
ray 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.
Complexity
22 Chapter 2. Array
2.4.4 Readable code
2.4.5 Exercise
Example 1
⎡ ⎤ ⎡ ⎤
1 2 3 7 4 1
⎣4 5 6⎦ −→ ⎣8 5 2⎦
7 8 9 9 6 3
2 https://en.cppreference.com/w/cpp/container/vector_bool
3 https://leetcode.com/problems/find-all-duplicates-in-an-array
1 https://leetcode.com/problems/rotate-image/
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.
Code
#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]);
}
}
(continues on next page)
24 Chapter 2. Array
(continued from previous page)
// 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);
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
Example 1
Input: n = 3
Output: [[1,2,3],[8,9,4],[7,6,5]]
2 https://en.cppreference.com/w/cpp/algorithm/swap
3 https://leetcode.com/problems/determine-whether-matrix-can-be-obtained-by-rotation/
1 https://leetcode.com/problems/spiral-matrix-ii/
26 Chapter 2. Array
Example 2
Input: n = 1
Output: [[1]]
Constraints
2.6.2 Solution
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;
(continues on next page)
28 Chapter 2. Array
(continued from previous page)
cout << "]";
}
cout << "]\n";
}
int main() {
auto m = generateMatrix(3);
printResult(m);
m = generateMatrix(1);
printResult(m);
}
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 Matrix2 .
Example 1
Example 2
2 https://leetcode.com/problems/spiral-matrix/
1 https://leetcode.com/problems/daily-temperatures/
30 Chapter 2. Array
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.
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]) {
(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, it-
erates through the remaining temperatures to find the next higher temperature. Stor-
ing the time difference between the current day and the next higher temperature day
constructs the resulting array representing the number of days until warmer temper-
atures.
32 Chapter 2. Array
Complexity
Example 1
#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;
while (j < temperatures.size() &&
temperatures[j] <= temperatures[i]) {
// some temperature is bigger than temperatures[j],
// go to that value
if (answer[j] > 0) {
j += answer[j];
} else {
j = temperatures.size();
}
}
if (j < temperatures.size()) {
answer[i] = j - i;
}
}
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};
auto answer = dailyTemperatures(temperatures);
print(answer);
temperatures = {30,40,50,60};
answer = dailyTemperatures(temperatures);
print(answer);
temperatures = {30,60,90};
answer = dailyTemperatures(temperatures);
(continues on next page)
34 Chapter 2. Array
(continued from previous page)
print(answer);
}
Output:
[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. Up-
dating 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).
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.
2.7.5 Exercise
2 https://leetcode.com/problems/next-greater-element-i/description/
36 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 reversing
lists.
4. Problem-Solving Strategies: Develop strategies to approach linked list prob-
lems systematically, including strategies for merging lists, detecting intersec-
tions, and more.
37
3.1 Merge Two Sorted Lists
Example 1
Example 2
1 https://leetcode.com/problems/merge-two-sorted-lists/
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.
Finally, it appends the remaining nodes from the non-empty list to the merged list,
ensuring the resulting list remains sorted.
Example 1
Example 2
Example 3
1 https://leetcode.com/problems/remove-linked-list-elements/
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;
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) {
// remove head if its value matches val
while (head && head->val == val) {
head = head->next;
}
if (head == nullptr) return nullptr;
(continues on next page)
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.
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* 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,]
[]
[]
Complexity
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 containing
pointers.
3.2.5 Exercise
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.
2 https://leetcode.com/problems/delete-node-in-a-linked-list/
1 https://leetcode.com/problems/intersection-of-two-linked-lists/
Example 2
Constraints
Follow up
• Could you write a solution that runs in O(m + n) time and use only O(1) mem-
ory?
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.
Code
#include <iostream>
#include <unordered_map>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
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);
two.next = &four;
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;
(continues on next page)
Output:
1
1
1
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 infor-
mation 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.
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;
(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);
two.next = &four;
ListNode one12(1);
(continues on next page)
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
2 https://leetcode.com/problems/minimum-index-sum-of-two-lists/
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
(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 additional
space, effectively transforming the list by rearranging pointers.
3.4.4 Exercise
2 https://leetcode.com/problems/swapping-nodes-in-a-linked-list/
Example 1
Example 2
Example 3
1 https://leetcode.com/problems/add-two-numbers/
• 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) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
Output:
[7,0,8,]
[0,]
[8,9,9,9,0,0,0,1,]
Complexity
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 straightfor-
ward implementation.
3.5.4 Exercise
2 https://leetcode.com/problems/double-a-number-represented-as-a-linked-list/
FOUR
HASH TABLE
This chapter is about the C++ Standard Template Library’s std::unordered_map and
how it can help your programming.
With hash-based data structures, you can store and retrieve information quickly,
like a well-organized library. Hash tables allow you to efficiently manage data
by inserting, locating, and removing elements, even from large datasets. C++’s
std::unordered_map makes it easy to use hash tables without manual implementa-
tion.
What this chapter covers:
1. Exploring std::unordered_map: Dive into the C++ Standard Template Li-
brary’s std::unordered_map container, learning how to use it effectively for
mapping keys to values.
2. Problem-Solving with Hash Tables: Learn strategies for solving many prob-
lems using hash tables, including frequency counting, anagram detection, and
more.
Symbol Value
I 1
(continues on next page)
1 https://leetcode.com/problems/roman-to-integer/
67
(continued from previous page)
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 instance,
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.
Example 2
Input: s = "LVIII"
Output: 58
Explanation: L = 50, V= 5, III = 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}
};
int romanToInt(const string& s) {
Output:
3
58
1994
Complexity
4.1.3 Conclusion
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.
• Integer to Roman2 .
Example 1
Example 2
2 https://leetcode.com/problems/integer-to-roman/
1 https://leetcode.com/problems/maximum-erasure-value/
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);
sum[0] = nums.at(0);
Output:
17
8
4.2.3 Conclusion
This solution computes the maximum sum of a subarray containing unique elements.
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 slid-
ing window and updating the sum accordingly, resulting in an overall efficient solu-
tion.
"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
Constraints
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) {
(continues on next page)
Output:
[mee,aqq,]
[a,b,c,]
Complexity
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 cre-
ating 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 sub-
strings, detecting patterns, and performing advanced search operations within
strings.
4. Anagrams and Palindromes: Tackle challenges related to anagrams and palin-
dromes, honing your ability to discern permutations and symmetric constructs.
5. Problem-Solving with Strings: Learn how to approach coding problems that
involve string manipulation, from simple tasks to intricate algorithms.
79
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/
80 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.
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
if (alphabet[c - 'a'] < 0) {
return false;
}
}
return true;
}
int main() {
cout << isAnagram("anagram", "nagaram") << endl;
cout << isAnagram("rat", "car") << endl;
}
82 Chapter 5. String
Output:
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;
}
}
return true;
(continues on next page)
Output:
1
0
Complexity
Instead of relying on a fixed-size array like the ASCII-based solutions, Solution 3 uses
an unordered_map to store character frequencies. Each character is used as a key in
the map, and the count of occurrences is stored as the associated value.
Unicode characters values are not restricted to a specific range. The unordered_map
approach accommodates this variability by allowing any character to be a key.
5.1.6 Exercise
2 https://leetcode.com/problems/find-resultant-array-after-removing-anagrams/
84 Chapter 5. String
5.2 Detect Capital
Example 1
Example 2
Constraints
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;
86 Chapter 5. String
(continued from previous page)
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.
5.2.4 Exercise
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.
Example 1
Example 2
88 Chapter 5. String
Constraints
Code
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
const vector<string> morse{
".-", "-...", "-.-.", "-..", ".", "..-.", "--.",
"....", "..", ".---", "-.-", ".-..", "--", "-.",
"---", ".--.", "--.-", ".-.", "...", "-", "..-",
"...-", ".--", "-..-", "-.--", "--.."
};
Output:
2
1
Complexity
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.
90 Chapter 5. String
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.
Example 1
Output: 2
Explanation: "[email protected]" and "[email protected]"␣
˓→actually receive mails.
Example 2
Constraints
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('@');
92 Chapter 5. String
(continued from previous page)
"[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
This solution parses a list of email addresses, normalizes each email address by re-
moving 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;
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"};
94 Chapter 5. String
(continued from previous page)
cout << numUniqueEmails(emails) << endl;
}
Output:
2
3
2
Complexity
• string::find(char, pos=0)3 returns the position of the first char which ap-
pears in the string string starting from pos.
2 https://en.cppreference.com/w/cpp/string/basic_string/substr
3 https://en.cppreference.com/w/cpp/string/basic_string/find
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.
4 https://en.cppreference.com/w/cpp/container/unordered_set
5 https://en.cppreference.com/w/cpp/container/unordered_map
1 https://leetcode.com/problems/longest-substring-without-repeating-characters/
96 Chapter 5. String
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.
Constraints
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
Code
#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;
98 Chapter 5. String
(continued from previous page)
cout << lengthOfLongestSubstring("bbbbb") << endl;
cout << lengthOfLongestSubstring("pwwkew") << endl;
}
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 en-
countered, 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
2 https://leetcode.com/problems/optimal-partition-of-string/
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]) {
(continues on next page)
Output:
0
0
-1
Complexity
This solution first converts the version strings into vectors of integers representing the
individual components of the version numbers. This conversion is done by iterating
through each character of the version string, accumulating digits until encountering
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 between
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
• std::stoi2 is used to convert a string to an int. It ignores the leading zeros for
you.
• std::accumulate3 is used to compute the sum of a container.
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 algorith-
mic 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 structures.
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
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!
105
6.1 Baseball Game
Example 1
1 https://leetcode.com/problems/baseball-game/
"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
Constraints
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);
}
int main() {
vector<string> ops{"5","2","C","D","+"};
cout << calPoints(ops) << endl;
ops = {"5","-2","4","C","D","9","+","+"};
cout << calPoints(ops) << endl;
}
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.
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)2 computes the sum of the vector stk.
6.1.4 Exercise
Input: s = "()"
Output: true
Example 2
Input: s = "()[]{}"
Output: true
Example 3
Input: s = "(]"
Output: false
Constraints
#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()) {
// start with a non-open parenthesis is invalid
return false;
} else if (c == ')' && stk.top() != '('
|| c == ']' && stk.top() != '['
|| c == '}' && stk.top() != '{') {
// the last open parenthesis does not match this closed one
return false;
} else {
// open-close match
stk.pop();
}
}
return stk.empty();
}
int main() {
cout << isValid("()") << endl;
cout << isValid("(){}[]") << endl;
cout << isValid("(]") << endl;
cout << isValid("([)]") << endl;
}
Output:
1
1
0
0
6.2.3 Conclusion
This solution efficiently checks the validity of a string of parentheses, brackets, and
curly braces by using a stack to ensure that each opening bracket is correctly matched
with its corresponding closing bracket.
6.2.4 Exercise
Example 1
2 https://leetcode.com/problems/check-if-word-is-valid-after-substitutions/
1 https://leetcode.com/problems/backspace-string-compare/
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] != '#') {
// s[i] is a normal letter
v.push_back(s[i]);
} else {
if (!v.empty()) {
(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.
Complexity
You can use the methods push and pop of the data structure stack2 to build and clean
the strings.
2 https://en.cppreference.com/w/cpp/container/stack
6.3.4 Exercise
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"
Example 3
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.
Code
#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
• 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.
• In the expression stk.back().back(): stk.back() is the latest string a of stk.
Then stk.back().back() = a.back() is the last character of a.
6.4.4 Exercise
2 https://leetcode.com/problems/remove-all-adjacent-duplicates-in-string/
SEVEN
This chapter explores priority queues (or heaps), the fascinating data structures de-
signed 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 Stan-
dard 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 or-
dering.
2. Leveraging std::priority_queue: Dive into the versatile std::priority_queue
container provided by the STL, mastering its usage for managing priorities ef-
fectively.
3. Operations and Methods: Explore the operations available in
std::priority_queue, including insertion, and extraction while maintain-
ing 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 priority
criteria.
5. Problem-Solving with Priority Queues: Learn strategies for tackling problems
where prioritization is key, from scheduling tasks to efficient data retrieval.
121
7.1 Last Stone Weight
Example 1
Example 2
1 https://leetcode.com/problems/last-stone-weight/
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();
}
int main() {
vector<int> stones{2,7,4,1,8,1};
cout << lastStoneWeight(stones) << endl;
stones = {1};
cout << lastStoneWeight(stones) << endl;
}
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.
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.
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 initializa-
tion, 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.
Complexity
• 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).
7.2.4 Conclusion
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 pri-
ority 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.
Example 1
Example 2
2 https://leetcode.com/problems/kth-largest-element-in-an-array/
1 https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/
• 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.
7.3.2 Solution 1: Transform the 2-D matrix into a 1-D vector then sort
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() {
(continues on next page)
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 complexity 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.
Complexity
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_queue2 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) {
(continues on next page)
2 https://en.cppreference.com/w/cpp/container/priority_queue
Output:
13
-5
Complexity
7.3.4 Conclusion
Example 1
3 https://leetcode.com/problems/find-k-pairs-with-smallest-sums/
1 https://leetcode.com/problems/construct-target-array-with-multiple-sums/
Example 3
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 previous
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;
}
if (sum == 0) {
// cannot change target
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 important
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) {
return false;
}
// perform the subtraction as much as possible
int pre = q.top() % sum;
(continues on next page)
Output:
1
0
1
Complexity
Solution 2 uses a max heap (priority_queue) to efficiently find and process the max-
imum 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.
7.4.5 Exercise
2 https://leetcode.com/problems/minimum-amount-of-time-to-fill-cups/
EIGHT
BIT MANIPULATION
In this chapter, we’re diving deep into Bit Manipulation, a fascinating computer sci-
ence and programming area that manipulates individual bits within data.
Bit Manipulation is crucial in various programming tasks, from optimizing algorithms
to working with hardware-level data. Whether you’re a seasoned programmer 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 numbers,
ensuring you’re comfortable with the fundamentals. You’ll learn to convert be-
tween 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 program-
ming 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 cod-
ing problems.
3. Bit Hacks: Discover the art of Bit Hacks – clever and often elegant tricks pro-
grammers 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.
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 Manipulation
141
and discover how the smallest data units can have a massive impact on your coding
skills and efficiency!
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/
2 https://en.wikipedia.org/wiki/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
}
Output:
2
1
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 distance
without the need for complex logic or additional data structures.
8.1.4 Exercise
• Number of 1 Bits3 .
Example 1
Input: n = 16
Output: true
3 https://leetcode.com/problems/number-of-1-bits/
1 https://leetcode.com/problems/power-of-four/
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) {
n /= 4;
}
// if n % 4 != 0, then n > 1
return n == 1;
}
int main()
{
cout << isPowerOfFour(16) << endl;
cout << isPowerOfFour(5) << endl;
(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
64 : 1000000
256 : 100000000
...
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
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;
}
Output:
1
0
1
Complexity
• Runtime: O(1).
• Extra space: O(1).
Recognizing the unique properties of powers of four, such as their binary representa-
tion, 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.2 .
Example 2
Constraints
Follow up
• 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]) {
(continues on next page)
Output:
2
3
The code relies on sorting to bring duplicate elements together, making it easy to
identify them during the linear pass.
Complexity
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;
}
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
• Runtime: O(n).
• Extra space: much less than O(n) since std::vector<bool> is optimized for
space-efficient.
Since n <= 10^5, you can use this size for a std::bitset3 to do the marking.
Code
#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
3 https://en.cppreference.com/w/cpp/utility/bitset
Complexity
• Runtime: O(n).
• Extra space: O(1).
8.3.7 Exercise
• Missing Number4 .
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"};
cout << maxProduct(words) << endl;
words = {"a","ab","abc","d","cd","bcd","abcd"};
cout << maxProduct(words) << endl;
words = {"a","aa","aaa","aaaa"};
(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 comparing the
characters of each pair of words, it identifies whether there are any common charac-
ters. 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 additional
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 opera-
tor AND on them.
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
// set the bit at that mapped position of mask[i] to 1
mask[i] |= 1 << (c - 'a');
}
for (int j = 0; j < i; j++) {
if ((mask[j] & mask[i]) == 0) {
// there is no common bit between mask[j] and mask[i]
maxP = max(maxP, (int) (words[i].length() * words[j].
˓→length()));
}
}
}
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;
(continues on next page)
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 en-
codes 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 bitwise
operations to efficiently check for common characters between pairs of words without
requiring additional space proportional to the length of the 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 uniqueness
in sorting and how they can impact the integrity and usefulness of sorted data.
3. Insights through Sorting: Discover scenarios where sorted data provides valu-
able insights, such as identifying trends, anomalies, or patterns that inform
decision-making.
159
The majority element is the element that appears more frequently in the array than
any other element, specifically, it appears more than n / 2 times.
You can assume that the majority element always exists in the given array.
Example 1
Example 2
Constraints
• n == nums.length.
• 1 <= n <= 5 * 10^4.
• -2^31 <= nums[i] <= 2^31 - 1.
Follow-up:
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;
(continues on next page)
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 majority
element; otherwise, it defaults to the first element of the array.
Complexity
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
(continues on next page)
Output:
3
2
This code leverages the property of a majority element, which guarantees that it oc-
cupies the middle position in the sorted list of elements. Sorting the array allows us
to easily access this middle element.
Complexity
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) {
(continues on next page)
Output:
3
2
The code uses the std::nth_element function to rearrange the elements in the nums
vector such that the element at index mid will be in its correct sorted position, and all
elements before it will be less than or equal to it, while all elements after it will be
greater than or equal to it.
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.
Example 1
2 https://leetcode.com/problems/most-frequent-even-element/
1 https://leetcode.com/problems/merge-sorted-array/
Example 3
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
Output:
[1,2,2,3,5,6,]
[1,]
[1,]
This solution merges two sorted arrays nums1 and nums2 into nums1 while maintaining
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) {
// nums1 is done
nums1[k--] = nums2[j--];
(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
#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}};
cout << removeCoveredIntervals(intervals) << endl;
intervals = {{1,4},{2,3}};
cout << removeCoveredIntervals(intervals) << endl;
}
Output:
(continues on next page)
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
Example 1
#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) {
// i's left bound is the same as exact previous one's
count++;
} else {
// update previous interval's left bound
preLeft = i[0];
}
// update max right bound
maxRight = max(maxRight, i[1]);
}
return intervals.size() - count;
}
int main() {
vector<vector<int>> intervals{{1,4},{3,6},{2,8}};
cout << removeCoveredIntervals(intervals) << endl;
intervals = {{1,4},{2,3}};
cout << removeCoveredIntervals(intervals) << endl;
}
Output:
2
1
9.4 My Calendar I
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.
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) {
(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::set2 to store the sorted unique events.
Code
#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;
}
2 https://en.cppreference.com/w/cpp/container/set
Complexity
9.4.5 Exercise
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";
}
int main() {
vector<int> nums{1,1,1,2,2,3};
printResult(removeDuplicates(nums), nums);
(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()2 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.
2 https://en.cppreference.com/w/cpp/container/vector/erase
#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] << ",";
}
cout << "]\n";
}
int main() {
vector<int> nums{1,1,1,2,2,3};
printResult(removeDuplicates(nums), nums);
nums = {0,0,1,1,1,1,2,3,3};
printResult(removeDuplicates(nums), nums);
}
Output:
Output:
5, [1,1,2,2,3,]
7, [0,0,1,1,2,3,3,]
9.5.4 Conclusion
Solution 2 effectively modifies the input array in-place, removing duplicates that oc-
cur 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
3 https://leetcode.com/problems/remove-duplicates-from-sorted-array/
TEN
GREEDY ALGORITHM
This chapter will explore a fascinating and highly practical problem-solving approach
known as greedy algorithms. Greedy algorithms are powerful tools for making de-
cisions 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 methods.
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 algo-
rithms and the ability to apply them to a wide range of problems, from optimization
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 complex opti-
mization challenges. So, let’s dive in and explore the world of greedy algorithms!
185
10.1 Can Place Flowers
Example 1
Example 2
Constraints
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) {
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.
10.1.4 Ecercise
• Teemo Attacking2 .
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 "aaabcc
˓→".
Another way is to delete one 'b' and one 'c' resulting in the good␣
˓→string "aaabbc".
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).
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']++;
}
(continues on next page)
Output:
0
2
2
Complexity
10.2.4 Exercise
Example 2
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.
Example 2
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
(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 efficiently
solved using a greedy approach. The solution iterates through the input array, identi-
fying alternating extremums (peaks and valleys) to form the wiggle subsequence.
By keeping track of the current trend direction (increasing or decreasing), the solution
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);
}
return maxDigit - '0';
}
int main() {
(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 solution
effectively determines the minimum number of deci-binary numbers required.
Example 1
Example 2
Constraints
#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
maxUnits += boxTypes[i][0] * boxTypes[i][1];
truckSize -= boxTypes[i][0];
} else {
// can put only truckSize < boxTypes[i][0] of boxTypes[i]
maxUnits += truckSize * boxTypes[i][1];
break;
}
i++;
}
return maxUnits;
}
int main() {
vector<vector<int>> boxTypes{{1,3},{2,2},{3,1}};
cout << maximumUnits(boxTypes, 4) << endl;
boxTypes = {{5,10},{2,5},{4,7},{3,9}};
cout << maximumUnits(boxTypes, 10) << endl;
}
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.
Note that two vectors2 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::sort3
algorithm will use the dictionary order to sort them by default.
10.5.4 Exercise
2 https://en.cppreference.com/w/cpp/container/vector
3 https://en.cppreference.com/w/cpp/algorithm/sort
4 https://leetcode.com/problems/maximum-bags-with-full-capacity-of-rocks/
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 pro-
gramming, including the balance between time and space complexity.
5. Problem-Solving Strategies: Develop systematic strategies for approaching dy-
namic programming problems, from identifying subproblems to deriving recur-
rence relations.
203
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 in-
creases 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.
When you call the fib function with a value of n, it generates a call stack with a depth
of n, as each call to fib leads to two more recursive calls (one for n - 1 and one for n
#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
2
3
• 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;
std::cout << fib(4) << std::endl;
}
Output:
1
2
3
This solution calculates the nth Fibonacci number iteratively using two variables to
keep track of the last two Fibonacci numbers.
• Runtime: O(n).
• Extra space: O(1).
11.1.5 Conclusion
The Fibonacci sequence can be efficiently computed using various techniques, includ-
ing recursion with memoization, bottom-up dynamic programming, or even optimiz-
ing 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.
11.1.6 Exercise
Input: m = 3, n = 7
Output: 28
Example 2
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:
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:
If the grid has only one row or only one column, then there is only one possible path.
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;
}
(continues on next page)
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.
Complexity
Code
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n) {
// store what have been calculated in dp
vector<vector<int> > dp(m, vector<int>(n,1));
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][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
Complexity
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];
(continues on next page)
Output:
28
28
3
6
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.
I am wondering if there is some mathematics behind this problem. Please share your
finding if you find a formula for the solution to this problem.
11.2.6 Exercise
Example 1
2 https://leetcode.com/problems/minimum-path-sum/
1 https://leetcode.com/problems/largest-divisible-subset/
Constraints
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].
Code
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
Output:
[2,1,]
[8,4,2,1,]
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.
Then all you need to find is only the last index iN of the largest maxSubset.
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 dynamically
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
Complexity
• Runtime: O(n^2), where n is the number of elements in the nums vector. The
nested loop searches for previous elements with divisibility relationships, which
may lead to quadratic time complexity in the worst case. However, it maintains
information about subset sizes and elements, reducing redundant calculations
and improving performance.
• Extra space: O(n).
In this interesting problem, we use index mapping to simplify everything. That im-
proves the performance in both runtime and memory.
11.4 Triangle
Example 1
Example 2
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.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minimumTotal(const vector<vector<int>>& triangle) {
const int n = triangle.size(); // triangle's height
vector<vector<int>> minPath(n);
minPath[0] = triangle[0];
for (int i = 1; i < n; i++) {
const int N = triangle[i].size();
minPath[i].resize(N);
// left most number
minPath[i][0] = triangle[i][0] + minPath[i-1][0];
for (int j = 1; j < N - 1; j++) {
minPath[i][j] = triangle[i][j] + min(minPath[i-1][j-1],␣
˓→minPath[i-1][j]);
}
// 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.
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];
for (int j = i - 1; j > 0; j--) {
minPath[j] = triangle[i][j] + min(minPath[j - 1],␣
(continues on next page)
Output:
11
-10
Complexity
Example 1
Example 2
• m == obstacleGrid.length.
• n == obstacleGrid[i].length.
• 1 <= m, n <= 100.
• obstacleGrid[i][j] is 0 or 1.
Code
#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
(continues on next page)
Output:
2
1
Complexity
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.
11.5.4 Exercise
2 https://leetcode.com/problems/minimum-path-cost-in-a-grid/
TWELVE
COUNTING
In this chapter, we will explore the benefits of counting elements and how it can en-
hance the efficiency of different algorithms and operations. By tallying occurrences,
you can gain valuable insights that simplify computations and give you a better un-
derstanding 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 signifi-
cance of counting elements, its role in performance enhancement, and the var-
ious 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 algo-
rithm that capitalizes on the power of element counting to achieve exceptional
performance.
4. Problem-Solving with Counts: Develop approaches to solve problems that
benefit from element counting, from optimizing search operations to identifying
anomalies.
231
12.1 Single Number
Example 1
Example 2
Example 3
Constraints
Count how many times each element appears in the array. Then return the one ap-
pearing 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;
nums = {4,1,2,1,2};
cout << singleNumber(nums) << endl;
nums = {1};
cout << singleNumber(nums) << endl;
}
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.
• 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) {
int single = 0;
for (auto& n : nums) {
single ^= n;
}
return single;
}
int main() {
vector<int> nums{2,2,1};
cout << singleNumber(nums) << endl;
nums = {4,1,2,1,2};
cout << singleNumber(nums) << endl;
nums = {1};
cout << singleNumber(nums) << endl;
}
Output:
1
4
1
• 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.
12.1.5 Exercise
• Missing Number2 .
Input: s = "leetcode"
Output: 0
Example 2
Input: s = "loveleetcode"
Output: 2
Example 3
Input: s = "aabb"
Output: -1
Constraints
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;
(continues on next page)
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.
By iterating through the string and populating the unordered map with the count of
each character, it constructs the character count. Then, it iterates through the string
again and returns the index of the first character with a count of 1, indicating that it
is non-repeating.
This approach optimizes the computation by efficiently tracking the count of each
character and identifying the first non-repeating character without requiring addi-
tional space proportional to the length of the string.
Complexity
From the constraints “s consists of only lowercase English letters”, you can use an
array of 26 elements to store the counts.
#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;
}
}
return -1;
}
int main() {
cout << firstUniqChar("leetcode") << endl;
cout << firstUniqChar("loveleetcode") << endl;
cout << firstUniqChar("aabb") << endl;
}
Output:
0
2
-1
Complexity
Utilizing hash maps or arrays to count the frequency of characters in a string provides
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 solutions
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 correspond-
ing to the lowercase English alphabet, avoiding the overhead associated with hash
maps.
12.2.5 Exercise
Example 1
2 https://leetcode.com/problems/first-letter-to-appear-twice/
1 https://leetcode.com/problems/max-number-of-k-sum-pairs/
Constraints
You can use a map to count the appearances of the elements of nums.
Example 2
#include <vector>
#include <iostream>
#include <unordered_map>
using namespace std;
int maxOperations(const vector<int>& nums, int k) {
unordered_map<int,int> m;
int count = 0;
for (auto& a : nums) {
m[a]++; // count a's occurences
if (m[k - a] > 0) {
// k-a appears in nums
if (a != k - a || m[a] >= 2) {
// if a == k - a, a is required to appear at least twice
count++;
m[a]--;
m[k - a]--;
}
}
}
return count;
}
int main() {
vector<int> nums{1,2,3,4};
cout << maxOperations(nums, 5) << endl;
nums = {3,1,3,4,3};
cout << maxOperations(nums, 6) << endl;
}
Output:
2
1
12.3.3 Conclusion
This solution utilizes an unordered map to store the frequency of each element en-
countered 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 re-
quiring additional space proportional to the size of the array.
12.3.4 Exercise
• Two Sum2 .
2 https://leetcode.com/problems/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 scenar-
ios 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 efficiently.
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 problems
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.
243
13.1 Running Sum of 1d Array
Example 1
Example 2
Example 3
Constraints
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};
rs = runningSum(nums);
printResult(rs);
}
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
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;
}
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};
rs = runningSum(nums);
(continues on next page)
Output:
[1,3,6,10,]
[1,2,3,4,5,]
[3,4,6,16,17,]
Complexity
13.1.4 Conclusion
Solution 2 directly modifies the input array nums to store the running sums by itera-
tively 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.
Example 2
Example 3
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
#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;
} else {
currSum = currSum + num;
}
// update max sum so far
maxSum = max(maxSum, currSum);
}
return maxSum;
}
int main() {
vector<int> nums = {-2,1,-3,4,-1,2,1,-5,4};
cout << maxSubArray(nums) << endl;
nums = {1};
cout << maxSubArray(nums) << endl;
nums = {5,4,-1,7,8};
cout << maxSubArray(nums) << endl;
}
Output:
6
1
23
13.2.3 Conclusion
This solution is the Kadane’s algorithm2 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
sum, it effectively handles both positive and negative numbers in the array. Finally, it
updates maxSum with the maximum value encountered during the iteration, ensuring
it holds the maximum sum of any contiguous subarray within the given array.
This approach optimizes the computation by tracking the maximum sum and dynam-
ically updating it as it iterates through the array.
13.2.4 Exercise
Example 2
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;
(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
array stores the product of all elements to the left of the current element, while the
suffix array stores the product of all elements to the right of the current element. By
Complexity
13.3.3 Solution 2: Use directly vector answer to store the prefix prod-
uct
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--) {
// compute suffix product and the final product the same time
suffix *= nums[i + 1];
answer[i] *= suffix;
}
return answer;
}
(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.
Complexity
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.
13.3.5 Exercise
Example 1
Example 2
2 https://leetcode.com/problems/construct-product-matrix/
1 https://leetcode.com/problems/subarray-sum-equals-k/
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.
Code
#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;
}
(continues on next page)
Output:
2
2
3
Complexity
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).
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++) {
sum[i] = sum[i-1] + nums[i];
}
int count = 0;
for (int i = 0; i < n; i++) {
if (sum[i] == k) {
// nums[0] + .. + nums[i] = k
count++;
}
for (int j = 0; j < i; j++) {
if (sum[i] - sum[j] == k) {
// nums[j+1] + nums[j+2] + .. + nums[i] = 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;
}
This solution uses the concept of prefix sum to efficiently calculate the sum of subar-
rays. 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.
Complexity
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) {
(continues on next page)
Output:
2
2
3
Complexity
13.4.6 Exercise
2 https://leetcode.com/problems/find-pivot-index/
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 simul-
taneous 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 unrav-
eling complex problems.
2. Two Pointers Approach: Dive into the mechanics of the technique, exploring
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 opti-
mizes sliding window problems, facilitating efficient substring or subarray anal-
ysis.
5. Intersection and Union: Uncover the technique’s versatility in solving prob-
lems that involve intersecting or uniting elements within different sequences.
6. Problem-Solving with Two Pointers: Develop strategies to address diverse
problems through the Two Pointers technique, from array manipulation to string
analysis.
263
14.1 Middle of the Linked List
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) {
std::cout << node->val << ",";
node = node->next;
}
std::cout << "]\n";
(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.
Complexity
#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 << ",";
node = node->next;
}
std::cout << "]\n";
}
int main() {
ListNode five(5);
ListNode four(4, &five);
ListNode three(3, &four);
ListNode two(2, &three);
ListNode one(1, &two);
auto result = middleNode(&one);
print(result);
ListNode six(6);
five.next = &six;
result = middleNode(&one);
print(result);
}
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
using two pointers to efficiently locate the middle node.
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 read-
able, scalable and maintainable.
Example 1
Example 2
2 https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
1 https://leetcode.com/problems/linked-list-cycle/
Constraints
• 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
(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.
Complexity
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)
Output:
1
1
(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 for-
ward while the slow pointer moves one step forward in each iteration. By comparing
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
2 https://leetcode.com/problems/linked-list-cycle-ii/
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;
}
void print(vector<int>& nums) {
for (auto num : nums) {
cout << num << " ";
(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.
By performing these swaps, the algorithm ensures that each element is at the correct
position based on its parity.
This approach optimizes the sorting process by performing a single pass through the
array and minimizing the number of swaps required to achieve the desired parity
arrangement.
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) {
// find the nums[evenPos] that is odd for swapping
while (evenPos < N && nums[evenPos] % 2 == 0) {
evenPos += 2;
}
// If not found, it means all even nums are in place. Done!
if (evenPos >= N) {
break;
}
// Otherwise, the problem's constraint makes sure
// there must be some nums[oddPos] that is even for swapping
(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
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 elements contain
odd values. This process iterates until all even and odd elements are correctly posi-
tioned.
14.3.5 Exercise
Example 2
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.
#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;
height = {1,1};
cout << maxArea(height) << endl;
}
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.
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.
Example 1
maxArea = 8.
#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--;
}
}
return maxA;
}
int main() {
vector<int> height{1,8,6,2,5,4,8,3,7};
cout << maxArea(height) << endl;
height = {1,1};
cout << maxArea(height) << endl;
}
Output:
49
1
14.4.4 Conclusion
Example 1
1 https://leetcode.com/problems/remove-nth-node-from-end-of-list/
Example 2
Example 3
Constraints
Follow up
Code
#include <iostream>
#include <vector>
struct ListNode {
int val;
ListNode *next;
(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);
(continues on next page)
Output:
[1,2,3,5,]
[]
[4,]
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 simplicity
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.
#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) {
ListNode* fast = head;
// let fast goes ahead n nodes
for (int i = 0; i < n; i++) {
fast = fast->next;
}
if (fast == nullptr) {
// remove head if n equals the list's length
return head->next;
}
ListNode* slow = head;
while (fast->next) {
slow = slow->next;
fast = fast->next;
}
// remove slow
slow->next = slow->next->next;
return head;
}
void printList(const ListNode *head) {
ListNode* node = head;
cout << "[";
while (node) {
cout << node->val << ",";
node = node->next;
}
cout << "]\n";
}
int main() {
(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.
Example 1
Example 2
Example 3
2 https://leetcode.com/problems/swapping-nodes-in-a-linked-list/
1 https://leetcode.com/problems/shortest-unsorted-continuous-subarray/
Follow up
Example 1
Code
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int findUnsortedSubarray(const vector<int>& nums) {
vector<int> sortedNums = nums;
sort(sortedNums.begin(), sortedNums.end());
int left = 0;
while (left < nums.size() && nums[left] == sortedNums[left]) {
left++;
}
int right = nums.size() - 1;
while (right >= 0 && nums[right] == sortedNums[right]) {
right--;
}
return left >= right ? 0 : right - left + 1;
(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
#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;
} else {
min = nums[j];
}
}
return left >= right ? 0 : right - left + 1;
}
int main() {
vector<int> nums{2,6,4,8,10,9,15};
cout << findUnsortedSubarray(nums) << endl;
nums = {1,2,3,4};
cout << findUnsortedSubarray(nums) << endl;
nums = {1};
cout << findUnsortedSubarray(nums) << endl;
}
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 math-
ematical 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 mathe-
matics and probability theory in solving problems related to permutations, com-
binations, and statistical analysis.
4. Problem-Solving with Mathematics: Develop strategies for leveraging mathe-
matical concepts to solve problems efficiently and elegantly, from optimization
tasks to simulation challenges.
297
15.1 Excel Sheet Column Number
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...
Example 1
Example 2
Example 3
1 https://leetcode.com/problems/excel-sheet-column-number/
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'
"AA" = 27 = 26*'A' + 'A'
"AZ" = 52 = 26*'A' + 'Z'
"ZZ" = 702 = 26*'Z' + 'Z'
"AAA" = 703 = 26*26*'A' + 26*'A' + 'A'
#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
The solution calculates the decimal representation of the Excel column title by pro-
cessing each character and updating the result.
Complexity
Implementation notes
If you write it as
15.1.3 Exercise
Example 1
Input: n = 27
Output: true
Explanation: 27 = 3^3.
2 https://leetcode.com/problems/excel-sheet-column-title/
1 https://leetcode.com/problems/power-of-three/
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).
Constraints
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;
}
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.
Complexity
• 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
Complexity
• 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
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++) {
for (int j = i + 1; j < prices.size(); j++) {
if (prices[j] > prices[i]) {
maxProfit = max(maxProfit, prices[j] - prices[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:
5
0
6
2
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;
}
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;
}
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 complex-
ity by iterating through the prices only once and updating the minimum price seen so
far. This approach avoids unnecessary comparisons and achieves the desired result in
a single pass through the array.
15.3.6 Exercise
2 https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/
Example 1
Example 2
Constraints
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.
1 https://leetcode.com/problems/subsets/
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 constructed
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;
for (auto subset : powerset) {
subset.push_back(nums[i]);
newSubsets.push_back(subset);
}
powerset.insert(powerset.end(), newSubsets.begin(), newSubsets.
˓→end());
i++;
}
return powerset;
}
void print(const vector<vector<int>>& powerset) {
for (auto& set : powerset ) {
cout << "[";
for (auto& element : set) {
cout << element << ",";
}
cout << "]";
}
(continues on next page)
Output:
[][1,][2,][1,2,][3,][1,3,][2,3,][1,2,3,]
[][1,]
Complexity
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 II2 .
2 https://leetcode.com/problems/subsets-ii/
Example 1
Example 2
Constraints
• n == nums.length.
• 1 <= nums.length <= 10^5.
• -10^9 <= nums[i] <= 10^9.
1 https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/
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. Appar-
ently!
We will prove that M will be the median2 of nums, which is nums[n/2] of the sorted
nums.
In other words, we will prove that if you choose M a value different from nums[n/2],
then the number of moves will be increased.
In fact, if you choose M = nums[n/2] + x, where x > 0, then:
• Each element nums[i] that is less than M needs more x moves, while each
nums[j] that is greater than M can reduce x moves.
• But the number of nums[i] is bigger than the number of nums[j].
• So the total number of moves is bigger.
The same arguments apply for x < 0.
Example 3
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
(continues on next page)
2 https://en.wikipedia.org/wiki/Median
Output:
2
16
This solution leverages the concept of the median to minimize the total absolute
differences 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_element3 to reduce the runtime complexity.
3 https://en.cppreference.com/w/cpp/algorithm/nth_element
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minMoves2(vector<int>& nums) {
const int mid = nums.size() / 2;
// make sure all elements that are less than or equals to nums[mid]
// are on the left
std::nth_element(nums.begin(), nums.begin() + mid, nums.end());
const int median = nums[mid];
int moves = 0;
for (int& a: nums) {
moves += abs(a - median);
}
return moves;
}
int main() {
vector<int> nums{1,2,3};
cout << minMoves2(nums) << endl;
nums = {1,10,2,9};
cout << minMoves2(nums) << endl;
}
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
Example 2
Constraints:
0 -> 5,
1 -> 4,
2 -> 0,
3 -> 3,
4 -> 1,
5 -> 6,
6 -> 2.
2 https://en.wikipedia.org/wiki/Permutation
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];
len++;
}
maxLen = max(len, maxLen);
}
return maxLen;
}
int main() {
(continues on next page)
Output:
4
1
2
3
Complexity
15.6.3 Conclusion
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.
Example 1
Input: n = 1
Output: 5
Explanation: The 5 sorted strings that consist of vowels only are ["a","e
˓→","i","o","u"].
Example 2
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
1 https://leetcode.com/problems/count-sorted-vowel-strings/
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.
• There are 15 strings starting from a: aaa, aae, aai, aao, aau, aee, aei, aeo, aeu,
aii, aio, aiu, aoo, aou, auu.
• In total: there are 35 strings that satisfy the problem.
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
Code
#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
Complexity
• 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
(︀ )︀ (︀𝑛)︀ 2
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 cal-
culate 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 cal-
culations and achieves the desired result more efficiently.
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".
1 https://leetcode.com/problems/concatenation-of-consecutive-binary-numbers/
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;
for (int i = 2; i <= n; i++) {
const int num_bits = std::log2(i) + 1;
result = ((result << num_bits) + i) % 1000000007;
(continues on next page)
Output:
1
27
505379714
Complexity
• Runtime: O(n*logn).
• Extra space: O(1).
15.8.3 Conclusion
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.
Example 4
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.
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);
(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.
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 Theorem2 , which states
every natural number can be represented as the sum of four integer squares.
It was proven by Lagrange in 1770.
Example 4
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 Theorem3 , 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.
2 https://en.wikipedia.org/wiki/Lagrange%27s_four-square_theorem
3 https://en.wikipedia.org/wiki/Legendre%27s_three-square_theorem
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;
}
// Legendre's three-square theorem
int m = n;
while (m % 4 == 0) {
m /= 4;
}
if (m % 8 == 7) {
return 4;
}
const int sq = sqrt(n);
for (int i = 1; i <= sq; i++) {
if (isSquare(n - i*i)) {
return 2;
}
}
return 3;
}
int main() {
cout << numSquares(12) << endl;
cout << numSquares(13) << endl;
}
Output:
3
2
Complexity
𝑛 = 4𝑎 · 𝑚.
#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;
}
// Legendre's three-square theorem
while (n % 4 == 0) {
n /= 4;
}
if (n % 8 == 7) {
return 4;
}
const int sq = sqrt(n);
for (int i = 1; i <= sq; i++) {
if (isSquare(n - i*i)) {
return 2;
}
}
return 3;
}
int main() {
cout << numSquares(12) << endl;
cout << numSquares(13) << endl;
}
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 something
about the mathematics behind it.
Hope you learn something interesting from this code challenge.
Have fun with coding and mathematics!
15.9.6 Exercise
4 https://leetcode.com/problems/ways-to-express-an-integer-as-sum-of-powers/
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 learn-
ing, and keep challenging yourself. With hard work and dedication, you can be-
come an expert in coding and a valuable asset to any team.
337
338 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.
339
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.
340
THANK YOU!
Dear Readers,
Thank you for reading my book! Your support means the world to me. If you enjoyed
the book and would like to share your thoughts, I would greatly appreciate it if you
could leave a review and join our vibrant reader community. Your feedback and
engagement help shape the future of our community, and I’m excited to connect with
you further.
Again, Thank you for your support and for participating in this journey.
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!
Warm regards,
Nhut Nguyen, Ph.D.
341
342
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 sev-
eral 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 under-
standing 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 nhutnguyen.com.
343
INDEX
A P
algorithm complexity, 2 Partial sort, 162
permutation, 318
B power set, 310
binomial coefficient, 324
bit masking, 156 R
bitwise AND, 143 readable code, 4
bitwise XOR, 143, 234
S
C sliding window, 74, 99
Coding challenges, 1 Sorting, 149
std::accumulate, 104
D std::bitset, 152
dictionary order, 172 std::nth_element, 163
dummy node, 45 std::priority_queue, 123, 128, 132,
138
F std::set, 177
Fast and Slow, 266, 272 std::sort, 201
Fibonacci Number, 204 std::stoi, 104
std::swap, 26
K
Kadane's algorithm, 250
L
LeetCode, 1
M
memoization, 219
Morse Code, 87
344