Go 算法专项之数组(一)

155 阅读7分钟

数组的基础知识

数组

数组是一片连续的内存区域,需要在初始化时被指定长度,不能进行扩容。

主要有三种声明方式:

var arr [3]int
var arr2 = [4]int{1,2,3,4}
arr3 := [...]int{2,3,4} // 语法糖,依靠编译器进行数组长度的推断。

获取数组长度:len(arr)

在复制和传递时为值复制。

var arr2 = [4]int{1,2,3,4}
arr4 := arr2
arr4[0] = 4
fmt.Printf("arr4: %+v, arr2: %+v\n", arr4, arr2)
// arr4: [4 2 3 4], arr2: [1 2 3 4]

切片

切片是长度可变的序列,不用指定固定长度。一般写作[]T,其中T代表slice中元素的类型。

声明切片如下所示,在只声明不赋初始值的情况下,切片slice1的值为预置的nil,切片的初始化需要使用内置的make函数:

var slice1 []int
slice2 := make([]int, 5)
slice3 := make([]int, 0, 5)
numbers := []int{1, 2, 3, 4, 5}

fmt.Printf("slice1: %+v, len: %d, cap: %d\n", slice1, len(slice1), cap(slice1))
fmt.Printf("slice2: %+v, len: %d, cap: %d\n", slice2, len(slice2), cap(slice2))
fmt.Printf("slice3: %+v, len: %d, cap: %d\n", slice3, len(slice3), cap(slice3))
fmt.Printf("numbers: %+v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers))
// slice1: [], len: 0, cap: 0
// slice2: [0 0 0 0 0], len: 5, cap: 5
// slice3: [], len: 0, cap: 5
// numbers: [1 2 3 4 5], len: 5, cap: 5

切片有长度和容量的区别,可以在初始化时指定。

内置的lencap函数可以分别获取切片的长度和容量。

切片的截取如下所示。可以通过下标的方式对切片进行截断,被截取后的数组仍然指向原始切片的底层数据。

numbers := []int{1, 2, 3, 4, 5}
num1 := numbers[1:3] // 截取范围:[1,3)
num2 := numbers[:2]  // [0,2)
num3 := numbers[2:]  // [2,len(num3))
fmt.Printf("num1: %+v, len: %d, cap: %d\n", num1, len(num1), cap(num1))
fmt.Printf("num2: %+v, len: %d, cap: %d\n", num2, len(num2), cap(num2))
fmt.Printf("num3: %+v, len: %d, cap: %d\n", num3, len(num3), cap(num3))
// num1: [2 3], len: 2, cap: 4
// num2: [1 2], len: 2, cap: 5
// num3: [3 4 5], len: 3, cap: 3

切片的复制,是底层指针的值复制,可以理解为数据的引用传递:

numbers := []int{1, 2, 3, 4, 5}
num4 := numbers
num4[0] = 4
fmt.Printf("num4: %+v, numbers: %+v\n", num4, numbers)
// num4: [4 2 3 4 5], numbers: [4 2 3 4 5]

切片的添加与删除元素:

// 切片添加元素
num5 := make([]int, 0)
num5 = append(num5, 0)
num5 = append(num5, 1, 2, 3)
num5 = append(num5, []int{4, 5, 6}...)
fmt.Printf("num5: %+v\n", num5)
// num5: [0 1 2 3 4 5 6]

// 切片删除元素
num5 = num5[1:] // 删除第一个元素
fmt.Printf("num5: %+v\n", num5)
// num5: [1 2 3 4 5 6]
num5 = num5[:len(num5)-1] // 删除最后一个元素
fmt.Printf("num5: %+v\n", num5)
// num5: [1 2 3 4 5]
num5 = append(num5[:2], num5[3:]...) // 删除下标为2的元素
fmt.Printf("num5: %+v\n", num5)
// num5: [1 2 4 5]

切片的深拷贝,新切片的修改不会影响旧切片:

num6 := []int{1, 2, 3}
num7 := make([]int, len(num6), len(num6))
copy(num7, num6)
num7[0] = 4
fmt.Printf("num6: %+v, len: %d, cap: %d\n", num6, len(num6), cap(num6))
fmt.Printf("num7: %+v, len: %d, cap: %d\n", num7, len(num7), cap(num7))
// num6: [1 2 3], len: 3, cap: 3
// num7: [4 2 3], len: 3, cap: 3

双指针

双指针是一种常用的解题思路,可以使用两个相反方向或相同方向的指针扫描数组从而达到解题目的。下面列举几道可以用双指针高效解决的算法题。

167. 两数之和 II - 输入有序数组

题目大意:输入一个递增排序的数组和一个值k,请问如何在数组中找出两个和为k的数字并返回它们的下标?
假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。下标从1开始。
例如,输入数组[1,2,4,6,10],k的值为8,数组中的数字2与6的和为8,由于下标从1开始,则它们的下标分别为2与4。

分析:
解法1: 扫描数字 i,和另外一个数字 j (k - i),i 的扫描时间复杂度为 O(n), 由于是递增,k 可以用二分查找法时间复杂度为 O(logn) 时间复杂度为 O(nlogn),空间复杂度为 O(1)

解法2: 遍历数组用哈希表存起来,假设扫描数字i,则查找数字 k-i 需要的时间为 O(1) 时间复杂度为 O(n),空间复杂度为 O(n)

解法3: 双指针分别指向第一个和最后一个元素,和比k大,则右指针往左移,和比k小,则左指针往右移动。 时间复杂度为 O(n),空间复杂度为 O(1)。

双指针是否会跳过正确答案?不会。假设左边的指针i先到达正确答案的下标,右边指针j未到达,那么和一定比目标值大,所以只会移动右边的指针。同理,如果右边的指针先到达正确答案时也一样。参考代码如下:

func twoSum(numbers []int, target int) []int {
   i, j := 0, len(numbers)-1
   for i < j {
      sum := numbers[i] + numbers[j]
      if sum == target {
         return []int{i+1, j+1}
      } else if sum > target {
         j--
      } else {
         i++
      }
   }
   return []int{}
}

15. 三数之和

题目大意:输入一个数组,如何找出数组中所有和为0的3个数字的三元组?需要注意的是,返回值中不得包含重复的三元组。
例如,在数组[-1,0,1,2,-1,-4]中有两个三元组的和为0,它们分别是[-1,0,1]和[-1,-1,2]。

分析: 上面一道题的升级版,不需要返回下标,只要值,并且需要去重。可以固定第一个数值,再用双指寻找剩下的两个数值。

解法步骤:

  1. 先排序数组;
  2. 固定第一个数字下标 i,用双指针寻找剩下的两个值的下标 j 和 k;
  3. 递增下标 i 和 j 的时候,需要跳过重复的值,从而实现去重。

参考代码如下:

func threeSum(nums []int) [][]int {
   sort.Ints(nums)
   res := make([][]int, 0)
   last := len(nums) - 1
   for i := 0; i < len(nums)-2; i++ {
      j, k := i+1, last

      for j < k {
         sum := nums[i] + nums[j] + nums[k]
         if sum == 0 {
            res = append(res, []int{nums[i], nums[j], nums[k]})

            for j+1 < len(nums) && nums[j] == nums[j+1] {
               j++
            }
            j++
         } else if sum > 0 {
            k--
         } else {
            j++
         }
      }

      for i+1 < len(nums) && nums[i] == nums[i+1] {
         i++
      }
   }
   return res
}

209. 长度最小的子数组

题目大意:输入一个正整数组成的数组和一个正整数k,请问数组中和大于或等于k的连续子数组的最短长度是多少?如果不存在所有数字之和大于或等于k的子数组,则返回0。
例如,输入数组[5,1,4,3],k的值为7,和大于或等于7的最短连续子数组是[4,3],因此输出它的长度2。

分析: 需要找出最短的连续子数组,可以用两个指针分别指向子数组的第一个元素和最后一个元素。

  1. 初始化指针 li 和 ri 指向数组第一个元素;
  2. ri 遍历数组元素,把 ri 指向的元素加入到子数组,当和大于等于目标值时,储存当前的子数组长度;然后把 li 指向的元素剔除,把 li 往右移,此时仍大于等于目标值时,则储存当前的子数组长度,重复 li 动作。

由于 li 和 ri 指针都是往右移动,所以时间复杂度为 O(n)。参考代码如下:

func minSubArrayLen(target int, nums []int) int {
   sum := 0
   res := math.MaxInt
   li := 0
   for ri := 0; ri < len(nums); ri++ {
      sum += nums[ri]
      for sum >= target {
         res = min(res, ri-li+1)
         if res == 1 {
            return res
         }
         sum -= nums[li]
         li++
      }
   }

   if res == math.MaxInt {
      return 0
   }
   return res
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

713. 乘积小于K的子数组

题目大意:输入一个由正整数组成的数组和一个正整数k,请问数组中有多少个数字乘积小于k的连续子数组? 例如,输入数组[10,5,2,6],k的值为100,有8个子数组的所有数字的乘积小于100,它们分别是[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]和[5,2,6]。

分析: 跟上一道解题思路相似,但这里求的是连续子数组个数,可以先固定右指针,然后如果乘积大于等于k,则移动左指针,直到得到乘积小于k的最长子数组,此时两个指针之间有多少个元素,则表示有多少个子数组。

func numSubarrayProductLessThanK(nums []int, k int) int {
   res := 0
   product := 1

   li := 0
   for ri := 0; ri < len(nums); ri++ {
      product *= nums[ri]
      for product >= k && li <= ri {
         product /= nums[li]
         li++
      }
      res += ri - li + 1
   }
   return res
}

参考