3、哈希表相关

上一节:2、链表相关

下一节:4、字符串相关

哈希表

参考 代码随想录 的 哈希总结 讲解

1、理论基础

参考 代码随想录 的 哈希理论 讲解

哈希表定义

哈希表是根据关键码的值而直接进行访问的数据结构。

常见的三种哈希结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)

这里数组就没啥可说的了,我们来看一下set。

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::set红黑树有序O(logn)O(logn)
std::multiset红黑树有序O(logn)O(logn)
std::unordered_set哈希表无序O(1)O(1)

std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::map红黑树key有序key不可重复key不可修改O(logn)O(logn)
std::multimap红黑树key有序key可重复key不可修改O(logn)O(logn)
std::unordered_map哈希表key无序key不可重复key不可修改O(1)O(1)

std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。

那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。

虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。

这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?

实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。

哈希表6

2、数组作为哈希表

leetcode 242. 有效的字母异位词

leetcode 242. 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:

  • 输入: s = “anagram”, t = “nagaram”
  • 输出: true

示例 2:

  • 输入: s = “rat”, t = “car”
  • 输出: false。

直接使用数组当作哈希表,数组下标为哈希表的键值key,数组元素为个字符出现的次数value

代码如下:

bool isAnagram(char* s, char* t) {
    int hash[26] = {0};
    for (int i = 0; i < strlen(s); i++) {
        hash[s[i] - 'a']++;
    }
    for (int i = 0; i < strlen(t); i++) {
        hash[t[i] - 'a']--;
    }
    for (int i = 0; i < 26; i++) {
        if (hash[i] != 0) {
            return false;
        }
    }
    return true;
}

leetcode 383. 赎金信

leetcode 383. 赎金信
给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。

(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)

注意:你可以假设两个字符串均只含有小写字母。

  • canConstruct(“a”, “b”) -> false
  • canConstruct(“aa”, “ab”) -> false
  • canConstruct(“aa”, “aab”) -> true

此题因为只有小写字母,所以直接使用数组当做哈希表即可,
C语言代码如下:

bool canConstruct(char * ransomNote, char * magazine) {
    /* 定义哈希表 */
    int hashtable[26] = {0};
    /* 计算magazine的哈希值 */
    for (int i = 0; i < strlen(magazine); i++) {
        hashtable[magazine[i] - 'a']++;
    }
    /* 判断ransomNote在magazine哈希表中的值是否符合 */
    for (int i = 0; i < strlen(ransomNote); i++) {
        /* magazine中的哈希值小于ransomNote中的哈希值,返回false  */
        if (--hashtable[ransomNote[i] - 'a'] < 0) {
            return false;
        }
    }
    return true;
}

3、set作为哈希表

leetcode 349. 两个数组的交集

leetcode 349. 两个数组的交集
题意:给定两个数组,编写一个函数来计算它们的交集。
在这里插入图片描述
说明: 输出结果中的每个元素一定是唯一的。 我们可以不考虑输出结果的顺序。

242题由于数组不是很大使用数组当作哈希表可以,而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

此时就要使用另一种结构体了,set ,关于set,C++ 给提供了如下三种可用的数据结构:

  • std::set
  • std::multiset
  • std::unordered_set

std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。

C++代码如下:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set; // 存放结果
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        for (int num : nums2) {
            // 发现nums2的元素 在nums_set里又出现过
            if (nums_set.find(num) != nums_set.end()) {
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());
    }
};

C语言代码如下:由于leetcode的输入数据还是比较小,所以此题可用数组当作哈希表

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
/* 题目里没说元素的大小是多少,为什么判断还能通过? */
 #define MAX_SIZE 1024
int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){
    int* hash  = (int*)calloc(MAX_SIZE, sizeof(int));
    int* res   = (int*)calloc(MAX_SIZE, sizeof(int));
    for (int i = 0; i < nums1Size; i++) {
        if (hash[nums1[i]] == 0) {
            hash[nums1[i]]++;
        } 
    }
    *returnSize = 0;
    for (int i = 0; i < nums2Size; i++) {
        if (hash[nums2[i]]) {
            hash[nums2[i]]--;
            res[(*returnSize)++] = nums2[i];
        }
    }
    return res;
}

leetcode 202. 快乐数

leetcode 202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。

示例:

  • 输入:19
  • 输出:true
  • *解释:
  • 1^2 + 9^2 = 82
  • 8^2 + 2^2 = 68
  • 6^2 + 8^2 = 100
  • 1^2 + 0^2 + 0^2 = 1

还是如果数据过大使用数组不合适
C++代码:

