Learning Plan for Quick Mastery
Some readers have given me feedback: The content on this site is indeed solid. If you study it seriously, you can thoroughly master data structures and algorithms. However, their current problem is a lack of time. What should they do?
Is it possible to focus on the most essential skills first, to quickly enhance their abilities just before an interview, and then slowly learn the rest when they have more time?
I believe this is indeed a need for a considerable number of readers, so I have specifically created this quick mastery directory to meet these readers' demands.
The quick mastery directory cannot cover everything, so if you have ample time, I still recommend studying according to the Main Site Directory.
Before You Start
To do a good job, an artisan needs the right tools. Please take a few minutes to read the Site Introduction to understand the site's supporting tools and practical functions. Read the first part of the Algorithm Visualization Panel Instructions to learn the basic usage of the visualization panel. Using these tools wisely based on your situation can greatly enhance your learning efficiency.
About Suggested Time
The suggested time for each article below is a rough, generous estimate, assuming readers don't have much time and can only spend about 1-2 fragmented hours a day learning.
Therefore, this suggested time is for reference only. The actual learning time will vary for each person. If you can spend complete, uninterrupted time learning, the speed will be much faster.
Furthermore, quick mastery readers shouldn't be overly persistent. If you can't understand a certain point, note it down and skip it for now. Once you establish a comprehensive knowledge system, many issues will naturally be resolved upon review.
About the Problem Sets
Readers often ask about company-specific problem sets, so I'll elaborate here.
Different companies have different problem sets, with varying difficulty. The quick mastery directory includes essential algorithm patterns and classic algorithm problems, not limited to a specific company's problem set.
The purpose of this directory is to fundamentally tackle the core of algorithms, allowing you to confidently apply algorithm frameworks to independently solve medium-difficulty, frequently tested algorithm problems, rather than relying on luck and memorization to remember specific problem sets.
Some readers start preparing for algorithms by immediately practicing company-specific problem sets, which is incorrect. My suggestion is to first understand common algorithm frameworks, and then consider practicing problem sets.
No matter which company's problem set you work on, the algorithms themselves have only a few frameworks and patterns. Once you've mastered these, you can complete a problem set in at most two to three hours. There's nothing magical about it.
Problem sets have two main purposes:
1. Understanding the difficulty and algorithm types of a company's exam in advance. For example, if your company explicitly states that dynamic programming algorithms won't be tested, you can skip the dynamic programming section of this directory and focus on other algorithm skills.
2. Convenient for review. After mastering the algorithm frameworks, you can independently solve problems. At this point, you can practice the problem sets several times to test your learning outcomes and consolidate knowledge points.
Below is a summary of the exercise problem sets involved in this directory. Although there seem to be many problems, most are framework problems that you can easily solve after reading the articles below. There's no need to practice them separately. This problem set is only for the convenience of readers who have completed this directory to review:
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 has been 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 Jetbrains plugin.

Principles of the Quick Mastery Directory
Why can the Quick Mastery Directory rapidly improve algorithm skills in a short time?
- Unified Solution Style Based on Template Framework.
This Quick Mastery Directory is not just a simple list of problems. It summarizes a framework template for each algorithm, and problems are solved using a unified process framework. You learn true algorithm-solving skills rather than rote memorization, so you are not limited to specific problem lists but can independently tackle unseen problems with ease.
- Gradual Progression, from Shallow to Deep, including Data Structures and Algorithms.
Algorithm technique A might be a fusion of algorithms B and C. If A is taught without any background context, it seems complex and elusive. However, if you fill in the basics of B and C, you can derive A yourself, making it straightforward. Many readers are intimidated by recursive algorithms, but I emphasize that the essence of algorithms is brute-force, and recursion is a classic brute-force method based on tree structures. Once you understand binary tree structures, you can grasp any recursive algorithm. Therefore, this directory starts with key data structures and then moves on to problem-solving. By solidifying the basics first, tackling problems becomes smooth and efficient.
- Scientific Learning Methods with Rich Auxiliary Tools. Combining examples and exercises, with explanations followed by practice, supplemented by problem-solving plugins and visualization panels, all efforts are made to help readers reduce understanding costs and improve learning efficiency.
I must emphasize again, I fully understand that some readers are pressed for time, but don't rush into problem-solving without learning the framework.
The problem with just solving problem sets is over-reliance on luck. If you're lucky and encounter a familiar problem, you might recall the solution. Without luck, you'll need to rely on an algorithm framework, and without learning it, you may fail. A shaky foundation leads to instability. Initially investing time to solidify the basics ultimately saves time.
Let's begin. The Quick Mastery Directory is divided into "Data Structures" and "Algorithm Problem-Solving" sections. The data structures section generally does not involve algorithm problems and is relatively simple. The problem-solving section is key, requiring careful thought and hands-on practice.
Data Structures
Array and Linked List
Introduces the basic concepts of arrays and linked lists, which are relatively simple. The main focus is the technique of circular arrays, which allows for insertion and deletion at the head of the array with a time complexity of .
Key Foundations, Recommended Duration: 1 Day
Principles and Applications of Hash Tables
As a quick mastery tutorial, we can skip the implementation of the two methods to resolve hash collisions in hash tables. However, 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 like LinkedHashMap
and ArrayHashMap
. These structures add new features to hash tables, and understanding their principles is necessary.
Essential Foundation, Recommended Time: 1~2 Days
Basics and Traversal of Binary Trees
Although this is just an introductory chapter, the section on binary trees must be emphasized, especially the traversal of binary trees, as it is key to understanding recursive thinking. When starting to tackle problems, the essence of all recursive algorithms is the traversal of binary trees.
Key Basics, Suggested Time: 1-2 Days
- Basics and Common Types of Binary Trees
- Recursive/Level Order Traversal of Binary Trees
- Recursive/Level Order Traversal of Multi-way Trees
Three Templates for Level Order Traversal
The first one, the 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 one, commonly used by leveraging 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 one, more complex but most flexible, using a custom State
class to maintain information of each node, 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 Day
A binary search tree is a special type of binary tree. Here, you only need to have a general idea of its application scenarios. There will be exercises later to help you master its problem-solving techniques.
Principles of Binary Heap
The key application of binary heap is the priority queue. As a quick mastery tutorial, we can skip the implementation of the priority queue, but it is important to understand the following key points:
A priority queue is a data structure that can automatically sort, with a complexity of for adding or removing elements, using a binary heap underneath.
A binary heap is a complete binary tree with special properties.
The core methods of priority queue (binary heap) are
swim
andsink
, which are used to maintain the properties of the binary heap.
Key Basics, Suggested Time: 0.5 Day
Use Cases of Segment Trees
Segment trees are a data structure that may be used in algorithm coding interviews. They can perform range queries and point updates in time complexity.
Key Fundamentals, Suggested Time: 0.5 Day
The above article mainly explains 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 solve problems efficiently.
Why the query and update complexity of segment trees is . This may be asked in interviews. The visualization panel on this site makes it easy to understand the underlying principles.
Segment trees can be implemented using linked structures or arrays. Why does the array-based implementation require 4 times the space during initialization?
As for the specific code implementation, you usually won't need to write it from scratch. You can refer to the code template provided in Segment Tree Code Implementation and use it directly when needed.
Graph Structures
Graph algorithms are a vast topic, but as part of the foundational data structures chapter, we only need to understand the logical representation and concrete implementation of graphs, as well as graph traversal algorithms.
It's not overly complex, essentially an extension of the binary tree structure discussed earlier. The following two articles provide a general implementation of graph structures and templates for traversal algorithms.
Key Fundamentals, Recommended Time: 1 Day
Start Practicing Problems
Now let's start practicing problems. If you are new to online coding platforms, you may first want to check LeetCode Guidelines to understand the basic rules of using LeetCode.
Core Framework, Recommended Time: 0.5 Day
This article serves as a guide for all content on this site. It not only summarizes the data structures mentioned above but also directly points out the essence of all algorithms.
There will be numerous links to other articles, which may be difficult to fully grasp on the first read. However, there's no need to be overly concerned; a general understanding of the thinking methods introduced here will suffice. As you proceed with further studies, the core insights of this article will become clearer.
Linked Lists
The patterns for problems related to linked lists are relatively fixed, primarily involving 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 typically only mentioned during interviews. In written exams, standard pointer operations are usually sufficient.
Core Framework, Recommended Time: 1 Day
Arrays
Techniques related to arrays mainly involve the two-pointer method, which can be categorized into fast-slow pointers and left-right pointers. Classic array two-pointer algorithms include binary search and sliding window.
Some readers ask why there is no special topic on string algorithms. This is because strings are essentially character arrays, and string algorithms are fundamentally array algorithms.
Core Framework, Recommended Time: 0.5 Days
Exercises, Recommended Time: 1~2 Days
Core Framework, Recommended Time: 1 Day
Sliding Window Code Template (Pseudocode)
// 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
// For example, if I want to record the
// frequency of elements in the window, I would
// 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
// For instance, if I want to record the
// frequency of elements in the window, I would
// 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
# For example, if I want to record the
# frequency of elements in the window, I would
# 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
// 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
// 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, Recommended Time: 1 Day
Core Framework, Recommended 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
// 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, Recommended Time: 1~2 Days
Prefix Sum Array Code Templates
1D 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];
};
2D 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, Recommended Time: 0.5 Days
Exercises, Recommended Time: 1-2 Days
Monotonic stacks and queues are two variations based on stacks and queues. They can solve certain special problems and need to be mastered.
Core Framework, Recommended Time: 1-2 Days
Exercises, Recommended Time: 1-2 Days
Binary Tree
The essence of all recursive algorithms is binary tree traversal, and binary tree algorithms often appear in interviews and written tests. Therefore, I have included several articles in the binary tree section, hoping everyone will study, understand, and practice diligently.
Core Framework, Recommended Time: 1 Day
This core outline serves as a general guide, mainly covering two parts: the first part explains how to understand preorder, inorder, and postorder positions of binary trees in practical algorithm problems; the second part introduces algorithms such as backtracking and dynamic programming from the perspective of binary trees.
Now that you understand binary tree traversal algorithms, focus on studying the first part. The advanced algorithms discussed in the second part have not yet been covered; just get a general impression and review it later after learning backtracking and dynamic programming for deeper understanding.
Core Framework, Recommended Time: 2-3 Days
The examples in these tutorials are classic binary tree problems that are essential to learn and master.
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 tree structures are the core abstraction of all brute-force algorithms. Understanding binary trees will make it easier to comprehend advanced algorithms later.
The site's Binary Tree Algorithm Exercise Collection dedicates an entire chapter to nearly 100 questions to practice binary tree algorithms, categorizing exercises into three main parts according to the Binary Tree Algorithms (Outline):
- Recursion, solving problems with a "traversal" mindset.
- Recursion, solving problems with a "divide and conquer" mindset.
- Non-recursion, solving problems with a "level-order traversal" mindset.
The "traversal" mindset is the prototype for DFS and backtracking algorithms discussed later. The "divide and conquer" mindset is the prototype for dynamic programming and divide-and-conquer algorithms. "Level-order traversal" is the prototype for BFS algorithms discussed later.
Therefore, it is essential to practice these types of binary tree algorithms. The Binary Tree Algorithm Exercise Collection is extensive; here are some moderate-difficulty and frequently tested exercises for quick mastery:
- Exercise: Problem Solving with "Traversal" Thinking I
- Exercise: Problem Solving with "Divide and Conquer" Thinking I
- Exercise: Solving Problems with Level-Order Traversal I
If you have the time and interest, feel free to 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 that must be mastered; LFU is more challenging and can be optionally studied.
Exercise, Suggested Time: 1 Day
Exercise, Suggested Time: 1 Day
Implementing a calculator is a classic data structure design task. If you're short on time, save the final calculator code provided. You can directly copy and paste it for use in written tests involving string calculation problems.
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
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
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
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
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. They essentially involve graph traversal and are not difficult to master.
Core Framework, Recommended Time: 1-2 Days
The Union Find algorithm is a practical graph algorithm. You need to have a general understanding of its principles and API. If you don't have time to read in detail, you can save the provided UF
class for use in 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. It's worth understanding the Kruskal algorithm, as it is essentially Union Find + sorting, which is relatively simple.
Core Framework, Recommended Time: 1 Day
The Dijkstra shortest path algorithm is a classic graph theory algorithm that needs to be mastered. It's essentially an improved binary tree BFS algorithm. You can save the template to quickly apply it during exams.
Core Framework, Recommended Time: 1 Day
Dijkstra Algorithm Template (Pseudocode)
// input a graph and a start node, calculate the shortest distance from start to other nodes
int[] dijkstra(int start, Graph graph) {
// the number of nodes in the graph
int V = graph.size();
// record the weight of the shortest path, you can think of it as a dp table
// definition: the value of distTo[i] is the
// weight of the shortest path from node start to
int[] distTo = new int[V];
// to find the minimum value, so initialize the dp table to positive infinity
Arrays.fill(distTo, Integer.MAX_VALUE);
// base case, the shortest distance from start to start is 0
distTo[start] = 0;
// priority queue, the smaller distFromStart is placed in front
Queue<State> pq = new PriorityQueue<>((a, b) -> {
return a.distFromStart - b.distFromStart;
});
// start BFS from the starting point start
pq.offer(new State(start, 0));
while (!pq.isEmpty()) {
State curState = pq.poll();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
if (curDistFromStart > distTo[curNodeID]) {
// there is already a shorter path to reach the curNode node
continue;
}
// put the adjacent nodes of curNode into the queue
for (int nextNodeID : graph.neighbors(curNodeID)) {
// see if the distance from curNode to nextNode will be shorter
int distToNextNode = distTo[curNodeID] + graph.weight(curNodeID, nextNodeID);
if (distTo[nextNodeID] > distToNextNode) {
// update the dp table
distTo[nextNodeID] = distToNextNode;
// put this node and distance into the queue
pq.offer(new State(nextNodeID, distToNextNode));
}
}
}
return distTo;
}
// input a graph and a start node, calculate the shortest distance from start to other nodes
vector<int> dijkstra(int start, Graph* graph) {
// number of nodes in the graph
int V = graph.size();
// record the weight of the shortest path, you can think of it as a dp table
// definition: the value of distTo[i] is the
// weight of the shortest path from node start to
int distTo[V];
// seeking the minimum value, so initialize the dp table to positive infinity
memset(distTo, INT_MAX, sizeof(distTo));
// base case, the shortest distance from start to start is 0
distTo[start] = 0;
// priority queue, with smaller distFromStart in front
priority_queue<State, vector<State>, decltype(&comparator)> pq(&comparator);
// start BFS from the start node
pq.push(State(start, 0));
while (!pq.empty()) {
State curState = pq.top();
pq.pop();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
if (curDistFromStart > distTo[curNodeID]) {
// there is already a shorter path to the curNode node
continue;
}
// put the neighboring nodes of curNode into the queue
for (int nextNodeID: graph.neighbors(curNodeID)) {
// check if the distance from curNode to nextNode would be shorter
int distToNextNode = distTo[curNodeID] + graph.weight(curNodeID, nextNodeID);
if (distTo[nextNodeID] > distToNextNode) {
// update the dp table
distTo[nextNodeID] = distToNextNode;
// put this node and distance into the queue
pq.push(State(nextNodeID, distToNextNode));
}
}
}
vector<int> result;
for (int i = 0; i < V; i++) {
result.push_back(distTo[i]);
}
return result;
}
from typing import List
from queue import PriorityQueue
class State:
def __init__(self, id, distFromStart):
self.id = id
self.distFromStart = distFromStart
# given a graph and a starting point, calculate the shortest distance from start to other nodes
def dijkstra(start:int, graph:Graph) -> List[int]:
# number of nodes in the graph
V = len(graph)
# record the weights of the shortest paths, you can think of it as a dp table
# definition: distTo[i] is the weight of the shortest path from the start node to node i
distTo = [0] * V
# we are looking for the minimum value, so initialize the dp table to positive infinity
for i in range(V):
distTo[i] = float('inf')
# base case, the shortest distance from start to start is 0
distTo[start] = 0
# priority queue, with smaller distFromStart at the front
pq = PriorityQueue(lambda a,b: a.distFromStart - b.distFromStart)
# start BFS from the starting point
pq.put(State(start, 0))
while not pq.empty():
curState = pq.get()
curNodeID = curState.id
curDistFromStart = curState.distFromStart
if curDistFromStart > distTo[curNodeID]:
# there is already a shorter path to reach the curNode
continue
# put the adjacent nodes of curNode into the queue
for nextNodeID in graph.neighbors(curNodeID):
# see if the distance from curNode to nextNode would be shorter
distToNextNode = distTo[curNodeID] + graph.weight(curNodeID, nextNodeID)
if distTo[nextNodeID] > distToNextNode:
# update the dp table
distTo[nextNodeID] = distToNextNode
# put this node and distance into the queue
pq.put(State(nextNodeID, distToNextNode))
return distTo
type State struct {
id int
distFromStart int
}
func dijkstra(start int, graph *Graph) []int {
// number of nodes in the graph
V := len(graph)
// record the weights of the shortest paths, you can understand it as a dp table
// definition: distTo[i] is the shortest path weight from node 'start' to node 'i'
distTo := make([]int, V)
// to find the minimum value, initialize the dp table to positive infinity
for i := range distTo {
distTo[i] = -1
}
// base case, the shortest distance from 'start' to 'start' is 0
distTo[start] = 0
// priority queue, with smaller distFromStart values at the front
pq := make(PriorityQueue, 0)
heap.Init(&pq)
heap.Push(&pq, &State{start, 0})
for pq.Len() > 0 {
curState := heap.Pop(&pq).(*State)
curNodeID := curState.id
curDistFromStart := curState.distFromStart
if curDistFromStart > distTo[curNodeID] {
// there is already a shorter path to reach curNode
continue
}
// put the adjacent nodes of curNode into the queue
for _, nextNodeID := range graph.neighbors(curNodeID) {
// check if the distance from curNode to nextNode can be shorter
distToNextNode := distTo[curNodeID] + graph.weight(curNodeID, nextNodeID)
if distTo[nextNodeID] < 0 || distTo[nextNodeID] > distToNextNode {
// update the dp table
distTo[nextNodeID] = distToNextNode
// put this node and its distance into the queue
heap.Push(&pq, &State{nextNodeID, distToNextNode})
}
}
}
return distTo
}
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{}) {
item := x.(*State)
*pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[:n-1]
return item
}
// input a graph and a start node, calculate the shortest distance from start to other nodes
var dijkstra = function(start, graph) {
// number of nodes in the graph
var V = graph.size();
// record the weight of the shortest path, you can think of it as a dp table
// definition: the value of distTo[i] is the
// weight of the shortest path from node start to
var distTo = new Array(V);
// to find the minimum value, so initialize the dp table to positive infinity
distTo.fill(Number.POSITIVE_INFINITY);
// base case, the shortest distance from start to start is 0
distTo[start] = 0;
// priority queue, nodes with smaller distFromStart are in front
var pq = new PriorityQueue((a, b) => {
return a.distFromStart - b.distFromStart;
});
// start BFS from the start node
pq.offer(new State(start, 0));
while (!pq.isEmpty()) {
var curState = pq.poll();
var curNodeID = curState.id;
var curDistFromStart = curState.distFromStart;
if (curDistFromStart > distTo[curNodeID]) {
// there is already a shorter path to the curNode node
continue;
}
// put the adjacent nodes of curNode into the queue
for (var nextNodeID of graph.neighbors(curNodeID)) {
// check if the distance from curNode to nextNode will be shorter
var distToNextNode = distTo[curNodeID] + graph.weight(curNodeID, nextNodeID);
if (distTo[nextNodeID] > distToNextNode) {
// update the dp table
distTo[nextNodeID] = distToNextNode;
// put this node and distance into the queue
pq.offer(new State(nextNodeID, distToNextNode));
}
}
}
return distTo;
};
DFS/Backtracking Algorithm
The backtracking algorithm is a purely brute-force method and is essential to master.
In exams, scoring is based on the number of test cases passed. If you cannot write the optimal solution for some problems, you can use a backtracking algorithm for brute-force attempts. Although it might not pass all test cases, it can pass some, earning partial scores.
The following articles list classic backtracking problems that are essential to understand thoroughly.
Core Framework, Recommended Duration: 1~2 Days
Core Framework, Recommended Duration: 1~2 Days
Core Framework, Recommended Duration: 1 Day
This article discusses the DFS algorithm:
The DFS algorithm and backtracking algorithm have slight differences. This article introduces them and provides some code style suggestions:
Exercises, Recommended Duration: 2 Days
Most backtracking algorithms essentially involve permutations and combinations. By understanding Backtracking Algorithm for All Permutation/Combination/Subset Problems, many backtracking problems can be quickly resolved.
The backtracking exercises on this site include:
- Exercise: Backtracking Classic Problems I
- Exercise: Backtracking Classic Problems II
- Exercise: Backtracking Classic Problems III
However, there are many problems in the exercise section. If you have time, it is recommended to review them all. If time is limited, I can help select a few. It is recommended to install the Chrome Extension to view solutions and code on the problem pages.
BFS Algorithm
BFS is also a brute-force algorithm that must be mastered. Essentially, it is a level-order traversal of a binary tree, suitable for solving shortest path problems.
Core Framework, Suggested Time: 1 Day
Exercises, Suggested Time: 2 Days
The BFS exercises on this site are as follows:
These two chapters contain many exercises. If you have time, you can complete them all. If time is tight, I can select a few for you to practice. It is recommended to install the Chrome Extension to view the site's solutions and code 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
For general algorithmic problems, it is necessary to exhaustively search all possible solutions to find the optimal one.
However, for some problems, if you fully utilize the available information, you can find the optimal solution without resorting to brute-force enumeration. This property is called the greedy choice property, and the algorithm that leverages it is called the greedy algorithm.
Therefore, the greedy algorithm does not have a fixed pattern. The key lies in careful observation to determine whether the available information can be fully utilized to eliminate some solutions that are unlikely to be optimal in advance.
Core Framework, Recommended Time: 1 Day
Divide and Conquer Algorithm
Some algorithm problems can be more efficiently solved using the divide and conquer method. The example problem discussed in this divide and conquer tutorial is a linked list problem mentioned earlier.
Core Framework, Recommended Time: 1 Day
Mathematics
In general, mathematical algorithms are less common in written tests, but it is still necessary to master some classic techniques.
Exercises, Recommended Time: 1-2 Days
Exercises, Recommended Time: 1 Day
Other Classic Interview Questions
Here are some classic algorithm problems, which are essentially applications of the algorithms introduced above. After mastering all the algorithms mentioned, you should be able to solve interview questions of general difficulty.
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 generally won't be asked to write code for sorting algorithms. However, in interviews, you may be asked about the principles, time complexities, and applicable scenarios of classic sorting algorithms, so you need to be familiar with the ten classic sorting algorithms.
Overview of Sorting Algorithms, Recommended Duration: 1 Day
It is recommended to watch the video in Introduction to the Ten Sorting Algorithms to understand the core principles, complexity analysis, and applicable scenarios of the ten sorting algorithms.