力扣/LeetCode 解题须知
本站所有的例题、习题都选自 力扣/LeetCode 的公开免费题目,并给出题目的链接,你可以直接跳转到官网进行练习。
为了照顾初学者,下面快速介绍一下 力扣/LeetCode 上提交代码的注意事项。
核心代码模式
在 力扣/LeetCode 上刷题,就是给你一个函数框架,你需要实现函数逻辑,这种模式一般称之为核心代码模式。
比如力扣第一题两数之和,就是让你实现这样一个 twoSum
函数:
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 {
}
意思就是给你输入数组 nums
和目标值 target
,你需要实现算法逻辑,最后计算返回一个数组。
你的解法代码提交到后台,它会给这个 twoSum
函数传入若干预定义的测试用例,对比你的返回值和正确答案,以此判断你的代码是否正确。
提示
对于 Java/C++,有些标准库会默认导入,你不需要显式导入就可以直接用一些常用的标准库数据结构,比如 Java 的 ArrayList
,C++ 的 vector
等。因为在网页上刷题时没有类似 IDE 的自动导入功能,所以这样还是比较省事的。
当然,如果你想要自动补全功能,可以考虑使用本站配套的 Jetbrains IDE 插件 或 vscode 插件。
从用户角度来说,核心代码模式应该是最方便的形式,因为你只需要专心写好算法逻辑就行了。目前大部分刷题平台和技术面试/笔试场景都是核心代码模式。但是以防万一,这里还是要介绍一下 ACM 模式。
ACM 模式
简单说,ACM 模式就是更麻烦一些,题目给你输入的是特定格式的字符串,你需要自己解析这个字符串,然后把计算结果输出到标准输出。
你的代码提交到后台后,系统会把每个测试用例(字符串)用标准输入传给你的程序,然后对比你程序的标准输出和预期输出是否相同,以此判断你的代码是否正确。
比如牛客网就提供这种 ACM 模式,我随便截个图你看看,代码编辑框没有任何初始代码,你需要从头自己写:
对于 ACM 模式,一个技巧就是要对代码分层,把对字符串输入的处理逻辑和真正的算法逻辑分开,类似这样:
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 主要负责接收处理输入的数据,构建输入的用例
int[] nums;
int target;
int N = scanner.nextInt();
...
// 调用算法逻辑进行求解
System.out.println(twoSum(nums, target));
}
public static int[] twoSum(int[] nums, int target) {
// 转化为核心代码模式,在这里写算法逻辑
}
}
这样看起来就很清晰,输入处理的逻辑可以作为模板直接复制粘贴,最终转化成核心代码模式,就和你在力扣上刷题就没什么区别了。
我的建议是,平时学习时,用核心代码形式就行了,简单方便。到了面试/笔试前夕,花上一个小时就能熟悉 ACM 模式了。我记得牛客网有专门练习这种 ACM 模式的例题,你自己搜索一下即可。
提交代码可能出现的错误
如果提交的代码可以通过后台的所有测试用例,则称为提交成功,我们也常说 AC(Accepted)。
如果提交的代码不能通过所有测试用例,则称为提交失败,常见的失败原因有以下几种:
编译错误(Compile Error)
代码无法通过编译,这种错误一般是语法错误,比如拼写错误、缺少分号等。一般网页上手搓代码容易出现这种错误,使用本站配套 Jetbrains IDE 插件 或 vscode 插件 的话,编辑器会有基本的语法检查功能,一般不会出现这种错误。
运行错误(Runtime Error)
代码可以编译通过,但是在运行时出现了数组越界、空指针异常等错误。这种错误一般是由于边界条件处理不当,你需要留意边界条件、特殊测试用例(也叫做 Corner case,比如输入为空等情况)。
答案错误(Wrong Answer)
代码可以编译通过,也可以运行,但是某些测试用返回的结果和正确答案不一致。这种错误一般是因为你的算法有问题,需要你根据出错的用例反思,甚至可能整个思路都错了,需要你推倒重来。
超时(Time Limit Exceeded,常简称为 TLE)
力扣预定义的测试用例中,越靠后的用例数据规模就越大。如果你的算法的时间复杂度太高,在运行这些大规模的测试用例时就容易超过系统规定的时间限制,导致超时错误。
想要解决这个问题,你需要检查以下几点:
1、是否有冗余计算,是否有更高效的解法思路来降低时间复杂度。
2、是否有编码错误,比如边界控制出错,错误地传值传引用导致无意义的数据拷贝等。
如果你卡在大规模的测试用例,一般就能说明你的算法结果是正确的,因为前面的小规模测试用例都通过了,只不过是时间复杂度太高,需要优化。
在笔试中,一般是按照通过的测试用例数量来计分的。所以如果你实在是想不出最优解法通过所有用例,提交一个暴力解,过几个用例捞点分,也是一种聪明的策略。
内存超限(Memory Limit Exceeded,常简称为 MLE)
和超时错误类似,内存超限一般是算法的空间复杂度太高,在运行大数据规模的测试用例时,占用的内存超过了系统规定的内存限制。
想要解决这个问题,你需要检查以下几点:
1、是否有申请了多余的空间,是否有更高效的解法思路来降低空间复杂度。
2、是否在递归函数中错误地使用了传值参数导致无意义的数据拷贝。
力扣提交代码的注意事项
对于初次使用力扣刷题的读者,我这里再介绍几个初学者容易踩的坑。
不要用文件级别的全局变量
前面说了力扣这种核心代码模式,会给你的算法代码输入若干预定义的测试用例,然后比较你的返回值和正确答案,那么就有很重要的一点:
你的代码不要用文件级别的全局变量,否则不同的测试用例之间可能会相互影响。力扣官网对于这个问题也有说明:https://support.leetcode.cn/hc/kb/article/1194344/
有些读者说他提交的解法通过不了,但是把出错的测试用例单独拎出来运行就是对的,一提交就错了,都是因为这个原因。你单独拎出来的时候只运行了一个测试用例,但提交后多个测试用例之间的数据产生了影响,就可能出错。
我在本站的教程中,尤其是讲解递归算法的部分,会经常提到「全局变量」这个词,但我指的全局变量并不是指文件级别的全局变量,而是类级别的全局变量。
具体举例说明吧,就比如二叉树的前序遍历这道题,题目输入一个二叉树的根节点,让你返回二叉树的前序遍历结果,你可以这样写:
class Solution {
// 正确示例,类级别的全局变量
LinkedList<Integer> res = new LinkedList<>();
public List<Integer> preorderTraversal(TreeNode root) {
traverse(root);
return res;
}
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 类内的其他函数都可以访问到 res
res.add(root.val);
traverse(root.left);
traverse(root.right);
}
}
// 错误示例,不要在文件级别定义全局变量
// vector<int> res;
class Solution {
public:
// 正确示例,类级别的全局变量
vector<int> res;
vector<int> preorderTraversal(TreeNode* root) {
traverse(root);
return res;
}
void traverse(TreeNode* root) {
if (!root) {
return;
}
// 类内的其他函数都可以访问到 res
res.push_back(root->val);
traverse(root->left);
traverse(root->right);
}
};
// 错误示例,不要在文件级别定义全局变量
// var res []int
func preorderTraversal(root *TreeNode) []int {
// 正确示例,运用闭包,让 traverse 函数内部也可以访问到 res 变量
var res []int
var traverse func(*TreeNode)
traverse = func(root *TreeNode) {
if root == nil {
return
}
// 访问修改 res 变量
res = append(res, root.Val)
traverse(root.Left)
traverse(root.Right)
}
traverse(root)
return res
}
# 错误示例,不要在文件级别定义全局变量
# res = []
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
# 正确示例,类级别的全局变量
self.res = []
self.traverse(root)
return self.res
def traverse(self, root):
if not root:
return
# 类内的其他函数都可以访问到 res
self.res.append(root.val)
self.traverse(root.left)
self.traverse(root.right)
// 错误示例,不要在文件级别定义全局变量
// let res = [];
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
// 正确示例,运用闭包,让 traverse 函数内部也可以访问到 res 变量
let res = [];
var traverse = (root) => {
if (!root) {
return;
}
// 访问修改 res 变量
res.push(root.val);
traverse(root.left);
traverse(root.right);
}
traverse(root);
return res;
};
对于 Java/C++/Python 这种给了一个 Solution
类的情况,你就把需要共享访问的变量定义在类里面,对于 Go/JavaScript 这种没有类的情况,你可以定义高阶函数,运用闭包的特性,让内部函数也可以访问到共享变量。
或者,你可以把这个变量作为函数的参数传递,这样也是一种解决方案:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
// 正确示例,作为参数传递给其他函数
LinkedList<Integer> res = new LinkedList<>();
traverse(root, res);
return res;
}
void traverse(TreeNode root, LinkedList<Integer> res) {
if (root == null) {
return;
}
// 访问修改 res 变量
res.add(root.val);
traverse(root.left, res);
traverse(root.right, res);
}
}
// 错误示例,不要在文件级别定义全局变量
// vector<int> res;
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
// 正确示例,作为参数传递给其他函数
vector<int> res;
traverse(root, res);
return res;
}
void traverse(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
// 访问修改 res 变量
res.push_back(root->val);
traverse(root->left, res);
traverse(root->right, res);
}
};
// 错误示例,不要在文件级别定义全局变量
// var res []int
func preorderTraversal(root *TreeNode) []int {
// 正确示例,作为参数传递给其他函数
var res []int
traverse(root, &res)
return res
}
func traverse(root *TreeNode, res *[]int) {
if root == nil {
return
}
// 访问修改 res 变量
*res = append(*res, root.Val)
traverse(root.Left, res)
traverse(root.Right, res)
}
# 错误示例,不要在文件级别定义全局变量
# res = []
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
# 正确示例,作为参数传递给其他函数
res = []
self.traverse(root, res)
return res
def traverse(self, root, res):
if not root:
return
# 访问修改 res 变量
res.append(root.val)
self.traverse(root.left, res)
self.traverse(root.right, res)
// 错误示例,不要在文件级别定义全局变量
// let res = [];
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
// 正确示例,作为参数传递给其他函数
let res = [];
traverse(root, res);
return res;
}
var traverse = (root, res) => {
if (!root) {
return;
}
// 访问修改 res 变量
res.push(root.val);
traverse(root.left, res);
traverse(root.right, res);
}
重要
对于这种方案,你要注意编程语言的特性,函数参数到底应该传值还是传引用/指针,否则会出现效率问题,甚至出错。
提交时清除标准输出
在运行测试用例时,力扣会把你的算法的标准输出返回给你,也就是说你可以通过 print 打印一些数据来调试代码。这里注意,准备提交代码前一定要把所有的打印语句注释掉。
因为标准输出是 IO 操作,非常消耗时间。如果包含了打印语句,也许你的算法代码本身就是最优解,但提交后发现执行效率非常差,甚至因为超时而无法通过。
好了,其他没啥了,开始学习吧!