leetcode 面试题 16.06. 最小差

442 阅读2分钟

力扣题目链接

解法步骤说明

  • 先对两个数组排序
  • 尽量减少要遍历的区间
  • 双指针遍历计算结果

可以把两个数组看成是整数数轴上的两个区间(各自是由数组最大值和最小值限定的离散的区间,但是可以把它们想象成连续的区间或者说线段)。通过分析这两个区间的位置关系可以理清对“减小要遍历的区间”的处理过程。

  • 存在区间边界相等的情况
  • 两区间没有交集
  • 两区间中的一个完全包含了另一个
  • 两区间有部分交集

这道题我花了大力气在 “尽量减小要遍历的区间” 这一点上。需要强调的是,排序的时间复杂度是O(nlogn)O(nlogn),后续遍历计算结果的时间复杂度是O(n)O(n),时间主要花在排序上,所以“尽量减小要遍历的区间”这一点优化对整体性能提升的作用不大。我只是觉得有趣,写写看而已。假设有这样的题目,输入的两个数组(区间)已经有序,要求合并数组(区间),或者合并是计算过程的一部分,此时减小要遍历的数组(区间)的长度是有意义的。
读者可以自行修改,将减小遍历区间的部分去掉以简化代码。具体细节请看下面的代码和注释。

import "math"
import "sort"

func smallestDifference(a []int, b []int) int {
	la := len(a)
	if la == 0 {
		panic("a is empty")
	}
	lb := len(b)
	if lb == 0 {
		panic("b is empty")
	}
	// la != 0 && lb != 0
	sort.Sort(sort.IntSlice(a))
	sort.Sort(sort.IntSlice(b))

	// 先看看能不能快速得到结果

	// 端点相等,4种情况
	if a[0] == b[0] ||
		a[0] == b[lb-1] ||
		a[la-1] == b[0] ||
		a[la-1] == b[lb-1] {
		return 0
	}

	// 不相交,两种情况
	if a[la-1] < b[0] {
		return b[0] - a[la-1]
	}
	if b[lb-1] < a[0] {
		return a[0] - b[lb-1]
	}

	// 下面先尝试缩小遍历区间,找比最小值小的最大值,找比最大值大的最小值。

	// 先判断完全包含,两种情况
	// a[0] < b[0] && b[lb-1] < a[la-1]
	// a包含b
	minDiff, ok := handleInclusion(a, b)
	if ok {
		return minDiff
	}
	// b[0] < a[0] && a[la-1] < b[lb-1]
	// b包含a
	minDiff, ok = handleInclusion(b, a)
	if ok {
		return minDiff
	}

	// 再判断相交(不完全包含),两种情况
	// a[la-1] > b[0] && a[la-1] < b[lb-1]
	// a的右端在b中
	minDiff, ok = handleIntersection(a, b)
	if ok {
		return minDiff
	}
	// b的右端在a中
	minDiff, ok = handleIntersection(b, a)
	if ok {
		return minDiff
	}

	// 兜底
	return getMinAbsDiff(a, b)
}

// 尝试处理相交的情况
func handleIntersection(a, b []int) (int, bool) {
	var (
		la = len(a)
		lb = len(b)
	)
	// 相交(不完全包含)
	if a[la-1] > b[0] && a[la-1] < b[lb-1] { // a的右端在b中
		// 尝试缩小b的要遍历的区间。
                // 在b中找比a[la-1]大的最小值。
		bRight, equal := searchMinValueGreaterThanX(b, a[la-1])
		if equal {
			return 0, true
		}
                // 尽量缩小a的要遍历的区间。
		// 在a中找比b[0]小的最大值。
		aLeft, equal := searchMaxValueSmallerThanX(a, b[0])
		if equal {
			return 0, true
		}
		return getMinAbsDiff(a[aLeft:], b[:bRight+1]), true
	}
	return 0, false
}

// 尝试处理包含的情况
func handleInclusion(a, b []int) (int, bool) {
	var (
		la = len(a)
		lb = len(b)
	)
	// 完全包含
	if a[0] < b[0] && b[lb-1] < a[la-1] { // a包含b
		// 缩小a的要遍历的区间
		// 在a中找比b[0]小的最大值
		aLeft, equal := searchMaxValueSmallerThanX(a, b[0])
		if equal {
			return 0, true
		}
		// 在a中找比b[lb-1]大的最小值
		aRight, equal := searchMinValueGreaterThanX(a, b[lb-1])
		if equal {
			return 0, true
		}
		return getMinAbsDiff(a[aLeft:aRight+1], b), true
	}
	return 0, false
}

// 双指针遍历,计算结果
func getMinAbsDiff(a, b []int) int {
	var (
		ia      int
		ib      int
		la      = len(a)
		lb      = len(b)
		minDiff = math.MaxInt32
	)
	for ia < la && ib < lb {
		if a[ia] == b[ib] {
			return 0
		}
		if a[ia] < b[ib] {
			x := b[ib] - a[ia]
			if x < minDiff {
				minDiff = x
			}
			ia++
		} else {
			x := a[ia] - b[ib]
			if x < minDiff {
				minDiff = x
			}
			ib++
		}
	}
	return minDiff
}

// 在有序数组a中找比x小的最大值。
// 如果找到,返回那个元素的索引。
// 如果都比x大,返回0。
// 如果返回值中的bool是true,说明找到了等于x的值。
func searchMaxValueSmallerThanX(a []int, x int) (int, bool) {
	var (
		low int
		high = len(a)-1
		maxVal int
		i = -1
	)
	for low <= high {
		mid := low + (high-low)>>1
		v := a[mid]
		if v == x {
			return mid, true
		}
		if v < x {
			if i != -1 {
				if v > maxVal {
					maxVal = v
					i = mid
				}
			} else {
				maxVal = v
				i = mid
			}
			low = mid+1
		} else {
			high = mid-1
		}
	}
	// 都比x大
	if high == -1 {
		return low, false
	}
	return i, false
}

// 在有序数组a中找比x大的最小值。
// 如果找到,返回那个元素的索引。
// 如果都比x小,返回a最大索引。
// 如果返回值中的bool是true,说明找到了等于x的值。
func searchMinValueGreaterThanX(a []int, x int) (int, bool) {
	var (
		low int
		high = len(a)-1
		minVal int
		i = -1
	)
	for low <= high {
		mid := low + (high-low)>>1
		v := a[mid]
		if v == x {
			return mid, true
		}
		if v < x {
			low = mid+1
		} else {
			if i != -1 {
				if v < minVal {
					minVal = v
					i = mid
				}
			} else {
				minVal = v
				i = mid
			}
			high = mid-1
		}
	}
	// 都比x小
	if low == len(a) {
		return high, false
	}
	return i, false
}

排序(假设基于快速排序)时间复杂度O(nlogn)O(nlogn),空间复杂度O(logn)O(logn)(系统栈开销)。
尝试减小遍历区间的操作基于二分查找,时间复杂度是O(logn)O(logn)
遍历计算结果的时间复杂度O(n)O(n)
整体上,时间复杂度O(nlogn)O(nlogn),空间复杂度O(logn)O(logn)