2.2 双指针
双指针是一种常见的解题思路, 通过相反或相同的方向来扫描数组以达成解题.
2.2.1 问题06: 排序数组中的两个数字之和
输入升序排列的数组和一个值k,在数组中找出两个元素之和等于k的元素的下标并返回; 假设数组中只存在一对符合条件的元素, 且单个元素不能使用两次.
例: [1, 2, 4, 6 ] 和 8, 返回[1, 3]
2.2.1.1 分析&题解
暴力解法
遍历数组, 将元素两两匹配求和.
时间复杂度: O()
func twoSumBruteForce(ints []int, n int) []int {
	for i := 0; i < len(ints); i++ {
		for j := i + 1; j < len(ints); j++ {
			if ints[i]+ints[j] == n {
				return []int{i, j}
			}
		}
	}
	return []int{}
}
二分查找
遍历数组, 对于数字i, 使用二分法寻找 k - i.
二分查找时间复杂度: O(logn)
总体时间复杂度: O(nlogn)
func twoSumBinarySearch(ints []int, n int) []int {
	for i := range ints {
		t := n - ints[i]
		j := binarySearch(ints, t)
		if j != -1 && i != j {
			return []int{i, j}
		}
	}
	return []int{}
}
func binarySearch(ints []int, t int) int {
	left := 0
	right := len(ints) - 1
	for left <= right {
		mid := (left + right) / 2
		midVal := ints[mid]
		if t == midVal {
			return mid
		}
		if t > midVal {
			left++
		}
		if t < midVal {
			right--
		}
	}
	return -1
}
HashTable
- 将数组元素记录于哈希表中
 - 遍历数组, 对于数字
i, 在哈希表中寻找k-i(哈希表查找时间复杂度O(1)) 
时间复杂度: O(n), 空间复杂度: O(n)
func twoSumHashTable(ints []int, n int) []int {
    m := make(map[int]int, len(ints))
    for i, v := range ints {
       m[v] = i
    }
    for i, v := range ints {
       if val, isPresent := m[n-v]; isPresent {
          if i != val {
             return []int{i, val}
          }
       }
    }
    return []int{}
}
双指针
由于数组是有序的, 故可以使用双指针, 假设为升序:
- 左指针(l), 右指针(r) 分别从两端开始反向移动
 - 计算两数之和, 
- 大于k: 右指针左移
 - 小于k: 左指针右移
 - 等于k: 返回结果
 
 
时间复杂度: O(n), 空间复杂度: O(1)
func twoSumTwoPointer(ints []int, n int) []int {
	left := 0
	right := len(ints) - 1
	for left < right {
		sum := ints[left] + ints[right]
		if sum == n {
			return []int{left, right}
		}
		if sum > n {
			right--
		}
		if sum < n {
			left++
		}
	}
	return []int{}
}
Benchmark

2.2.2 问题07: 数组中和为0的3个数字
输入数组, 找出数组中所有和为0的3个数字的三元组. 返回值不得包含重复的三元组
例: [-1, 0, 1, 2, -1, 4], 返回 [-1, 0, 1], [-1, -1, 2]
2.2.2.1 分析&题解
三数之和为0, 可将其转化为两数之和的问题, 即对于数字 k, 在数组中寻找和为 -k 的数字.
去除重复三元组, 对于满足条件的三元组: [a, b, c], 只要原数组进行排序, 那么一定有 , 只会出现一个三元组 [a, b, c] (不会有 [a , c , b], [b, c, a]...)
流程
- 将原数组进行排序, TC: O(nlogn)
 - 遍历数组, 对于元素 i : 
- 使用双指针方法找出两数之和为 -i 的元素 j 和 k
 - 指针跳过所有的相同元素, 避免重复, 跳过相同元素 i, j 即可. (三元组 [i, j, k], 因为三者之和为0, 那么可以视作[i, j, -i-j], 只需要 i, j 不重复那么三元组就不会重复 )
 
 
总体时间复杂度为: O(nlogn) + O() 即 O()
func threeSum(nums []int) [][]int {
	// qSortArr array in ascending order
	sort.Slice(nums, func(i, j int) bool {
		return nums[i] < nums[j]
	})
	result := make([][]int, 0)
	// threeSum
	for i := 0; i < len(nums); {
		result = twoSum07(nums, i, result)
		// skip identical element
		ie := nums[i]
		for i < len(nums) && nums[i] == ie {
			i++
		}
	}
	return result
}
func twoSum07(nums []int, i int, result [][]int) [][]int {
	// skip i
	left, right := i+1, len(nums)-1
	for left < right {
		sum := nums[i] + nums[left] + nums[right]
		if sum < 0 {
			left++
		}
		if sum > 0 {
			right--
		}
		if sum == 0 {
			result = append(result, []int{nums[i], nums[left], nums[right]})
			// skip identical nums
			le := nums[left]
			for left < right && nums[left] == le {
				left++
			}
		}
	}
	return result
}
2.2.3 问题08: 和大于或等于k的最短子数组
输入一个正整数组和正整数 k , 求数组中和大于或等于 k 的连续子数组的最短长度, 若不存在则返回0
例: [5 1 4 3]和7, [4, 3] 符合条件, 那么最短长度为 2
2.2.3.1 分析
子数组有一个或多个连续数字组成, 那么一个数组可以由两个指针表示.
对于子数组 sub, p1 指向第一个数字, p2 指向最后一个数字, 那么子数组就是由 p1 到 p2 之间的数字组成.
对于子数组之和, 因为元素均为正整数
- p1 右移时, 元素减少则和减少.
 - p2 右移时, 元素增加则和增加.
 
通过一次遍历 (以 p2 进行遍历), 即可寻找出最短的符合条件的连续子数组
2.2.3.2 流程
- 指针 p1, p2 都指向第一个元素
 - 计算指针之间的元素之和 sum: 
- sum < k, 需增加元素 p2 右移, 和增加
 - sum >= k, 满足条件, 更新最小长度, p1 右移, 移除一个元素(和减小), 直到 sum < k
 
 - 流程结束, 返回最小长度, 否则返回0
 
因为两个循环中, left, right 只增加不减小且均从 0 增加到 n-1, 故时间复杂度为 O(n)
func minSubArrayLen(nums []int, k int) int {
	// two pointers
	left, right := 0, 0
	minLen := 0
	sum := 0
	for ; right < len(nums); right++ {
		sum += nums[right]
		for left <= right && sum >= k {
			minLen = minLength(minLen, right-left+1)
			// next sub
			sum -= nums[left]
			left++
		}
	}
	return minLen
}
func minLength(m, l int) int {
	if m > 0 {
		if l < m {
			return l
		}
		return m
	}
	return l
}
2.2.4 问题09: 乘积小于k的子数组个数
输入正整数组和正整数k, 输出乘积小于k的连续子数组的个数
例: [10 5 2 6] and 100, 满足条件的有 [10], [5], [2], [6], [10 5], [5 2], [2 6], [5 2 6], 输出: 8
2.2.4.1 分析
和前一个问题类似, 使用双指针解决.
对于一个连续子数组 [n1 n2 ... ni], 若元素乘积小于 k, 那么其所有子数组的乘积一定小于 k 且满足条件的长度大于1的子数组个数就为 数组长度
例如: [5 2 6] 和 100, 满足条件的子数组为 [5 2] [2 6] [5 2 6] 为 3 个
2.2.4.2 流程
- 指针 p1, p2 从第一个元素开始
 - 计算元素乘积 prod : 
- prod < k, 子数组的个数就是数组长度, p2 右移, 积增大, 直到 prod >=k
 - prod >= k, p1 左移, 积减小
 
 
func numSubarrayLessThanK(nums []int, k int) int {
	left, right := 0, 0
	count := 0
	prod := 1
	for ; right < len(nums); right++ {
		prod *= nums[right]
		for left <= right && prod >= k {
			prod /= nums[left]
			left++
		}
		if left <= right {
			count += right - left + 1
		}
	}
	return count
}
