Leetcode刷题笔记--哈希表

系列文章目录


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

一、哈希表基础知识

在力扣习题当中,哈希表是非常常见的知识点。 哈希表通过哈希函数将键映射到索引位置,因此在理想情况下,查找和插入操作的时间复杂度为O(1)。这使得哈希表非常适合于需要快速检索的场景。 因此,在某些场景中有重要的作用。本文中主要介绍哈希表的两种常见形式:unordered_map和unordered_set,这也是做题中最常用的两种类型。

1.1 unordered_map

unordered_map是一个将key和value关联起来的容器,它可以高效的根据单个key值查找对应的value。key应该是唯一的,key和value的数据类型可以不相同。
与 map 不同,unordered_map 不保证元素的顺序,而是通过哈希函数将键映射到桶(buckets),因此插入、删除和查找操作的平均时间复杂度都是常数时间 O(1)。适用于检索单个元素,效率很高。
下面是unordered_map的C++基本用法。其中,最重要的是find函数。他用于查找key所在的元素。如果找到则返回指向对应键值对的迭代器,否则返回 end() 迭代器,表示未找到该键。

//STL中map的基本用法
#include <unordered_map>
void basic()
{
	std::unordered_map<std::string, int> myMap;

	// 添加键值对
	myMap["Alice"] = 25;
	myMap["Bob"] = 30;
	myMap["Charlie"] = 35;


	//删除键值对
	myMap.erase("Bob");

	// 检查键是否存在
	if (myMap.find("David") != myMap.end()) {
		std::cout << "David's age is: " << myMap["David"] << std::endl;
	}
	else {
		std::cout << "David is not in the map." << std::endl;
	}
	// 遍历哈方法1
	for (auto it = myMap.begin(); it != myMap.end(); it++) {
    	std::cout << it->first << "'s age is: " << it->second << std::endl;
}
	// 遍历方法2
	for (const auto& pair : myMap) {
		std::cout << pair.first << "'s age is: " << pair.second << std::endl;
	}
}

在构造哈希表的时候,最容易出现问题的地方在于分不清键(key)和值(value),从而导致代码逻辑混乱。对于不同类型的键值,比较好区分,但是当有嵌套哈希表或者键值表示的意义接近时候,就很容易混淆。
首先我们明确一下,对于初始化的定义,第一个类型string是键,第二个类型int是值。

std::unordered_map<std::string, int> myMap;

我们插入元素的操作通常类似于数组形式,如下:

myMap["Alice"] = 25;

此时,在[]里面的Alice是键,而等于号右边的25是值。同时,在find函数中,我们给定的参数是键,函数会在哈希表中寻找该键。

这样说可能有点抽象,咱们来一个使用哈希表的常见案例。

对于一个字符串,我们想要统计每个字母出现的次数。

这就很好理解了,我们可以把26个字母作为哈希表的键,而他们出现的次数作为相对应的值。对每个字符遍历,如果字母已经存在,则增加其计数,否则将其计数设置为 1。最后,通过遍历 unordered_map,可以输出每个字母的出现次数。代码如下:

void countCharacters(const std::string& str) {
    std::unordered_map<char, int> charCount;

    // 遍历字符串,统计每个字母出现的次数
    for (char c : str) {
        if (std::isalpha(c)) { // 只统计字母
            charCount[c]++;
        }
    }

    // 输出结果
    for (const auto& pair : charCount) {
        std::cout << "'" << pair.first << "' 出现了 " << pair.second << " 次" << std::endl;
    }
}

1.2 unordered_set

第二个比较常用的数据结构是std::unordered_set ,它是一个集合容器,用于存储唯一的元素,没有重复的值。它类似于数学上的集合。下面代码介绍他的基本用法。

void unorder_set_basic() {

	unordered_set<int> myHashSet;

	// 插入元素
	myHashSet.insert(10);
	myHashSet.insert(5);
	myHashSet.insert(8);
	myHashSet.insert(12);

	// 查找元素并输出
	int searchValue = 8;
	std::unordered_set<int>::iterator it = myHashSet.find(searchValue);
	if (it != myHashSet.end()) {
		std::cout << "元素 " << searchValue << " 存在于集合中。" << std::endl;
	}
	else {
		std::cout << "元素 " << searchValue << " 不存在于集合中。" << std::endl;
	}

	// 删除元素
	myHashSet.erase(5);

	// 遍历集合并输出
	std::cout << "集合中的元素:";
	for (const int& value : myHashSet) {
		std::cout << value << " ";
	}
	std::cout << std::endl;

	// 检查集合是否为空
	if (myHashSet.empty()) {
		std::cout << "集合为空。" << std::endl;
	}
	else {
		std::cout << "集合不为空,大小为:" << myHashSet.size() << std::endl;
	}

	// 使用 count 检查元素是否存在
	int elementToCheck = 3;
	if (myHashSet.count(elementToCheck) == 1) {
		std::cout << elementToCheck << " 存在于集合中。" << std::endl;
	}
	else {
		std::cout << elementToCheck << " 不存在于集合中。" << std::endl;
	}
}

这个数据结构适用的场所在于,当题目中存在重复的元素,而我们想要不重复的结果时候,就可以用到unordered_set了。下面举一个例子。

给定一个整数数组,判断是否存在重复的元素。

这个题目可以很简单的用unordered_set 解决,而且他展示了unordered_set 的核心功能:输入有重复元素的数组,返回无重复元素的数组。代码如下所示。

bool containsDuplicate(std::vector<int>& nums) {
    std::unordered_set<int> numSet;

    for (int num : nums) {
        if (numSet.count(num) > 0) {
            return true; // 已存在重复元素
        }
        numSet.insert(num);
    }

    return false; // 未找到重复元素
}

但通常题目没有这么简单,一般情况下,unordered_set 会作为解决题目的一个步骤。咱们先看两个简单的题目,体会一下map和set的区别。
两个数组的交集1
求两个数组的交集,但是要求输出结果中的每个元素一定是 唯一的。这时我们就会想到使用unordered_set来解决问题了。首先利用set统计其中一个数组的元素,然后遍历剩余一个数组,使用count函数判断nums2中是否出现nums1中相同的元素,为了保证返回的元素是唯一的,每当检测出一个元素,就使用erase函数消除这个元素。代码如下。

vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
	unordered_set<int>set(nums1.begin(),nums1.end());
	vector<int>ans;

	for (auto num : nums2) {
		if (set.count(num) > 0) {
			ans.push_back(num);
			set.erase(num);
		}
	}
	return ans;
}

但是需要注意的是,unordered_set中的 count 函数只能返回 0 或 1,并不能检测某个元素出现的次数!咱们看下一题,就可以很快明白了。
两个数组的交集2
这道题目和上一题唯一的区别在于,题目中描述 返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致。这个才是我们直观上的题目描述思路。举个栗子,nums1 = [1,2,2,3],nums2 = [1,2,2]。按照上一个题目的要求,答案应该返回[1,2]。而这个题应该返回[1,2,2]。
这样的话,我们应该统计其中一个数组的数字出现次数,这时候使用unordered_set就不太合适了,改进的办法是使用unordered_map,代码和上面的非常相似,便可以轻松秒杀了!

vector<int> intersection1(vector<int>& nums1, vector<int>& nums2) {
	unordered_map<int,int>hashset;
	vector<int>ans;
	for (auto num : nums1) {
		hashset[num]++;
	}

	for (auto num : nums2) {
		if (hashset[num] > 0) {
			ans.push_back(num);
			hashset[num]--;
		}
	}
	return ans;
}

二、相关习题

2.1 字母异位数相关习题

字母异位词简直是为哈希表量身定制的,一般这类题目都可以用哈希表来解决,让我们看几个例题。
1.有效的字母异位词

