LeetCode Practice Guide
All example problems and exercises on this site are selected from the publicly available free problems on LeetCode, and links to the problems are provided so you can directly practice on the official website.
To accommodate beginners, here is a quick introduction to submitting code on LeetCode.
Core Code Pattern
When solving problems on LeetCode, you are given a function framework, and you need to implement the function logic. This is generally referred to as the Core Code Pattern.
For example, the first problem on LeetCode, Two Sum, requires you 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 to provide you with an input array nums
and a target value target
. You need to implement the algorithm logic and finally return an array.
When you submit your solution code to the backend, it will run the twoSum
function with several predefined test cases. Your returned values will be compared to the correct answers to determine if your code is correct.
Tips
For Java/C++, some standard libraries are imported by default, so you can directly use common standard library data structures like Java's ArrayList
and C++'s vector
without explicit import. This is convenient since online coding platforms don't have an IDE's auto-import feature.
However, if you prefer auto-completion, consider using our site's related Jetbrains IDE Plugin or vscode Plugin.
From a user's perspective, the core code mode should be the most convenient form because you only need to focus on writing the algorithm logic. Most online coding platforms and technical interview/written test environments use the core code mode. However, just in case, I will introduce the ACM mode here.
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.
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 variable
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 internally
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 be aware of the characteristics of the programming language, specifically whether function parameters should be passed by value or by reference/pointer. Failing to do so may lead to efficiency issues or even errors.
Clear Standard Output Before Submission
When running test cases, LeetCode returns the standard output of your algorithm to you, which means you can use print statements to debug your code. Here’s an important note: Make sure to comment out all print statements before submitting your code.
This is because standard output operations are I/O operations and can be very time-consuming. Even if your algorithm code is the optimal solution, including print statements might cause it to perform poorly upon submission, or even fail due to timeouts.
That's all, let's start learning!