算法之哈希表

  1. 哈希表是什么?

本质上来说哈希表只是一个存储数据的容器

  1. 为什么用哈希表?

哈希表查找元素的速度很快, O(1)级别; 但是代价是O(n)的空间复杂度, 属于空间换时间

  1. 什么时候用哈希表?

频繁的查找某个数的时候, 但是并不是所有的查找都用哈希表, 对顺序排列的元素用二分搜索O(logN)

  1. 怎么用哈希表?

  2. 一般常用的高级语言的库都会提供哈希表, 直接使用即可

  3. 用数组模拟一个简易的哈希表, 这样会比容器更快

  1. 比如字符串里的字符, 如果全是大写/小写英文字母, int hash[26]; hash[c-'A']即为c字符的个数, 键值对的形式是: <index, hash[index]> ,value可以是字符出现的次数, 索引等等

  2. 数据范围很小的时候,整数的范围在 [1~10^x], x在[3,7] , 但是出现负数就不推荐使用数组, 因为需要加一个偏移量, 比较麻烦.

我们同样可以用哈希函数去模拟哈希表, 但是可能没必要


题目1: 两数之和

1. 暴力解法

暴力解法有两种思路:

第一种是从 i=0 开始, j = [i+1 , nums.size()-1] 依次遍历剩下的元素查找是否有和为target的元素.

第二种也是从i=0开始, 但是j从i开始向前寻找, 也是遍历了所有的元素, 时间复杂度没有任何区别.

第二种:

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        for(int i = 0; i < nums.size(); i++)
        {
            for(int j = i; j > 0;j--)
            {
                if(nums[i] + nums[j-1] == target)
                    return {i,j-1};
            }
        }
        return {0,0};
    }
};

2. 哈希表

哈希的思路是从第二种暴力解法得来的, 哈希表里存储<数值, index>.

i 也是从 0 开始向后遍历, 查找另一个元素实际可以转化成查找hash表里是否有 target-nums[i] 这个key, 如果有返回{i, hash[target-nums[i]] } 即可. 没有就在hash里添加这个键值对, 实质是把寻找的过程从 O(n) 降为了 O(1)

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> hash;
        int i = 0;
        while(i < nums.size())
        {
            int num = target-nums[i];
            if(hash.find(num) != hash.end())
                return {i, hash[num]};
            hash[nums[i]] = i;
            i++;            
        }
        return {0,0};
    }
};

从第一种暴力解法延伸而来的哈希也可以, 但是由于在第一遍寻找时就把所有元素加入到了哈希表, 因此如果一个值 2*nums[i] == target, 那它会查找到自身返回{i,i}因此需要特殊处理这种情况


题目2: 判断是否互为字符重排

思路: 如果s1重排后能得到s2, 那么s1中每个字符出现的个数 一定等于s2中对应字符出现的个数

写法1: 两个哈希表统计s1和s2字符个数, 然后比较即可:

cpp 复制代码
class Solution {
public:
    bool CheckPermutation(string s1, string s2) {
        if(s1.length() != s2.length())
            return false;

        unordered_map<char,int> hash1;
        unordered_map<char,int> hash2;
        for(int i = 0; i < s1.length();i++)
        {
            hash1[s1[i]]++;
            hash2[s2[i]]++;
        }
        
        for(const auto& e : hash1)
        {
            if(hash2[e.first] != e.second)
                return false;
        }
        return true;
    }
};

写法2: 只使用一个哈希表统计 s1 的字符出现次数, 再遍历 s2 依次对hash表元素次数减一, 如果可以重排, 最终哈希表所有元素的值都应该是0, 如果出现负值, 则无法重排.

cpp 复制代码
class Solution {
public:
    bool CheckPermutation(string s1, string s2) {
        if(s1.length() != s2.length())
            return false;
        int hash[26] = {0};
        for(int i = 0; i < s1.length();i++)
            hash[s1[i]-'a']++;
        
        for(int i = 0; i < s2.length();i++)
        {
            if(--hash[s2[i]-'a'] < 0)
                return false; 
        }
        return true;
    }
};

注意: 两种写法都需要对长度不相等的字符串特殊处理, 长度不相等一定无法重排


题目3: 存在重复元素 I

和两数之和思路类似, 遍历一次向前查找hash里是否有数存在, 不存在就存入哈希表, 继续遍历:

cpp 复制代码
class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        unordered_set<int> hash;
        for(int i = 0; i < nums.size(); ++i)
        {
            if(hash.count(nums[i]))
                return true;
            hash.insert(nums[i]);
        }
        return false;
    }
};

题目4: 存在重复元素 I I

这题和存在重复元素I 类似, 也是遍历一次向前查找哈希表即可, 但是每次寻找还需要比较下标之差gap和k的关系, 因此需要用unordered_map存储下标.

注意: 有一个细节需要注意, 因为unordered_map相同的key只能存在一个, 所以下一次遇到相同元素如果不满足gap <= k, 要覆盖吗?

要, 因为我们是从前向后遍历, gap = j - i 的值会随 j 的增大而增大, 所以如果本次gap > k, 随着 j 增大gap会越来越大, 不可能符合要求. 所以要覆盖掉原来的 i , 这样使之后gap的值变得更小, 更容易满足要求.

cpp 复制代码
class Solution {
public:
    bool containsNearbyDuplicate(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        for(int i = 0; i < nums.size(); i++)
        {
            if(hash.count(nums[i]))
            {
                if(i - hash[nums[i]] <=k)
                    return true;
            }
            hash[nums[i]] = i;
        }
        return false;
    }
};

题目5: 字⺟异位词分组

此题是题目2的升级版, 需要把字母异位词全组合到一起, 这题充分体现出hash容器和泛型编程的好处.

对于所有的字母异位词, 它们按照 某种规则排序后的词字母顺序一定是一样的, 因此利用一个unordered_map<string, vector<string>>, 将一类字母异位词全部映射到一个vector里即可:

cpp 复制代码
class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string, vector<string>> hash;
        vector<vector<string>> ret;

        for(const auto& s : strs)
        {
            string tmp = s;
            sort(tmp.begin(),tmp.end());
            hash[tmp].push_back(s);
        }
        
        for(auto & pair: hash)
            ret.push_back(pair.second);
        return ret;
    }
};

暂时结束

相关推荐
火星机器人life1 小时前
基于ceres优化的3d激光雷达开源算法
算法·3d
虽千万人 吾往矣1 小时前
golang LeetCode 热题 100(动态规划)-更新中
算法·leetcode·动态规划
arnold662 小时前
华为OD E卷(100分)34-转盘寿司
算法·华为od
ZZTC2 小时前
Floyd算法及其扩展应用
算法
lshzdq2 小时前
【机器人】机械臂轨迹和转矩控制对比
人工智能·算法·机器人
2401_858286113 小时前
115.【C语言】数据结构之排序(希尔排序)
c语言·开发语言·数据结构·算法·排序算法
猫猫的小茶馆3 小时前
【数据结构】数据结构整体大纲
linux·数据结构·算法·ubuntu·嵌入式软件
u0107735143 小时前
【字符串】-Lc5-最长回文子串(中心扩展法)
java·算法
帅逼码农4 小时前
K-均值聚类算法
算法·均值算法·聚类
姚先生974 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode