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 techniques for using coding practice platforms and how these platforms determine if your solution is correct.
How to Read a Problem
Take 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 in Code Submission
When the submitted code passes all test cases on 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 such as typos or missing semicolons. These errors often occur when writing code directly on a webpage. Using the Jetbrains IDE Plugin or VSCode Plugin provided by the site can help, as the editor has basic syntax checking features to prevent such errors.
Runtime Error
The code compiles successfully but encounters errors during execution, such as array out-of-bounds or null pointer exceptions. These errors generally result from improper handling of boundary conditions. Pay attention to boundary conditions and special test cases (also known as corner cases, such as empty inputs).
Wrong Answer
The code compiles and runs, but the results for some test cases do not match the correct answers. This is usually due to a flaw in your algorithm. You need to reflect on the failing test cases and possibly rethink your entire approach.
Time Limit Exceeded (TLE)
In pre-defined test cases on platforms like LeetCode, later test cases have larger data scales. If your algorithm's time complexity is too high, it may exceed the system's time limit when running large-scale test cases, resulting in a time limit exceeded error.
To resolve this issue, consider the following:
- Check for redundant calculations and explore more efficient solutions to reduce time complexity.
- Look for coding errors, such as incorrect boundary control or unnecessary data copying due to improper passing of values or references.
If you find yourself stuck on large-scale test cases, it often indicates that your algorithm is correct since it passed smaller test cases but needs optimization for time complexity.
In written exams, scores are usually based on the number of test cases passed. If you cannot devise an optimal solution, submitting a brute-force one to pass a few cases and score some points can be a smart move.
Memory Limit Exceeded (MLE)
Similar to time limit errors, memory limit exceeded errors occur when the algorithm's space complexity is too high, causing memory usage to surpass the system's limits when handling large-scale test cases.
To address this issue, consider the following:
- Check for unnecessary space allocation and explore more efficient solutions to reduce space complexity.
- Ensure that recursive functions do not unnecessarily use value parameters leading to pointless 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 need to consider the characteristics of the programming language, specifically whether function parameters should be passed by value or by reference/pointer. Failure to do so may lead to efficiency issues or even errors.
Remove Standard Output Before Submission
When running test cases, LeetCode will return the standard output of your algorithm to you. This means you can use print statements to debug your code. However, be sure to comment out all print statements before submitting your code.
Standard output is an I/O operation that consumes a significant amount of time. If your code includes print statements, your algorithm might be optimal, but upon submission, you may find its execution highly inefficient, or it might even fail due to timeouts.
That's all I have to say for now. Next, I will guide you through solving some simple problems on LeetCode as practical exercises. The true understanding of a concept comes from hands-on practice!