从头再来!社招找工作——算法题复习六:哈希
哈希
哈希思想及其应用
**哈希(Hash)**是一个专有的名词,在计算机、网络安全等领域有着广泛应用,在其他行业也是屡见不鲜。我这个专栏博客为了简化这个概念,只想把它在算法题中的用法说清楚,所以简单地解释一下,那就是“用一个(key, value)映射对来反映每个不同的key的某个属性value”。
比如,给定一个数字串"1,2,3,1,2,4,2,3",让你统计出现了几个数字,每个数字各出现几次,相信一个小学生数几遍之后都能回答出来:“1出现了2次,2出现了3次,3出现了2次,4出现了1次”。其实,这个小学生数数遍历的过程,就完成了一次哈希映射。
当然,如果真的有个专业的人问你哈希是什么,那么你还是乖乖地百度或者问一下Deepseek,这些平台肯定能给出标准答案!
那么在算法题中我们怎么去实现哈希映射呢?即使是刚才小学生做的题目,如果数字串中的数字没有规定上限和下限,或者有负数,我们很难用一个数组去统计出现次数。
我们这些学过面向对象编程的,自然能想到用一个对象,将数字转化成字符串,再将这个字符串作为对象的属性名,那么次数就能当做属性值啦!没错,如果你选用Python或者JavaScript等语言,可以快速使用{}来构建出一个对象的话,我很推荐你直接使用{“key": “value”}实现哈希;而Java这种需要强声明对象才能使用的编程语言,则需要其他特殊的数据结构来帮忙了,那就是Map<K,V>数据结构。一般我们使用HashMap<K,V>就够了,其他的LinkedHashMap和ConcurrentHashMap在基础算法题中还用不到。在接下来的实战中,我们会看到它的用法,重点关注其get, put, getOrDefault方法。
要指出的是,如果有的题目规定了key的范围,且这个范围很容易用数组表示出来,那么我们也不要麻烦对象了,直接用数组实现哈希就好。这种情况包括且不限于:key是0到9的数字字符、key是英文字符(大小写无所谓,反正是26个或者52个)。
两数之和
Leetcode
给定一个整数数组和一个数字 target,其中数组的元素是大小不设限的数字。如果数组中存在两个不同数字,使得它们的和等于给定的数字 target,那么就返回这两个数字的下标(顺序无所谓)。假设数组中不存在相同数字,且题目必有唯一解。
这道题太简单了,是Leetcode题库的第一题。掌握了哈希思想,代码自然不难:
public int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
Map<Integer, Integer> map = new HashMap<>(); // 这里我们创建一个map,其key为某个数字,value为key的下标值
for (int i = 0;i < nums.length;i++) {
map.put(nums[i], i);
}
for (int i = 0;i < nums.length;i++) {
int gap = target - nums[i];
if (map.containsKey(gap) && map.get(gap) != i) { // containsKey()方法很有用;后半个判断条件是避免某个数字自己加自己的情况
result[0] = i;
result[1] = map.get(gap);
break; // 题目保证了有且只有一个解,因此可以返回
}
}
return result;
}
存在重复元素
Leetcode
给定一个整数数组 nums,如果某一个数字在数组中出现至少两次,则返回true;如果数组中每个元素互不相同,返回false。
代码如下:
public boolean containsDuplicate(int[] nums) {
Map<Integer, Integer> map = new HashMap<>(); // key是nums中的每个数字,value是出现次数
for (int num : nums) {
if (map.containsKey(num)) {
return true;
}
else {
map.put(num, 1);
}
}
return false;
}
观察一下上述代码,发现value其实没有发挥出任何作用,因为这里表示次数的value,其实只可能是1,连0或2都不可能。所以这里其实我们可以用set来代替map,直接放弃存储value。代码如下:
public boolean containsDuplicate(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (set.contains(num)) {
return true;
}
else {
set.put(num);
}
}
return false;
}
Leetcode
升级一下本题:给定一个整数数组 nums 和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,满足 nums[i] == nums[j] 且 abs(i - j) <= k。如果存在,返回true;否则,返回false。
那么此处就不得不用Map的value来存储index了。代码如下:
public boolean containsNearbyDuplicate(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>(); // key是nums中的每个数字,value是index
for (int i = 0;i < nums.length;i++) {
if (map.containsKey(nums[i]) && i - map.get(nums[i]) <= k) { // 不需要计算绝对值,因为我们是顺序遍历的,i一定大于map.get(num)
return true;
}
else {
// 无论是map中不包含num,或是index之差不满足条件,都要存储(key, value)对
map.put(nums[i], i);
}
}
return false;
}
字符串中的第一个唯一字符
Leetcode
给定一个字符串 s,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回-1。字符串s只包含小写字母。
这里规定了只有小写字母,那我们就不需要用Map了,只需要创建一个长度为26的数组就行。比如遇到c = ‘m’,就将它的value存进arr[c - ‘a’]中。
代码如下:
public int firstUniqChar(String s) {
char[] cs = s.toCharArray();
int[] charIndex = new int[26];
for (int i = 0;i < 26;i++) {
charIndex[i] = -1; // 代表该字符未在字符串中被找到
}
boolean[] charFlag = new boolean[26]; // 判断是否重复出现过。由于默认值为false,所以用true代表出现过不止一次
for (int i = 0;i < cs.length;i++) {
// 使用charIndex数组和charFlag数组实现哈希
if (charIndex[cs[i]-'a'] != -1) {
charFlag[cs[i]-'a'] = true;
}
else {
charIndex[cs[i]-'a'] = i;
}
}
int minIndex = cs.length + 1;
for (int i = 0;i < 26;i++) {
if (charIndex[i] != -1 && !charFlag[i]) {
minIndex = Math.min(minIndex, charIndex[i]);
}
}
return (minIndex == cs.length + 1) ? -1 : minIndex;
}
前K个高频元素
Leetcode
给定一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。可以按任意顺序返回答案。
本题的难度在于,通过Map获取到频率之后,要完整获取Map的所有key值和所有value值,并对其进行排序。由于这个只需要取前 k 个高频元素,我们直接使用堆排序的思想,用优先队列来实现。
完整代码如下:
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0)+1); // 重点关注getOrDefault方法,必须掌握,很加分
}
// 创建一个递增的优先队列
PriorityQueue<Integer, Integer> pq = new PriorityQueue<>(new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1]; //
}
});
//
for (Map.Entry<Integer, Integer> entry : map.entrySet) { // 重点关注map.entrySet,取出所有键值对
int key = entry.getKey();
int value = entry.getValue();
if (p.size() == k) {
if (p.peek()[1] < value) {
p.poll();
p.offer(new int[]{key, value};
}
}
else {
p.offer(new int[]{key, value};
}
}
int[] result = new int[k];
for (i = 0;i < k;i++) {
result[i] = p.poll()[0];
}
return result;
}
判断链表中是否有环
让我们梦回本专栏的第二篇博客:链表的第六题“判断链表中是否有环”。当时我们用巧妙的快慢指针方法解决了。实际上,最基础的解法应该是用哈希的思想,在遍历链表的时候判断每个结点是否唯一。
当然,在面试时如果用这种方法来解题,一定不会给你加分。所以大家如果有兴趣可以自行解题:牛客网
数组中出现次数超过一半的数字
牛客网
给定一个数组,其中数字大小不限。现保证数组中存在一个数字出现次数超过一半,请返回这个数字。如果没有的话,返回-1.
这道题当然可以用哈希来解:
public int moreThanHalfNum(int [] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
int times = map.getOrDefault(num, 0) + 1;
if (times > nums.length / 2) {
return num;
}
map.put(num, times);
}
return -1;
}
这道题使用哈希的方法来解在面试时并不会失分,但是还有一种能加分的解法:“摩尔投票法”。这属于是数论的范畴了,所以本篇博客不展开讨论了。大家可以去题解看一下,或者通过百度和Deepseek查询一下,有兴趣的朋友们可以自行尝试。
社招算法题复习:哈希思想及应用

被折叠的 条评论
为什么被折叠?



