代码随想录算法训练营Day5| LeetCode 242 有效的字母异位词、349 两个数组的交集、202 快乐数、1 两数之和

哈希表基本概念

哈希表(hash table)是一种数据结构,用于储存键值对数据。它可以理解为一个固定大小( N N N)的桶数组,每个桶都有一个编号( [ 0 , N − 1 ] [0,N-1] [0,N1])。当你想存一个键值对时,哈希函数会把键转换成一个对应的索引,告知你这个值应该存入哪个桶。即将条目 ( k , v ) (k,v) (k,v)储存在桶 A [ h ( k ) ] A[h(k)] A[h(k)]中。查找时,只需用相同的哈希函数计算出键对应的桶,就能直接在该桶中找到对应的值,实现快速查找。

哈希函数

由于数组的索引必须是整数,而键可以是任何类型(如字符串、对象等),因此需要一个哈希函数来“转换”键。哈希函数接收一个键,并输出一个整数(通常经过取余运算保证在数组索引范围内),这个整数就是数据在数组中存储的位置。

哈希冲突

不同的键经过哈希函数计算后可能得到相同的数组下标,这会导致哈希冲突。解决冲突的常见方式有:

  • 拉链法: 在每个数组位置存储一个链表,将所有冲突的键值对都放到这个链表中。
  • 线性探测法: 当发生冲突时,寻找数组中下一个空闲的位置存储数据。由于需要依靠空位来解决冲突,因此需要保证tableSize大于dataSize。

常见的三种哈希结构

一般涉及到哈希表会使用到数组,set和map三种数据结构。这里参考代码随想录的哈希表部分,mark一下c++除了数组以外的标准库使用。

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

力扣 242 有效的字母异位词

不是很熟练,得二刷。

  • 哈希表Map解法:时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)

    在 C++ 的 STL 中,使用 unordered_mapoperator[] 访问一个不存在的键时,会自动为该键插入一个默认构造的值。对于 int 类型来说,默认值是 0。因此,直接写 map[i]++ 是安全的,因为当 i 不存在时,map[i] 会自动被初始化为 0,然后再执行自增操作。

    class Solution {
    public:
        bool isAnagram(string s, string t) {
            if(s.length()!=t.length()) return false;
    
            unordered_map<char, int> map;
            for(char i:s){
                map[i]++; //
            }
            for(char i:t){
                map[i]--;
            }
            for (auto pair : map) {
                if(pair.second != 0) return false;
            }
            return true;
        }
    };
    
  • 排序(力扣官方题解)时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度 O ( log ⁡ n ) O(\log n) O(logn)

    class Solution {
    public:
        bool isAnagram(string s, string t) {
            if (s.length() != t.length()) {
                return false;
            }
            sort(s.begin(), s.end());
            sort(t.begin(), t.end());
            return s == t;
        }
    };
    

力扣 349 两个数组的交集

思路上没有问题,但还需要加强对标准库的熟练程度。我的解法时间复杂度同样是 O ( m + n ) O(m+n) O(m+n)

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> uset, newSet;
        for(auto& i:nums1){
            uset.insert(i);
        }
        for(auto& i:nums2){
            auto it = uset.find(i);
            if (it != uset.end()) {
                newSet.insert(i);
            }
        }
        vector<int> ans;
        ans.assign(newSet.begin(), newSet.end());
        return ans;
    }
};

力扣 202 快乐数 AC

居然过了!泪目了,这是我第一个一次过的哈希表题目。

class Solution {
public:
    bool isHappy(int n) {
        unordered_set<int> usedNum;
        usedNum.insert(n);
        while(true){
            
            n = getNewNum(n);
            if(n == 1) return true;

            auto it = usedNum.find(n);
            if(it != usedNum.end()){
                return false;
            }else{
                usedNum.insert(n);
            }
        }
    }

    int getNewNum(int n){
        int sum = 0;
        while(n != 0){
            int cur = n % 10;
            sum += cur * cur;
            n = n / 10;
        }
        return sum;
    }
};

然,我对时间复杂度的计算还不熟练。这道题力扣上显示的是 O ( M ) O(M) O(M),但代码随想录上相同思路的解是 O ( log ⁡ n ) O(\log n) O(logn),遂感到困惑,去问了下GPT老师。GPT老师大致意思如下:

