Day07 哈希表 part02

61 阅读6分钟

概念

golang对整形切片的排序

import "sort"
 
func main() {
    nums := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
    sort.Ints(nums)
    fmt.Println(nums) // 输出:[1 1 2 3 3 4 5 5 5 6 9]
}

对字符串切片排序

package main

import (
   "fmt"
   "sort"
)

func main() {
   strs := []string{"banana", "apple", "mango", "peach", "orange"}
   sort.Strings(strs)
   fmt.Println(strs) // 输出:[apple banana mango orange peach]
}

对结构体的排序,需要指定结构体函数Len, Less和Swap函数

import (
   "fmt"
   "sort"
)

type Person struct {
   Name string
   Age  int
}

type PersonSlice []Person

func (p PersonSlice) Len() int           { return len(p) }
func (p PersonSlice) Less(i, j int) bool { return p[i].Age < p[j].Age }
func (p PersonSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

func main() {
   people := PersonSlice{
      {"Alice", 31},
      {"Bob", 29},
      {"Charlie", 33},
   }
   sort.Stable(people)
   fmt.Println(people) // 输出:[Bob 29 Alice 31 Charlie 33]
}

make 函数主要用于创建和初始化内置的数据结构,主要是slice,map和channel make函数的基本用法:

make(Type, len, cap) Type

// Type: 数据类型,只能slice,map和channel
// len: 数据类型实际占用的内存空间长度,对于slice是必要参数
// cap: 为数据类型提前预留的内存空间长度,可选参数, 可选参数,所谓的**提前预留**是当前为数据类型申请内存空间的时候,提前申请好额外的内存空间,这样可以避免二次分配内存带来的开销,大大提高程序的性能

make函数的具体场景

  • 创建slice: make([]int, 5, 10) 创建一个长度为5,容量为10的int slice make([]int, 2, 4) 创建一个长度为2,容量为4的int slice

  • 创建map: make(map[string]int,cap): 创建一个空的map,key类型是string,值类型是int 和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity

  • 创建channel: make(chan int, cap) 创建一个缓冲大小为cap 的int channel

刷题

454 四数相加II

leetcode.cn/problems/4s…

image.png

本题解题步骤:

  1. 首先定义 一个map,key放a和b两数之和,value 放a和b两数之和出现的次数。
  2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
  4. 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
  5. 最后返回统计值 count 就可以了
package main

func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int {
   // 使用make定义变量的长度,可以加快执行速度
   before := make(map[int]int, len(nums1)*len(nums2))
   result := 0
   for _, i := range nums1 {
      for _, j := range nums2 {
         // int默认值为0,不需要判断是否存在key
         before[i+j] ++
      }
   }

   for _, i := range nums3 {
      for _, j:= range nums4 {
         result += before[0-i-j]
      }
   }
   return result
}

383 赎金信

leetcode.cn/problems/ra…

image.png

使用长度为26的 数组代替map,因为 ransomNote 和 magazine 由小写英文字母组成,索引就变成int(char)-int('a')

15 三数之和

leetcode.cn/problems/3s…

双指针

其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。

而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。

接下来我来介绍另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些,那么来讲解一下具体实现的思路。

动画效果如下:

15.三数之和

拿这个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相遇为止。

时间复杂度:O(n^2)。

image.png

package main

import "sort"

func threeSum(nums []int) [][]int {
   result := [][]int{}
   sort.Ints(nums)
   // 排序之后最后一个元素小于0,无论如何组合三个数都是小于0
   if nums[len(nums)-1] < 0 {
      return result
   }
   // 排序之后第一个元素大于0,无论如何组合三个数都大于0
   for first:=0; first<len(nums) && nums[first]<=0 ; first++ {
      // 结果中的第一个元素去重,因为已经排序过
      if first != 0 && nums[first] == nums[first-1] {
         continue
      }
      left := first + 1
      right := len(nums) - 1
      for left < right {
         if nums[first] + nums[left] + nums[right] < 0 {
            left++
         }else if nums[first] + nums[left] + nums[right] > 0 {
            right--
         }else {
            result = append(result, []int{nums[first], nums[left], nums[right]})
            // 找到三元组之后,对nums[left] 和 nums[right] 去重
            left++
            for left < right && nums[left-1] == nums[left] {
               left++
            }
            right--
            for left < right && nums[right+1] == nums[right] {
               right--
            }
         }
      }
   }
   return result
}

问题: 在剪枝过程中,只判断了最小的四个数是否小于target,没有计算最大值 result 没有设置

18 四数之和

leetcode.cn/problems/4s…

image.png

package main

import "sort"

func fourSum(nums []int, target int) [][]int {
   result := [][]int{}
   sort.Ints(nums)
   // 剪枝: 如果长度小于4,直接返回空
   if len(nums) < 4 {
      return result
   }
   // 遍历a第一个数,从0到len(nums)-4
   // 剪枝:如果最小的四个数加起来大于target,不再遍历
   for a := 0; a < len(nums)-3 && nums[a] + nums[a+1] + nums[a+2] + nums[a+3] <= target; a++ {
   // 去重: 如果遍历后的数和前一个数相等,跳过
      if a != 0 && nums[a] == nums[a-1] {
         continue
      }
      // 剪枝:如果最小的四个数加起来大于target,不再遍历
      for b := a+1; b < len(nums)-2 && nums[a] + nums[b] + nums[b+1] + nums[b+2] <= target; b++ {
         // 去重: 如果遍历后的数和前一个数相等,跳过
         if b != a+1 && nums[b] == nums[b-1] {
            continue
         }
         c := b+1
         d := len(nums)-1
         for c < d {
            if nums[a] + nums[b] + nums[c] + nums[d] < target {
               c++
            }else if nums[a] + nums[b] + nums[c] + nums[d] > target {
               d--
            }else {
               result = append(result, []int{nums[a], nums[b], nums[c], nums[d]})
               // 双指针去重
               c++
               for c<d && nums[c] == nums[c-1] {
                  c++
               }
               d--
               for c<d && nums[d] == nums[d+1] {
                  d--
               }
            }
         }

      }
   }
   return result
}

总结

这道题使用了双指针法,双指针将时间复杂度O(n^2)的解法优化为O(n)的解法,也就是降一个数量级,要理解快指针和慢指针的究竟是什么含义, 题目如下:

链表相关双指针题目: