学习内容
学习文档:
收获总结
-
哈希表的基本应用: 哈希表是一种通过键值对存储数据的数据结构,具有O(1)的平均查找时间复杂度。其优势在于能够快速判断一个元素是否存在于集合中。通过哈希函数将键映射到存储位置,我们可以在常数时间内完成插入、删除和查找操作。这在需要频繁查找或去重的算法问题中非常有用。
-
哈希函数的理解与设计: 哈希函数是哈希表的核心,负责将输入(键)映射到特定的存储桶位置。一个好的哈希函数应当能够均匀地分布输入数据,避免哈希碰撞的发生。哈希碰撞是指不同的输入映射到了同一存储桶,这会导致性能下降。常用的哈希函数设计包括除留余数法、乘积法和平方取中法等。
-
哈希碰撞及其处理策略: 哈希碰撞是无法完全避免的,因此需要设计合理的碰撞处理策略。常见的方法有链地址法(即使用链表处理同一存储桶中的多个元素)和开放地址法(即在发生碰撞时寻找下一个可用位置存储)。链地址法的优点是简单易实现,且能处理多种数据类型;而开放地址法则可以更好地利用空间,但在负载因子较高时性能下降明显。
-
数组作为特殊的哈希表: 在某些特定的算法问题中,我们可以使用数组来模拟哈希表。特别是当键值范围固定且有限时(如小写字母a-z),数组能够提供与哈希表类似的功能,但由于数组的访问速度更快且无哈希碰撞,因此在特定场景下表现更佳。例如,在统计字符频次的题目中,使用固定大小的数组可以大幅提升性能。
-
set数据结构的应用: 在某些问题中,需要频繁检查某个元素是否已经存在,或需要确保数据不重复。此时,集合(set)是一种理想的数据结构。Go语言中没有原生的set结构,但可以通过
map[T]struct{}来模拟实现,其中T是元素类型。由于struct{}{}在Go语言中不占用额外空间,因此这种方法既节省内存,又能够实现集合所需的所有操作(如插入、删除、查找)。 -
map作为哈希表的高级应用: Go语言中的map不仅可以用来模拟集合,还能够用于更复杂的场景,如计数器、查找表等。在处理组合问题时,map常用于记录不同组合出现的次数,并通过查找实现快速匹配。例如在"四数相加II"问题中,使用两个map分别记录前两组数的和与后两组数的和,从而在O(1)时间内完成匹配,大大提升了算法效率。
-
拓展理解:哈希表在实际问题中的应用: 在实际开发中,哈希表广泛应用于缓存(如LRU缓存)、数据库索引、计数器统计等场景。学习哈希表的基础知识并理解其实现细节,能够帮助我们更好地应对这些实际问题。通过这次学习,我们不仅掌握了哈希表的理论知识,还在实践中理解了如何通过优化哈希函数、处理哈希碰撞等方法,提升算法的效率和可靠性。
题目解析
题目1:第454题.四数相加II
-
题目描述: 给定四个整数数组
nums1、nums2、nums3和nums4,统计有多少个四元组(i, j, k, l)使得nums1[i] + nums2[j] + nums3[k] + nums4[l] = 0。为了简化问题,假设所有的四个数组长度相同,且长度不超过500。 -
示例:
输入: nums1 = [1, 2], nums2 = [-2,-1], nums3 = [-1, 2], nums4 = [0, 2] 输出: 2 解释: 两个符合条件的四元组为: (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0 -
解法总结: 该题目要求我们找到所有满足条件的四元组。直接暴力枚举四个数组中的元素组合会导致O(n^4)的时间复杂度,无法在合理时间内解决问题。因此,利用哈希表的快速查找特性可以将问题转化为两两分组求和。首先,我们遍历
nums1和nums2,计算每对元素的和,并将其存储在哈希表中,键为和,值为出现的次数。然后遍历nums3和nums4,计算它们的和并检查哈希表中是否存在该和的相反数,如果存在则说明找到了符合条件的四元组,结果增加该和的出现次数。通过这种方法,时间复杂度降低到了O(n^2)。func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int { resultMap := map[int]int{} result := 0 // 1. 第一部分for循环循环nums1,nums2,之和作为key,value为组合次数 for _, v := range nums1 { for _, sV := range nums2 { resultMap[v+sV]++ } } // 2. 第二部分for循环直接比对resultMap是否有答案 for _, v := range nums3 { for _, sV := range nums4 { if ssV, ok := resultMap[0-(v+sV)]; ok { result += ssV } } } return result } -
时间复杂度: O(n^2),其中n是每个数组的长度。我们首先计算两两数组组合的和,这需要O(n^2)的时间,然后在第二部分查找哈希表也需要O(n^2)的时间。
-
空间复杂度: O(n^2),用于存储前两组数的和的哈希表。这个哈希表最多存储n^2个不同的和,因此空间复杂度为O(n^2)。
题目2:383. 赎金信
-
题目描述: 给定一个
ransomNote字符串和一个magazine字符串,判断ransomNote能否由magazine里面的字符构成。如果可以,返回true;否则返回false。magazine中的每个字符只能在ransomNote中使用一次。 -
示例:
输入: ransomNote = "a", magazine = "b" 输出: false 输入: ransomNote = "aa", magazine = "ab" 输出: false 输入: ransomNote = "aa", magazine = "aab" 输出: true -
解法总结: 这道题的关键在于判断
magazine中是否有足够的字符可以构成ransomNote。解法是遍历magazine字符串并统计每个字符的出现次数,然后遍历ransomNote字符串,检查每个字符是否可以在magazine中找到并且次数足够。如果某个字符的数量不够,则直接返回false。如果所有字符都满足条件,则返回true。通过使用一个固定大小的数组来记录字符的出现次数,可以在O(1)时间内完成查找和更新操作,这种方法有效地利用了数组作为特殊哈希表的特性。func canConstruct(ransomNote string, magazine string) bool { magazineMap := make([]int, 26) // 1. magazine只能用一次,magazine映射到map,+1 for _, v := range magazine { magazineMap[v-rune('a')]++ } // 2. 循环ransomNote进行-1,如果减完了小于0,则返回false;最后返回true for _, v := range ransomNote { magazineMap[v-rune('a')]-- if magazineMap[v-rune('a')] < 0 { return false } } return true } -
时间复杂度: O(n + m),其中n和m分别是
ransomNote和magazine的长度。我们需要分别遍历这两个字符串来统计和检查字符的出现次数。 -
空间复杂度: O(1),因为我们使用了一个固定大小的数组(长度为26)来存储字符的频次,不随输入大小变化。
题目3:第15题. 三数之和
-
题目描述: 给定一个包含n个整数的数组
nums,判断nums中是否存在三个元素a,b,c,使得a + b + c = 0。请找出所有和为0且不重复的三元组。 -
示例:
输入: nums = [-1, 0, 1, 2, -1, -4] 输出: [[-1, 0, 1], [-1, -1, 2]] -
解法总结: 该题目要求找出所有和为0的三元组,且不能有重复的三元组。暴力解法会尝试每个三元组合,时间复杂度为O(n^3),效率低下。为提高效率,可以首先对数组进行排序,然后利用双指针法进行查找:固定一个数作为基准,剩下的两个数通过左右双指针向中间搜索,确保找到的三元组和为零。排序能够帮助我们轻松跳过重复的元素,避免产生重复的结果。最终的时间复杂度为O(n^2),远优于暴力解法。
暴力解法: 注意,暴力解法虽然正确,但是会超时;第二种双指针写法更优func threeSum(nums []int) [][]int { checkMap := map[int]int{} result := [][]int{} seenMap := map[string]struct{}{} // 1. 第一轮循环,从头扫描到尾,把对应的值放checkMap进去,key是0-v,value是index for index, v := range nums { checkMap[0-v] = index } // 2. 第二轮循环,双指针循环(不重复),同时需要相加判断checkMap是否有,index是否重合 for i := 0; i < len(nums); i++ { for j := i + 1; j < len(nums); j++ { if v, ok := checkMap[nums[i]+nums[j]]; ok { if v != i && v != j { tmp := []int{nums[v], nums[i], nums[j]} sort.Ints(tmp) if _, ok := seenMap[fmt.Sprintf("%d%d%d", tmp[0], tmp[1], tmp[2])]; !ok { result = append(result, tmp) seenMap[fmt.Sprintf("%d%d%d", tmp[0], tmp[1], tmp[2])] = struct{}{} } } } } } return result }对撞双指针解法import "sort" func threeSum(nums []int) [][]int { result := [][]int{} // 0. 排序数组(默认由小到大),这个是后面对撞双指针的前提 sort.Ints(nums) // 1. 由于要找三个数,在不重复的情况下,i应该<len-2 for i := 0; i < len(nums)-2; i++ { // 2. 并且排序后如果已经>0了都直接跳过 if nums[i] > 0 { break } // 3. 去重a if i >= 1 && nums[i] == nums[i-1] { continue } // 4. 对撞双指针找b和c left := i + 1 right := len(nums) - 1 for left < right { b, c := nums[left], nums[right] if nums[i]+b+c == 0 { tmp := []int{nums[i], b, c} result = append(result, tmp) // 去重b for left < right && nums[left] == b { left++ } // 去重c for left < right && nums[right] == c { right-- } } else if nums[i]+b+c < 0 { left++ } else { right-- } } } return result } -
时间复杂度: O(n^2)。排序需要O(n log n),双指针查找需要O(n^2)。
-
空间复杂度: O(1)。除了排序所需的空间外,不需要额外的空间来存储数据。
题目4:第18题. 四数之和
-
题目描述: 给定一个包含n个整数的数组
nums和一个目标值target,判断nums中是否存在四个元素a,b,c,d,使得a + b + c + d的和与target相等。请找出所有符合条件且不重复的四元组。 -
示例:
输入: nums = [1, 0, -1, 0, -2, 2], target = 0 输出: [[-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2]] -
解法总结: 该题目是“三数之和”的扩展版,要求找到四个数的和等于目标值的所有不重复四元组。解法与“三数之和”类似,可以通过排序和双指针法来解决。首先对数组进行排序,然后固定两个数,剩下的两个数通过左右双指针进行搜索,确保找到的四元组和为目标值。为了避免重复解,固定的数以及双指针搜索过程中都需要进行去重处理。最终时间复杂度为O(n^3),其中n是数组的长度。
对撞双指针解法import "sort" func fourSum(nums []int, target int) [][]int { result := [][]int{} // 双指针解法 // 0. 排序 sort.Ints(nums) // 1. a+b+c+d 第一个循环a for i := 0; i < len(nums)-3; i++ { // a去重 a := nums[i] if i > 0 && a == nums[i-1] { continue } // 2. 往后循环b for j := i + 1; j < len(nums)-2; j++ { b := nums[j] if j > i+1 && b == nums[j-1] { continue } // 3. 双指针c,d(left, right) left := j + 1 right := len(nums) - 1 for left < right { c := nums[left] d := nums[right] sum := a + b + c + d if sum == target { result = append(result, []int{a, b, c, d}) // 如果下一个数字和已经加入result的相同,就直接按照方向跳过 for left < right && nums[left] == nums[left+1] { left++ } for left < right && nums[right] == nums[right-1] { right-- } // 双指针同时移动 left++ right-- } else if sum < target { left++ } else { right-- } } } } return result } -
时间复杂度: O(n^3)。排序需要O(n log n),三层嵌套循环分别是O(n^3)。
-
空间复杂度: O(1)。除了排序所需的空间外,不需要额外的空间来存储数据。