class Solution {
public:
    // 取数值各个位上的单数之和
    int getSum(int n) {
        int sum = 0;
        while (n) {
            sum += (n % 10) * (n % 10);
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        unordered_set<int> set;
        while(1) {
            int sum = getSum(n);
            if (sum == 1) {
                return true;
            }
            // 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
            if (set.find(sum) != set.end()) {
                return false;
            } else {
                set.insert(sum);
            }
            n = sum;
        }
    }
};

C语言代码:此题leetcode的输入数据还不是很大

#define SIZE 1024
int getSum(int n) {
    int sum = 0;
    while (n) {
        sum += (n % 10) * (n % 10);
        n /= 10;
    }
    return sum;
}
bool isHappy(int n){
    int* hash = (int*)calloc(SIZE, sizeof(int));
    while (1) {
        int sum = getSum(n);
        if (sum == 1) {
            return true;
        }
        if (hash[sum] != 0) {
            return false;
        } else {
            hash[sum]++;
        }
        n = sum;
    }
}

leetcode 15.三数之和(哈希复杂,双指针简单)

leetcode 15.三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意: 答案中不可以包含重复的三元组。
示例:

  • 给定数组 nums = [-1, 0, 1, 2, -1, -4],
  • 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]

此题可以使用set当做哈希表来求解,但是题目要求不能出现重复的三元组,所以哈希表求解不合适参考代码随想录的讲解
C++使用set:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[j], c = -(a + b)
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
            if (nums[i] > 0) {
                continue;
            }
            if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
                continue;
            }
            unordered_set<int> set;
            for (int j = i + 1; j < nums.size(); j++) {
                if (j > i + 2
                        && nums[j] == nums[j-1]
                        && nums[j-1] == nums[j-2]) { // 三元组元素b去重
                    continue;
                }
                int c = 0 - (nums[i] + nums[j]);
                if (set.find(c) != set.end()) {
                    result.push_back({nums[i], nums[j], c});
                    set.erase(c);// 三元组元素c去重
                } else {
                    set.insert(nums[j]);
                }
            }
        }
        return result;
    }
};

此题使用双指针:
C语言代码:

/**
 * Return an array of arrays of size *returnSize.
 * The sizes of the arrays are returned as *returnColumnSizes array.
 * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
 */
 /* qsort的比较函数 */
int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
    /* 先记录返回的行数为0 */
    *returnSize = 0;
    /* 输入为空、或元素个数小于3则返回NULL */
    if (nums == NULL || numsSize < 3) {
        return NULL;
    }
    /* 将nums排序为升序排列 */
    qsort(nums, numsSize, sizeof(int), cmp);
    /* 分配返回数组、返回数组的列数 */
    int** ret = (int**)malloc(numsSize * numsSize * sizeof(int*));
    *returnColumnSizes = (int*)malloc(numsSize * numsSize *sizeof(int));
    /* 排序后的数组从头到尾进行遍历 */
    for (int i = 0; i < numsSize; i++) {
        /* 当前数组大于0,表示后续没有三数之和为0,结束遍历 */
        if (nums[i] > 0) {
            break;
        }
        /* 当前元素与上一次相等,跳过此次计算,去重 */
        if (i > 0 && nums[i] == nums[i - 1]) {
            continue;
        }
        /* 定义左右指针 */
        int left = i + 1, right = numsSize - 1;
        /* 开始查找三数之和为0 */
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum == 0) {
                ret[*returnSize] = (int*)malloc(sizeof(int) * 3);
                ret[*returnSize][0] = nums[i];
                ret[*returnSize][1] = nums[left];
                ret[*returnSize][2] = nums[right];
                /* 返回数组当前行的列数为3 */
                (*returnColumnSizes)[*returnSize] = 3;
                /* 返回数组的行数自加1 */
                (*returnSize)++;
                /* 对左右指针进行去重 */
                while (left < right && nums[left]  == nums[++left]);
                while (left < right && nums[right] == nums[--right]);
            } else if (sum < 0) {
                left++;
            } else {
                right--;
            }
        }
    }
    return ret;
}

leetcode 18.四树之和(哈希复杂,双指针简单)

leetcode 18.四树之和(哈希复杂,双指针简单)
题意:

  • 给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意:答案中不可以包含重复的四元组。

示例:

  • 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]

C语言代码:

  • 此题也是可以使用哈希表进行求解,但是比较复杂,涉及到重复元素去重问题,所以改为双指针求解
/**
 * Return an array of arrays of size *returnSize.
 * The sizes of the arrays are returned as *returnColumnSizes array.
 * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
 */
int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int** fourSum(int* nums, int numsSize, int target, int* returnSize, int** returnColumnSizes){
    *returnSize = 0;
    if (nums == NULL || numsSize < 4) {
        return  NULL;
    }

    qsort(nums, numsSize, sizeof(int), cmp);
    int** ret = (int**)malloc(numsSize * numsSize * sizeof(int*));
    *returnColumnSizes = (int*)malloc(numsSize * numsSize * sizeof(int));

    for (int i = 0; i < numsSize; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) {
            continue;
        }
        for (int j = i + 1; j < numsSize; j++) {
            int sum1 = nums[i] + nums[j];
            if (j > i + 1 && nums[j] == nums[j - 1]) {
                continue;
            }
            int L = j + 1, R = numsSize - 1;           
            while (L < R) {
                int sum2 = nums[L] + nums[R];
                if (sum1 + sum2 == target) {
                    ret[*returnSize] = (int*)malloc(sizeof(int) * 4);
                    ret[*returnSize][0] = nums[i];
                    ret[*returnSize][1] = nums[j];
                    ret[*returnSize][2] = nums[L];
                    ret[*returnSize][3] = nums[R];
                    (*returnColumnSizes)[*returnSize] = 4;
                    (*returnSize)++;
                    while (L < R && nums[L] == nums[++L]);
                    while (L < R && nums[R] == nums[--R]);
                } else if (sum1 + sum2 < target) {
                    L++;
                } else {
                    R--;
                }
            }
        }
    }
    return ret;
}

4、map作为哈希表

leetcode 1. 两数之和

leetcode 1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。

示例 1:

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

C++代码:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        /* 此题不需要排序,使用无需map更高效 */
        std::unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); i++) {
            /* 在map中查找target - nums[i] */
            auto iter = map.find(target - nums[i]);
            /* 找到目标就返回 */
            if (iter != map.end()) {
                return {iter->second, i};
            }
            /* 找不到就将当前元素插入map */
            map.insert(pair<int, int>(nums[i], i));
        }
        return {};
    }
};

leetcode 454. 四数相加 II(C语言的hashtable)

leetcode 454. 四数相加 II
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。

例如:

  • 输入: A = [ 1, 2] B = [-2,-1] C = [-1, 2] D = [ 0, 2] 输出: 2 解释: 两个元组如下:
  • (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
  • (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

将A、B之和当做键值key,再对C、D进行双重循环遍历进行查找即可:

C语言代码:

struct hashTable {
    int key;    /* 键值 */
    int value;  /* 键值出现的次数 */
    UT_hash_handle hh;  /* uthash需要用户定义自己的数据结构,一个包含UT_hash_handle hh的结构体 */
};

int fourSumCount(int* A, int ASize, int* B, int BSize, int* C, int CSize, int* D, int DSize){
    struct hashTable* hashtable = NULL;
    for (int i = 0; i < ASize; i++) {
        for (int j = 0; j < BSize; j++) {
            int nkey = A[i] + B[j];
            struct hashTable* tmp;
            HASH_FIND_INT(hashtable, &nkey, tmp);
            if (!tmp) { /* 此键值未出现过 */
                struct hashTable* new_hashNode = malloc(sizeof(struct hashTable));
                new_hashNode->key = nkey;
                new_hashNode->value = 1;
                HASH_ADD_INT(hashtable, key, new_hashNode); /* 这里的key就是结构体中的key、不是ikey */
            } else {    /* 此键值出现过 */
                tmp->value++;
            }
        }
    }
    int ret = 0;    /* 返回满足条件的次数 */
    for (int i = 0; i < CSize; i++) {
        for (int j = 0; j < DSize; j++) {
            int mkey = -(C[i] + D[j]);
            struct hashTable* tmp;
            HASH_FIND_INT(hashtable, &mkey, tmp);
            if (tmp) {
                ret += tmp->value;
            }
        }
    }
    return ret;
}  

C++代码:

class Solution {
public:
    int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
        unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
        // 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
        for (int a : A) {
            for (int b : B) {
                umap[a + b]++;
            }
        }
        int count = 0; // 统计a+b+c+d = 0 出现的次数
        // 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
        for (int c : C) {
            for (int d : D) {
                if (umap.find(0 - (c + d)) != umap.end()) {
                    count += umap[0 - (c + d)];
                }
            }
        }
        return count;
    }
};

上一节:2、链表相关

下一节:4、字符串相关

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

「已注销」

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

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

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

打赏作者

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

抵扣说明:

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

余额充值