LeetCode Guide
All examples and exercises on this site are selected from the publicly available free problems on LeetCode, with links provided to the problems so you can directly go to the official website for practice.
To assist beginners, here is a quick introduction to the problem-solving mechanism and tips for using the online judging platform.
Core Code Pattern
When solving problems on LeetCode, you are given a function framework that you need to complete with your logic. This approach is commonly referred to as the Core Code Pattern.
For example, in LeetCode's first problem, "Two Sum," you are required to implement a twoSum
function like this:
class Solution {
public int[] twoSum(int[] nums, int target) {
}
}
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
};
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
}
};
func twoSum(nums []int, target int) []int {
}
The idea is that you are given an input array nums
and a target value target
. You need to implement the algorithm logic and eventually return an array as the result.
When you submit your solution code to the backend, it will pass several predefined test cases to the twoSum
function. By comparing your return values with the correct answers, it determines whether your code is correct.
From the user's perspective, the core code mode should be the most convenient form, as you only need to focus on writing the algorithm logic. Currently, most coding platforms and technical interviews/tests use the core code mode. However, just in case, here is an introduction to the ACM mode.
ACM Mode
In simple terms, ACM mode is a bit more complex. The problem provides you with input in a specific format as a string, and you need to parse this string yourself and then output the computation result to the standard output.
After submitting your code to the backend, the system will pass each test case (string) to your program via standard input, then compare your program's standard output with the expected output to determine if your code is correct.
For instance, platforms like Nowcoder offer this ACM mode. Here is a screenshot for illustration, where the code editor has no initial code, and you need to start writing from scratch:

For ACM mode, a useful technique is to separate the code into layers, dividing the logic for handling string input from the core algorithm logic, like this:
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// mainly responsible for receiving and processing input data, constructing input cases
int[] nums;
int target;
int N = scanner.nextInt();
...
// call the algorithm logic to solve
System.out.println(twoSum(nums, target));
}
public static int[] twoSum(int[] nums, int target) {
// convert to core code mode, write algorithm logic here
}
}
This approach makes everything clear. The input processing logic can be used as a template to copy and paste, eventually transforming into core code patterns, similar to solving problems on LeetCode.
My suggestion is to use the core code format during regular practice, as it is simple and convenient. As the interview or test approaches, spending an hour to get familiar with the ACM format will suffice. I recall that Nowcoder has practice problems specifically for the ACM format; you can search for them yourself.
Below is an introduction to the techniques for using problem-solving platforms and how these platforms determine the correctness of your solutions.
How to Read Problems
For example, LeetCode Problem 704 "Binary Search":
704. Binary Search | LeetCode | 🟢
Given an array of integers nums
which is sorted in ascending order, and an integer target
, write a function to search target
in nums
. If target
exists, then return its index. Otherwise, return -1
.
You must write an algorithm with O(log n)
runtime complexity.
Example 1:
Input: nums = [-1,0,3,5,9,12], target = 9 Output: 4 Explanation: 9 exists in nums and its index is 4
Example 2:
Input: nums = [-1,0,3,5,9,12], target = 2 Output: -1 Explanation: 2 does not exist in nums so return -1
Constraints:
1 <= nums.length <= 104
-104 < nums[i], target < 104
- All the integers in
nums
are unique. nums
is sorted in ascending order.
The problem provides you with an empty function body, and you need to implement this search
function:
class Solution {
public int search(int[] nums, int target) {
// your code here
}
}
class Solution {
public:
int search(vector<int>& nums, int target) {
// your code here
}
};
class Solution:
def search(self, nums: List[int], target: int) -> int:
# your code here
func search(nums []int, target int) int {
// your code here
}
var search = function(nums, target) {
// your code here
};
The problem states that you are given an ascending sorted array nums
, and you need to return the index of the element target
. If the target is not found, return -1
.
Key Point: Understand the Problem Requirements
Read the problem carefully, considering all the information provided. Many readers focus only on the main question and examples, neglecting additional details about data constraints provided below the problem statement.
For instance, in this problem, there are additional notes about the range of the length of nums
, the range of its elements, and an important point: there are no duplicate elements in nums
.
This is crucial information because if there were multiple identical elements, which index should be returned? Such details are usually given in the supplementary information, so be sure to read them thoroughly.
How to Solve
This problem is intended to test the binary search algorithm, which will be discussed later. However, for now, let's write a straightforward solution:
class Solution {
public int search(int[] nums, int target) {
// traverse the array, return the index if found
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) {
return i;
}
}
return -1;
}
}
class Solution {
public:
int search(vector<int>& nums, int target) {
// traverse the array, return the index if found
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == target) {
return i;
}
}
return -1;
}
};
class Solution:
def search(self, nums: List[int], target: int) -> int:
# traverse the array, return the index if found
for i in range(len(nums)):
if nums[i] == target:
return i
return -1
func search(nums []int, target int) int {
// traverse the array, return the index if found
for i, num := range nums {
if num == target {
return i
}
}
return -1
}
var search = function(nums, target) {
// traverse the array, return the index if found
for (let i = 0; i < nums.length; i++) {
if (nums[i] === target) {
return i;
}
}
return -1;
};
You can copy this code into LeetCode's editor. Click the "Run" button to execute the test cases shown in the lower right corner. Click the "Submit" button to submit your code and see if it passes all test cases.
How to Debug
The ideal method is to copy the code and test cases into a local IDE, where you can set breakpoints for debugging.
However, if you prefer a simpler approach, you can directly write print statements in the editor, and LeetCode will display your output results in the lower right corner.
For simpler bugs, reviewing the output can also help you identify the problem.
Remember to Remove Print Statements Before Submitting
Since print statements involve I/O operations, they can affect the execution efficiency of your code. Therefore, remember to remove them before submitting.
Judging Mechanism
At the time of writing this article, the solution code mentioned above could pass all test cases on LeetCode and could be successfully submitted.
LeetCode has many pre-defined test cases, such as:
Test case: nums = [-1,0,3,5,9,12], target = 9
Expected output: 4
Test case: nums = [-1,0,3,5,9,12], target = 2
Expected output: -1
Test case: nums = [5], target = 5
Expected output: 0
...
These test cases will be passed as parameters to the search
function you implement, and then it checks whether your function's return value matches the expected output.
If they match, the test case passes. If all test cases pass, then your code is considered correct.
The solution code provided above is not the most efficient, but it does produce the correct result and does not exceed the time limits set by the judging platform, so the platform considers it correct.
Taking Shortcuts
Based on this mechanism, the judging platform does not actually inspect your code. Your code acts as a black box; it only cares if the input and output match the expectations.
If the problem has some soft restrictions, such as prohibitions on using long
type or standard libraries, these are merely suggestions without a way to enforce them.
In a written exam, if you encounter such problems, you can ignore these restrictions. As long as your code passes all the test cases, it is deemed correct.
Of course, this is a shortcut. During your learning process, it is best to adhere to the problem constraints. Otherwise, in an interview, you cannot resort to these tricks in front of the interviewer.
Common Errors When Submitting Code
If the submitted code passes all the test cases in the backend, it is considered a successful submission, often referred to as AC (Accepted).
If the submitted code fails to pass all test cases, it is considered a failed submission. Common reasons for failure include:
Compile Error
The code cannot be compiled, usually due to syntax errors like spelling mistakes or missing semicolons. This kind of error often occurs when writing code directly on the webpage. Using our site's accompanying Jetbrains IDE plugin or vscode plugin can help prevent these errors with basic syntax checking features.
Runtime Error
The code compiles successfully but encounters errors like array out-of-bounds or null pointer exceptions during execution. These errors are usually due to improper handling of boundary conditions. Pay attention to boundary conditions and special test cases (also known as corner cases, such as empty input).
Wrong Answer
The code compiles and runs, but the results for some test cases do not match the correct answers. This error is generally due to a problem with your algorithm, requiring you to reflect on the failing test cases, and possibly rethink your entire approach.
Time Limit Exceeded (TLE)
In predefined test cases, the size of the data increases in later cases. If your algorithm's time complexity is too high, it may exceed the system's time limit when running large-scale test cases, leading to a timeout error.
To solve this problem, check the following:
Are there redundant calculations, and is there a more efficient approach to reduce time complexity?
Are there coding errors, such as incorrect boundary control leading to infinite loops, or incorrect passing of values and references causing meaningless data copying?
If you are stuck on large-scale test cases, it generally indicates your algorithm's results are correct since the smaller test cases have passed, but the time complexity needs optimization.
In written exams, scoring is usually based on the number of test cases passed. If you cannot find the optimal solution to pass all test cases, submitting a brute-force solution to pass a few test cases for partial credit is also a clever strategy.
Memory Limit Exceeded (MLE)
Similar to timeout errors, memory limit exceeded errors usually occur when the algorithm's space complexity is too high, using more memory than the system's limit when running large-scale test cases.
To solve this problem, check the following:
Is there unnecessary space allocation, and is there a more efficient approach to reduce space complexity?
Are there mistakes in using value parameters in recursive functions causing meaningless data copying?
Important Notes for Submitting Code on LeetCode
For those new to solving problems on LeetCode, I would like to highlight a few common pitfalls for beginners.
Avoid Using File-Level Global Variables
As previously mentioned, the core code pattern on LeetCode involves providing several predefined test cases as input to your algorithm code and then comparing your output with the expected answer. Therefore, it's crucial to ensure that:
Do not use file-level global variables in your code, as different test cases may interfere with each other. LeetCode's official website also addresses this issue: https://support.leetcode.cn/hc/kb/article/1194344/
Some readers may find that their solution fails when submitted, but works fine when the problematic test case is run individually. This is often due to the aforementioned reason. When you run a single test case, only that case is executed without interference, but when submitted, data from multiple test cases may interfere, causing errors.
In the tutorials on this site, especially those explaining recursive algorithms, I frequently mention "global variables." However, I refer to class-level global variables, not file-level global variables.
To illustrate this with an example, consider the problem of preorder traversal of a binary tree. The problem provides the root node of a binary tree as input and asks you to return the preorder traversal result. You might write it like this:
class Solution {
// correct example, class-level global variable
LinkedList<Integer> res = new LinkedList<>();
public List<Integer> preorderTraversal(TreeNode root) {
traverse(root);
return res;
}
void traverse(TreeNode root) {
if (root == null) {
return;
}
// other functions within the class can access res
res.add(root.val);
traverse(root.left);
traverse(root.right);
}
}
// Incorrect example, do not define global variables at the file level
// vector<int> res;
class Solution {
public:
// Correct example, class-level global variable
vector<int> res;
vector<int> preorderTraversal(TreeNode* root) {
traverse(root);
return res;
}
void traverse(TreeNode* root) {
if (!root) {
return;
}
// Other functions within the class can access res
res.push_back(root->val);
traverse(root->left);
traverse(root->right);
}
};
// Incorrect example, do not define global variables at the file level
// var res []int
func preorderTraversal(root *TreeNode) []int {
// Correct example, use closure to allow the
// traverse function to also access the res
var res []int
var traverse func(*TreeNode)
traverse = func(root *TreeNode) {
if root == nil {
return
}
// Access and modify the res variable
res = append(res, root.Val)
traverse(root.Left)
traverse(root.Right)
}
traverse(root)
return res
}
# Wrong example, do not define global variables at the file level
# res = []
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
# Correct example, class-level global variable
self.res = []
self.traverse(root)
return self.res
def traverse(self, root):
if not root:
return
# Other functions within the class can access res
self.res.append(root.val)
self.traverse(root.left)
self.traverse(root.right)
// Incorrect example, do not define global variables at the file level
// let res = [];
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
// Correct example, use closure to allow the
// traverse function to access the res variable
let res = [];
var traverse = (root) => {
if (!root) {
return;
}
// Access and modify the res variable
res.push(root.val);
traverse(root.left);
traverse(root.right);
}
traverse(root);
return res;
};
For languages like Java/C++/Python, where a Solution
class is provided, you should define the variables that need shared access within the class. For languages like Go/JavaScript, which do not have classes, you can define higher-order functions and use closures to allow inner functions to access shared variables.
Alternatively, you can pass the variable as a function parameter, which is also a viable solution:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
// correct example, passed as an argument to other functions
LinkedList<Integer> res = new LinkedList<>();
traverse(root, res);
return res;
}
void traverse(TreeNode root, LinkedList<Integer> res) {
if (root == null) {
return;
}
// access and modify the res variable
res.add(root.val);
traverse(root.left, res);
traverse(root.right, res);
}
}
// Incorrect example, do not define global variables at the file level
// vector<int> res;
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
// Correct example, pass as a parameter to other functions
vector<int> res;
traverse(root, res);
return res;
}
void traverse(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
// Access and modify the res variable
res.push_back(root->val);
traverse(root->left, res);
traverse(root->right, res);
}
};
// Wrong example, do not define global variables at the file level
// var res []int
func preorderTraversal(root *TreeNode) []int {
// Correct example, pass as a parameter to other functions
var res []int
traverse(root, &res)
return res
}
func traverse(root *TreeNode, res *[]int) {
if root == nil {
return
}
// Access and modify the res variable
*res = append(*res, root.Val)
traverse(root.Left, res)
traverse(root.Right, res)
}
# Wrong example, do not define global variables at the file level
# res = []
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
# Correct example, pass as a parameter to other functions
res = []
self.traverse(root, res)
return res
def traverse(self, root, res):
if not root:
return
# Access and modify the res variable
res.append(root.val)
self.traverse(root.left, res)
self.traverse(root.right, res)
// Wrong example, do not define global variables at the file level
// let res = [];
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
// Correct example, pass it as a parameter to other functions
let res = [];
traverse(root, res);
return res;
}
var traverse = (root, res) => {
if (!root) {
return;
}
// Access and modify the res variable
res.push(root.val);
traverse(root.left, res);
traverse(root.right, res);
}
Important
For this approach, you must pay attention to the characteristics of the programming language and whether function parameters should be passed by value or by reference/pointer, otherwise it may lead to efficiency issues or even errors.
Clearing Standard Output Before Submission
When running test cases, LeetCode will return your algorithm's standard output to you. This means you can debug your code by printing some data using print statements. However, before submitting your code, make sure to comment out all print statements.
Standard output is an IO operation and can be time-consuming. If you include print statements, even if your algorithm code is optimal, you may find that the execution efficiency is very poor after submission, or it may fail due to timeouts.
I'll say no more. Next, I'll guide you through some simple problems on LeetCode as practice. "What is learned from the books is not as effective as what is gained through practice."