為什么有時在 LeetCode (力扣)上,Run Code(執行代碼) 正確,Submit(提交代碼)時提示 Wrong Answer(解答錯誤)?
看這篇文章你就懂了,真不是 LeetCode 出 Bug 了。
大家好,我是 「負雪明燭」,一位堅持 7 年寫了 1000 篇 LeetCode 算法題題解的程序員。歡迎關注。
面試必會的算法題系列在寫作中——
今天分享的是刷題小經驗 —— 為什么一個測試用例,在「執行代碼」的結果正確,「提交」運行出錯?
往事
談起這個話題,我忍不住想起往事。
第一次遇到這個問題是我在大二剛開始刷 LeetCode 的時候(還沒中文版力扣),那時候我的主語言是 Java,在本地編譯器 Eclipse 上寫完代碼粘貼到 LeetCode 的代碼框里,點擊「Run Code」后,看測試用例能通過,然后就「Submit」了。
滿心期待着出現了一個綠色的 Accept!
幾秒種后,我傻眼了,我看到的是紅色的 Wrong Answer!
當時是新入門的小白,完全不能理解為什么會出錯,還以為是 LeetCode 出現 Bug 了 😂
當時我的同學中在刷題的只有我一個,也沒有微信群交流,只能自己想辦法。捯飭了半天才在網上找到了答案——原來這和 LeetCode 的評測機制有關。
復現
下面我就在中文力扣上復現一下當時的場景。
就像下面這樣,LeetCode 第 1 題:兩數之和。
假如我的代碼是下面這樣,注意 visited
定義的位置:
unordered_map<int, int> visited;
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); ++i) {
if (visited.count(target - nums[i])) {
return {visited[target - nums[i]], i};
}
visited[nums[i]] = i;
}
return {};
}
};
題目默認的測試用例是 :
[2,7,11,15]
9
點擊「執行代碼」—— OK,沒問題,輸出和預期結果一致。
那就點擊「提交」唄 —— 出現了「解答錯誤」!
出錯的測試用例是:
[3,3]
6
然鵝,我看自己的代碼應該沒問題呢。所以我把這個測試用例放到測試用例的執行框里,點擊「執行代碼」,結果是 [0, 1]
!和預期結果是一樣的!
這到底是怎么回事呢?是力扣有 Bug 嗎?
原因
其實不是力扣有 Bug,是我們沒有理解力扣的評測機制。
力扣的判題機在讀取您的代碼后,對每個測試用例,都會初始化一次類,但全局變量和類內靜態變量需要您手動初始化。
你可以把力扣的評測過程想象成下面這樣:
unordered_map<int, int> visited;
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 你的代碼
}
};
int main() {
string line;
while (getline(cin, line)) {
// 讀取輸入的 nums
vector<int> nums = stringToIntegerVector(line);
// 讀取輸入的 target
getline(cin, line);
int target = stringToInteger(line);
// 每次實例化一個 Solution(),並執行其 twoSum 方法
vector<int> ret = Solution().twoSum(nums, target);
// 輸出結果
string out = integerVectorToString(ret);
cout << out << endl;
}
return 0;
}
看到了嗎?
- 對於每個測試用例,力扣會實例化一個
Solution()
,並執行其twoSum
方法 - 如果把
visited
放在了類Solution
的外邊,作為「全局變量」,那么對於所有測試用例是共享的。因此上一個測試用例的運行結果會影響下一個測試用例,導致「解答錯誤」。 - 把「解答錯誤」的測試用例放到「測試用例」框里,再運行的結果是對的,因為只運行了一個測試用例,不會互相干擾。
正確做法
為了避免「全局變量」或者「類內的靜態變量」在不同測試用例之間的干擾,我們有兩種辦法:
- 推薦做法:不使用「全局變量」或者「類內的靜態變量」;
- 在 類內/函數內 對「全局變量」或者「類內的靜態變量」執行初始化。
比方說,我們把 visited
的位置調整到 類內/函數內 ,從而避免了「全局變量」。
class Solution {
private:
// 類內
unordered_map<int, int> visited;
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 函數內
// unordered_map<int, int> visited;
for (int i = 0; i < nums.size(); ++i) {
if (visited.count(target - nums[i])) {
return {visited[target - nums[i]], i};
}
visited[nums[i]] = i;
}
return {};
}
};
或者在函數內初始化 visited
,比如在 twoSum()
方法中執行 visited.clear()
。
unordered_map<int, int> visited;
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
visited.clear();
for (int i = 0; i < nums.size(); ++i) {
if (visited.count(target - nums[i])) {
return {visited[target - nums[i]], i};
}
visited[nums[i]] = i;
}
return {};
}
};
上面寫法中,我最推薦把 visited
變量寫到 twoSum
以內。
為什么呢?
這樣符合「最小作用域原則」。
最小作用域原則是指:把每個變量定義成只對需要看到它的、最小范圍的代碼段可見。
這樣能規避很多意想不到的錯誤。
總結
在刷題的時候,應盡量避免使用「全局變量」或者「類內的靜態變量」,因為它們可能導致不同「測試用例」互相干擾,導致「解答錯誤」。
定義變量應遵循「最小作用域原則」,能規避很多意想不到的錯誤。
明白了 LeetCode(力扣)的評測機制之后,能讓我們刷題不糊塗👨💻👩💻
參考: