[1].俩数之和


俩数之和


题目

给定一个整数数组 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);
   
   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;
}

算法设计:暴力

俩数之和,这题目可以用暴力试试。

双重循环:

  • 当外循环在第 i i 个元素时($0 <= i < numsSize $);
  • 内循环从第 i + 1 i+1 个(不能重复利用这个数组中同样的元素)到最后一个元素;

细节在于:循环的【结束条件】。

内循环的结束条件,是最后一个元素,有俩种写法:

  • <= 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;
}

暴力的:

  • 时间复杂度是 Θ ( n 2 ) \Theta (n^{2})
  • 空间复杂度是 Θ ( n ) \Theta (n)

算法设计:逆向思维

逆向思维,反着想想。

我们可以把目标值 target 也利用上,这次不用加法,用减法。

a + b = target

逆向思维,反过来想,target能不能利用上?

target - a = b

思路是:一个循环遍历数组里面所有的数,这些数让 target 相减,再用相减的结果在数组里查找即可。

测试数据是排好序的,那可以直接用二分查找呀:

  • 时间复杂度是: Θ ( n l o g ( n )   ) \Theta (n*log(n)~)
  • 空间复杂度是: Θ ( n ) \Theta (n)
// 调用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]

哎呦,还得去一下重。

如果再加一个二分查找去重的时间复杂度是 Θ ( n l o g ( n ) l o g ( n )   ) \Theta (n*log(n)*log(n)~) ,渐进复杂度还是比暴力要好,可以采用。

#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循环 + 俩次二分查找

这个时候,我们发现,二分查找也许能优化,因为二分查找的时间复杂度不是所有查找算法里面最好的,而且题目只需要索引(数组下标),保持数组中的每个元素与其索引相互对应的最好方法是什么?

哈希算法。

哈希查找的复杂度是 Θ ( 1 ) \Theta (1) ,以上俩点都符合,是吗~
 


算法设计:哈希表

C++、Python都可以直接使用哈希(调用 map 容器);Hash算法本身也是一种思想,所以我们自己实现也不难的。

Hash算法一般是用一个数组来记录一个key对应的有无元素,或者统计对应的个数。

用key作为数组的下标,而下标对应的值用来表示该key的存在与否以及统计对应的个数。

数组:    array[下标] = 值

哈希数组,
- 检查有无元素:hash_array[] =;
- 统计对应个数:hash_array[]++;
- 让数组中的每个元素与其索引相互对应: hash_array[] = 下标;

也就是将 nums 中的元素值当下标,nums的下标当值存储在 hash 数组中 :has_array[ nums[i] ] = i; 
  1. 创建一个数组,做Hash容器;
  2. Hash数组初始化为 -1;
  3. 让数组中的每个元素与其索引相互对应: 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;
}
  1. 逆向思维,利用 target - nums[i]
  2. 将上一项得到的值,进行哈希查找,条件是不能重复使用
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;
}
发布了125 篇原创文章 · 获赞 365 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_41739364/article/details/105344365
今日推荐