数据结构与算法
哈希表入门介绍
哈希表
- 常用操作
/* 初始化哈希表 */ Map<Integer, String> map = new HashMap<>(); /* 添加操作 */ // 在哈希表中添加键值对 (key, value) map.put(12836, "小哈"); map.put(15937, "小啰"); map.put(16750, "小算"); map.put(13276, "小法"); map.put(10583, "小鸭"); /* 查询操作 */ // 向哈希表中输入键 key ,得到值 value String name = map.get(15937); /* 删除操作 */ // 在哈希表中删除键值对 (key, value) map.remove(10583);
- 遍历方式
/* 遍历哈希表 */ // 遍历键值对 key->value for (Map.Entry <Integer, String> kv: map.entrySet()) { System.out.println(kv.getKey() + " -> " + kv.getValue()); } // 单独遍历键 key for (int key: map.keySet()) { System.out.println(key); } // 单独遍历值 value for (String val: map.values()) { System.out.println(val); }
哈希冲突
- 哈希冲突介绍
- 多个输入对应同一输出的情况称为哈希冲突(hash collision)。
- 负载因子(load factor)是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件。例如在 Java 中,当负载因子超过 0.75时,系统会将哈希表扩容至原先的 2倍。
- 解决方式
—hello算法-哈希冲突- 链式地址
- 开放寻址
- 编程语言的选择
哈希算法
力扣题目
242. 有效的字母异位词
题目链接 : 242. 有效的字母异位词
class Solution {
public boolean isAnagram(String s, String t) {
// 定义一个数组作为简单哈希表(key确定)
int[] record = new int[26];
// 将字符串s中的字符传递到数组中,索引为各个元素,内容为个数
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i) - 'a']++;
}
// 将字符串t中的字符消去数组中表示的字符
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i) - 'a']--;
}
// 数组中的字符刚好全被消为0,说明有效
for (int count: record) {
// 若有不为0的,说明不完全对应,无效
if (count != 0) {
return false;
}
}
return true;
}
}
- 时间复杂度 : O(n)
- 空间复杂度 : O(1)
思路
1. 数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
2. 遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。
3. 遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
4. record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
代码随想录提供思路 : 代码随想录
49. 字母异位词分组
题目链接 : 49. 字母异位词分组
思路
438. 找到字符串中所有字母异位词
题目链接 : 438. 找到字符串中所有字母异位词
思路
1. 输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序
2. 传入和传出都不用重复, 只要有这个元素就算交集
349. 两个数组的交集
题目链接 : 349. 两个数组的交集
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
// 剪枝,提前进行非空判断
if (nums1 == null || nums1.length == 0
|| nums2 == null || nums2.length == 0) {
return new int[0];
}
Set<Integer> set = new HashSet<>();
Set<Integer> result = new HashSet<>();
// 将数组1中的数字传入set集合中,不重复
for (int i : nums1) {
set.add(i);
}
// 将数组2中与set集合重合的数字传入新的Set型result集合中,同样不能重复
for (int i : nums2) {
if (set.contains(i)) {
result.add(i);
}
}
// 返回结果result集合中包含的数字转为的数组
return result.stream().mapToInt(i -> i).toArray();
}
}
- 时间复杂度 : O(n + m) m
- 空间复杂度 : O(n)
思路
代码随想录提供思路 : 代码随想录
350. 两个数组的交集II
题目链接 : 349. 两个数组的交集
思路
202. 快乐数
题目链接 : 第202题. 快乐数
class Solution {
public boolean isHappy(int n) {
// 使用set集合来解决重复的问题
Set<Integer> record = new HashSet<>();
// 循环条件是n不等于1并且set集合中不包含(避免重复然后一直循环)
while (n != 1 && ! record.contains(n)) {
// 符合条件就加入set集合
record.add(n);
// 获取新的n的步骤
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
n = res;
}
// 返回n是否等于1
return n == 1;
}
}
- 时间复杂度 : O(n)
- 空间复杂度 : O(n)
思路
1. 循环中可能会碰到重复然后循环不断的情况, 所以用set集合来避免此情况
代码随想录提供思路 : 代码随想录
1. 两数之和
题目链接 : 1. 两数之和
class Solution {
public int[] twoSum(int[] nums, int target) {
// 结果数组
int[] res = new int[2];
// 进行非空判断
if (nums == null || nums.length == 0) {
return res;
}
// 使用map接收,查询已存数据复杂度低
Map<Integer, Integer> map = new HashMap<>();
// 判断另一个数组中的元素是否符合
for (int i = 0; i < nums.length; i++) {
int temp = target - nums[i];
// 符合就存入结果数组中退出循环
if (map.containsKey(temp)) {
res[1] = i;
res[0] = map.get(temp);
break;
}
// 不符合就放入map中作为以存元素继续循环
map.put(nums[i], i);
}
return res;
}
}
- 时间复杂度 : O(n)
- 空间复杂度 : O(n)
思路
1. 什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
2. 可以先判断后加入map,这样可以少写一个判断是否索引相同的代码
代码随想录提供思路 : 代码随想录 (涵盖其他思路)
454. 四数相加II
题目链接 : 454. 四数相加II
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
// 结果数组
int res = 0;
// map收集前两个集合的交叉和
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i : nums1) {
for (int j : nums2) {
int sum = i + j;
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
}
// 如果已存map中发现了的符合的结果,将次数加上去
for (int i : nums3) {
for (int j : nums4) {
res += map.getOrDefault(0 - i - j, 0);
}
}
return res;
}
}
- 时间复杂度 : O(n^2)
- 空间复杂度 : O(n^2)
思路
1. 使用哈希法在不超时的情况下做到对结果去重是很困难的,这道题不能去重, 所以使用哈希法合适
2. 将四重循环分割为类似二叉树型, 可以减少循环次数
代码随想录提供思路 : 代码随想录
383. 赎金信
题目链接 : 383. 赎金信
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// 使用数组代替哈希表(key确定)
int[] record = new int[26];
// 遍历字符串
for (char c : magazine.toCharArray()) {
record[c - 'a'] += 1;
}
for (char c : ransomNote.toCharArray()) {
record[c - 'a'] -= 1;
}
// 判断是否count小于0, 小于0说明供不应求,返回错误
for (int count : record) {
if (count < 0) {
return false;
}
}
// 循环结束后返回true
return true;
}
}
思路
1. 和 242. 有效的字母异位词 很像, 在此不作赘述
代码随想录提供思路 : 代码随想录
15. 三数之和
题目链接 : 15. 三数之和
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 结果动态数组
List<List<Integer>> result = new ArrayList<>();
// 对数组参数排序
Arrays.sort(nums);
// 从头开始循环
for (int i = 0; i < nums.length; i++) {
// 剪枝
if (nums[i] > 0) {
return result;
}
// 对a进行去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 左指针
int left = i + 1;
// 右指针
int right = nums.length - 1;
// 对和进行判断, 循环指针变动位置
while (left < right) {
// 求和sum
int sum = nums[i] + nums[left] + nums[right];
// sum大于零,右指针左移
if (sum > 0) {
right--;
} else if (sum < 0) { // sum小于零,左指针右移
left++;
} else {
// sum等于零, 存入结果动态数组, 然后进行去重判断
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 在第一次存入结果动态数组后再进行去重, 可以记录唯一的第一个遇到的值
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
// sum等于零的其他逻辑判断完之后, 指针变动
left++;
right--;
}
}
}
return result;
}
}
- 时间复杂度 : O(n^2)
- 空间复杂度 : O(1)
思路
1. 使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限
2. 这道题目使用双指针法 要比哈希法高效一些
3. 不能有重复的三元组,但三元组内的元素是可以重复的
代码随想录提供思路 : 代码随想录 (涵盖其他思路)
18. 四数之和
题目链接 : 18. 四数之和
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
// 结果动态数组
List<List<Integer>> result = new ArrayList<>();
// 对参数数组进行排序
Arrays.sort(nums);
// 进入第一个数循环
for (int i = 0; i < nums.length; i++) {
// 剪枝操作
if (nums[i] > 0 && nums[i] > target) {
return result;
}
// 对第一个数去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 进入第二个数循环
for (int j = i + 1; j < nums.length; j++) {
// 对第二个数进行去重
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
// 左指针 第三个数
int left = j + 1;
// 右指针 第四个数
int right = nums.length - 1;
// 把0换成target后与上文 三数之和 思路相同
while (left < right) {
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return result;
}
}
- 时间复杂度 : O(n^3)
- 空间复杂度 : O(1)
思路
1. 因为题目把 0 换成了 target ,在剪枝操作中应该重点小心坑
2. 其余思路与上文 三数之和 类似
代码随想录提供思路 : 代码随想录