判断两个字符串是否为字母异位数,也就是说,这两个字符串的字母组成相同,但是顺序可能不一样,这就要发挥unordered的数据结构的魔力了!先构建一个哈希表,储存第一个字符串的元素,然后遍历第二个字符串,如果第二个字符串的元素包含在哈希表里,那么每遍历一个,哈希表的该元素就会减一;如果第二个字符串的元素不包含在哈希表里,说明第二个字符串中有第一个字符串没有的字母,直接返回false即可。最后,为了避免第二个字符串的元素小于第一个字符串的情况,直接开局判断字符串长度,不相等则返回false。代码如下。

    bool isAnagram(string s, string t) {
	unordered_map<char, int>map;
    if(s.length() != t.length()) return false;
	for (auto a : s) map[a]++;
	for (auto a : t) {
		if (map[a] > 0) map[a]--;
		else return false;
	}
	/*
	for (auto it = map.begin();it!=map.end();it++)
	{
		if (it->second > 0) return false;

	}
	*/
	return true;
    }

但是上面的代码还是有些问题,直接使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效。数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,长度为26即可,来记录字符串s里字符出现的次数。代码与上面的类似。

 bool isAnagram(string s, string t) {
        int record[26] = {0};
        for (int i = 0; i < s.size(); i++) {
            // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
            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;
    }

所以在使用哈希表解决问题的时候,要先考虑能否用数组解决问题,这样可以对空间进行一些优化。

2.字母异位词分组
众所周知,正常情况下,一个哈希表只能找到一个字母异位数,那如果我要找两个呢,要两个哈希表。好吧,那我要找很多个,而且个数位置的情况下,应该如何处理呢?我们可以看出,只使用一个简单的<char,int>哈希表的话,不能判断一个字符串组中的异位数,这个题目就是这样的一个问题。
对于本题,我们想要让好几个字符串都对应一个相同的字符串。例如strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”],我们想要让“eat",“tea”,"ate"都对应一个相同的字符串,他们排序后都对应字符串(aet),反之,如果排序后对应的不是这个字符串,那说明不是一个字母异位词。那么应该如何构造哈希表的对应关系呢,首先我们知道哈希表的键(key)是固定的,而值是可以多个,所以我们就可以构造出下面的哈希表。

unordered_map<string, vector<string>>map;

之后我们只需把每个将要遍历的字符串赋值给key,之后排序,即可找到对应关系,然后把相同字母异位数都对应到相同的映射中。

map[key].emplace_back(str);

这样的话,每个哈希表储存的值vector>就是输出结果的每个元素。完整代码如下,简直是太精妙了!!

    vector<vector<string>> groupAnagrams(vector<string>& strs) {
    vector<vector<string>>ans;   
	unordered_map<string, vector<string>>map;
	for (auto str : strs) {
		string key = str;
		sort(key.begin(), key.end());
		map[key].emplace_back(str);
	}
	for (auto it = map.begin(); it != map.end(); ++it)
	{
		ans.emplace_back(it->second);
	}
	return ans;
    }

3.找到字符串中所有字母异位词

这是一个滑动窗口与哈希结合的题目,看起来就有些难。自从我做了上面一题,我发现把字符串排序比较是否相同太方便了,结果迅速写出了相关代码,然后就超时了,,,。
这道题的解法思路也非常巧妙。我们可以使用两个数组代表哈希表,分别统计在p的大小区间中,两个字符串出现的字母次数,然后先排除一些特殊情况。接下来到关键时刻了,创建一个p长度的滑动窗口,需要依次遍历s的字串了。这个时候,每次向前移动一步,滑窗就会少一个字母和多一个字母,那么我们是不是只需要把这个少的字母在哈希表中减去,同时加上多的字母的映射,然后判断两者是否相同即可。

vector<int> findAnagrams(string s, string p) {
	int sLen = s.size(), pLen = p.size();

	if (sLen < pLen) {
		return vector<int>();
	}

	vector<int> ans;
	vector<int> sCount(26);
	vector<int> pCount(26);
	for (int i = 0; i < pLen; ++i) {
		++sCount[s[i] - 'a'];
		++pCount[p[i] - 'a'];
	}

	if (sCount == pCount) {
		ans.emplace_back(0);
	}
	for (int i = 0; i < sLen - pLen; ++i) {
		--sCount[s[i] - 'a'];
		++sCount[s[i + pLen] - 'a'];

		if (sCount == pCount) {
			ans.emplace_back(i + 1);
		}
	}

	return ans;

4.无重复的最长子串的长度

这道题目也是哈希表和滑窗结合的问题。和上面的题目非常相似,但是更简单。

int lengthOfLongestSubstring(string s) {
	unordered_map<char, int>map;
	int i =0, j = 0;
	int maxlen = 0;
	while (j<s.size())
	{
		if (map[s[j]] == 0) { map[s[j]]++; j++; }
		else { map[s[i]]--; i++; }

		maxlen = max(maxlen, j - i);
	}
	return maxlen;
}

2.2 unordered_set相关题目

1.快乐数
这道题目相对来说还是比较简单的,只需要注意一点,当出现 无限循环 但始终变不到 1的情况时候,我们可以创建一个set统计是否出现重复的结果。如果重复了,说明已经是无限循环了,返回false即可。同时我们需要编写一个对数字的每位进行处理求平方和的函数。代码如下:

int getSum(int n) {
	int ans = 0;
	while (n)
	{
		int a = n % 10;
		ans += a * a;
		n /= 10;
	}
	return ans;

}


bool isHappy(int n) {
	
	unordered_set<int>set;
	int ans = 0;
	while (1)
	{
		ans = getSum(n);
		if (ans == 1)return true;
		else if (set.count(ans) > 0) return false;
		else set.insert(ans);
		n = ans;
	}
	
}

2.最长连续序列

看到本题的第一眼,我想到的是使用一个有序表set记录数组中的数字,然后就可以遍历有序表,记录每次连续的序列,统计最大的次数,直接就拿下了!但是有序表的查找速度是O(logn),还需要一层遍历,而题目中要求使用O(n)时间解决,看来不太符合要求。那我们想要更快一点,就使用无序表unordered_set!这样的话会出现新的问题了,数组是无序的,无序表也是无序的,而我们要找的连续序列确是有序的!这简直是强人所难!

为了解决这个问题,我们可以先将数组的元素存到无序表中,然后判断有无与当前元素相邻的元素即可。这样的话,还有一个问题,我们并不知道刚开始检测的元素是最大最小还是在中间,所以他的相邻元素有两个,分别是a+1和a-1。这个时候就需要一个巧妙的方法解决问题了。

我们继续遍历nums,对每个元素先进行一个判断

if (!set.count(num - 1))

也就是说,判断这个元素的相邻前一个元素是否在数组中,为什么要这样呢?因为如果这个元素的前一个元素不存在,说明以这个元素为开头的最长序列是最长的!!,如果如果这个元素的前一个元素存在,那么肯定是按照前一个元素为开头的序列更长。这也是解决本题的关键。
当我们判断这个元素可以作为起始元素后,就可以判断他的后一个相邻元素是否存在了。存在一个,加一下长度,最后进行比较,取能达到最长连续序列的长度即可!代码如下:

int longestConsecutive(vector<int>& nums) {
	unordered_set<int>set;
	for (auto num : nums) set.insert(num);
	int curlen = 1, maxlen = 0;
	for (auto& num : nums) {
		if (set.count(num - 1) == 0) {
			int cur = num;
			curlen = 1;
			while (set.count(cur + 1))
			{
				cur++;
				curlen++;
			}
		}
		maxlen = max(curlen, maxlen);
	}
	return maxlen;

}

2.3 unordered_map相关题目

总结

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值