俩数之和
- 题目
- 函数原型
- 边界条件
- 算法设计:枚举
- 算法设计:逆向思维+查表
- 算法设计:查找表
题目
给定一个整数数组 nums
和一个目标值 target
,请你在该数组中找出和为目标值的那 两个
整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
函数原型
C的函数原型:
int* twoSum(int* nums, int numsSize, int target, int* returnSize){}
分析:
- 返回的是下标,返回类型是
int *
, 即上面例子的[0, 1]
- 参数列表有
4
个,nums
是给定数组,numsSize
是给定数组的长度,target
是目标值,returnSize
是返回长度,而类型是int*
,所以是返回数组的长度。
也就是说,我们得在程序里创建一个数组存储需要返回的下标。
int *result_nums = (int*)malloc(sizeof(int) * numSize);
边界条件
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
int *result_nums = (int*)malloc(sizeof(int) * numSize);
// 没写 free() 是因为调用方负责释放
return result_nums;
}
函数原型确定后,接着判断参数列表的合理性,对于输入参数需要做一个严格的检查。
如果是空数组,或者数组元素个数小于 2
,那就直接返回 NULL
了,输入参数不合理。
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
if( nums == NULL || numsSize < 2 )
return NULL;
int *result_nums = (int*)malloc(sizeof(int) * numSize);
return result_nums;
}
算法设计:枚举
俩数之和,这题目可以用枚举试试。
双重循环:
- 当外循环在第
个元素时(
0 <= i < numsSize
); - 内循环从第
细节在于:循环的【结束条件】。
内循环的结束条件,是最后一个元素,有俩种写法:
<= numsSize - 1
< numsSize
而外循环的结束条件,是倒数第二个元素,不是最后一个。
为什么呢?
因为不能重复利用这个数组中同样的元素,当外循环指向最后一个元素,内循环指向的是最后一个元素再+1
个元素,这一步其实越界了。
外循环的结束条件:
<= numsSize-2
< numsSize-1
for(int i=0; i<numsSize-1; i++)
{
for(int j=i+1; j<numsSize; j++)
{
// do sth...
}
}
内外循环一一对比, 如果相加等于 target
,就记录在返回数组 result_nums
里。
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 返回下标的数组
int index;
// 记录符合target值的元素个数
for(int i=0; i<numsSize-1; i++)
{
for(int j=i+1; j<numsSize; j++)
{
if( nums[i] + nums[j] == target )
{
result_nums[index] = i;
result_nums[index+1] = j;
// 相加等于 target,记录下标
index += 2;
// 下标往后移俩位,方便下次存储新的 target 组合
*returnSize = index;
// 把返回数组的长度给 returnSize
}
}
return return_nums;
}
完整代码:
// 枚举:双循环遍历
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
if( nums == NULL || numsSize < 2 )
return NULL;
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 开辟 numsSize 个空间
int index = 0;
// 记录符合target值的元素个数
for(int i=0; i<numsSize-1; i++)
{
for(int j=i+1; j<numsSize; j++)
{
// 如果俩数之和 = 目标值,加入到返回数组里,否则继续寻找
if( nums[i] + nums[j] == target ){
result_nums[index] = i;
result_nums[index+1] = j;
// 把下标加入到返回数组 result_nums 里
index += 2;
*returnSize = index;
}
}
}
return result_nums;
}
枚举的复杂度:
- 时间复杂度:
- 空间复杂度:
算法设计:逆向思维+查表
逆向思维,反着想想。
我们可以把目标值 target
也利用上,这次不用加法,用减法。
a + b = target
逆向思维,反过来想,target
能不能利用上?
target - a = b
思路是:一个循环遍历数组里面所有的数,这些数让 target
相减,再用相减的结果在数组里查找即可。
测试数据是排好序的,那可以直接用二分查找呀:
- 时间复杂度是:
- 空间复杂度是:
// 调用C标准库的二分查找,需要一个回调函数做控制参数
int numeric(const int *p1, const int *p2){ return (*p1 - *p2 ); }
int search_key = 0;
// 相减的结果作为二分查找的值
for (int i = 0; i < numsSize; i++)
{
search_key = target - nums[i];
// 二分查找 O(log(n))
int *p =
(int *)bsearch(&search_key, nums, sizeof(nums) / sizeof(nums[0]), sizeof(nums[0]),
(int (*)(const void *, const void *))numeric);
if (p == NULL)
{
continue;
// 如果没找到,就跳过这个元素,查找下一个
}else{
result_nums[index] = i;
result_nums[index + 1] = p - nums;
index += 2;
*returnSize = index;
}
}
部分代码照搬。
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
if( nums == NULL || numsSize < 2 )
return NULL;
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 开辟 numsSize 个空间
int index = 0;
// 记录符合target值的元素个数
return result_nums;
}
结合起来:
#include<stdlib.h> /* 二分查找接口 */
// 调用C标准库的二分查找,需要一个回调函数做控制参数
int numeric(const int *p1, const int *p2)
{
return (*p1 - *p2);
}
int *twoSum(int *nums, int numsSize, int target, int *returnSize)
{
if (nums == NULL || numsSize < 2)
return NULL;
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 开辟 numsSize 个空间
int index = 0;
// 记录符合target值的元素个数
for (int i = 0; i < numsSize; i++)
{
int search_key = target - nums[i];
// 相减的结果作为二分查找的值
// 调用二分查找 O(log(n))
int *p =
(int *)bsearch(&search_key, nums, sizeof(nums) / sizeof(nums[0]), sizeof(nums[0]),
(int (*)(const void *, const void *))numeric);
if (p == NULL)
{
continue;
// 如果没找到,就跳过这个元素,查找下一个
}else{
result_nums[index] = i;
result_nums[index + 1] = p - nums;
index += 2;
*returnSize = index;
}
}
return result_nums;
}
发现,提交错误。
程序输出:
[0,1,1,0]
而标准输出:
[0,1]
哎呦,还得去一下重。
如果再加一个二分查找去重的时间复杂度是 ,渐进复杂度还是比枚举要好,可以采用。
#include<stdlib.h> /* 二分查找接口 */
// 调用C标准库的二分查找,需要一个回调函数做控制参数
int numeric(const int *p1, const int *p2)
{
return (*p1 - *p2);
}
int *twoSum(int *nums, int numsSize, int target, int *returnSize)
{
if (nums == NULL || numsSize < 2)
return NULL;
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 开辟 numsSize 个空间
int index = 0;
// 记录符合target值的元素个数
for (int i = 0; i < numsSize; i++)
{
int search_key = target - nums[i];
// 相减的结果作为二分查找的值
// 调用二分查找 O(log(n))
int *p =
(int *)bsearch(&search_key, nums, sizeof(nums) / sizeof(nums[0]), sizeof(nums[0]),
(int (*)(const void *, const void *))numeric);
if (p == NULL)
{
continue;
// 如果没找到,就跳过这个元素,查找下一个
}else{
// 去重
int *s = (int *)bsearch(&i, result_nums, sizeof(result_nums) / sizeof(result_nums[0]), sizeof(result_nums[0]),
(int (*)(const void *, const void *))numeric);
if( s == NULL ) {
result_nums[index] = i;
result_nums[index + 1] = p - nums;
index += 2;
*returnSize = index;
}
}
}
return result_nums;
}
回顾一下:
- 枚举:双重
for
循环的排列组合 - 逆向:一重
for
循环+
俩次二分查找
这个时候,我们发现,二分查找也许能优化,因为二分查找的时间复杂度不是所有查找算法里面最好的,而且题目只需要索引(数组下标),保持数组中的每个元素与其索引相互对应的最好方法是什么?
哈希算法。
哈希查找的复杂度是 ,以上俩点都符合,是吗~
算法设计:查找表
C++
、Python
都可以直接使用哈希(调用 map
容器);Hash算法本身也是一种思想,所以我们自己实现也不难的。
Hash
算法一般是用一个数组来记录一个key对应的有无元素,或者统计对应的个数。
用key
作为数组的下标,而下标对应的值用来表示该key
的存在与否以及统计对应的个数。
数组: array[下标] = 值
哈希数组,
- 检查有无元素:hash_array[值] = 真;
- 统计对应个数:hash_array[值]++;
- 让数组中的每个元素与其索引相互对应: hash_array[值] = 下标;
也就是将 nums 中的元素值当下标,nums的下标当值存储在 hash 数组中 :has_array[ nums[i] ] = i;
- 创建一个数组,做
Hash
容器; -
Hash
数组初始化为-1
; - 让数组中的每个元素与其索引相互对应:
hash_array[值] = 下标
;
代码如下:
#define MAX_SIZE 2<<10
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
if( nums == NULL || numsSize < 2 )
return NULL;
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 开辟 numsSize 个空间
int index = 0;
// 记录符合target值的元素个数
int hash_array[MAX_SIZE];
memset(hash_array, -1, sizeof(hash_array));
// 哈希数组初始化为 -1, 因为for循环从0开始,所以不能赋值为0
for(int i=0; i<numsSize; i++)
hash_array[nums[i]] = i;
// 让数组中的每个元素与其索引相互对应
return result_nums;
}
- 逆向思维,利用
target - nums[i]
- 将上一项得到的值,进行哈希查找,条件是不能重复使用
for(int i=0; i<numsSize; i++){
int search_key = target - nums[i];
// 相减的结果作为哈希查找的值
if( hash_array[search_key] != 0)
// 如果哈希数组里有 search_key
// 还有一个条件:
if( hash_array[search_key] != 0 && hash_array[search_key] != i)
// 如果哈希数组里有 search_key,且不是同一个元素
// 什么叫同一个元素呢?
// e.g. target = 4, target - 2 = 2
// 这俩个2不能是同一个元素,所以得判断
}
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 开辟 numsSize 个空间
int index = 0;
// 记录符合target值的元素个数
for(int i=0; i<numsSize; i++){
int search_key = target - nums[i];
// 相减的结果作为哈希查找的值
if( hash_array[search_key] != 0 && hash_array[search_key] != i )
{
result_nums[index] = i;
result_nums[index + 1] = hash_array[search_key];
index += 2;
*returnSize = index;
}
}
完整代码:
#define MAX_SIZE 1024
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
if( nums == NULL || numsSize < 2 )
return NULL;
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
// 开辟 numsSize 个空间
int index = 0;
// 记录符合target值的元素个数
int hash_array[MAX_SIZE];
memset(hash_array, -1, sizeof(hash_array));
// 哈希数组初始化为 -1, 因为for循环从0开始,所以不能赋值为0
for(int i=0; i<numsSize; i++)
hash_array[nums[i]] = i;
// 让数组中的每个元素与其索引相互对应
for(int i=0; i<numsSize; i++){
int search_key = target - nums[i];
// 相减的结果作为哈希查找的值
if( hash_array[search_key] != -1 && hash_array[search_key] != i )
{
result_nums[index] = i;
result_nums[index + 1] = hash_array[search_key];
index += 2;
*returnSize = index;
}
}
return result_nums;
}
提交了:
runtime error: index 7 out of bounds for type 'int [*]'
因为测试用例中存在负数,所以在散列时会访问越界。
所以,哈希数组对于区间为负的,需要设定下标补偿值。
我想到了:
- 百度 -> 哈希如何处理负数
看到了这篇:《负数下标+偏移量解决负数》。
好好思量了一下,我觉得这种方式需要考虑的细节比较多,我决定学一下哈希。
使用求余法解决负数问题,因为哈希表是我们创建的,当然知道大小,所以可以用求余法。
求余法会将负数散列到数组尾部,查找时也要如此,就是负数放到后面。
求余法:若已知整个哈希表的最大长度 m
,可以取一个不大于 m
的数 p
,然后对该关键字 key
做取余运算,即:
H(key)= key % p
hash_array[(值 + 哈希数组的长度) % 哈希数组的长度] = 下标
防止负数下标越界,循环散列:
hash_array[(nums[i] + MAX_SIZE) % MAX_SIZE] = i;
完整代码:
#define MAX_SIZE 2048
int *twoSum(int *nums, int numsSize, int target, int *returnSize)
{
int i, hash_array[MAX_SIZE];
int *result_nums = (int *)malloc(sizeof(int) * numsSize);
memset(hash_array, -1, sizeof(hash_array));
// 哈希数组初始化为 -1, 因为for循环从0开始,所以不能赋值为0
int index = 0;
// 记录符合target值的元素个数
for (i = 0; i < numsSize; i++)
{
int search_key = hash_array[(target - nums[i] + MAX_SIZE) % MAX_SIZE];
// 俩数相减后结果的差,经过哈希函数的加工变成了索引(数组下标),而后把 hash_array[index] 的值赋给 search_key
if ( search_key != -1 ) // 如果差存在,那就是找到了
{
result_nums[index] = search_key;
result_nums[index+1] = i;
index += 2;
*returnSize = index;
}
hash_array[(nums[i] + MAX_SIZE) % MAX_SIZE] = i;
// 防止负数下标越界,循环散列
// target - a = b
// target - a 的哈希值 和 b 的哈希值 是一样,如果hash[i]有俩次 != 1(或者说是再重新赋值一次,不等于初始值了), 那就是说明找到了。
}
return result_nums;
}