这里主要说一个暴力解法的注意点,或者说优化点。在暴力解法中第二层循环的循环初始条件可以为第一层循环循环条件加1。
for(int i = 0; i < n; i++)
{
for(int j = i+1; j < n; j++)
{
if(arr[i]+arr[j] == target)
{
return {i,j};
}
}
}
因为两数之和问题说到底就是一个组合问题,n个数中选两个数。从外层第一趟循环开始,外层循环条件 i 为0,代表着两个数中其中一个的下标为0, 而此时的第二层循环 j 则从 1 遍历到 n-1,把两个数中其中一个下标为0的情况考虑完了,后续的遍历不需要再考虑下标为0的元素。以此类推,当遍历到外层循环条件 i = m 时,下标小于m的元素出现的情况都已经考虑完了,第二层循环从 m+1 开始就可以了。
在使用这种方法之前,应该先考虑为什么要排序?排序之后对求解两数之和问题有什么帮助?首先,我们这样去思考,当选择了两个数相加之后,若与目标值不相等,这时在选择新的组合之前,是否可以利用前一次的选择,对新的选择作一些限定以缩小新的选择的范围,比如说,若两数之和大与目标值应该怎么办,小于该怎么办。你可能会因此想到二分查找,或者是快排的子过程,这两者都是很常用的思想和方法,我们应该熟练掌握。二分查找给我们的启发是通过与目标值比较改变上下界缩小范围,它需要排序的元素,而快排子过程则让我们想到首尾指针。结合两者,我们需要需要排序的元素和指向首尾的指针,首尾指针代表要寻找的两个数,当两数和比目标值小时,首指针前移,两数和增加,两数和比目标值大时,尾指针后移,两数和减小,而排序的元素则保证不会漏掉还可能达成目标值的情况。
vector<int> twoSum(vector<int>& nums, int target)
{
vector<int> res;
if(nums.size() < 2) //判断nums是否合法
{
return res;
}
sort(nums.begin(), nums.end());
int start = 0, end = nums.size() - 1; //nums.size()返回的结果为无符号的整数,这里没有做过多处理,但必须注意并认识到它是无符号数很重要
while(start < end)//这里使用等号是因为题意中要求两数之和,不能一个数算两次
{
int value = nums[start] + nums[end];
if(value == target)
{
res.push_back(start);
res.push_back(end);
return res;
}
else if(value < target)
start++;
else
end++;
}
return res;
}
利用hash数组,hash-map的方式处理问题的方式很常见,但应注意元素的取值范围,当元素取值范围很大的情况下,为了达到 O(1) 的效率,需要消耗大量的内存空间,这在一些嵌入式开发中是无法忍受的。下面给出C++的实现,这个代码使用C++中的无序可重复map(unordered_multimap),除此之外还针对不可一个元素使用两次等情况做相应的处理
vector<int> twoSum(vector<int>& nums, int target) {
//nums中元素可能重复所以用multi,而使用unordered则是为了更接近O(1)
//的查找复杂度(另一种map使用红黑树,一种平衡二叉树来组织元素,比较节省空间,
//但查找效率只是接近 O(lgn))
unordered_multimap<int, int> temp;
vector<int> res;
int i = 0;
//初始化hash
for(auto it = nums.begin(); it != nums.end(); it++,i++)
{
temp.insert({*it, i});
}
//判断是否为一个数利用两次得到目标值
if((target & 1) == 0)
{
int tar = target/2;
if(temp.count(tar) >= 2)
{
int i=0;
for(auto it = temp.find(tar) ; i<2; i++, it++)
{
res.push_back((*it).second);
}
return res;
}
}
i = 0;
for(auto it = nums.begin(); it != nums.end(); it++,i++)
{
if(temp.find(target - *it) != temp.end())
{
//前面已经处理过可能两个相等值得到目标结果的情况,这里直接过滤掉
if(target - *it != *it)
{
res.push_back(i);
res.push_back((*temp.find(target - *it)).second);
return res;
}
}
}
return res;
}
多数之和的问题可以建立在两数之和的问题之上,这里提供一个结合暴力解法与先排序后查找的三数之和代码(假定目标值为0),四数之和也可以用类似的写法。
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;//返回多组可能取值
if(nums.size() < 3) //异常输入判断
return res;
sort(nums.begin(), nums.end());
//排序后若元素中的最大值乘3(因为是三数之和)小于目标值(这里目标值为0,所以
//代码中没有乘3),或者最小值乘3大于目标值则可以直接退出。
if(nums.back() < 0 || nums.front() > 0)
return res;
//错误一,没有判空就取最大值(现在这份代码没问题)
int max = *nums.rbegin();
//错误二,nums.size()为无符号数
int n = int(nums.size()) - 2;
for(int i = 0; i< n; i++)
{
//类似与上面最大值乘3的优化,具体效果如何还有待探讨
if((nums[i] + max * 2) < 0)
continue;
//这里因为目标值是0,所以可以这样写,正常情况下还应注意负数的问题
//(两个负数相加值会变小)
if(nums[i] > 0)
break;
//题目中要求无重复,这里跳过相邻重复元素,至于这样做会不会造成情况丢失
//可以参考文章开头暴力解法的注意点进行思考
if(i > 0 && nums[i] == nums[i-1])
continue;
int left = i+1;
int right = nums.size() -1;
while(left < right)
{
int value = nums[i] + nums[left] + nums[right];
if(value > 0)
right--;
else if(value < 0)
left++;
else
{
res.push_back({nums[i],nums[left], nums[right]});
//跳过重复元素
int leftValue = nums[left];
while(left < right && leftValue == nums[++left]);
int rightValue = nums[right];
while(left < right && rightValue == nums[--right]);
}
}
}
return res;
}
在n个数中找出m个数满足某一条件,最直白的解法就是找出所有排列组合的情况,本人能力有限,这里只给出一个基于递归的求所有排列组合情况的解法(如果你能基于循环实现或是有其他更优的解法,优化方法等那肯定更好,也希望你能留言告诉我)主要思想为对于每个元素,有两种情况,选或不选。例如,n个数选m个,若第一个数被选中,则剩下的n-1个中选择m-1个,若第一个数未被选中,则在剩下的n-1个选择m个。附:此算法时间复杂度真的是非常高(O(2^n)),使用价值不大,不过这种思想可以认真了解一下,对于解决 n皇后问题,字符串排列问题等很有帮助。
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
vector<int> t;
core(res, nums, target, nums.size()-1, t);
return res;
}
void core(vector<vector<int>> & res, vector<int>& nums, int target, int n, vector<int> t)
{
if(t.size() == 4)
{
int value = 0;
for(auto it = t.begin(); it != t.end(); it++)
{
value += *it;
}
if(value == target)
res.push_back(t);
return;
}
if(n == 0)
return;
core(res, nums, target, n-1, t);
t.push_back(nums[n]);
core(res, nums, target, n-1, t);
}