单次迭代的时间复杂度

  • getNewNum 对数字 n n n 的每一位进行处理得到新的数字。假设 n n n d d d 位,那么这一步的时间复杂度是 O ( d ) O(d) O(d)
  • 而对于一个正整数 n n n,如果用十进制表示,其位数 d d d 满足: d = ⌊ l o g 10 ( n ) ⌋ + 1 d=⌊log10(n)⌋+1 d=log10(n)⌋+1 ⌊   ⌋ \lfloor \ \rfloor  表示向下取整)。所以从数学上讲, d d d n n n 之间的关系是 d ≈ log ⁡ 10 ( n ) d \approx \log_{10}(n) dlog10(n)。因此,getNewNum 的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn)
    • e.g. n = 123 n = 123 n=123
      • 计算: log ⁡ 10 ( 123 ) ≈ 2.089 \log_{10}(123) \approx 2.089 log10(123)2.089
      • 向下取整: ⌊ 2.089 ⌋ = 2 \lfloor 2.089 \rfloor = 2 2.089=2
      • 数字的位数: ⌊ log ⁡ 10 ( 123 ) ⌋ + 1 = 2 + 1 = 3 \lfloor \log_{10}(123) \rfloor + 1 = 2 + 1 = 3 log10(123)⌋+1=2+1=3
    • e.g. n = 1000 n = 1000 n=1000
      • 计算: log ⁡ 10 ( 1000 ) = 3 \log_{10}(1000) = 3 log10(1000)=3
      • 向下取整: ⌊ 3 ⌋ = 3 \lfloor 3 \rfloor = 3 3=3
      • 数字的位数: ⌊ log ⁡ 10 ( 1000 ) ⌋ + 1 = 3 + 1 = 4 \lfloor \log_{10}(1000) \rfloor + 1 = 3 + 1 = 4 log10(1000)⌋+1=3+1=4

while循环的迭代次数

  • 在每一次迭代中, n n n 被替换为各位数字的平方和。对于任意一个多位数,这个平方和的结果最多为 81 × d 81×d 81×d(当每位都是9时),而 81 × d 81×d 81×d 相比原始 n n n(如果 n n n 很大)通常要小很多。所以实际上 n n n 是在快速变小的。
  • 最终进入固定范围:经过一两次迭代后,n 会迅速降到一个固定的范围内。一旦进入这个范围,后续迭代的次数就不会随着 n 的大小增加,而是被一个常数上限所限制(即最多只能出现有限个不同的数字,超过这个范围就会检测到循环)。

综上可以得出:每次迭代中,单次getNewNum 的代价是 O ( log ⁡ n ) O(\log n) O(logn),而由于 n n n 实际上一直在变小,因此理论上,最坏情况就是 O ( log ⁡ n ) O(\log n) O(logn)。而由于 n n n 很快降到一个常数范围内,while 循环中的迭代次数在最坏情况也是一个常数,不会随着初始 n n n 的大小而增长。因此,总的时间复杂度大致为:

O ( 迭代次数 ) × O ( log ⁡ ⁡ n ) = O ( 1 ) × O ( log ⁡ ⁡ n ) = O ( log ⁡ ⁡ n ) O(迭代次数)×O( \log⁡ n)=O(1)×O(\log⁡ n)=O(\log ⁡n) O(迭代次数)×O(logn)=O(1)×O(logn)=O(logn)

力扣 1 两数之和

一上来还是惯性用暴力解法,结果超时。这道题的哈希表方法比较巧妙,充分的利用了哈希表能够快速查找的优势。大体思路是每遍历到新的索引,去之前遍历过的值中寻找是否有合适的对象,没有的话将当前值和索引作为key和value存入哈希表,有则结束方法,直接return二者索引。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> uset;
        for(int i = 0; i < nums.size(); i++){
            auto it = uset.find(target - nums[i]);
            if (it !=  uset.end()){
                return {it->second, i};
            }
            uset.insert({nums[i], i});
        }
        return {};
    }
};

你可能感兴趣的:(代码随想录算法训练营跟练,算法,leetcode,哈希算法)