Learning Plan for Quick Mastery
Some readers have given feedback that the content on this site is indeed solid. If studied seriously, it is possible to fully master data structures and algorithms. However, their current issue is the lack of time. How can this be addressed?
Is it possible to focus on the essentials, first learning the must-know techniques, and quickly improve skills before gradually studying more in the future?
I believe this is a demand from a significant portion of readers. Therefore, I have specifically written this quick mastery directory to meet these readers' needs.
The quick mastery directory cannot cover everything, so if you have ample time, I still recommend learning in the order of the main site directory.
Before You Start
To do a good job, one must first sharpen one's tools. Please spend a few minutes to read the site introduction to understand the site's tools and useful features. Also, read the first part of the Algorithm Visualization Panel Instructions to learn the basic usage of the visualization panel. Using tools reasonably based on your situation can greatly enhance learning efficiency.
About Suggested Study Time
The suggested study time for each article below is a rough estimate on the higher side, assuming the reader's time is limited, learning only 1-2 hours a day in fragmented time.
This suggested time is for reference only, as actual learning time will vary for each person. If you can dedicate whole blocks of time to learning, the progress will be much faster.
Moreover, quick mastery readers should not be too persistent. If you cannot understand some points immediately, note them down and skip for now. Once the entire knowledge system is established, many problems will be naturally resolved.
About Problem Sets
Readers often ask questions related to company problem sets, so I will elaborate here.
Different companies have different problem sets, with varying difficulty levels. The quick mastery directory includes must-know algorithm techniques and classic algorithm problems, not limited to any specific company’s problem set.
This directory aims to fundamentally tackle the tough challenge of algorithms, allowing you to independently solve medium-difficulty common algorithm problems with ease, rather than relying on luck and memorization of specific problem sets.
Some readers start preparing algorithms by directly tackling company problem sets, which is incorrect. My suggestion is to first understand the common algorithm frameworks, then consider practicing problem sets.
Regardless of the company’s problem set, algorithms are based on certain frameworks and techniques. Once you master these techniques, you can go through a problem set in just two to three hours at most, with no mystery involved.
Problem sets have only two values:
1. To understand the difficulty and types of algorithms a company might test. For example, if your company clearly states that dynamic programming algorithms will not be tested, you can skip that part in this directory and focus on other algorithm techniques.
2. For review purposes. After mastering the algorithm frameworks, you can independently solve problems, and then practice through problem sets to test learning outcomes and solidify knowledge points.
Below is a summary of the problem sets involved in this directory. Although there seem to be many problems, most are framework questions, and you can complete them all smoothly after learning the articles below, without needing to practice separately. This problem set is provided for review by readers who have completed this directory:
Quick Mastery Directory Problem List
Linked List Two Pointers
Recursion on Linked List
LeetCode | Difficulty |
---|---|
234. Palindrome Linked List | |
206. Reverse Linked List | |
92. Reverse Linked List II | 🟠 |
25. Reverse Nodes in k-Group | 🔴 |
Array Two Pointers
Two-Dimensional Array Operations
Sliding Window Algorithm
Binary Search Algorithm
LeetCode | Difficulty |
---|---|
704. Binary Search | |
34. Find First and Last Position of Element in Sorted Array | 🟠 |
875. Koko Eating Bananas | 🟠 |
1011. Capacity To Ship Packages Within D Days | 🟠 |
410. Split Array Largest Sum | 🔴 |
Prefix Sum/Difference Technique
LeetCode | Difficulty |
---|---|
303. Range Sum Query - Immutable | |
304. Range Sum Query 2D - Immutable | 🟠 |
1109. Corporate Flight Bookings | 🟠 |
1094. Car Pooling | 🟠 |
Stack
LeetCode | Difficulty |
---|---|
71. Simplify Path | 🟠 |
143. Reorder List | 🟠 |
20. Valid Parentheses | |
150. Evaluate Reverse Polish Notation | 🟠 |
225. Implement Stack using Queues | |
388. Longest Absolute File Path | 🟠 |
Queue
LeetCode | Difficulty |
---|---|
933. Number of Recent Calls | |
622. Design Circular Queue | 🟠 |
641. Design Circular Deque | 🟠 |
1670. Design Front Middle Back Queue | 🟠 |
2073. Time Needed to Buy Tickets |
Monotonic Stack Technique
Monotonic Queue Technique
Binary Tree
Binary Search Tree
Data Structure Design
Graph Algorithms
DFS/Backtracking Algorithms
BFS Algorithms
Dynamic Programming
Greedy Algorithms
LeetCode | Difficulty |
---|---|
55. Jump Game | 🟠 |
45. Jump Game II | 🟠 |
134. Gas Station | 🟠 |
Divide and Conquer Algorithms
LeetCode | Difficulty |
---|---|
23. Merge k Sorted Lists | 🔴 |
241. Different Ways to Add Parentheses | 🟠 |
Math Algorithms
LeetCode | Difficulty |
---|---|
292. Nim Game | |
877. Stone Game | 🟠 |
319. Bulb Switcher | 🟠 |
382. Linked List Random Node | 🟠 |
398. Random Pick Index | 🟠 |
384. Shuffle an Array | 🟠 |
204. Count Primes | 🟠 |
372. Super Pow | 🟠 |
Other Classic Interview Questions
The Quick Mastery problem set is fully integrated with the site's plugins. After installing the Chrome plugin, you can view the site's solution explanations directly on the LeetCode problem pages. The problem list is also integrated into the VSCode plugin and the JetBrains plugin:

Principles of the Quick Mastery Catalog
Why can the Quick Mastery catalog rapidly enhance algorithm skills in a short time?
- Template-based framework and unified solution style.
This catalog is not just a list of problems; it summarizes a set of framework templates for each algorithm. All exercises are solved using a unified framework. You learn genuine problem-solving skills rather than rote memorization, enabling you to independently tackle unfamiliar problems without being confined to a specific problem list.
- Gradual progression from basic to advanced, including both data structures and algorithms.
An algorithm technique A might be derived from algorithms B and C. Without any background, directly learning A might seem esoteric. However, if you understand B and C, you can easily deduce A. For instance, many fear recursive algorithms; I often emphasize that the essence of algorithms is brute-force, and recursion is a classic brute-force method based on tree structures. Mastering binary tree structures allows you to excel in all recursive algorithms. This catalog starts with key data structures before tackling exercises, ensuring a strong foundation for efficient problem-solving.
- Scientific learning methods and ample auxiliary tools. By combining examples and exercises, practicing after learning, and using problem-solving plugins and visualization panels, we strive to reduce comprehension costs and enhance learning efficiency.
I reiterate, I understand some readers are pressed for time, but do not skip frameworks and dive straight into problem-solving.
Relying solely on problem lists depends heavily on luck. Encountering a familiar problem might allow you to recall the solution, but without luck, solving requires algorithm frameworks. Without learning them, success is unlikely. A firm foundation saves time in the long run.
Let's begin. The Quick Mastery catalog is divided into "Data Structures" and "Algorithm Exercises" sections: the data structures section is simpler, focusing on fundamentals, while the exercises section is crucial, requiring thoughtful engagement and hands-on practice.
Data Structures
Arrays and Linked Lists
Introduce basic concepts of arrays and linked lists, which are relatively simple. The key focus is the circular array technique, which allows time complexity for insertion and deletion at the array's head.
Key Foundation, Suggested Time: 1 Day
Principles and Applications of Hash Tables
As a quick mastery tutorial, we can skip the code implementations of the two methods for resolving hash collisions in hash tables, but understanding the principles of hash tables is essential.
Hash tables can be combined with arrays and doubly linked lists to form more powerful data structures such as LinkedHashMap
and ArrayHashMap
. These structures add new features to hash tables. Understanding their principles will help you recognize these tools when encountering applicable scenarios.
Key Fundamentals, Suggested Time 1-2 Days
Basics and Traversal of Binary Trees
Although this is just the basic knowledge section, the part on binary trees must be taken seriously. Especially the traversal of binary trees, which is key to understanding recursive thinking. Once you start practicing problems, all recursive algorithms are essentially traversals of binary trees.
Key Basics, Suggested Time: 1-2 Days
- Basics and Common Types of Binary Trees
- Recursion/Level Order Traversal of Binary Trees
- Recursion/Level Order Traversal of Multi-way Trees
- Scenarios for DFS and BFS
Three Templates for Level Order Traversal
The first, simplest method:
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
// visit the cur node
System.out.println(cur.val);
// add the left and right children of cur to the queue
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
void levelOrderTraverse(TreeNode* root) {
if (root == nullptr) {
return;
}
std::queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* cur = q.front();
q.pop();
// visit the cur node
std::cout << cur->val << std::endl;
// add the left and right children of cur to the queue
if (cur->left != nullptr) {
q.push(cur->left);
}
if (cur->right != nullptr) {
q.push(cur->right);
}
}
}
from collections import deque
def levelOrderTraverse(root):
if root is None:
return
q = deque()
q.append(root)
while q:
cur = q.popleft()
# visit cur node
print(cur.val)
# add cur's left and right children to the queue
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
func levelOrderTraverse(root *TreeNode) {
if root == nil {
return
}
q := []*TreeNode{}
q = append(q, root)
for len(q) > 0 {
cur := q[0]
q = q[1:]
// visit cur node
fmt.Println(cur.Val)
// add cur's left and right children to the queue
if cur.Left != nil {
q = append(q, cur.Left)
}
if cur.Right != nil {
q = append(q, cur.Right)
}
}
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
var q = [];
q.push(root);
while (q.length !== 0) {
var cur = q.shift();
// visit cur node
console.log(cur.val);
// add cur's left and right children to the queue
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
}
The second, commonly used method utilizes step
to record level information:
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// record the current level being traversed (root node is considered as level 1)
int depth = 1;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
// visit the cur node and know its level
System.out.println("depth = " + depth + ", val = " + cur.val);
// add the left and right children of cur to the queue
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
depth++;
}
}
void levelOrderTraverse(TreeNode* root) {
if (root == nullptr) {
return;
}
queue<TreeNode*> q;
q.push(root);
// record the current level being traversed (the root node is considered as level 1)
int depth = 1;
while (!q.empty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode* cur = q.front();
q.pop();
// visit the cur node, knowing its level
cout << "depth = " << depth << ", val = " << cur->val << endl;
// add the left and right children of cur to the queue
if (cur->left != nullptr) {
q.push(cur->left);
}
if (cur->right != nullptr) {
q.push(cur->right);
}
}
depth++;
}
}
from collections import deque
def levelOrderTraverse(root):
if root is None:
return
q = deque()
q.append(root)
# record the current depth being traversed (root node is considered as level 1)
depth = 1
while q:
sz = len(q)
for i in range(sz):
cur = q.popleft()
# visit cur node and know its depth
print(f"depth = {depth}, val = {cur.val}")
# add cur's left and right children to the queue
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
depth += 1
func levelOrderTraverse(root *TreeNode) {
if root == nil {
return
}
q := []*TreeNode{root}
// record the current depth being traversed (root node is considered as level 1)
depth := 1
for len(q) > 0 {
// get the current queue length
sz := len(q)
for i := 0; i < sz; i++ {
// dequeue the front of the queue
cur := q[0]
q = q[1:]
// visit the cur node and know its depth
fmt.Printf("depth = %d, val = %d\n", depth, cur.Val)
// add cur's left and right children to the queue
if cur.Left != nil {
q = append(q, cur.Left)
}
if cur.Right != nil {
q = append(q, cur.Right)
}
}
depth++
}
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
let q = [];
q.push(root);
// record the current depth (root node is considered as level 1)
let depth = 1;
while (q.length !== 0) {
let sz = q.length;
for (let i = 0; i < sz; i++) {
let cur = q.shift();
// visit the cur node and know its depth
console.log("depth = " + depth + ", val = " + cur.val);
// add the left and right children of cur to the queue
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
depth++;
}
};
The third, more complex yet flexible method uses a custom State
class to maintain node information, often seen in graph algorithms or complex BFS algorithms:
class State {
TreeNode node;
int depth;
State(TreeNode node, int depth) {
this.node = node;
this.depth = depth;
}
}
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<State> q = new LinkedList<>();
// the path weight sum of the root node is 1
q.offer(new State(root, 1));
while (!q.isEmpty()) {
State cur = q.poll();
// visit the cur node, while knowing its path weight sum
System.out.println("depth = " + cur.depth + ", val = " + cur.node.val);
// add the left and right child nodes of cur to the queue
if (cur.node.left != null) {
q.offer(new State(cur.node.left, cur.depth + 1));
}
if (cur.node.right != null) {
q.offer(new State(cur.node.right, cur.depth + 1));
}
}
}
class State {
public:
TreeNode* node;
int depth;
State(TreeNode* node, int depth) : node(node), depth(depth) {}
};
void levelOrderTraverse(TreeNode* root) {
if (root == nullptr) {
return;
}
queue<State> q;
// the path weight sum of the root node is 1
q.push(State(root, 1));
while (!q.empty()) {
State cur = q.front();
q.pop();
// visit the cur node, while knowing its path weight sum
cout << "depth = " << cur.depth << ", val = " << cur.node->val << endl;
// add the left and right children of cur to the queue
if (cur.node->left != nullptr) {
q.push(State(cur.node->left, cur.depth + 1));
}
if (cur.node->right != nullptr) {
q.push(State(cur.node->right, cur.depth + 1));
}
}
}
class State:
def __init__(self, node, depth):
self.node = node
self.depth = depth
def levelOrderTraverse(root):
if root is None:
return
q = deque()
# the path weight sum of the root node is 1
q.append(State(root, 1))
while q:
cur = q.popleft()
# visit the cur node, and know its path weight sum
print(f"depth = {cur.depth}, val = {cur.node.val}")
# add the left and right child nodes of cur to the queue
if cur.node.left is not None:
q.append(State(cur.node.left, cur.depth + 1))
if cur.node.right is not None:
q.append(State(cur.node.right, cur.depth + 1))
type State struct {
node *TreeNode
depth int
}
func levelOrderTraverse(root *TreeNode) {
if root == nil {
return
}
q := []State{{root, 1}}
for len(q) > 0 {
cur := q[0]
q = q[1:]
// visit the cur node, also know its path weight sum
fmt.Printf("depth = %d, val = %d\n", cur.depth, cur.node.Val)
// add cur's left and right children to the queue
if cur.node.Left != nil {
q = append(q, State{cur.node.Left, cur.depth + 1})
}
if cur.node.Right != nil {
q = append(q, State{cur.node.Right, cur.depth + 1})
}
}
}
function State(node, depth) {
this.node = node;
this.depth = depth;
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
// @visualize bfs
var q = [];
// the path weight sum of the root node is 1
q.push(new State(root, 1));
while (q.length !== 0) {
var cur = q.shift();
// visit the cur node, while knowing its path weight sum
console.log("depth = " + cur.depth + ", val = " + cur.node.val);
// add the left and right children of cur to the queue
if (cur.node.left !== null) {
q.push(new State(cur.node.left, cur.depth + 1));
}
if (cur.node.right !== null) {
q.push(new State(cur.node.right, cur.depth + 1));
}
}
};
Key Basics, Suggested Time: 0.5 Days
The binary search tree is a special type of binary tree. Here, you only need to have a general understanding of its application scenarios. Exercises will help you master its problem-solving techniques later.
Principles of Binary Heaps
The key application of binary heaps is the priority queue. As a quick mastery tutorial, we can skip the implementation of the priority queue, but it's important to understand the following key points:
A priority queue is a data structure that automatically sorts, with the complexity of adding and removing elements being , implemented using a binary heap.
A binary heap is a complete binary tree with special properties.
The core methods of the priority queue (binary heap) are
swim
andsink
, used to maintain the properties of the binary heap.
Key Basics, Suggested Time: 0.5 Days
Use Cases for Segment Tree
Segment trees are a type of data structure that may be used in algorithm interviews to perform interval queries and updates with a time complexity of .
Key Foundation, Recommended Time: 0.5 Days
The above article mainly clarifies the following points:
The functionality and use cases of segment trees. When you encounter similar scenarios, you will know that this special data structure can be used to efficiently solve the problem.
The reason why the query and update complexity of segment trees is . This point may be asked in interviews. The visualization panel on our site helps in understanding the underlying principles.
Segment trees can be implemented using a linked structure or an array. The array implementation of a segment tree requires four times the space during initialization.
For specific code implementations, you can save the AllInOneSegmentTree
implementation provided at the end of Lazy Propagation in Segment Trees, which has all optimizations and APIs implemented. You can directly use it during interviews when needed.
Graph Structure
Graph algorithms are a vast topic, but as part of the foundational data structures chapter, we only need to understand the logical representation and specific implementation of graphs, as well as graph traversal algorithms.
In essence, they are not very complex and are an extension of the binary tree structures discussed earlier. The following two articles provide general implementations of graph structures and templates for traversal algorithms.
Key Foundation, Recommended Time: 1 Day
Start Practicing
Begin your practice now. If this is your first time using an online coding platform, you may want to check LeetCode Guide to understand the basic rules of solving problems on LeetCode.
Core Framework, Recommended Time: 0.5 Day
This article serves as the outline for all content on this site. It not only summarizes the data structures introduced above but also points out the essence of all algorithms.
There will be many links to other articles, and it may not be easy to fully understand at first read. However, there is no need to struggle; just grasp the general thinking methods introduced. As you study further content, you will gradually comprehend the essence of this article.
Linked List
The pattern of linked list problems is relatively fixed, mainly using the two-pointer technique.
Core Framework, Recommended Time: 1 Day
Exercises, Recommended Time: 1 Day
An advanced technique for singly linked lists is recursive operations, though this approach is generally mentioned during interviews. In written tests, standard pointer operations are sufficient.
Core Framework, Recommended Time: 1 Day
Arrays
Techniques related to arrays mainly involve the two-pointer approach, which can be divided into different types such as fast and slow pointers, and left and right pointers. Classic two-pointer algorithms for arrays include binary search and sliding windows.
Some readers ask why there isn't a special topic on string algorithms, as strings are essentially character arrays, and string algorithms are fundamentally array algorithms.
Core Framework, Suggested Time: 0.5 Days
Exercises, Suggested Time: 1~2 Days
Core Framework, Suggested Time: 1 Day
Sliding Window Code Template (Pseudo Code)
// Pseudocode framework of sliding window algorithm
void slidingWindow(String s) {
// Use an appropriate data structure to record the data in the
// window, which can vary according to the specific scenario
// For example, if I want to record the frequency
// of elements in the window, I would use a map
// If I want to record the sum of elements in the window, I could just use an int
Object window = ...
int left = 0, right = 0;
while (right < s.length()) {
// c is the character that will be added to the window
char c = s[right];
window.add(c)
// Expand the window
right++;
// Perform a series of updates to the data within the window
...
// *** Position of debug output ***
// Note that in the final solution code, do not use print
// Because IO operations are time-consuming and may cause timeouts
printf("window: [%d, %d)\n", left, right);
// ***********************
// Determine whether the left side of the window needs to shrink
while (left < right && window needs shrink) {
// d is the character that will be removed from the window
char d = s[left];
window.remove(d)
// Shrink the window
left++;
// Perform a series of updates to the data within the window
...
}
}
}
// Pseudocode framework of sliding window algorithm
void slidingWindow(string s) {
// Use an appropriate data structure to record the data
// in the window, varying with the specific scenario
// For instance, if I want to record the frequency
// of elements in the window, I would use a map
// If I want to record the sum of elements in the window, I can simply use an int
auto window = ...
int left = 0, right = 0;
while (right < s.size()) {
// c is the character that will be entering the window
char c = s[right];
window.add(c);
// Expand the window
right++;
// Perform a series of updates to the data within the window
...
// *** position of debug output ***
printf("window: [%d, %d)\n", left, right);
// Note that printing should be avoided in the final solution code
// Because IO operations are time-consuming and may cause timeouts
// Determine whether the left side of the window needs to shrink
while (window needs shrink) {
// d is the character that will be removed from the window
char d = s[left];
window.remove(d);
// Shrink the window
left++;
// Perform a series of updates to the data within the window
...
}
}
}
# Pseudocode framework for sliding window algorithm
def slidingWindow(s: str):
# Use an appropriate data structure to record the data in
# the window, which can vary depending on the scenario
# For example, if I want to record the frequency
# of elements in the window, I would use a map
# If I want to record the sum of elements in the window, I could just use an int
window = ...
left, right = 0, 0
while right < len(s):
# c is the character that will be added to the window
c = s[right]
window.add(c)
# Expand the window
right += 1
# Perform a series of updates on the data within the window
...
# *** position for debug output ***
# Note that you should not print in the final solution code
# because IO operations are time-consuming and may cause timeouts
# print(f"window: [{left}, {right})")
# ***********************
# Determine whether the left side of the window needs to be contracted
while left < right and window needs shrink:
# d is the character that will be removed from the window
d = s[left]
window.remove(d)
# Shrink the window
left += 1
# Perform a series of updates on the data within the window
...
// Pseudocode framework for sliding window algorithm
func slidingWindow(s string) {
// Use an appropriate data structure to record the data in the
// window, which can vary according to the specific scenario
// For example, if I want to record the frequency of elements in the window, I use a map
// If I want to record the sum of elements in the window, I can just use an int
var window = ...
left, right := 0, 0
for right < len(s) {
// c is the character to be moved into the window
c := rune(s[right])
window[c]++
// Expand the window
right++
// Perform a series of updates to the data within the window
...
// *** debug output location ***
// Note that in the final solution code, do not print
// Because IO operations are time-consuming and may cause timeouts
fmt.Println("window: [",left,", ",right,")")
// ***********************
// Determine whether the left window needs to shrink
for left < right && window needs shrink {
// d is the character to be moved out of the window
d := rune(s[left])
window[d]--
// Shrink the window
left++
// Perform a series of updates to the data within the window
...
}
}
}
// Pseudocode framework for sliding window algorithm
var slidingWindow = function(s) {
// Use an appropriate data structure to record the data in
// the window, which can vary according to the scenario
// For example, if I want to record the frequency of elements in the window, I use a map
// If I want to record the sum of the elements in the window, I can just use an int
var window = ...;
var left = 0, right = 0;
while (right < s.length) {
// c is the character that will be moved into the window
var c = s[right];
window.add(c);
// Expand the window
right++;
// Perform a series of updates on the data within the window
...
// *** debug output position ***
// Note that in the final solution code, do not print
// Because IO operations are time-consuming and may cause timeouts
console.log("window: [%d, %d)\n", left, right);
// *********************
// Determine whether the left side of the window needs to shrink
while (window needs shrink) {
// d is the character that will be moved out of the window
var d = s[left];
window.remove(d);
// Shrink the window
left++;
// Perform a series of updates on the data within the window
...
}
}
};
Exercises, Suggested Time: 1 Day
Core Framework, Suggested Time: 1~2 Days
Three Binary Search Code Templates
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// return directly
return mid;
}
}
// return directly
return -1;
}
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// do not return, lock the left boundary
right = mid - 1;
}
}
// determine if target exists in nums
if (left < 0 || left >= nums.length) {
return -1;
}
// check if nums[left] is the target
return nums[left] == target ? left : -1;
}
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// do not return, lock the right boundary
left = mid + 1;
}
}
// since the while loop ends with right == left - 1
// and we are now looking for the right boundary
// it's easier to remember to use right instead of left - 1
if (right < 0 || right >= nums.length) {
return -1;
}
return nums[right] == target ? right : -1;
}
int binary_search(vector<int>& nums, int target) {
int left = 0, right = nums.size()-1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// return directly
return mid;
}
}
// return directly
return -1;
}
int left_bound(vector<int>& nums, int target) {
int left = 0, right = nums.size()-1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// do not return, lock the left boundary
right = mid - 1;
}
}
// determine if target exists in nums
if (left < 0 || left >= nums.size()) {
return -1;
}
// check if nums[left] is target
return nums[left] == target ? left : -1;
}
int right_bound(vector<int>& nums, int target) {
int left = 0, right = nums.size()-1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// do not return, lock the right boundary
left = mid + 1;
}
}
// since the while loop ends when right == left -
// 1, and we are now finding the right boundary
// so it's easier to remember to use right instead of left - 1
if (right < 0 || right >= nums.size()) {
return -1;
}
return nums[right] == target ? right : -1;
}
def binary_search(nums: List[int], target: int) -> int:
# set left and right indexes
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
elif nums[mid] == target:
# found the target value
return mid
# target value not found
return -1
def left_bound(nums: List[int], target: int) -> int:
# set left and right indexes
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
elif nums[mid] == target:
# target exists, narrow the right boundary
right = mid - 1
# determine if the target exists
if left < 0 or left >= len(nums):
return -1
# determine if the left boundary found is the target value
return left if nums[left] == target else -1
def right_bound(nums: List[int], target: int) -> int:
# set left and right indexes
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
elif nums[mid] == target:
# target exists, narrow the left boundary
left = mid + 1
# determine if the target exists
if right < 0 or right >= len(nums):
return -1
# determine if the right boundary found is the target value
return right if nums[right] == target else -1
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right - left) / 2
if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid - 1
} else if nums[mid] == target {
// return directly
return mid
}
}
// return directly
return -1
}
func leftBound(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right - left) / 2
if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid - 1
} else if nums[mid] == target {
// do not return, lock the left boundary
right = mid - 1
}
}
// determine if target exists in nums
if left < 0 || left >= len(nums) {
return -1
}
// determine if nums[left] is target
if nums[left] == target {
return left
}
return -1
}
func rightBound(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right - left) / 2
if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid - 1
} else if nums[mid] == target {
// do not return, lock the right boundary
left = mid + 1
}
}
if right < 0 || right >= len(nums) {
return -1
}
if nums[right] == target {
return right
}
return -1
}
var binary_search = function(nums, target) {
var left = 0, right = nums.length - 1;
while(left <= right) {
var mid = left + Math.floor((right - left) / 2);
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// return directly
return mid;
}
}
// return directly
return -1;
}
var left_bound = function(nums, target) {
var left = 0, right = nums.length - 1;
while (left <= right) {
var mid = left + Math.floor((right - left) / 2);
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// do not return, lock the left boundary
right = mid - 1;
}
}
// determine if target exists in nums
if (left < 0 || left >= nums.length) {
return -1;
}
// check if nums[left] is the target
return nums[left] == target ? left : -1;
}
var right_bound = function(nums, target) {
var left = 0, right = nums.length - 1;
while (left <= right) {
var mid = left + Math.floor((right - left) / 2);
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// do not return, lock the right boundary
left = mid + 1;
}
}
// since the loop ends when right == left - 1 and we are looking for the right boundary
// so using right instead of left - 1 is easier to remember
if (right < 0 || right >= nums.length) {
return -1;
}
return nums[right] == target ? right : -1;
}
Core Framework, Suggested Time: 1~2 Days
Prefix Sum Array Code Templates
One-Dimensional Prefix Sum:
class NumArray {
// prefix sum array
private int[] preSum;
// input an array to construct the prefix sum
public NumArray(int[] nums) {
// preSum[0] = 0, to facilitate the calculation of accumulated sums
preSum = new int[nums.length + 1];
// calculate the accumulated sums of nums
for (int i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
// query the sum of the closed interval [left, right]
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
}
#include <vector>
class NumArray {
// prefix sum array
std::vector<int> preSum;
// input an array to construct the prefix sum
public:
NumArray(std::vector<int>& nums) {
// preSum[0] = 0, to facilitate the calculation of accumulated sums
preSum.resize(nums.size() + 1);
// calculate the accumulated sums of nums
for (int i = 1; i < preSum.size(); i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
// query the sum of the closed interval [left, right]
int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
};
class NumArray:
# prefix sum array
def __init__(self, nums: List[int]):
# input an array to construct the prefix sum
# preSum[0] = 0, to facilitate the calculation of accumulated sums
self.preSum = [0] * (len(nums) + 1)
# calculate the accumulated sums of nums
for i in range(1, len(self.preSum)):
self.preSum[i] = self.preSum[i - 1] + nums[i - 1]
# query the sum of the closed interval [left, right]
def sumRange(self, left: int, right: int) -> int:
return self.preSum[right + 1] - self.preSum[left]
type NumArray struct {
// prefix sum array
PreSum []int
}
// input an array to construct the prefix sum
func Constructor(nums []int) NumArray {
// PreSum[0] = 0, to facilitate the calculation of accumulated sums
preSum := make([]int, len(nums)+1)
// calculate the accumulated sums of nums
for i := 1; i < len(preSum); i++ {
preSum[i] = preSum[i-1] + nums[i-1]
}
return NumArray{PreSum: preSum}
}
// query the sum of the closed interval [left, right]
func (this *NumArray) SumRange(left int, right int) int {
// The following line includes the missing comment:
// PreSum[0] = 0, to facilitate the calculation of accumulated sums
return this.PreSum[right+1] - this.PreSum[left] // Here we are using the prefix sum property, no need to repeat the comment here.
}
var NumArray = function(nums) {
// prefix sum array
let preSum = new Array(nums.length + 1).fill(0);
// preSum[0] = 0, to facilitate the calculation of accumulated sums
preSum[0] = 0;
// input an array to construct the prefix sum
for (let i = 1; i < preSum.length; i++) {
// calculate the accumulated sums of nums
preSum[i] = preSum[i - 1] + nums[i - 1];
}
this.preSum = preSum;
};
// query the sum of the closed interval [left, right]
NumArray.prototype.sumRange = function(left, right) {
return this.preSum[right + 1] - this.preSum[left];
};
Two-Dimensional Prefix Sum:
class NumMatrix {
// preSum[i][j] records the sum of elements in the matrix [0, 0, i-1, j-1]
private int[][] preSum;
public NumMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
if (m == 0 || n == 0) return;
// construct the prefix sum matrix
preSum = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// calculate the sum of elements for each matrix [0, 0, i, j]
preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1];
}
}
}
// calculate the sum of elements in the submatrix [x1, y1, x2, y2]
public int sumRegion(int x1, int y1, int x2, int y2) {
// the sum of the target matrix is obtained by operations on four adjacent matrices
return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1];
}
}
#include <vector>
class NumMatrix {
// preSum[i][j] records the sum of elements in the matrix [0, 0, i-1, j-1]
std::vector<std::vector<int>> preSum;
public:
NumMatrix(std::vector<std::vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
if (m == 0 || n == 0) return;
// construct the prefix sum matrix
preSum.resize(m + 1, std::vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// calculate the sum of elements for each matrix [0, 0, i, j]
preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1];
}
}
}
// calculate the sum of elements in the submatrix [x1, y1, x2, y2]
int sumRegion(int x1, int y1, int x2, int y2) {
// the sum of the target matrix is obtained by operations on four adjacent matrices
return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1];
}
};
class NumMatrix:
# preSum[i][j] records the sum of elements in the matrix [0, 0, i-1, j-1]
def __init__(self, matrix: List[List[int]]):
m = len(matrix)
n = len(matrix[0])
if m == 0 or n == 0:
return
# construct the prefix sum matrix
self.preSum = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
# calculate the sum of elements for each matrix [0, 0, i, j]
self.preSum[i][j] = (self.preSum[i - 1][j] + self.preSum[i][j - 1] +
matrix[i - 1][j - 1] - self.preSum[i - 1][j - 1])
# calculate the sum of elements in the submatrix [x1, y1, x2, y2]
def sumRegion(self, x1: int, y1: int, x2: int, y2: int) -> int:
# the sum of the target matrix is obtained by operations on four adjacent matrices
return (self.preSum[x2 + 1][y2 + 1] - self.preSum[x1][y2 + 1] -
self.preSum[x2 + 1][y1] + self.preSum[x1][y1])
type NumMatrix struct {
// preSum[i][j] records the sum of elements in the matrix [0, 0, i-1, j-1]
preSum [][]int
}
func Constructor(matrix [][]int) NumMatrix {
m := len(matrix)
if m == 0 {
return NumMatrix{}
}
n := len(matrix[0])
if n == 0 {
return NumMatrix{}
}
// construct the prefix sum matrix
preSum := make([][]int, m+1)
for i := range preSum {
preSum[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
// calculate the sum of elements for each matrix [0, 0, i, j]
preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i-1][j-1] - preSum[i-1][j-1]
}
}
return NumMatrix{preSum: preSum}
}
// calculate the sum of elements in the submatrix [x1, y1, x2, y2]
func (this *NumMatrix) SumRegion(x1, y1, x2, y2 int) int {
// the sum of the target matrix is obtained by operations on four adjacent matrices
return this.preSum[x2+1][y2+1] - this.preSum[x1][y2+1] - this.preSum[x2+1][y1] + this.preSum[x1][y1]
}
var NumMatrix = function(matrix) {
let m = matrix.length, n = matrix[0].length;
// preSum[i][j] records the sum of elements in the matrix [0, 0, i-1, j-1]
this.preSum = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
if (m == 0 || n == 0) return;
// construct the prefix sum matrix
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
// calculate the sum of elements for each matrix [0, 0, i, j]
this.preSum[i][j] = this.preSum[i-1][j] + this.preSum[i][j-1] + matrix[i - 1][j - 1] - this.preSum[i-1][j-1];
}
}
};
NumMatrix.prototype.sumRegion = function(x1, y1, x2, y2) {
// calculate the sum of elements in the submatrix [x1, y1, x2, y2]
// the sum of the target matrix is obtained by operations on four adjacent matrices
return this.preSum[x2+1][y2+1] - this.preSum[x1][y2+1] - this.preSum[x2+1][y1] + this.preSum[x1][y1];
};
Difference Array Code Template
// Difference Array Utility Class
class Difference {
// difference array
private int[] diff;
// input an initial array, range operations will be performed on this array
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// construct the difference array based on the initial array
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
// increment the closed interval [i, j] by val (can be negative)
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
// return the result array
public int[] result() {
int[] res = new int[diff.length];
// construct the result array based on the difference array
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
// Difference Array Tool Class
class Difference {
// difference array
private:
vector<int> diff;
// input an initial array, range operations will be performed on this array
public:
Difference(vector<int>& nums) {
diff = vector<int>(nums.size());
// construct the difference array based on the initial array
diff[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
// add val (can be negative) to the closed interval [i, j]
void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.size()) {
diff[j + 1] -= val;
}
}
// return the result array
vector<int> result() {
vector<int> res(diff.size());
// construct the result array based on the difference array
res[0] = diff[0];
for (int i = 1; i < diff.size(); i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
};
# Difference Array Tool Class
class Difference:
# difference array
def __init__(self, nums: List[int]):
assert len(nums) > 0
self.diff = [0] * len(nums)
# construct the difference array based on the initial array
self.diff[0] = nums[0]
for i in range(1, len(nums)):
self.diff[i] = nums[i] - nums[i - 1]
# increment the closed interval [i, j] by val (can be negative)
def increment(self, i: int, j: int, val: int) -> None:
self.diff[i] += val
if j + 1 < len(self.diff):
self.diff[j + 1] -= val
# return the result array
def result(self) -> List[int]:
res = [0] * len(self.diff)
# construct the result array based on the difference array
res[0] = self.diff[0]
for i in range(1, len(self.diff)):
res[i] = res[i - 1] + self.diff[i]
return res
// Difference array utility class
type Difference struct {
// difference array
diff []int
}
// Input an initial array, interval operations will be performed on this array
func NewDifference(nums []int) *Difference {
diff := make([]int, len(nums))
// construct the difference array based on the initial array
diff[0] = nums[0]
for i := 1; i < len(nums); i++ {
diff[i] = nums[i] - nums[i-1]
}
return &Difference{diff: diff}
}
// Add val (can be negative) to the closed interval [i, j]
func (d *Difference) Increment(i, j, val int) {
d.diff[i] += val
if j+1 < len(d.diff) {
d.diff[j+1] -= val
}
}
// Return the result array
func (d *Difference) Result() []int {
res := make([]int, len(d.diff))
// construct the result array based on the difference array
res[0] = d.diff[0]
for i := 1; i < len(d.diff); i++ {
res[i] = res[i-1] + d.diff[i]
}
return res
}
class Difference {
constructor(nums) {
// difference array
this.diff = new Array(nums.length);
// construct the difference array based on the initial array
this.diff[0] = nums[0];
for (let i = 1; i < nums.length; i++) {
this.diff[i] = nums[i] - nums[i - 1];
}
}
// increase the closed interval [i, j] by val (can be negative)
increment(i, j, val) {
this.diff[i] += val;
if (j + 1 < this.diff.length) {
this.diff[j + 1] -= val;
}
}
// return the result array
result() {
let res = new Array(this.diff.length);
// construct the result array based on the difference array
res[0] = this.diff[0];
for (let i = 1; i < this.diff.length; i++) {
res[i] = res[i - 1] + this.diff[i];
}
return res;
}
}
Queue/Stack
Queues and stacks are relatively simple data structures, but their application in algorithm problems requires dedicated practice.
Core Framework, Suggested Time: 0.5 Days
Exercises, Suggested Time: 1-2 Days
Monotonic stacks and queues are two variants based on stacks and queues, capable of solving specific problems that need to be mastered.
Core Framework, Suggested Time: 1-2 Days
Exercises, Suggested Time: 1-2 Days
Binary Trees & Recursive Thinking
The essence of all recursive algorithms is the traversal of binary trees. Binary tree algorithms frequently appear in interviews and tests, so I have included several articles in the binary tree section. I hope everyone will study and understand them thoroughly and practice hands-on.
Core Framework, Recommended Time: 0.5 Days
This article combines several simple classic algorithm problems to understand all recursive algorithms from the perspective of trees and classifies all recursive algorithms into two thinking patterns.
Core Framework, Recommended Time: 1 Day
This core outline is a comprehensive guide, mainly consisting of two parts: The first part is how to understand the pre-order, in-order, and post-order positions of binary trees in real algorithm problems; the second part introduces backtracking/dynamic programming algorithms from the perspective of binary trees.
Since you are already familiar with binary tree traversal algorithms, please study the first part carefully. The advanced algorithms mentioned in the second part do not require in-depth study at this moment. You just need to have an impression, which will deepen after learning backtracking/dynamic programming in the future.
Core Framework, Recommended Time: 2~3 Days
The examples in these tutorials are essential binary tree classic problems that need to be mastered.
Exercises, Recommended Time: 1 Day
Exercises, Recommended Time: 2 Days
I emphasize the importance of binary tree-related algorithm problems because the essence of algorithms is brute-force, and the tree structure is the core abstraction of all brute-force algorithms. Once you master binary trees, understanding advanced algorithms will become easier.
Our Binary Tree Algorithm Exercises Collection dedicates an entire chapter to practicing binary tree algorithms. According to the classification in Binary Tree Algorithms (Outline), the exercises are divided into three main parts:
Recursion, solving problems using the "traversal" thinking pattern.
Recursion, solving problems using the "problem decomposition" thinking pattern.
Non-recursion, solving problems using the "level-order traversal" thinking pattern.
The "traversal" thinking pattern is the prototype of DFS and backtracking algorithms explained later. The "problem decomposition" thinking pattern is the prototype of dynamic programming and divide-and-conquer algorithms. "Level-order traversal" is the prototype of BFS algorithms explained later.
Therefore, it is essential to practice these binary tree algorithms. The Binary Tree Algorithm Exercises Collection contains a substantial number of exercises. Below are exercises of moderate difficulty and high frequency of assessment for quick mastery readers:
- 【Exercise】Solving Problems with the "Traversal" Thinking I
- 【Exercise】Solving Problems with the "Problem Decomposition" Thinking I
- 【Exercise】Solving Problems with Level-Order Traversal I
If you have time and interest, you may explore other exercise sections for practice.
Binary Search Tree
A binary search tree is a special type of binary tree characterized by "left small, right large." Utilize this feature to optimize the traversal process of the binary tree.
Core Framework, Recommended Time: 1-2 Days
Core Framework, Recommended Time: 1 Day
Data Structure Design
LRU is a classic data structure design problem and must be mastered; LFU is more challenging and can be optional.
Exercise, Suggested Time 1 Day
Exercise, Suggested Time 1 Day
Implementing a calculator is a classic data structure design problem. If you don't have time, you can save the final calculator code provided and use it directly in case you encounter problems related to string calculation in exams.
Common Calculator Code Implementation
class Solution {
public int calculate(String s) {
// key is the index of the left parenthesis, value is the
// corresponding index of the right parenthesis
Map<Integer, Integer> rightIndex = new HashMap<>();
// use stack structure to find the corresponding parentheses
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i);
} else if (s.charAt(i) == ')') {
rightIndex.put(stack.pop(), i);
}
}
return _calculate(s, 0, s.length() - 1, rightIndex);
}
// Definition: return the result of the expression within s[start..end]
private int _calculate(String s, int start, int end, Map<Integer, Integer> rightIndex) {
// need to convert the string to a deque for easy operation
Stack<Integer> stk = new Stack<>();
// record the number in the formula
int num = 0;
// record the sign before num, initialized to +
char sign = '+';
for (int i = start; i <= end; i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
num = 10 * num + (c - '0');
}
if (c == '(') {
// recursively calculate the expression inside the parentheses
num = _calculate(s, i + 1, rightIndex.get(i) - 1, rightIndex);
i = rightIndex.get(i);
}
if (c == '+' || c == '-' || c == '*' || c == '/' || i == end) {
int pre;
switch (sign) {
case '+':
stk.push(num);
break;
case '-':
stk.push(-num);
break;
// just take out the previous number and perform the corresponding operation
case '*':
pre = stk.pop();
stk.push(pre * num);
break;
case '/':
pre = stk.pop();
stk.push(pre / num);
break;
}
// update the sign to the current sign, reset the number to zero
sign = c;
num = 0;
}
}
// sum all results in the stack to get the answer
int res = 0;
while (!stk.isEmpty()) {
res += stk.pop();
}
return res;
}
}
class Solution {
public:
int calculate(string s) {
// key is the index of the left parenthesis, value is the
// index of the corresponding right parenthesis
unordered_map<int, int> rightIndex;
// use stack structure to find the corresponding parentheses
stack<int> stack;
for (int i = 0; i < s.length(); i++) {
if (s[i] == '(') {
stack.push(i);
} else if (s[i] == ')') {
rightIndex[stack.top()] = i;
stack.pop();
}
}
return _calculate(s, 0, s.length() - 1, rightIndex);
}
private:
// definition: return the calculation result of the expression in s[start..end]
int _calculate(string s, int start, int end, unordered_map<int, int> &rightIndex) {
// need to convert the string into a deque for easy operation
stack<int> stk;
// record the number in the formula
int num = 0;
// record the sign before num, initialized to +
char sign = '+';
for (int i = start; i <= end; i++) {
char c = s[i];
if (isdigit(c)) {
num = 10 * num + (c - '0');
}
if (c == '(') {
// recursively calculate the expression in the parentheses
num = _calculate(s, i + 1, rightIndex[i] - 1, rightIndex);
i = rightIndex[i];
}
if (c == '+' || c == '-' || c == '*' || c == '/' || i == end) {
int pre;
switch (sign) {
case '+':
stk.push(num);
break;
case '-':
stk.push(-num);
break;
// just take out the previous number and perform the corresponding operation
case '*':
pre = stk.top(); stk.pop();
stk.push(pre * num);
break;
case '/':
pre = stk.top(); stk.pop();
stk.push(pre / num);
break;
}
// update the sign to the current sign, reset the number to zero
sign = c;
num = 0;
}
}
// summing all the results in the stack gives the answer
int res = 0;
while (!stk.empty()) {
res += stk.top();
stk.pop();
}
return res;
}
};
class Solution:
def calculate(self, s: str) -> int:
# key is the index of the left parenthesis, value is the
# corresponding index of the right parenthesis
rightIndex = {}
# use stack structure to find the corresponding parenthesis
stack = []
for i in range(len(s)):
if s[i] == '(':
stack.append(i)
elif s[i] == ')':
rightIndex[stack.pop()] = i
return self._calculate(s, 0, len(s) - 1, rightIndex)
# Definition: return the calculation result of the expression within s[start..end]
def _calculate(self, s, start, end, rightIndex):
# need to convert the string to a deque for easy operation
stk = []
# record the number in the formula
num = 0
# record the sign before num, initialized to +
sign = '+'
i = start
while i <= end:
c = s[i]
if c.isdigit():
num = 10 * num + int(c)
if c == '(':
# recursively calculate the expression inside the parenthesis
num = self._calculate(s, i + 1, rightIndex[i] - 1, rightIndex)
i = rightIndex[i]
if c in '+-*/' or i == end:
if sign == '+':
stk.append(num)
elif sign == '-':
stk.append(-num)
elif sign == '*':
pre = stk.pop()
stk.append(pre * num)
elif sign == '/':
pre = stk.pop()
stk.append(int(pre / num))
# update the sign to the current sign, reset the number to zero
sign = c
num = 0
i += 1
# sum all results in the stack to get the answer
res = 0
while stk:
res += stk.pop()
return res
func calculate(s string) int {
// key is the index of the left parenthesis, value is
// the corresponding index of the right parenthesis
rightIndex := make(map[int]int)
// use stack structure to find the corresponding parentheses
stack := []int{}
for i := 0; i < len(s); i++ {
if s[i] == '(' {
stack = append(stack, i)
} else if s[i] == ')' {
pop := stack[len(stack)-1]
stack = stack[:len(stack)-1]
rightIndex[pop] = i
}
}
return _calculate(s, 0, len(s)-1, rightIndex)
}
// definition: return the result of the expression within s[start..end]
func _calculate(s string, start int, end int, rightIndex map[int]int) int {
// need to convert the string into a deque for easy operations
stk := []int{}
// record the number in the expression
num := 0
// record the sign before num, initialize to +
// change to byte type
sign := byte('+')
for i := start; i <= end; i++ {
c := s[i]
if c >= '0' && c <= '9' {
num = 10*num + int(c-'0')
}
if c == '(' {
// recursively calculate the expression inside the parentheses
num = _calculate(s, i+1, rightIndex[i]-1, rightIndex)
i = rightIndex[i]
}
if c == '+' || c == '-' || c == '*' || c == '/' || i == end {
var pre int
switch sign {
case '+':
stk = append(stk, num)
case '-':
stk = append(stk, -num)
// just take out the previous number to do the corresponding operation
case '*':
pre = stk[len(stk)-1]
stk[len(stk)-1] = pre * num
case '/':
pre = stk[len(stk)-1]
stk[len(stk)-1] = pre / num
}
// update the sign to the current sign, reset the number to zero
sign = c
num = 0
}
}
// summing all the results in the stack is the answer
res := 0
for len(stk) != 0 {
res += stk[len(stk)-1]
stk = stk[:len(stk)-1]
}
return res
}
var calculate = function(s) {
// key is the index of the left parenthesis, value is
// the index of the corresponding right parenthesis
let rightIndex = new Map();
// use stack structure to find corresponding parentheses
let stack = [];
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') {
stack.push(i);
} else if (s[i] === ')') {
rightIndex.set(stack.pop(), i);
}
}
return _calculate(s, 0, s.length - 1, rightIndex);
};
var _calculate = function(s, start, end, rightIndex) {
// need to convert the string to a deque for easy manipulation
let stk = [];
// record the number in the formula
let num = 0;
// record the sign before num, initialize as +
let sign = '+';
for (let i = start; i <= end; i++) {
let c = s[i];
if (/\d/.test(c)) {
num = 10 * num + (c - '0');
}
if (c === '(') {
// recursively calculate the expression inside the parentheses
num = _calculate(s, i + 1, rightIndex.get(i) - 1, rightIndex);
i = rightIndex.get(i);
}
if ('+-*/'.includes(c) || i === end) {
let pre;
switch (sign) {
case '+':
stk.push(num);
break;
case '-':
stk.push(-num);
break;
// just take out the previous number for corresponding operation
case '*':
pre = stk.pop();
stk.push(pre * num);
break;
case '/':
pre = stk.pop();
// correct integer division
stk.push(Math.trunc(pre / num));
break;
}
// update the sign to the current sign, reset number to zero
sign = c;
num = 0;
}
}
// summing up all the results in the stack is the answer
let res = 0;
while (stk.length) {
res += stk.pop();
}
return res;
};
Exercise, Suggested Time 1 Day
Graph Algorithms
Cycle detection, topological sorting, and bipartite graph determination are classic graph algorithms. Essentially, they involve traversing the graph and are not difficult to master.
Core Framework, Recommended Time: 1-2 Days
The Union Find algorithm is a practical graph algorithm. You should understand its principles and API. If you don't have time to read in detail, you can save the UF
class provided at the end for use in written exams.
Core Framework, Recommended Time: 1 Day
Union Find Code Template
Here is the most efficient path compression code implementation:
class UF {
// the number of connected components
private int count;
// store the parent of each node
private int[] parent;
// n is the number of nodes in the graph
public UF(int n) {
this.count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// connect node p and node q
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
parent[rootQ] = rootP;
// merge two connected components into one
count--;
}
// determine if node p and node q are connected
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// return the number of connected components in the graph
public int count() {
return count;
}
}
class UF {
private:
// the number of connected components
int _count;
// store the parent of each node
vector<int> parent;
public:
// n is the number of nodes in the graph
UF(int n) {
this->_count = n;
this->parent.resize(n);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// connect node p and node q
void union_(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
parent[rootQ] = rootP;
// merge two connected components into one
_count--;
}
// determine if node p and node q are connected
bool connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// return the number of connected components in the graph
int count() {
return _count;
}
};
class UF:
# the number of connected components
_count: int
# store the parent of each node
parent: List[int]
# n is the number of nodes in the graph
def __init__(self, n: int):
self._count = n
self.parent = [i for i in range(n)]
# connect node p and node q
def union(self, p: int, q: int):
rootP = self.find(p)
rootQ = self.find(q)
if rootP == rootQ:
return
self.parent[rootQ] = rootP
# merge two connected components into one
self._count -= 1
# determine if node p and node q are connected
def connected(self, p: int, q: int) -> bool:
rootP = self.find(p)
rootQ = self.find(q)
return rootP == rootQ
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
# return the number of connected components in the graph
def count(self) -> int:
return self._count
type UF struct {
// the number of connected components
count int
// store the parent of each node
parent []int
}
// n is the number of nodes in the graph
func NewUF(n int) *UF {
parent := make([]int, n)
for i := 0; i < n; i++ {
parent[i] = i
}
return &UF{
count: n,
parent: parent,
}
}
// connect node p and node q
func (u *UF) Union(p, q int) {
rootP := u.Find(p)
rootQ := u.Find(q)
if rootP == rootQ {
return
}
u.parent[rootQ] = rootP
// merge two connected components into one
u.count--
}
// determine if node p and node q are connected
func (u *UF) Connected(p, q int) bool {
rootP := u.Find(p)
rootQ := u.Find(q)
return rootP == rootQ
}
func (u *UF) Find(x int) int {
if u.parent[x] != x {
u.parent[x] = u.Find(u.parent[x])
}
return u.parent[x]
}
// return the number of connected components in the graph
func (u *UF) Count() int {
return u.count
}
class UF {
// n is the number of nodes in the graph
constructor(n) {
// number of connected components
this._count = n
// store the parent of each node
this.parent = Array.from({ length: n }, (_, index) => index)
}
// connect node p and node q
union(p, q) {
const rootP = this.find(p)
const rootQ = this.find(q)
if (rootP === rootQ) {
return
}
this.parent[rootQ] = rootP
// merge two connected components into one
this._count--
}
// determine if node p and node q are connected
connected(p, q) {
const rootP = this.find(p)
const rootQ = this.find(q)
return rootP === rootQ
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x])
}
return this.parent[x]
}
// return the number of connected components in the graph
count() {
return this._count
}
}
You need to know the definition of a minimum spanning tree. Kruskal and Prim are two classic minimum spanning tree algorithms. You can learn about the Kruskal algorithm since it is basically Union Find + sorting, which is relatively simple.
Core Framework, Recommended Time: 1 Day
Dijkstra's shortest path algorithm is a classic graph theory algorithm that you need to master. It is essentially an improved binary tree BFS algorithm. You can save the template for quick use in written exams.
Core Framework, Recommended Time: 1 Day
Dijkstra Algorithm Template (Pseudocode)
class State {
// Current node ID
int node;
// Minimum path weight from the start node s to the current node
int distFromStart;
public State(int node, int distFromStart) {
this.node = node;
this.distFromStart = distFromStart;
}
}
// Input the weighted graph (without negative weight edges) graph and the starting node src
// Return the minimum path weight from the starting node src to other nodes
int[] dijkstra(Graph graph, int src) {
// Record the minimum path weight from the starting node src to other nodes
// distTo[i] represents the minimum path weight from the starting node src to node i
int[] distTo = new int[graph.size()];
// Because there are no negative weight edges, initialize the values of distTo array to -1
// This can distinguish whether the node has been visited
Arrays.fill(distTo, -1);
// Priority queue, nodes with smaller distFromStart are in front
Queue<State> pq = new PriorityQueue<>((a, b) -> {
return a.distFromStart - b.distFromStart;
});
// Start BFS from the starting node src
pq.offer(new State(src, 0));
while (!pq.isEmpty()) {
State state = pq.poll();
int curNode = state.node;
int curDistFromStart = state.distFromStart;
if (distTo[curNode] != -1) {
// Note this condition, different from the standard BFS algorithm
// In Dijkstra's algorithm, the queue may contain duplicate nodes state
// According to the greedy idea, the first dequeued node state is the
// minimum path weight from the src to the current node
continue;
}
// When curNode node is first dequeued, the minimum
// path weight from the src to curNode is found
// Update distTo[curNode]
distTo[curNode] = curDistFromStart;
for (Edge e : graph.neighbors(curNode)) {
int nextNode = e.to;
int nextDistFromStart = curDistFromStart + e.weight;
if (distTo[nextNode] != -1) {
continue
}
// Add nextNode node to the priority queue
pq.offer(new State(nextNode, nextDistFromStart));
// Note that this is also different from the standard BFS algorithm
// We cannot update distTo[nextNode] when enqueuing
// Because only when dequeued, the shortest path may
// be found, and distTo can be updated
// So in Dijkstra's algorithm, the queue may contain duplicate nodes state
}
}
return distTo;
}
// State stored in the priority queue
struct State {
// Current node ID
int node;
// Minimum path weight from start node s to the current node
int distFromStart;
State(int _node, int _distFromStart)
: node(_node), distFromStart(_distFromStart) {}
};
// Custom comparator so that State with smaller distFromStart dequeues first
struct Compare {
bool operator()(const State& a, const State& b) const {
return a.distFromStart > b.distFromStart;
}
};
// Input the weighted graph (without negative weight edges) graph and the starting node src
// Return the minimum path weight from the starting node src to other nodes
std::vector<int> dijkstra(Graph& graph, int src) {
// Record the minimum path weight from the starting node src to other nodes
std::vector<int> distTo(graph.size(), -1);
// Priority queue, nodes with smaller distFromStart are in front
std::priority_queue<State, std::vector<State>, Compare> pq;
// Start from src
pq.emplace(src, 0);
while (!pq.empty()) {
State state = pq.top();
pq.pop();
int curNode = state.node;
int curDistFromStart = state.distFromStart;
if (distTo[curNode] != -1) {
// Note this condition, different from the standard BFS algorithm
// In Dijkstra's algorithm, the queue may contain duplicate nodes state
// According to the greedy idea, the first dequeued node state
// is the minimum path weight from the src to the current node
continue;
}
// Update distTo[curNode]
distTo[curNode] = curDistFromStart;
for (const Edge& e : graph.neighbors(curNode)) {
int nextNode = e.to;
int nextDistFromStart = curDistFromStart + e.weight;
if (distTo[nextNode] != -1) {
continue;
}
// Add nextNode to the priority queue
pq.emplace(nextNode, nextDistFromStart);
// Note that this is also different from the standard BFS algorithm
// We cannot update distTo[nextNode] when enqueuing
// Because only when dequeued, the shortest path may
// be found, and distTo can be updated
}
}
return distTo;
}
import heapq
from typing import List
class State:
# Current node ID
def __init__(self, node: int, distFromStart: int):
self.node = node
# Minimum path weight from the start node s to the current node
self.distFromStart = distFromStart
# Define comparison for min-heap
def __lt__(self, other: "State") -> bool:
return self.distFromStart < other.distFromStart
# Input the weighted graph (without negative weight edges) graph and the starting node src
# Return the minimum path weight from the starting node src to other nodes
def dijkstra(graph: Graph, src: int) -> List[int]:
# Record the minimum path weight from the starting node src to other nodes
# distTo[i] represents the minimum path weight from the starting node src to node i
distTo: List[int] = [-1] * graph.size()
# Priority queue, nodes with smaller distFromStart are in front
pq: List[State] = []
# Start BFS from the starting node src
heapq.heappush(pq, State(src, 0))
while pq:
state = heapq.heappop(pq)
curNode = state.node
curDistFromStart = state.distFromStart
if distTo[curNode] != -1:
# Note this condition, different from the standard BFS algorithm
# In Dijkstra's algorithm, the queue may contain duplicate nodes state
# According to the greedy idea, the first dequeued node state is the
# minimum path weight from the src to the current node
continue
# When curNode node is first dequeued, the minimum
# path weight from the src to curNode is found
# Update distTo[curNode]
distTo[curNode] = curDistFromStart
for e in graph.neighbors(curNode):
nextNode = e.to
nextDistFromStart = curDistFromStart + e.weight
if distTo[nextNode] != -1:
continue
# Add nextNode node to the priority queue
heapq.heappush(pq, State(nextNode, nextDistFromStart))
# Note that this is also different from the standard BFS algorithm
# We cannot update distTo[nextNode] when enqueuing
# Because only when dequeued, the shortest path may
# be found, and distTo can be updated
# So in Dijkstra's algorithm, the queue may contain duplicate nodes state
return distTo
package main
import (
"container/heap"
)
// State stored in the priority queue
type State struct {
// Current node ID
node int
// Minimum path weight from the start node s to the current node
distFromStart int
}
// Priority queue, nodes with smaller distFromStart are in front
type PriorityQueue []*State
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].distFromStart < pq[j].distFromStart
}
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
func (pq *PriorityQueue) Push(x interface{}) {
*pq = append(*pq, x.(*State))
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[:n-1]
return item
}
// Input the weighted graph (without negative weight edges) graph and the starting node src
// Return the minimum path weight from the starting node src to other nodes
func dijkstra(graph Graph, src int) []int {
// Record the minimum path weight from the starting node src to other nodes
distTo := make([]int, graph.Size())
// Because there are no negative weight edges, initialize the values of distTo array to -1
for i := range distTo {
distTo[i] = -1
}
pq := &PriorityQueue{}
heap.Init(pq)
// Start BFS from the starting node src
heap.Push(pq, &State{node: src, distFromStart: 0})
for pq.Len() > 0 {
state := heap.Pop(pq).(*State)
curNode := state.node
curDistFromStart := state.distFromStart
if distTo[curNode] != -1 {
// Note this condition, different from the standard BFS algorithm
// In Dijkstra's algorithm, the queue may contain duplicate nodes state
// According to the greedy idea, the first dequeued node state
// is the minimum path weight from the src to the current node
continue
}
// When curNode node is first dequeued, the minimum path weight from the src to curNode is found
// Update distTo[curNode]
distTo[curNode] = curDistFromStart
for _, e := range graph.neighbors(curNode) {
nextNode := e.to
nextDistFromStart := curDistFromStart + e.weight
if distTo[nextNode] != -1 {
continue
}
// Add nextNode node to the priority queue
heap.Push(pq, &State{node: nextNode, distFromStart: nextDistFromStart})
// Note that this is also different from the standard BFS algorithm
// We cannot update distTo[nextNode] when enqueuing
// Because only when dequeued, the shortest path may
// be found, and distTo can be updated
// So in Dijkstra's algorithm, the queue may contain duplicate nodes state
}
}
return distTo
}
// State stored in the priority queue
class State {
// Current node ID
constructor(node, distFromStart) {
this.node = node;
// Minimum path weight from the start node s to the current node
this.distFromStart = distFromStart;
}
}
// Input the weighted graph (without negative weight edges) graph and the starting node src
// Return the minimum path weight from the starting node src to other nodes
function dijkstra(graph, src) {
// Record the minimum path weight from the starting node src to other nodes
// distTo[i] represents the minimum path weight from the starting node src to node i
const distTo = Array(graph.size()).fill(-1);
// Priority queue, nodes with smaller distFromStart are in front
const pq = new PriorityQueue((a, b) => a.distFromStart - b.distFromStart);
// Start BFS from the starting node src
pq.enqueue(new State(src, 0));
while (!pq.isEmpty()) {
const state = pq.dequeue();
const curNode = state.node;
const curDistFromStart = state.distFromStart;
if (distTo[curNode] !== -1) {
// Note this condition, different from the standard BFS algorithm
// In Dijkstra's algorithm, the queue may contain duplicate nodes state
// According to the greedy idea, the first dequeued node state is the
// minimum path weight from the src to the current node
continue;
}
// When curNode node is first dequeued, the minimum
// path weight from the src to curNode is found
// Update distTo[curNode]
distTo[curNode] = curDistFromStart;
for (const e of graph.neighbors(curNode)) {
const nextNode = e.to;
const nextDistFromStart = curDistFromStart + e.weight;
if (distTo[nextNode] !== -1) {
continue
}
// Add nextNode node to the priority queue
pq.enqueue(new State(nextNode, nextDistFromStart));
// Note that this is also different from the standard BFS algorithm
// Note that this is also different from the standard BFS algorithm
// We cannot update distTo[nextNode] when enqueuing
// Because only when dequeued, the shortest path may
// be found, and distTo can be updated
// So in Dijkstra's algorithm, the queue may contain duplicate nodes state
}
}
return distTo;
}
DFS/Backtracking Algorithm
Backtracking is a pure brute-force algorithm that is essential to master.
During written tests, scores are calculated based on the number of test cases passed. If you cannot derive the optimal solution for some problems, writing a backtracking algorithm for brute-force enumeration can still earn you partial credit, even if it doesn't pass all test cases.
The following listed articles contain classic backtracking problems that are indispensable and must be thoroughly understood.
Core Framework, Recommended Time: 1~2 Days
Core Framework, Recommended Time: 1~2 Days
Core Framework, Recommended Time: 1 Day
This article discusses the DFS algorithm:
There is a slight difference between DFS and backtracking algorithms. This article introduces these differences and provides some code style recommendations:
Exercises, Recommended Time: 2 Days
The essence of most backtracking algorithms is permutations and combinations. Once you understand Backtracking Algorithm for All Permutation/Combination/Subset Problems, many backtracking problems can be quickly solved.
The backtracking exercises on this site are as follows:
- 【Exercise】Classic Backtracking Problems I
- 【Exercise】Classic Backtracking Problems II
- 【Exercise】Classic Backtracking Problems III
However, there are quite a few problems in the exercise section. If you have time, you can review them all. If time is limited, I can help you select a few. It is recommended to install the Chrome Extension to view the site's approach and solution code on the problem pages.
BFS Algorithm
BFS is also a brute-force algorithm and is essential to master. Its essence is the level-order traversal of a binary tree, suitable for solving shortest path problems.
Core Framework, Recommended Time: 1 Day
Exercises, Recommended Time: 2 Days
The BFS exercise sections on this site are as follows:
These two chapters contain many exercises. If you have the time, you can complete all of them. If time is tight, I can select a few for you to practice. It is recommended to install the Chrome Extension, where you can view our solutions and code explanations on the problem pages.
Dynamic Programming
Dynamic programming is essentially brute-force search but with overlapping subproblems. These problems can be optimized using memorization. Such algorithms are commonly referred to as dynamic programming algorithms.
The brute-force solutions for dynamic programming problems are generally recursive. The optimization methods are quite standardized: either add a memo or convert it into an iterative form.
The challenge with dynamic programming lies in writing the brute-force solution (state transition equation). Please read the following articles, paying special attention to the thought process for deriving the state transition equation.
Core Framework, Recommended Time 1~2 Days
Core Framework, Recommended Time 1~2 Days
Exercises, Recommended Time 1~2 Days
Exercises, Recommended Time 1~2 Days
Greedy Algorithm
In general algorithm problems, you need to use brute-force to exhaust all solutions to find the optimal one.
However, in certain algorithm problems, if you make full use of available information, you can find the optimal solution without exhausting all possibilities. This is known as the greedy choice property, and such algorithms are called greedy algorithms.
Therefore, greedy algorithms do not have a fixed pattern. The key is careful observation to see if you can fully utilize the information to eliminate some possibilities that cannot be the optimal solution in advance.
Core Framework, Recommended Time 1 Day
Divide and Conquer Algorithm
Some algorithm problems are more efficiently solved by dividing and conquering. The following tutorial on divide and conquer algorithms uses a linked list problem discussed earlier as an example.
Core Framework, Recommended Time 1 Day
Mathematics
Mathematics-related algorithms are relatively rare in typical written tests, but mastering some classic techniques is necessary.
Exercises, Recommended Time 1-2 Days
Exercises, Recommended Time 1 Day
Other Classic Interview Questions
Here are some classic algorithm problems that essentially apply the algorithms introduced above. After mastering all the algorithms mentioned, you should be able to tackle generally difficult interview questions.
Exercises, Recommended Time: 1-2 Days
Exercises, Recommended Time: 1-2 Days
nSum Universal Function
// Note: you must sort the nums array before calling this function
// n is the sum of how many numbers you want to find, start is the index to start
// calculation (usually fill in 0), target is the target sum you want to achieve
List<List<Integer>> nSumTarget(int[] nums, int n, int start, long target) {
int sz = nums.length;
List<List<Integer>> res = new ArrayList<>();
// at least it should be 2Sum, and the array size should not be less than n
if (n < 2 || sz < n) return res;
// 2Sum is the base case
if (n == 2) {
// the operation with two pointers
int lo = start, hi = sz - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.add(new ArrayList<>(Arrays.asList(left, right)));
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
} else {
// when n > 2, recursively calculate the result of (n-1)Sum
for (int i = start; i < sz; i++) {
List<List<Integer>> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (List<Integer> arr : sub) {
// (n-1)Sum plus nums[i] is nSum
arr.add(nums[i]);
res.add(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
}
// Note: you must sort the nums array before calling this function
// n is the sum of how many numbers you want, start is the index to start
// calculation (usually fill 0), target is the target sum you want to make
vector<vector<int>> nSumTarget(vector<int>& nums, int n, int start, long target) {
int sz = nums.size();
vector<vector<int>> res;
// at least it's a 2Sum and the array size should not be less than n
if (n < 2 || sz < n) return res;
// 2Sum is the base case
if (n == 2) {
// the operation with two pointers
int lo = start, hi = sz - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push_back({left, right});
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
} else {
// when n > 2, recursively calculate the result of (n-1)Sum
for (int i = start; i < sz; i++) {
vector<vector<int>> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (vector<int>& arr : sub) {
// (n-1)Sum plus nums[i] is nSum
arr.push_back(nums[i]);
res.push_back(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
}
# Note: make sure to sort the nums array before calling this function
# n is the sum of how many numbers you want, start is the index to start
# calculation (usually fill 0), target is the target sum you want to make
def nSumTarget(nums: List[int], n: int, start: int, target: int) -> List[List[int]]:
sz = len(nums)
res = []
# at least it should be 2Sum, and the array size should not be less than n
if n < 2 or sz < n:
return res
# 2Sum is the base case
if n == 2:
# the operation with two pointers
lo, hi = start, sz-1
while lo < hi:
sum = nums[lo] + nums[hi]
left, right = nums[lo], nums[hi]
if sum < target:
while lo < hi and nums[lo] == left:
lo += 1
elif sum > target:
while lo < hi and nums[hi] == right:
hi -= 1
else:
res.append([left, right])
while lo < hi and nums[lo] == left:
lo += 1
while lo < hi and nums[hi] == right:
hi -= 1
return res
else:
# when n > 2, recursively calculate the result of (n-1)Sum
for i in range(start, sz):
if i > start and nums[i] == nums[i - 1]:
# skip duplicate elements
continue
subs = nSumTarget(nums, n-1, i+1, target-nums[i])
for sub in subs:
# (n-1)Sum plus nums[i] is nSum
sub.append(nums[i])
res.append(sub)
return res
// Note: you must sort the nums array before calling this function
// n is the sum of how many numbers you want, start is the index to start
// calculation (usually fill in 0), target is the target sum you want to make
func nSumTarget(nums []int, n int, start int, target int64) [][]int {
sz := len(nums)
res := [][]int{}
// at least it's a 2Sum and the array size should not be less than n
if n < 2 || sz < n {
return res
}
// 2Sum is the base case
if n == 2 {
// the twin pointers operation
lo, hi := start, sz-1
for lo < hi {
sum := nums[lo] + nums[hi]
left, right := nums[lo], nums[hi]
if int64(sum) < target {
for lo < hi && nums[lo] == left {
lo++
}
} else if int64(sum) > target {
for lo < hi && nums[hi] == right {
hi--
}
} else {
res = append(res, []int{left, right})
for lo < hi && nums[lo] == left {
lo++
}
for lo < hi && nums[hi] == right {
hi--
}
}
}
} else {
// when n > 2, recursively calculate the result of (n-1)Sum
for i := start; i < sz; i++ {
subs := nSumTarget(nums, n-1, i+1, target-int64(nums[i]))
for _, sub := range subs {
// (n-1)Sum plus nums[i] is nSum
sub = append(sub, nums[i])
res = append(res, sub)
}
for i < sz-1 && nums[i] == nums[i+1] {
i++
}
}
}
return res
}
// note: make sure to sort the array nums before calling this function
// n is the sum of how many numbers you want, start is the index to start
// calculation (usually fill 0), target is the target sum you want to make
var nSumTarget = function(nums, n, start, target) {
let sz = nums.length;
let res = [];
// at least it should be 2Sum, and the size of the array should not be less than n
if (n < 2 || sz < n) return res;
// 2Sum is the base case
if (n == 2) {
// the twin-pointer operation
let lo = start, hi = sz - 1;
while (lo < hi) {
let sum = nums[lo] + nums[hi];
let left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.push([left, right]);
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
// when n > 2, recursively calculate the result of (n-1)Sum
} else {
for (let i = start; i < sz; i++) {
let sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (let arr of sub) {
// (n-1)Sum plus nums[i] is nSum
arr.push(nums[i]);
res.push(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
}
Sorting Algorithms
In written tests and interviews, you are generally not asked to write sorting algorithm code; however, you might be asked about the principles, time complexity, and applicable scenarios of classic sorting algorithms, so you need to be familiar with the ten classic sorting algorithms.
Sorting Algorithms Overview, Recommended Time: 1 Day
It is recommended to watch the video in Introduction to Ten Sorting Algorithms to understand the core principles, complexity analysis, and applicable scenarios of the ten sorting algorithms.