常见算法之数组

145 阅读6分钟

基础知识

数组是一种简单的数据结构,是由相同类型的元素组成的数据集合,并且占据一块连续的内存并按照顺序存储数据。最简单的数组是一维的,其中元素的存取以单一的下标表示。二维数组对应于数学上矩阵的概念,其中元素的存取需要行和列两个下标。

创建数组时需要先指定数组的容量大小,然后根据容量大小分配内存。即使只在数组中存储一个数字,也需要为所有的数据预先分配内存。因此,数组的空间效率不一定很高,可能会有空闲的区域没有得到充分利用。为了解决数组空间效率不高的问题,人们又设计实现了动态数组,如Java中的ArrayList;golang中的slice。

动态数组既保留了数组时间效率高的特性,又能够在数组中不断添加新的元素。为了避免浪费,可以先为数组分配较小的内存空间,然后在需要的时候在数组中添加新的数据。当数据的数目增加导致数组的容量不足时,需要重新分配一块更大的空间(通常新的容量是之前容量的2倍),把之前的数据复制到新的数组中,再把之前的内存释放。这样能减少内存的浪费,但每次扩充数组容量时都有大量的额外操作,这对时间性能有负面影响。

常见算法题目

一、数组中和为0的3个数字

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

解题思路: 排序加双指针。双指针:方向相反的双指针经常用来求排序数组中的两个数字之和。一个指针P1指向数组的第1个数字,另一个指针P2指向数组的最后一个数字,然后比较两个指针指向的数字之和及一个目标值。如果两个指针指向的数字之和大于目标值,则向左移动指针P2;如果两个指针指向的数字之和小于目标值,则向右移动指针P1。此时两个指针的移动方向是相反的。

先排序,时间复杂度为O(nlogn)。然后遍历每个数字,同时用双指针遍历当前数字后面的数组,三数相加为0的做记录,最后返回记录列表。时间复杂度为O(n2)+O(nlogn)=O(n2)。

Golang代码:

func threeSum(nums []int) [][]int {
	var (
		list = make([][]int, 0)
		i int
	)

	sort.Ints(nums)

	for i < len(nums) - 2 {
		list = twoSum(nums, i, list)
		tmp := nums[i]
		for i < len(nums) && tmp == nums[i] {
			i++
		}
	}

	return list
}

func twoSum(nums []int, i int, list [][]int) [][]int {
	var (
		left = i + 1
		right = len(nums) - 1
	)

	for left < right {
		sum := nums[i] + nums[left] + nums[right]
		if sum == 0 {
			tmp := []int{nums[i], nums[left], nums[right]}
			list = append(list, tmp)

			tmpNum := nums[right]
			for left < right && tmpNum == nums[right] {
				right--
			}
		} else if sum < 0 {
			left++
		} else {
			right--
		}
	}
	return list
}

二、和大于或等于k的最短子数组

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

解题思路: 同向双指针:方向相同的双指针通常用来求正数数组中子数组的和或乘积。初始化的时候两个指针P1和P2都指向数组的第1个数字。如果两个指针之间的子数组的和或乘积大于目标值,则向右移动指针P1删除子数组最左边的数字;如果两个指针之间的子数组的和或乘积小于目标值,则向右移动指针P2在子数组的右边增加新的数字。此时两个指针的移动方向是相同的。

指针P1指向子数组第一个元素,指针P2指向子数组最后一个元素。最开始P1和P2都指向数组第一个元素,P2开始向右移动,移动到子数组和第一次大于等于K时,记录一下子数组长度。然后P1向右移动一个值。然后重复上面动作,直到数组结束。

Golang代码:

func minSubArrayLen(target int, nums []int) int {

   var (
      left, right int
      sum int
      min = 1 << 31
      childLen int
   )

   for right < len(nums) {
      sum += nums[right]

      for sum >= target && left <= right {
         childLen = right - left + 1
         if childLen < min {
            min = childLen
         }
         sum -= nums[left]
         left++
      }
      right++
   }

   if min == 1 << 31 {
      return 0
   }

   return min
}

三、和为k的子数组

题目:输入一个整数数组和一个整数k,请问数组中有多少个数字之和等于k的连续子数组?例如,输入数组[1,1,1],k的值为2,有2个连续子数组之和等于2。

解题: 在从头到尾逐个扫描数组中的数字时求出前i个数字之和,并且将和保存下来。数组的前i个数字之和记为x。如果存在一个j(j<i),数组的前j个数字之和为x-k,那么数组中从第i+1个数字开始到第j个数字结束的子数组之和为k。这个题目需要计算和为k的子数组的个数。当扫描到数组的第i个数字并求得前i个数字之和是x时,需要知道在i之前存在多少个j并且前j个数字之和等于x-k。所以,对每个i,不但要保存前i个数字之和,还要保存每个和出现的次数。分析到这里就会知道我们需要一个哈希表,哈希表的键是前i个数字之和,值为每个和出现的次数。

func subarraySum(nums []int, k int) int {
    memo := make(map[int]int)
    memo[0] = 1
	var (
		count int
		sum int
	)
        
	for _, item := range nums {
		sum += item
		if _, ok := mem[sum - k]; ok {
			count += mem[sum-k]
		}
		mem[sum] += 1
	}
	return count
}

四、0和1个数相同的子数组

题目:输入一个只包含0和1的数组,请问如何求0和1的个数相同的最长连续子数组的长度?例如,在数组[0,1,0]中有两个子数组包含相同个数的0和1,分别是[0,1]和[1,0],它们的长度都是2,因此输出2。

解题: 首先把输入数组中所有的0都替换成-1,那么题目就变成求包含相同数目的-1和1的最长子数组的长度。在一个只包含数字1和-1的数组中,如果子数组中-1和1的数目相同,那么子数组的所有数字之和就是0,因此这个题目就变成求数字之和为0的最长子数组的长度。

func findMaxLength(nums []int) int {
   var (
      maxLen int
      sum int
      mem = make(map[int]int)
   )
   mem[0] = -1

   for i, item := range nums {
      if item == 0 {
         item = -1
      }
      sum += item
      
      if j, ok := mem[sum]; ok {
         tmp := i - j
         if tmp > maxLen {
            maxLen = tmp
         }
      } else {
         mem[sum] = i
      }
   }

   return maxLen
}