算法相关数据结构总结:
文章目录
一、哈希表理论基础
1. 哈希表的介绍
哈希表(Hash table,也叫散列表), 是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希表是由 HashTable
实现的,相关的实现有HashMap
、HashSet
。
HashMap 和 HashTable 的区别?请看这里Java面试手册——高频问题总结(二)。这里主要介绍HashMap和HashSet的算法实现。
2. 哈希表的实现
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。
HashMap 是无序的,即不会记录插入的顺序。
HashMap的创建:
HashMap<String,String> hm1 = new HashMap<String,String>();
HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
HashSet 允许有 null 值。
HashSet 是无序的,即不会记录插入的顺序。
HashSet的创建:
HashSet<String> sites = new HashSet<String>();
3. 哈希表的函数
在使用HashMap时,需要熟练掌握相关函数:
put(); //将键/值对添加到 hashMap 中
putAll(); //将所有键/值对添加到 hashMap 中
remove(); //删除 hashMap 中指定键 key 的映射关系
containsKey(); //检查 hashMap 中是否存在指定的 key 对应的映射关系。
containsValue(); //检查 hashMap 中是否存在指定的 value 对应的映射关系。
size(); //计算 hashMap 中键/值对的数量
get(); //获取指定 key 对应对 value
getOrDefault(); // 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值
isEmpty(); //判断 hashMap 是否为空
replace(); //替换 hashMap 中是指定的 key 对应的 value。
replaceAll(); //将 hashMap 中的所有映射关系替换成给定的函数所执行的结果。
4. 哈希表的常用方法
(1)统计字符出现的次数:
方法一:(使用containsKey
判断key是否已经记载过)
for(int i = 0; i<nums.length;i++){
if(hash.containsKey(nums[i])){
hash.put(nums[i], hash.get(nums[i])+1);
}
else{ // 如果hash表里没有出现,则将该值加入key,value置为1
hash.put(nums[i], 1);
}
}
方法二:(使用getOrDefault
判断key如果记载过,则返回对应的value,否则返回默认值0)
for(int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
hash.put(ch, hash.getOrDefault(ch, 0) + 1);
}
(2)遍历hash表:
for (Integer i : hash.keySet()) {
}
首先学习一下Map.Entry:
Map的entrySet()方法返回一个实现Map.Entry接口的对象集合。集合中每个对象都是底层Map中一个特定的键/值对。通过这个集合的迭代器,获得每一个条目(唯一获取方式)的键或值并对值进行更改。Map.Entry中的常用方法如下所示:
(1) Object getKey(): 返回条目的关键字
(2) Object getValue(): 返回条目的值
(3) Object setValue(Object value): 将相关映像中的值改为value,并且返回旧值
Map.Entry是为了更方便的输出map键值对。一般情况下,要输出Map中的key 和 value 是先得到key的集合keySet(),然后再迭代(循环)由每个key得到每个value。values()方法是获取集合中的所有值,不包含键,没有对应关系。而Entry可以一次性获得这两个值。
常用的遍历Map的方法:
第二种和第三种可以直接获取key和value。Map.entrySet迭代器会生成EntryIterator,其返回的实例是一个包含key/value键值对的对象。
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一种:普遍使用,由于二次取值,效率会比第二种和第三种慢一倍
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三种:无法在for循环时实现remove等操作
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四种:只能获取values,不能获取key
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
二、leetcode例题讲解哈希表问题
1. 数组作为哈希表
242. 有效的字母异位词
leetcode题目链接:242. 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例一:
输入: s = "anagram", t = "nagaram"
输出: true
示例二:
输入: s = "rat", t = "car"
输出: false
解题思路:
方法一:排序
有效的字母异位词就是排序后相等的字符串。
class Solution {
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
// 排序
char[] str1 = s.toCharArray();
char[] str2 = t.toCharArray();
Arrays.sort(str1);
Arrays.sort(str2);
return Arrays.equals(str1, str2);
}
}
方法二:数组作为哈希表
class Solution {
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
// 数组
int[] res = new int[26];
for(int i = 0; i < s.length(); i++) {
res[s.charAt(i) - 'a']++;
}
for(int i = 0; i < t.length(); i++) {
res[t.charAt(i) - 'a']--;
if(res[t.charAt(i) - 'a'] < 0) {
return false;
}
}
// for(int i = 0; i< res.length; i++) {
// if(res[i] != 0) {
// return false;
// }
// }
return true;
}
}
方法三:映射作为哈希表
class Solution {
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) return false;
// 哈希表
HashMap<Character, Integer> hash = new HashMap<Character, Integer>();
for(int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
hash.put(ch, hash.getOrDefault(ch, 0) + 1);
}
for(int i = 0; i < t.length(); i++) {
char ch = t.charAt(i);
hash.put(ch, hash.getOrDefault(ch, 0) - 1);
if(hash.get(ch) < 0) {
return false;
}
}
return true;
}
}
383. 赎金信
leetcode题目链接:383. 赎金信
为了不在赎金信中暴露字迹,从杂志上搜索各个需要的字母,组成单词来表达意思。
给你一个赎金信 (ransomNote) 字符串和一个杂志(magazine)字符串,判断 ransomNote 能不能由 magazines 里面的字符构成。
如果可以构成,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例一:
输入:ransomNote = "a", magazine = "b"
输出:false
示例二:
输入:ransomNote = "aa", magazine = "ab"
输出:false
解题思路:
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。
- 第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思” 这里说明杂志里面的字母不可重复使用。
- 第二点 “你可以假设两个字符串均只含有小写字母。” 说明只有小写字母,这一点很重要。
Java代码实现:
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// 统计ransomNote和magazine出现的字符次数
if (ransomNote.length() > magazine.length()) {
return false;
}
int[] cnt = new int[26];
for (char c : magazine.toCharArray()) {
cnt[c - 'a']++;
}
for (char c : ransomNote.toCharArray()) {
cnt[c - 'a']--;
if(cnt[c - 'a'] < 0) {
return false;
}
}
return true;
}
}
2. Map(映射)作为哈希表
1. 两数之和
leetcode题目链接:1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例一:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
解题思路:
本题使用HashMap来存储target-nums[i] 与 i 的值,然后在遍历过程中,如果发现key已经存在,则输出对应的value。
Java代码实现:
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
HashMap<Integer, Integer> hashmap = new HashMap<Integer, Integer>();
for(int i = 0; i < nums.length; i++){
if(hashmap.containsKey(nums[i])){
res[0] = i;
res[1] = hashmap.get(nums[i]);
return res;
}
hashmap.put(target - nums[i], i);
}
return res;
}
}
454. 四数相加 II
leetcode题目链接:454. 四数相加 II
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例一:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
解题思路:
这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和
,题目15.三数之和
,还是简单了不少!
我们可以将四个数组分成两部分,A 和 B 为一组,C 和 D 为另外一组。
对于 A 和 B,我们使用二重循环对它们进行遍历,得到所有 A[i]+B[j] 的值并存入哈希映射中。对于哈希映射中的每个键值对,每个键表示一种 A[i]+B[j],对应的值为 A[i]+B[j] 出现的次数。
对于 C 和 D,我们同样使用二重循环对它们进行遍历。当遍历到 C[k]+D[l] 时,如果 −(C[k]+D[l]) 出现在哈希映射中,那么将 −(C[k]+D[l]) 对应的值累加进答案中。
最终即可得到满足 A[i]+B[j]+C[k]+D[l]=0 的四元组数目。
Java代码实现:
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
HashMap<Integer, Integer> hash = new HashMap<Integer, Integer>();
for(int a : nums1) {
for(int b : nums2) {
hash.put(a + b, hash.getOrDefault(a + b, 0) + 1);
}
}
int res = 0;
for(int c : nums3) {
for(int d : nums4) {
if(hash.containsKey(-c-d)) {
res += hash.get(-c-d);
}
}
}
return res;
}
}
15. 三数之和
leetcode题目链接:15. 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例一:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例二:
输入:nums = []
输出:[]
解题思路:
这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。
而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。
这道题目使用双指针法 要比哈希法高效一些。
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i], b = nums[left], c = nums[right]
。
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0
就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0
说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
Java代码实现:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 双指针,计算两数之和的基础上将target设置为-nums[i],然后计算left和right
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length <= 2) return res;
Arrays.sort(nums); // 先排序
for(int i = 0; i < nums.length - 2; i++){
if(nums[i] > 0) break;
if(i > 0 && nums[i] == nums[i-1]) continue; //去重, 用过一次的数字不能用第二次
int target = -nums[i];
int left = i+1, right = nums.length-1;
while(left < right){
if(nums[left] + nums[right] == target){
res.add(new ArrayList<>(Arrays.asList(nums[i], nums[left], nums[right]))); //将数组转化为list,省去add过程
// 先进行加减操作
left++;
right--;
// 然后去掉重复的,如[-2, -1, -1, -1, 3, 3, 3]
while(left < right && nums[left] == nums[left-1]) left++;
while(left < right && nums[right] == nums[right+1]) right--;
}else if(nums[left] + nums[right] < target){
left++;
}else{
right--;
}
}
}
return res;
}
}
18. 四数之和
leetcode题目链接:18. 四数之和
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例一:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例二:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
解题思路:
四数之和,和15.三数之和
是一个思路,都是使用双指针法, 基本解法就是在15.三数之和
的基础上再套一层for循环。
但是有一些细节需要注意,例如: 不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。
15.三数之和
的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下表作为双指针,找到nums[i] + nums[left] + nums[right] == 0
。
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下表作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target
的情况,三数之和的时间复杂度是O(n^ 2),四数之和的时间复杂度是O(n^3) 。
Java代码实现:
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
// 参考三数之和,使用双指针,多加一些判断
// 测试用例中有较大的数,则使用long类型
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length <= 3) return res;
Arrays.sort(nums); // 先排序
int length = nums.length;
for(int i = 0; i < nums.length - 3; i++){
if(i > 0 && nums[i] == nums[i-1]) continue; //去重, 用过一次的数字不能用第二次
if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) { // 和超过则直接退出循环
break;
}
if ((long) nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
// 三数之和时,只要确定一个i,就可以使用双指针找target-nums[i]
// 四数之和时,需要确定两个数,故多加一层循环j,然后双指针找另外两个数
for (int j = i + 1; j < length - 2; j++) {
// 和上一层循环一样的判断条件
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
if ((long) nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
// 双指针查找
int left = j + 1, right = length - 1;
while (left < right) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
// 去掉重复的后,加加减减
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
}
return res;
}
}
3. Set(集合)作为哈希表
349. 两个数组的交集
leetcode题目链接:349. 两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
示例一:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例二:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解题思路:
题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序。
使用set1遍历数组1,然后遍历数组2,将相同元素加入set2,最后将set2的结果输出到数组。
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> hash1 = new HashSet<>();
Set<Integer> hash2 = new HashSet<>();
for(int i : nums1) {
hash1.add(i);
}
for(int i : nums2) {
if(hash1.contains(i)) {
hash2.add(i);
}
}
int[] res = new int[hash2.size()];
int index = 0;
for(int i : hash2) {
res[index++] = i;
}
return res;
}
}
202. 快乐数
leetcode题目链接:202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 true ;不是,则返回 false 。
示例一:
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
示例二:
输入:n = 2
输出:false
解题思路:
方法一:哈希表HashSet
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
class Solution {
public boolean isHappy(int n) {
Set<Integer> res = new HashSet<>();
// 判断数字是否为1,或者是否该数字重复出现即无限循环
while(n != 1 && !res.contains(n)) {
res.add(n);
n = getNextNumber(n);
}
return n == 1;
}
// 重组的各个位置的平方和的数字
public int getNextNumber(int n) {
int ans = 0;
while(n > 0) {
int tmp = n % 10;
ans += tmp * tmp;
n = n / 10;
}
return ans;
}
}
方法二:快慢指针法
通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针是通过调用 getNext(n) 函数获得。
意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。
我们不是只跟踪链表中的一个值,而是跟踪两个值,称为快跑者和慢跑者。在算法的每一步中,慢速在链表中前进 1 个节点,快跑者前进 2 个节点(对 getNext(n) 函数的嵌套调用)。
如果 n 是一个快乐数,即没有循环,那么快跑者最终会比慢跑者先到达数字 1。
如果 n 不是一个快乐的数字,那么最终快跑者和慢跑者将在同一个数字上相遇。
与双指针判断循环链表是否有环相同的方法:算法分析之链表问题:142. 环形链表Ⅱ
Java代码实现:
class Solution {
public boolean isHappy(int n) {
// 快慢双指针,判断是否有环
int slow = n;
int fast = getNextNumber(n);
while(fast != 1 && slow != fast) {
slow = getNextNumber(slow);
fast = getNextNumber(getNextNumber(fast));
}
return fast == 1;
}
// 重组的各个位置的平方和的数字
public int getNextNumber(int n) {
int ans = 0;
while(n > 0) {
int tmp = n % 10;
ans += tmp * tmp;
n = n / 10;
}
return ans;
}
}
三、其它算法分析
1. 动态规划之背包问题——01背包
2. 动态规划之背包问题——完全背包
3. 动态规划之子序列问题
4. 算法分析之数组问题
5. 算法分析之链表问题
6. 算法分析之哈希表
7. 算法分析之字符串
参考: