658 - 找到 K 个最接近的元素
8月25日 周四 雨🌧 每日一题

658. 找到 K 个最接近的元素

给定一个 排序好 的数组 arr ,两个整数 kx ,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。

整数 a 比整数 b 更接近 x 需要满足:

  • |a - x| < |b - x| 或者
  • |a - x| == |b - x|a < b

示例 1:

输入:arr = [1,2,3,4,5], k = 4, x = 3
输出:[1,2,3,4]

示例 2:

输入:arr = [1,2,3,4,5], k = 4, x = -1
输出:[1,2,3,4]

提示:

  • 1 <= k <= arr.length
  • 1 <= arr.length <= 104
  • arr升序 排列
  • -104 <= arr[i], x <= 104

题意是找数组中最靠近xk个数,并且原数组也是有序的,那么基于数组有序的前提,我们可以快速找到最接近x的数i,然后从i开始向两侧搜索与x绝对值最小的数字加入答案中,直至添加k个后结束。

二分解法

// 二分搜索target右侧的下标
func bisectRight(arr []int, target int) int {
    l, r := 0, len(arr)
    for l < r {
        m := l + (r-l)>>1
        if arr[m] <= target {
            l = m+1
        } else {
            r = m
        }
    }
    return l
}

func findClosestElements(arr []int, k int, x int) []int {
  	// 找到右边界l,下标l及之前小于等于x
    l := bisectRight(arr, x) - 1
    // 下标r及之后大于x
    r := l+1
    cnt := 0
    n := len(arr)
    for cnt < k {
        cnt++
        if l < 0 {
            r++
        } else if r >= n || (x-arr[l]) <= (arr[r]-x) {
            // 此处可以不用绝对值,因为l所在必然小于等于x,r所在必然大于x
            l--
        } else {
            r++
        }
    }
    return arr[l+1:r]
}

排序解法

使用一种稳定排序的算法,保证按与x的绝对值从小到大排序后,相对顺序依旧不变,然后取前k个重新排序即可。

// 归并排序
func MergeSort(arr []int, l, r int, less func(i, j int) bool) {
    if r-l <= 1 {
        return
    }
    m := l + (r-l)>>1
    MergeSort(arr, l, m, less)
    MergeSort(arr, m, r, less)
    Merge(arr, l, r, m, less)
}

func Merge(arr []int, l, r, m int, less func(i, j int) bool) {
    left := make([]int, m-l)
    right := make([]int, r-m)
    copy(left, arr[l:m])
    copy(right, arr[m:r])
    il, ir := 0, 0
    for i := l; i < r; i++ {
        if il >= len(left) {
            arr[i] = right[ir]
            ir++
        } else if ir >= len(right) || !less(right[ir], left[il]) {
            arr[i] = left[il]
            il++
        } else {
            arr[i] = right[ir]
            ir++
        }
    }
}

func abs(a, b int) int {
    if a > b {
        return a - b
    }
    return b - a
}

func findClosestElements(arr []int, k int, x int) []int {
    MergeSort(arr, 0, len(arr), func(i, j int) bool {
        return abs(i, x) < abs(j, x)
    })

    arr = arr[:k]
    sort.Ints(arr)
    return arr
}

大顶堆解法

type Item struct {
	idx     int
	val     int
	realVal int
}

type Heap struct {
	arr  []Item
	less func(i, j int) bool
	swap func(i, j int)
}

func (h *Heap) Sink() {
	n := 0
	for {
		l := n*2 + 1
		if l >= len(h.arr) {
			break
		}
		j := l
		r := l + 1
		if r < len(h.arr) && h.less(r, l) {
			j = r
		}
		if h.less(n, j) {
			break
		}
		h.swap(n, j)
		n = j
	}
}

func (h *Heap) Swim() {
	n := len(h.arr) - 1
	for {
		p := (n - 1) / 2
		if n == 0 || h.less(p, n) {
			break
		}
		h.swap(p, n)
		n = p
	}
}

func (h *Heap) Pop() Item {
	v := h.arr[0]
	h.swap(0, len(h.arr)-1)
	h.arr = h.arr[:len(h.arr)-1]
	h.Sink()
	return v
}

func (h *Heap) Push(v Item) {
	h.arr = append(h.arr, v)
	h.Swim()
}

func abs(a, b int) int {
	if a > b {
		return a - b
	}
	return b - a
}

func findClosestElements(arr []int, k int, x int) []int {
	h := &Heap{arr: make([]Item, 0)}
	h.less = func(i, j int) bool {
		// 先按绝对值从大到小排序,后按索引从大到小排序
		vi, vj := h.arr[i].val, h.arr[j].val
		ii, ij := h.arr[i].idx, h.arr[j].idx
		return vi > vj || vi == vj && ii > ij
	}
	h.swap = func(i, j int) {
		h.arr[i], h.arr[j] = h.arr[j], h.arr[i]
	}

	for i, v := range arr {
		// 将值放入大顶堆
		h.Push(Item{i, abs(v, x), v})
		// 当大顶堆的大小超过k时,表示数据条目溢出,可以把最大的(最不符合条件的)弹出
		if len(h.arr) > k {
			h.Pop()
		}
	}
	ans := make([]int, k)
	for i := 0; i < k; i++ {
		ans[i] = h.Pop().realVal
	}

	// 重新排序
	sort.Ints(ans)
	return ans
}

最后修改于 2022-08-25