每日一题——两数之和

问题描述

给定一个整数数组 nums 和一个整数 target,你需要在数组中找出两个数,它们的和为目标值 target。返回这两个数的数组下标。每个输入只会有一个有效答案,并且你不能使用相同的元素两次。

示例

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9,返回 [0, 1]。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示

  • 2 <= nums.length <= 10^4
  • -10^9 <= nums[i] <= 10^9
  • -10^9 <= target <= 10^9
  • 只会存在一个有效答案

思路分析

这个问题可以通过哈希表来高效地解决。具体思路如下:

  1. 创建一个哈希表,存储已经遍历过的数和它们对应的索引。
  2. 对于每个数,计算 target 减去当前数的差值,即 complement
  3. 如果差值 complement 在哈希表中已经存在,则找到了符合条件的两个数,直接返回它们的下标。
  4. 如果差值不在哈希表中,则将当前数加入哈希表。

通过哈希表的查找特性,我们可以在一次遍历中完成求解,时间复杂度为 O(n),空间复杂度为 O(n)。

代码实现

#include <stdio.h>
#include <stdlib.h>

typedef struct { 
    int* table; // 哈希表数组
    int size;   // 哈希表大小
} HashTable;

// 初始化哈希表
void initHash(HashTable* hashTable, int size) {
    hashTable->size = size;
    hashTable->table = (int*)malloc(size * sizeof(int));  // 分配内存
    for (int i = 0; i < size; i++) {
        hashTable->table[i] = -1;  // 初始化哈希表中的值为-1,表示没有存储任何数据
    }
}

// 释放哈希表内存
void freeHash(HashTable* hashTable) { 
    free(hashTable->table);  // 释放哈希表中的数组内存
    free(hashTable);  // 释放哈希表结构体内存
}

// 查找和为目标值的两个数的下标
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
    int size = 20001;  // 哈希表的大小,范围[-10^9, 10^9]需要加上10000进行偏移,保证索引为正
    HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable)); // 分配哈希表结构体内存
    initHash(hashTable, size);  // 初始化哈希表

    for (int i = 0; i < numsSize; i++) {
        // 计算当前数的补数
        int complement = target - nums[i];
        int index = complement + 10000;  // 补数的哈希表索引,偏移+10000确保索引为正

        // 如果补数已经在哈希表中,说明找到了两个数,它们的和等于目标值
        if (hashTable->table[index] != -1) {
            int* result = (int*)malloc(2 * sizeof(int));  // 分配结果数组
            result[0] = i;  // 当前数的下标
            result[1] = hashTable->table[index];  // 补数的下标

            freeHash(hashTable);  // 释放哈希表内存
            *returnSize = 2;  // 设置返回结果的大小
            return result;  // 返回结果
        }

        // 如果补数不在哈希表中,将当前数添加到哈希表
        int numIndex = nums[i] + 10000;  // 当前数的哈希表索引
        hashTable->table[numIndex] = i;  // 将当前数的下标存入哈希表
    }

    freeHash(hashTable);  // 释放哈希表内存
    *returnSize = 0;  // 没有找到结果,返回0
    return NULL;  // 返回空值
}

代码解析

1. 哈希表结构体定义

我们定义了一个 HashTable 结构体,它包含两个成员:

  • table:一个数组,用于存储哈希表中的数据。我们使用一个整数数组,哈希表的大小为 20001,以适应数据范围 [-10^9, 10^9],通过加上 10000 来保证索引为正。
  • size:哈希表的大小。

2. 初始化哈希表

initHash 函数中,我们为哈希表分配内存,并将哈希表中的所有值初始化为 -1,表示该位置还没有存储数据。

3. 释放哈希表内存

freeHash 函数中,我们释放了哈希表的内存,包括哈希表的数组和结构体本身。

4. 主函数 twoSum

  • 我们首先计算目标值 target 和当前数的差值 complement
  • 通过偏移量 10000 将补数的范围映射到哈希表的索引范围内,确保索引为正。
  • 如果哈希表中已经存在补数,则返回当前数和补数的下标。
  • 如果哈希表中不存在补数,则将当前数及其下标存入哈希表。

5. 返回结果

  • 如果找到了符合条件的两个数,我们将它们的下标存入一个结果数组并返回。
  • 如果没有找到符合条件的数对,返回 NULL

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。我们只需要遍历一次数组,每次操作的时间复杂度为 O(1)(哈希表查找和插入操作)。
  • 空间复杂度:O(n),我们需要一个哈希表来存储最多 n 个元素。
    可惜,这种解法需要空间过大,无法满足所有案例。

第二种解法

// 定义哈希表的结构体
struct hashTable {
    int key;                  // 哈希表的键,用于存储数值
    int val;                  // 哈希表的值,用于存储索引
    UT_hash_handle hh;        // UT_hash库提供的宏,用于处理哈希表操作
};

// 声明一个全局指针,用于指向哈希表的头节点
struct hashTable* hashtable;

// 查找哈希表中是否存在某个键
struct hashTable* find(int ikey) {
    struct hashTable* tmp;                        // 定义一个临时指针用于存储查找结果
    HASH_FIND_INT(hashtable, &ikey, tmp);         // 使用UT_hash库提供的宏查找键为ikey的元素
    return tmp;                                   // 返回查找结果
}

// 向哈希表中插入键值对
void insert(int ikey, int ival) {
    struct hashTable* it = find(ikey);            // 首先查找键是否已存在
    if (it == NULL) {                             // 如果键不存在
        struct hashTable* tmp = malloc(sizeof(struct hashTable)); // 分配内存
        tmp->key = ikey, tmp->val = ival;         // 初始化键值对
        HASH_ADD_INT(hashtable, key, tmp);        // 使用UT_hash库提供的宏将新元素添加到哈希表中
    } else {                                      // 如果键已存在
        it->val = ival;                           // 更新值
    }
}

// 解决“两数之和”问题
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
    hashtable = NULL;                             // 初始化哈希表为空
    for (int i = 0; i < numsSize; i++) {          // 遍历数组
        struct hashTable* it = find(target - nums[i]); // 查找当前元素的补数是否在哈希表中
        if (it != NULL) {                         // 如果找到补数
            int* ret = malloc(sizeof(int) * 2);   // 分配内存用于存储结果
            ret[0] = it->val, ret[1] = i;         // 存储两个数的索引
            *returnSize = 2;                      // 设置返回结果的大小为2
            return ret;                           // 返回结果
        }
        insert(nums[i], i);                       // 将当前元素及其索引插入哈希表
    }
    *returnSize = 0;                              // 如果未找到结果,设置返回结果的大小为0
    return NULL;                                  // 返回NULL
}

作者:力扣官方题解
链接:https://leetcode.cn/problems/two-sum/solutions/434597/liang-shu-zhi-he-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

官方解法虽然看着复杂,实际上最重要的是多了#include “uthash.h”,自己得学会如何调库。

代码功能概述

这段代码使用了UTHash库来实现“两数之和”问题。目标是在给定数组中找到两个数,它们的和等于目标值 target,并返回这两个数的索引。

代码详细注释

1. 哈希表结构体定义
struct hashTable {
    int key;          // 键:存储数组中的数值
    int val;          // 值:存储数组中数值对应的索引
    UT_hash_handle hh; // UTHash宏定义的句柄,用于内部管理哈希表节点
};
  • key:哈希表的键,用于存储数组中的数值。
  • val:哈希表的值,用于存储数组中数值对应的索引。
  • UT_hash_handle hh:这是UTHash库要求的字段,用于管理哈希表的内部结构(例如,存储链表指针、哈希值等)。用户不需要直接操作这个字段。
2. 哈希表指针
struct hashTable* hashtable;
  • 这是一个全局变量,指向哈希表的根节点。UTHash库通过这个指针来管理整个哈希表。
3. 查找函数
struct hashTable* find(int ikey) {
    struct hashTable* tmp;
    HASH_FIND_INT(hashtable, &ikey, tmp); // 使用UTHash宏查找键为ikey的节点
    return tmp; // 返回找到的节点,如果未找到则返回NULL
}
  • HASH_FIND_INT:这是UTHash库提供的宏,用于在哈希表中查找键为 ikey 的节点。
    • 第一个参数是哈希表的根节点指针(hashtable)。
    • 第二个参数是指向键值的指针(&ikey)。注意,这里传入的是键值的地址。
    • 第三个参数是输出参数,用于存储查找结果。
  • 如果找到键为 ikey 的节点,则返回该节点;否则返回 NULL
4. 插入函数
void insert(int ikey, int ival) {
    struct hashTable* it = find(ikey); // 先查找键是否已存在
    if (it == NULL) { // 如果键不存在
        struct hashTable* tmp = malloc(sizeof(struct hashTable)); // 分配新节点
        tmp->key = ikey, tmp->val = ival; // 初始化新节点的键和值
        HASH_ADD_INT(hashtable, key, tmp); // 使用UTHash宏将新节点插入哈希表
    } else { // 如果键已存在
        it->val = ival; // 更新节点的值
    }
}
  • HASH_ADD_INT:这是UTHash库提供的宏,用于将一个新节点插入哈希表。
    • 第一个参数是哈希表的根节点指针(hashtable)。
    • 第二个参数是哈希表结构体中用于哈希的字段名(key)。
    • 第三个参数是要插入的新节点(tmp)。
  • 如果键已存在,则更新该键对应的值。
5. 两数之和函数
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
    hashtable = NULL; // 初始化哈希表为空
    for (int i = 0; i < numsSize; i++) {
        struct hashTable* it = find(target - nums[i]); // 查找补数是否已存在
        if (it != NULL) { // 如果找到补数
            int* ret = malloc(sizeof(int) * 2); // 分配返回数组
            ret[0] = it->val, ret[1] = i; // 返回补数的索引和当前索引
            *returnSize = 2; // 设置返回数组的大小
            return ret; // 返回结果
        }
        insert(nums[i], i); // 将当前数值及其索引插入哈希表
    }
    *returnSize = 0; // 如果未找到结果,返回大小为0
    return NULL; // 返回NULL
}
  • 逻辑流程
    1. 初始化哈希表:将 hashtable 设置为 NULL,表示哈希表为空。
    2. 遍历数组
      • 对于数组中的每个数字 nums[i],计算它的补数(target - nums[i])。
      • 使用 find 函数查找补数是否已经在哈希表中。
      • 如果找到补数:
        • 分配一个大小为2的数组 ret,存储补数的索引和当前索引。
        • 设置返回数组的大小为2。
        • 返回结果。
      • 如果未找到补数:
        • 调用 insert 函数,将当前数字及其索引插入哈希表。
    3. 未找到结果
      • 如果遍历结束后仍未找到结果,设置返回大小为0,并返回 NULL

6. 主函数(示例)

int main() {
    int nums[] = {2, 7, 11, 15}; // 输入数组
    int target = 9;              // 目标值
    int returnSize;              // 返回结果的大小
    int* result = twoSum(nums, 4, target, &returnSize); // 调用两数之和函数

    if (result) { // 如果找到结果
        printf("Result: [%d, %d]\n", result[0], result[1]); // 打印结果
        free(result); // 释放结果内存
    } else { // 如果未找到结果
        printf("No solution found.\n");
    }

    return 0;
}
  • 输出
    Result: [0, 1]
    
  • 解释
    • 数组 [2, 7, 11, 15] 中,nums[0] + nums[1] = 2 + 7 = 9,因此返回 [0, 1]

总结

这段代码通过UTHash库实现了“两数之和”问题,具有以下优点:

  1. 动态哈希表:UTHash库可以动态管理哈希表,适合处理大范围的整数。
  2. 简洁高效:UTHash提供了简洁的宏来操作哈希表,代码易于理解和维护。
  3. 时间复杂度低:哈希表操作的时间复杂度为O(1),整体算法时间复杂度为O(n)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tt555555555555

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值