215. Kth Largest Element in an Array
...大约 7 分钟
给定整数数组
nums
和整数k
,请返回数组中第**k**
个最大的元素。请注意,你需要找的是数组排序后的第
k
个最大的元素,而不是第k
个不同的元素。你必须设计并实现时间复杂度为
O(n)
的算法解决此问题。示例 1:
输入: [3,2,1,5,6,4], k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
提示:
1 <= k <= nums.length <= 10^5
-104 <= nums[i] <= 10^4
1. 快速选择 (快速排序分区)
快速选择使用的是快速排序的分区算法, 快速排序的分区算法有两种:
- Lumoto Partition
- Hoare Partition
关于基准的选择, 随机选取选取基准可以尽量避开极端情况.
1.1 Lomuto Partition
快速排序流程:
- 选取基准 Pivot
- 将小于于
Pivot
的数字放在其左边, 大于等于Pivot
的放在右边, 形成左右分区; 左分区所有数字小于基准, 右分区所有数字大于等于基准; 对于左右分区, 继续 步骤1). 直到区间为空为止. - 所有数字排序完毕
分区流程:
- 选取最右边的数作为基准
- 左分区索引
i
, 遍历数组, 将小于基准的数字和左分区数字交换; 直到结束为止; - 将左分区边界右边的数字和基准交换, 返回当前基准位置
伪代码如下:
// Sorts a (portion of an) array, divides it into partitions, then sorts those
algorithm quicksort(A, lo, hi) is
// Ensure indices are in correct order
if lo >= hi || lo < 0 then
return
// Partition array and get the pivot index
p := partition(A, lo, hi)
// Sort the two partitions
quicksort(A, lo, p - 1) // Left side of pivot
quicksort(A, p + 1, hi) // Right side of pivot
// Divides array into two partitions
algorithm partition(A, lo, hi) is
pivot := A[hi] // Choose the last element as the pivot
// Temporary pivot index
i := lo - 1
for j := lo to hi - 1 do
// If the current element is less than or equal to the pivot
if A[j] <= pivot then
// Move the temporary pivot index forward
i := i + 1
// Swap the current element with the element at the temporary pivot index
swap A[i] with A[j]
// Move the pivot element to the correct pivot position (between the smaller and larger elements)
i := i + 1
swap A[i] with A[hi]
return i // the pivot index
由于 Lumoto Partition 返回的是当前基准所在位置, 那么一定能保证基准左右两边数字的大小关系.
- 若基准位置正好为
n-k
, 则表示其右边的k-1
个数字比基准大, 那么基准就是第k
大的数字. - 若基准位置小于
n-k
, 表示第k
大数字在其若右分区, 继续对右分区进行Partition
, 寻找基准. - 若基准位置大于
n-k
, 表示第k
大数字在其若左分区, 继续对左分区进行Partition
, 寻找基准.
每次只需要寻找一个分区即可, 无需等待数组排序完毕. 题解如下(Lumoto 分区法现在会超时, 因为加了新的测试用例):
func findKthLargest(nums []int, k int) int {
n := len(nums)
pi := lomutoPartition(nums, 0, n-1)
t := n - k
for pi != t {
if pi < t {
pi = lomutoPartition(nums, pi+1, n-1)
} else {
pi = lomutoPartition(nums, 0, pi-1)
}
}
return nums[n-k]
}
func lomutoPartition(nums []int, left, right int) int {
// random pivot
pi := rand.Intn(right-left+1) + left
p := nums[pi]
// move to end
swap(nums, pi, right)
// partition
i, j := left-1, left
for j < right {
if nums[j] < p {
i++
swap(nums, i, j)
}
j++
}
i++
swap(nums, i, right) // move pivot to right position
return i
}
func swap(nums []int, i, j int) {
nums[i], nums[j] = nums[j], nums[i]
}
1.2 Hoare Partition
快速排序流程:
- 选取基准
Pivot
- 将小于于
Pivot
的数字放在其左边, 大于等于Pivot
的放在右边, 形成左右分区; 左分区所有数字小于基准, 右分区所有数字大于等于基准; 对于左右分区, 继续 步骤1). 直到区间为空为止. - 所有数字排序完毕
分区流程:
选择中间数字为基准
使用双指针
i
,j
; 分别从数组两端向中间移动, 每次都移动一步:- 若
nums[i] < pivot
, 则i
继续向右移动, 跳过这些数字 - 若
nums[j] > pivot
, 则j
继续向左移动, 跳过这些数字
当
i<j
, 则交换. 直到i >= j
- 若
返回
j
.
算法伪代码:
// Sorts a (portion of an) array, divides it into partitions, then sorts those
algorithm quicksort(A, lo, hi) is
if lo >= 0 && hi >= 0 && lo < hi then
p := partition(A, lo, hi)
quicksort(A, lo, p) // Note: the pivot is now included
quicksort(A, p + 1, hi)
// Divides array into two partitions
algorithm partition(A, lo, hi) is
// Pivot value
pivot := A[ floor((hi - lo)/2) + lo ] // The value in the middle of the array
// Left index
i := lo - 1
// Right index
j := hi + 1
loop forever
// Move the left index to the right at least once and while the element at
// the left index is less than the pivot
do i := i + 1 while A[i] < pivot
// Move the right index to the left at least once and while the element at
// the right index is greater than the pivot
do j := j - 1 while A[j] > pivot
// If the indices crossed, return
if i >= j then return j
// Swap the elements at the left and right indices
swap A[i] with A[j]
由于 Hoare Partition 返回的值j
不一定是基准位置, 所以要使用递归的方式计算, 直到区间长度为1, 此时nums[n-k]
为第k大数.
- 若
j < n-k
, 那么第k
大在右分区, 递归调用partition(nums, j+1, right)
, - 若
j >= n-k
, 那么第k
大在左分区, 递归调用partition(nums, left, j)
, 注意: 和 lomuto 不同, 左分区包含j
自身; 所以左分区的条件一定是j >= n-k
func findKthLargest(nums []int, k int) int {
n := len(nums)
return hoarePartition(nums, 0, n-1, n-k)
}
func hoarePartition(nums []int, left, right, t int) int {
if left == right {
return nums[t]
}
pi := rand.Intn(right-left+1) + left
p := nums[pi]
i, j := left-1, right+1
for i < j {
for i++; nums[i] < p; {
i++
}
for j--; nums[j] > p; {
j--
}
if i < j {
nums[i], nums[j] = nums[j], nums[i]
}
}
if j < t {
return hoarePartition(nums, j+1, right, t)
} else {
return hoarePartition(nums, left, j, t)
}
}
2. 堆排序
因为需要求第K大的数, 那么结果是K个最大数字中最小的那个. 流程如下:
- 构建最小堆
- 遍历数组存入数字:
- 若长度小于k, 直接存储
- 若长度等于k, 比较堆顶元素, 若大于堆顶, 则入堆.
- 堆顶元素即位第K大数.
Golang 标准库可以使用heap
包实现堆的操作, 结构体需要实现heap.Interface
接口:
type Interface interface {
sort.Interface
Push(x any) // add x as element Len()
Pop() any // remove and return element Len() - 1.
}
sort.Interface
:
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with index i
// must sort before the element with index j.
//
// If both Less(i, j) and Less(j, i) are false,
// then the elements at index i and j are considered equal.
// Sort may place equal elements in any order in the final result,
// while Stable preserves the original input order of equal elements.
//
// Less must describe a transitive ordering:
// - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well.
// - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well.
//
// Note that floating-point comparison (the < operator on float32 or float64 values)
// is not a transitive ordering when not-a-number (NaN) values are involved.
// See Float64Slice.Less for a correct implementation for floating-point values.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
题解:
func findKthLargest(nums []int, k int) int {
// minIntHeap
h := &MinIntHeap{}
for _, n := range nums {
if h.Len() < k {
heap.Push(h, n)
} else if h.Len() == k {
if (*h)[0] < n {
heap.Pop(h)
heap.Push(h, n)
}
}
}
return (*h)[0]
}
type MinIntHeap []int
func (h *MinIntHeap) Len() int {
return len(*h)
}
func (h *MinIntHeap) Less(i, j int) bool {
return (*h)[i] < (*h)[j]
}
func (h *MinIntHeap) Swap(i, j int) {
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
}
func (h *MinIntHeap) Push(x any) {
*h = append(*h, x.(int))
}
func (h *MinIntHeap) Pop() any {
oldLen := len(*h)
val := (*h)[oldLen-1]
*h = (*h)[: oldLen-1]
return val
}
Reference
Powered by Waline v2.15.2