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

发布时间 2023-12-20 19:00:56作者: Constrel

题目和相关内容的链接

哈希表倒不是一种明确的容器,他更像是一种存储和处理数据的结构和思想,通过用空间换时间,通过索引的方式直接访问元素,从而大大降低了遍历容器的时间开销。所以哈希表是一种基于key - value的处理思路,在具体的实现过程中,会考虑到哈希函数哈希碰撞(拉链法、线性探索法等等)。

今天是哈希表相关内容的第一天练习,主要在于理解思想和基本的实现思路。


一个很重要的想法是:当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

关于哈希表的c++实现

在哈希表的实现中,用来存储数据的一般是三个数据结构:(额外的)数组、mapset

c++中set提供了三种类型,其底层实现方法以及优劣性如下所示:

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

同理,对于map,也有类似的情况:

map 底层实现思路 内部是否有序 数值是否可以重复 是否能更改数值 查询效率 增删效率
std::map 红黑树 key有序 key不可重复 key不可更改 O(log n) O(log n)
std::multimmap 红黑树 key有序 key可以重复 key不可更改 O(log n) O(log n)
std::unordered_map 哈希表 key无序 key不可重复 key不可更改 O(1) O(1)

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

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

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

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

242.有效字母异位词

这是一个很老的题目了,"有效的字母异位词"我认为这样理解更为直观一些:将一个单词中的所有字母打乱重组,所构成的都是有效的字母异位词,即Anagram

最基础的是暴力解法,使用两层for循环,一次用来遍历单词的每个字母,然后判断字母出现的次数,再使用一次for循环,在另一个字母中迭代判断

这样做会有很大的开销(虽然这个题评测机给的数据规模很小),而且暴力解法需要额外判断一些重复的情况,大大提高了debug难度。所以直接想想其它的办法:

  • 两个词语满足anagram结构的要素总结起来就两个:单词长度相同;每个单词在单词中的出现频率相同。这个应该算是充要条件了,除非我对题意理解有误

  • 在怎样具体实现上面这个判断方式呢? 我们的方法是使用一个record的数组的结构实现哈希表,只需要经过两个O(1)的遍历,第一次统计原单词中每个字母的出现次数(这样的话数组的size只需要给26,数组下表使用每个字母相对于'a'的大小就行了,甚至不需要知道 具体的ASCII Code;当然,leetcode上的题目是有所简化的,没有考虑大写字母的情况),第二次遍历第二个用于比较的单词,遍历到一个就从record中删一个。如果最后的record数组为空,那么显然这两个单词满足anagram。如果不为空,显然不满足;

代码如下:

class Solution {
public:
    bool isAnagram(string s, string t) {
        int record[26] = {0};
        for(int i = 0; i < s.size(); i++){
            record[s[i] - 'a']++;
        }
        for(int i = 0; i < t.size(); i++){
            record[t[i] - 'a']--;
        }
        for(int i = 0; i < 26; i++){
            if(record[i] != 0){
                return false;
            }
        }
        return true;
    }
};

349.两个数字的交集

给定两个数组,判定他们的交集,从输入样例来看,给定的两个数组中是可以有元素的重复的,但是最后输出的交集中是没有的;

由此看来,这个题目就很满足我们哈希表的适用范围:快速判断元素是否在容器中。同时,由于最后交集的元素是不可重复的,所以在这里选用set最好不过了,而且需要注意的是:对给定的数组进行去重,是完全不影响我们后来的操作的

具体的实现思路是:首先定义一个用于存放答案的set。将其中一个给定的数组(vector)转化为set,随后使用迭代器进行遍历。如果迭代器指向的当前元素在第二个给定的数组中存在(使用set内置find进行判断),就把该元素存入答案set

代码如下:

class Solution {
public:
     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(auto num : nums2){
        if(nums_set.find(num) != nums_set.end()){
            result_set.insert(num);
        }
       }
       return vector<int>(result_set.begin(), result_set.end());
    } 
};

这道题某种程度上也是对stl set的基本操作的练习

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语言风格的对于数位的处理知识)

然后我们使用循环的方式进行判断。"is Happynum"意味着平方和是1,只要平方和是1就可以跳出循环。
"is not Happynum"意味着无限循环,怎么判断呢?只要平方和与之前计算过的某一次平方和相同,那就意味着陷入了无限循环。


所以我们在循环的过程中需要不断存储平方和的值,并且判断本次循环中的平方和是否在其中出现

"判断一个元素是否在现有容器中",很贴合哈希的使用场景,通过set就可以了

题目没有其他的难度,代码如下:

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;
       int sum = getSum(n);
       
         while(1){
             if(sum == 1){
        return true;
       }
           
            if(set.find(sum) != set.end()){
                return false;
            }
            else{
                set.insert(sum);
            }
            sum = getSum(sum);
       }  
    }
};

1.两数之和

这是leetcode的第一题,还是很有意思的

给定一个整数数组 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 <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案

进阶:你可以想出一个时间复杂度小于 O(\(n^2\)) 的算法吗?


暴力方法非常简单,使用两层for循环,但是时间复杂度是O(\(n^2\))
暴力代码如下:

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

分析一下暴力解法的不足:大量的重复判断造成了不必要的开销,由于暴力思路中是顺序遍历的,所以开销很大。所以,我们自然而然地想到:既然问题的实现思路是直观的:对于集合中的每一个元素,能否找到集合中的另一个元素使得两元素之和等于target?所以优化思路是:通过哈希表的结构,提高我们的"随机读写"效率。

具体的做法是:

  • 首先使用迭代器遍历vector,对于其中的每一个元素,查询是否在我们定义的(用来存储已经访问过的元素的)map

为什么要用map呢?因为还要能够访问元素在原有vector中的下标。所以set在这里并不好用,更确切地来讲,因为我们地元素是无序的,所以使用unordered_map效率最高

*然后对于每一次迭代中的元素,在map中查询是否有与之配对的元素,如果有,直接返回答案(题目给出了满足要求的答案只有一对)

代码如下:

public:
    vector<int> twoSum(vector<int>& nums, int target) {
        std::unordered_map <int,int> map;
        for(int i = 0; i < nums.size(); i++) {
            // 遍历当前元素,并在map中寻找是否有匹配的key
            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 {};
    }
};

我觉得这个办法是有点凑巧的感觉了,hmmm……


也许不是很直观,其实只要通过迭代器将vector的内容存入map就好了,但是这里面需要返回下标,所以我尝试的时候遇到了一点困难,也许当我深入了解这些关联容器的用法之后会解决这个思路吧


下面是我尝试中失败的代码,唉,实在是对c++不了解!