代码随想录-06|哈希表理论、242.有效的字母异位词、349.两个数组的交集、202.快乐数、1.两数之和

本文探讨了哈希表的基本概念、查找方法(O(1)),以及C++中set、map、unordered_set和unordered_map的底层实现、性质和适用场景,包括它们在查找、增删操作中的效率对比和为何红黑树用于底层实现的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Before Coding

代码随想录

06进入到哈希表的学习中,哈希表是查找方法中的一种,在查找过程中的时间复杂度为 O ( 1 ) O(1) O(1),当然相比于链表中的查找是通过时间换空间。初次接触哈希表可以通过懒猫老师的视频快速了解哈希表

关于基本的查找方法如下图所示:
在这里插入图片描述


哈希表理论部分

可参考我的笔记,这里补充一些关于代码随想录的内容:

  • Q:对于哈希表常用的三种结构
    • 数组

    • set 集合

    • map 映射
      在C++中set和map分别提供以下三种数据结构:

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

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

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

      std::unordered_map底层实现为哈希表,而std::mapstd::multimap底层为红黑树,所以key值是有序的,但key不可以修改,改动key值会导致整颗树的错乱,所以只能删除和增加。

    • Q:为什么优先选择unordered_set作为哈希表,什么情况下用multiset和set
      std::unordered_set的查询和增删效率是最优的( O ( 1 ) O(1) O(1)),如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。

    • Q:相比于set,map的(key,value)数据结构优点:
      map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

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

    • Q:为什么set,multiset,map,multimap底层是红黑树却可以作为哈希表?
      虽然std::setstd::multiset 的底层实现是红黑树,不是哈希表,std::setstd::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。


Let’s Code

242.有效的字母异位词

通过数组构建哈希表,英文字母是26个,通过将字母映射到26个连续的空间中即可完成哈希表,数组内存就是连续的。

class Solution {
public:
    bool isAnagram(string s, string t) {
        int record[26] = {};

        for (int i = 0; i < s.size(); i++) {
            record[s[i] - 'a']++;
        }
        for (int j = 0; j < t.size(); j++) {
            record[t[j] - 'a']--;
        }

        for (auto character : record) {
            if (character != 0) return false;
        }

        return true;
    }
};

349.两个数组的交集

建议:本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和 测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。

  • 关于set的说明:C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入排序删除查找等。

  • 关于set,必须说明的是set 关联式容器set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。

  • 关于set的几个问题:

    • Q:为何map和set的插入删除效率比用其他序列容器高?
      对于关联容器来说,不需要做内存拷贝和内存移动,set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。
      A
      /\
      C D
      /\ /\
      EF GH
    • Q:为何每次insert之后,以前保存的iterator不会失效?
      iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。
    • Q:当数据元素增多时,set的插入和搜索速度变化如何?
      在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。
  • 关于set的使用方法:

    方法作用
    begin()返回set容器的第一个元素
    end()返回set容器的最后一个元素
    clear()删除set容器中的所有的元素
    empty()判断set容器是否为空
    max_size()返回set容器可能包含的元素最大个数
    size()返回当前set容器中的元素个数
    rbegin返回的值和end()相同
    rend()返回的值和rbegin()相同

题目描述

给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序。

题解思路

显然使用哈希查找,最好构建哈希表的时候能够去除重复的元素。
这里使用unordered_set,因为unordered_set在构建时不用额外耗时排序。

我的代码:

相比于代码随想录本方法在最后少用了一次循环,通过set的erase方法来实现在查找的循环中边找边加入输出result。
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        vector<int> inter;
        set<int> set1(nums1.begin(), nums1.end());
        // for (auto i : nums1) {
        //     set1.insert(i);
        // }
        for (auto mem : nums2) {
            if (set1.find(mem) != set1.end()) {
                inter.push_back(mem);
                set1.erase(mem);
            }
        }

        return inter;
    }
};

代码随想录代码

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set; // 存放结果,之所以用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());
    }
};

202.快乐数

题目描述:

编写一个算法来判断一个数 n 是不是快乐数。

题解思路

主要是观察到是否为快乐数的条件:即按照题目的方式进行运算后是否会陷入循环。

我的代码

class Solution {
public:
    bool isHappy(int n) {
        // set<int> nums;
        unordered_set<int> nums;

        while (n != 1) {
            nums.insert(n);
            n = calculateNum(n);
            if (nums.find(n) != nums.end()) return false;
        }

        return true;
    }
	
	// 按照快乐数的运算法则计算
    int calculateNum (int n) {
        int calNum = 0;
        while (n != 0) {
            calNum += (n % 10) * (n % 10);
            n /= 10;
        }
        return calNum;
    }   
};

1.两数之和

题目描述

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

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

你可以按任意顺序返回答案。

题解思路

关于这道题,很容易想到双指针法,但是题目要求返回满足的元素下标,这也注定我们不能通过提前排序后使用双指针,因为这样会改变下标。

因此使用map构造哈希表,通过value来存储数组下标。

我的代码

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> maps;

        for (int i = 0; i < nums.size(); ++i) {

            if (maps.find(target - nums[i]) != maps.end()) {
                return {i, maps.find(target - nums[i])->second};
            }
            maps.insert(pair<int, int>(nums[i], i));
        }

        return {};
    }
};

Q

Q:对于哈希表常用的三种结构
Q:set集合的底层实现及性质
Q:map映射的底层实现及性质
Q:为什么优先选择unordered_set作为哈希表,什么情况下用multiset和set
Q:相比于set,map的(key,value)数据结构优点
Q:为什么set,multiset,map,multimap底层是红黑树却可以作为哈希表?
Q:为何map和set的插入删除效率比用其他序列容器高?
Q:为何每次insert之后,以前保存的iterator不会失效?
Q:当数据元素增多时,set的插入和搜索速度变化如何?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值