当然,我们会在这里为您提供帮助。
从一门新语言开始从来都不是那么容易,而且可能有些事情一开始并没有立即清楚。此外,对于您可能看到的任何粗鲁的 cmets,我深表歉意,但您可以放心,SO 的绝大多数成员都非常支持。
我想先给你一些关于 Leetcode 和 Codeforces 等页面的信息。通常也称为“竞争性编程”页面。有时人们会误解这一点,他们认为您提交代码的时间有限。但事实并非如此。有这样的比赛,但通常不在提到的页面上。不好的是,在线页面上也使用了真实比赛中使用的编码风格。这真的很糟糕。因为这种编码风格太可怕了,以至于没有认真的开发人员能在一家真正的公司里生存一天,需要通过软件赚钱,然后为此负责。
因此,这些页面永远不会教您或指导您如何编写好的 C++ 代码。更糟糕的是,如果新手开始学习该语言并看到这些糟糕的代码,那么他们就会养成坏习惯。
但是这些页面的目的是什么?
目的是找到一个好的算法,主要针对运行时执行速度进行优化,并且通常还针对低内存消耗。
所以,我们的目标是一个好的设计。语言或编码风格对他们来说并不重要。所以,你甚至可以提交完全混淆的代码或“代码高尔夫”的解决方案,只要它快,没关系。
因此,切勿在第一步就立即开始编码。首先,考虑 3 天。然后,拿一些设计工具,比如一张纸,画一个设计草图。然后重构您的设计,然后重构您的设计,然后重构您的设计,然后重构您的设计,然后重构您的设计等等。这可能需要一周时间。
接下来,寻找一种你知道并且可以处理你的设计的合适的编程语言。
最后,开始编码。因为你之前做了一个很好的设计,你可以使用长而有意义的变量名,写很多很多cmets,这样其他人(一个月后还有你)可以理解你的代码AND你的设计。
好的,大概明白了。
现在,让我们分析您的代码。您选择了具有三重嵌套循环的蛮力解决方案。这可能适用于少量元素,但在大多数情况下会导致所谓的 TLE(超出时间限制)错误。这些页面上的几乎所有问题都无法通过蛮力解决。蛮力解决方案始终表明您没有执行上述设计步骤。这会导致额外的错误。
您的代码存在过多的语义错误。
您在开头定义了一个名为“v”的std::vector。然后,在循环中,当你找到满足给定条件的三元组后,push_back 将得到std::vector 中的结果。这意味着,您将 3 个值添加到 std::vector“v”,现在其中有 3 个元素。在下一个循环运行中,在找到下一个合适的位置后,您再次将 push_back 3 个附加值添加到您的 std::vector ”v” 中,现在其中有 6 个元素。在下一轮 9 个元素等等。
如何解决?
您可以使用std::vector 的clear 函数从最内层循环开始处if 语句之后的std::vector 中删除旧元素。但这基本上不是那么好,而且还很耗时。更好的是遵循一般习语,尽可能晚地定义变量,并在需要的时候。所以,如果你在 if 语句之后定义你的std::vector“v”,那么问题就消失了。但是,您还会注意到它仅在此处使用,而在其他任何地方都没有。因此,您根本不需要它。
您可能已经看到可以通过使用初始化列表向std::vector 添加值。比如:
std::vector<int> v {1,2,3};
有了这个诀窍,你可以删除你的std::vector“v”和所有相关代码,直接写:
ans.push_back( { nums[i], nums[j], nums[k] } );
那么您将节省 3 个不必要的 push_back(和一个 clear)操作,更重要的是,您不会获得超过 3 个元素的结果集。
下一个问题。重复。您尝试通过编写 && i!=j && i!=k && j!=k 来防止存储重复项。但这通常不起作用,因为您比较的是索引而不是值,而且比较也是错误的。布尔表达式是重言式。它总是正确的。您使用i+1 初始化变量j,因此“i”永远不会等于“j”。因此,条件i != j 始终为真。其他变量也是如此。
但是如何防止重复条目。您可以进行一些逻辑比较,或者首先存储所有三元组,然后使用std::unique(或其他函数)来消除重复项,或者使用只存储唯一元素的容器,例如std::set。对于给定的设计,时间复杂度为 O(n^3),这意味着它已经非常慢,添加 std::set 不会让事情变得更糟。我在一个小型基准测试中检查了这一点。因此,唯一的解决方案是完全不同的设计。我们稍后会谈到。让我们先修复代码,仍然使用蛮力方法。
请看下面的简短而优雅的解决方案。
vector<vector<int>> threeSum(vector<int>& nums) {
std::set<vector<int>> ans;
int n = nums.size();
sort(nums.begin(), nums.end());
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
for (int k = j + 1; k < n; k++)
if (nums[i] + nums[j] + nums[k] == 0)
ans.insert({ nums[i], nums[j], nums[k] });
return { ans.begin(), ans.end() };
}
但是,不幸的是,由于不幸的设计决策,大输入比更好的设计慢 20000 倍。而且,由于在线测试程序将使用大输入向量,因此程序不会通过运行时约束。
如何找到更好的解决方案。我们需要仔细分析需求,也可以使用一些现有的知识来解决类似的问题。
如果您阅读一些书籍或互联网文章,那么您经常会得到提示,即所谓的“滑动窗口”是获得合理解决方案的正确方法。
您将找到有用的信息here。但你当然也可以在 SO 上搜索答案。
对于这个问题,我们会使用典型的 2 指针方法,但是针对这个问题的具体要求进行了修改。基本上是一个起始值和一个移动和关闭窗口。 . .
需求分析得出以下思路。
- 如果所有计算的数字都 > 0,那么我们永远不会有 0 的总和。
- 很容易识别重复的数字,如果它们彼此位于下方
--> 对输入值进行排序会非常有益。
这将消除对随机分布输入向量的一半值的测试。见:
std::vector<int> nums { 5, -1, 4, -2, 3, -3, -1, 2, 1, -1 };
std::sort(nums.begin(), nums.end());
// Will result in
// -3, -2, -1, -1, -1, 1, 2, 3, 4, 5
我们看到,如果我们将窗口向右移动,那么只要窗口的开始达到正数,我们就可以停止评估。此外,我们可以立即识别重复的数字。
接下来。如果我们从排序向量的开头开始,这个值很可能非常小。如果我们以当前窗口的开头加一开始下一个窗口,那么我们将得到“非常”的负数。要通过将 2 个“非常”负数相加得到 0,我们需要一个非常正数。这是std::vector的末尾。
开始
startPointerIndex 0,值 -3
窗口开始 = startPointerIndex + 1 --> 值 -2
窗口结束 = lastIndexInVector --> 5
是的,我们已经找到了解决方案。现在我们需要检查重复项。如果倒数第二个位置还有 5 个,那么我们可以跳过。它不会添加额外的不同解决方案。因此,在这种情况下,我们可以减少结束窗口指针。同样有效,如果窗口开头会有一个额外的-2。然后我们需要增加起始窗口指针,以避免从那一端重复发现。
Some 对起始指针索引有效。示例:startPointerIndex = 3(从 0 开始计数索引),值为 -1。但之前的值,在索引 2 处也是 -1。所以,没必要评价。因为我们已经评估过了。
以上方法将防止创建重复条目。
但是如何继续搜索。如果我们找不到解决方案,我们将缩小窗口。我们也会以一种聪明的方式做到这一点。如果总和太大,显然右边的窗口值太大了,我们最好使用下一个较小的值进行下一次比较。
在窗口的起始侧也是如此,如果总和很小,那么我们显然需要更大的值。所以,让我们增加开始窗口指针。我们这样做(使窗口变小)直到我们找到解决方案或直到窗口关闭,这意味着开始窗口指针不再小于结束窗口指针。
现在,我们已经开发出某种不错的设计,可以开始编码了。
我们还尝试实现良好的编码风格。并重构代码以实现更快的实现。
请看:
class Solution {
public:
// Define some type aliases for later easier typing and understanding
using DataType = int;
using Triplet = std::vector<DataType>;
using Triplets = std::vector<Triplet>;
using TestData = std::vector<DataType>;
// Function to identify all unique Triplets(3 elements) in a given test input
Triplets threeSum(TestData& testData) {
// In order to save function oeverhead for repeatingly getting the size of the test data,
// we will store the size of the input data in a const temporary variable
const size_t numberOfTestDataElements{ testData.size()};
// If the given input test vector is empty, we also immediately return an empty result vector
if (!numberOfTestDataElements) return {};
// In later code we often need the last valid element of the input test data
// Since indices in C++ start with 0 the value will be size -1
// With taht we later avoid uncessary subtractions in the loop
const size_t numberOfTestDataElementsMinus1{ numberOfTestDataElements -1u };
// Here we will store all the found, valid and unique triplets
Triplets result{};
// In order to save the time for later memory reallocations and copying tons of data, we reserve
// memory to hold all results only one time. This will speed upf operations by 5 to 10%
result.reserve(numberOfTestDataElementsMinus1);
// Now sort the input test data to be able to find an end condition, if all elements are
// greater than 0 and to easier identify duplicates
std::sort(testData.begin(), testData.end());
// This variables will define the size of the sliding window
size_t leftStartPositionOfSlidingWindow, rightEndPositionOfSlidingWindow;
// Now, we will evaluate all values of the input test data from left to right
// As an optimization, we additionally define a 2nd running variable k,
// to avoid later additions in the loop, where i+1 woild need to be calculated.
// This can be better done with a running variable that will be just incremented
for (size_t i = 0, k = 1; i < numberOfTestDataElements; ++i, ++k) {
// If the current value form the input test data is greater than 0,
// As um with the result of 0 will no longer be possible. We can stop now
if (testData[i] > 0) break;
// Prevent evaluation of duplicate based in the current input test data
if (i and (testData[i] == testData[i-1])) continue;
// Open the window and determin start and end index
// Start index is always the current evaluate index from the input test data
// End index is always the last element
leftStartPositionOfSlidingWindow = k;
rightEndPositionOfSlidingWindow = numberOfTestDataElementsMinus1;
// Now, as long as if the window is not closed, meaning to not narrow, we will evaluate
while (leftStartPositionOfSlidingWindow < rightEndPositionOfSlidingWindow) {
// Calculate the sum of the current addressed values
const int sum = testData[i] + testData[leftStartPositionOfSlidingWindow] + testData[rightEndPositionOfSlidingWindow];
// If the sum is t0o small, then the mall value on the left side of the sorted window is too small
// Therefor teke next value on the left side and try again. So, make the window smaller
if (sum < 0) {
++leftStartPositionOfSlidingWindow;
}
// Else, if the sum is too biig, the the value on the right side of the window was too big
// Use one smaller value. One to the left of the current closing address of the window
// So, make the window smaller
else if (sum > 0) {
--rightEndPositionOfSlidingWindow;
}
else {
// Accodring to above condintions, we found now are triplet, fulfilling the requirements.
// So store this triplet as a result
result.push_back({ testData[i], testData[leftStartPositionOfSlidingWindow], testData[rightEndPositionOfSlidingWindow] });
// We know need to handle duplicates at the edges of the window. So, left and right edge
// For this, we remember to c
const DataType lastLeftValue = testData[leftStartPositionOfSlidingWindow];
const DataType lastRightValue = testData[rightEndPositionOfSlidingWindow];
// Check left edge. As long as we have duplicates here, we will shift the opening position of the window to the right
// Because of boolean short cut evaluation we will first do the comparison for duplicates. This will give us 5% more speed
while (testData[leftStartPositionOfSlidingWindow] == lastLeftValue && leftStartPositionOfSlidingWindow < rightEndPositionOfSlidingWindow)
++leftStartPositionOfSlidingWindow;
// Check right edge. As long as we have duplicates here, we will shift the closing position of the window to the left
// Because of boolean short cut evaluation we will first do the comparison for duplicates. This will give us 5% more speed
while (testData[rightEndPositionOfSlidingWindow] == lastRightValue && leftStartPositionOfSlidingWindow < rightEndPositionOfSlidingWindow)
--rightEndPositionOfSlidingWindow;
}
}
}
return result;
}
};
上述解决方案将优于 99% 的其他解决方案。我做了很多基准测试来证明这一点。
它还包含大量的 cmets 来解释那里发生了什么。如果我选择了“会说话”且有意义的变量名以便更好地理解。
希望能帮到你一点。
最后:我将这个答案献给 Sam Varshavchik 和 PaulMcKenzie。