记录 1 道算法题
数组中的第 K 个大元素
ps:堆在另一篇文章有详细的介绍。不想重复写大家跳过去看吧。这道题用堆的代码会直接写在最后。
这道题最简单的方法就是排序之后返回下标为 k - 1 的元素。
function findKthLargest(nums, k) {
nums.sort((a, b) => b - a)
return nums[k - 1]
}
复制代码
这样似乎过于简单,我们可以手写一些实现,例如通过快排,或者堆来答题。
1. 快排
快排其实就是找一个基准点,然后把大于它的数放它左边,小于它的数放它右边。左边还是右边取决于你想升序还是降序。
这种把一个大的问题切割成一个个小问题的递归方式,叫做分治。递归的终止条件就是区间内只有一个元素,就直接返回了。
但是我们会发现其实只要排序保证前 k 个是降序的或者升序的,我们就知道第k大元素或第k小元素是什么。所以我们要的第k个数在基准点左边的时候,我们递归处理左边的数组,反之递归右边的数组,这样就减掉了一半的工作量。如果第k个刚好是基准点,就直接返回了不做任何递归操作。
[3, 2, 5, 6, 7]
假入我们的基准点是 5, 然后要第 2 大元素
我们只需要递归处理 3,2
复制代码
快排比较直观的写法是每次递归生成两个数组,然后返回的时候做拼接。例如:
// 伪代码
const left = []
const right = []
if (arr[i] > 基准点) {
right.push(arr[i])
} else {
left.push(arr[i])
}
return [...递归(left), 基准点, ...递归(right)]
复制代码
另一种空间复杂度为1的操作是直接在原数组上进行排序,但是怎么做节点的交换是个关键而且有难度的问题。也是这篇文章的重点之一。
这时候要指针出场了,而且为了方便交换,我们需要把基准点设置到数组的开头 arr[0] 上面。
先假定我们有一个交换的函数 swap
,传入数组和下标就会自动进行交换。
总是固定那数组的第一个作为基准点也行,随机取一个数做基准点也行,这里使用随机取一个数做基准点的做法。
首先,因为是在原数组上进行操作,所以我们只有区间的起始下标和终点下标,不一定是 0 和 arr.length - 1。
// 取这个区间的随机下标
let pivotIdx = Math.floor(Math.random() * (right - left + 1)) + left
const pivot = arr[pivotIdx]
swap(arr, left, pivotIdx)
pivotIdx = left
left += 1
/*
* 实现的效果是 a 为基准点, left 和 right 为排序的区间。
* [a, b, c, d, e, f]
* left right
*/
复制代码
然后,使用for循环遍历区间内的节点,比 a 大时进行交换,这时候我们需要有一个新的指针,他指着可以被交换的边界。这里我们可以用 left,因为他已经没有其他作用了。
当我们判断到比基准点大时,进行当前for循环的 i 节点与 left 的交换。交换完之后left++
,通过慢慢缩小的方法,确定了 left 之前的值比基准点大。因为一旦发现比基准点大就马上进行交换,所以分区就出现了。
另外,将决定升序或降序的条件抽取出来,规则和 sort 一样。这里用到了降序 compare = (a, b) => b - a
。
for(let i = left; i <= right; i++) {
if (compare(arr[i], pivot) < 0) {
swap(arr, i, left)
left++
}
}
// 当分割完之后,我们已知上一次的 left 是最后一次交换后自增1。
// 那 left - 1 就是基准点左边的第一个元素。假如 [4, 5, 6, 3, 2], 6是最后一次交换。
// 他和基准点进行交换,就刚好把基准点放入到正确的位置。 [6, 5, 4, 3, 2]
swap(arr, pivotIdx, left - 1)
// 然后返回基准点的下标
return left - 1
// 下面这些是升序排列的例子, 当比基准点小时进行交换
// [3, 4, 6, 2, 1]
/*
3 i 1 index 1 -- 4 < 3
4 i 2 index 1 -- 6 < 3
6 i 3 index 1 -- 2 < 3 --> index 2 [3, 2 , 6, 4, 1]
2 i 4 index 2 -- 1 < 3 --> index 3 [3, 2, 1, 4, 6]
1 i 5 break
基准点交换 [1, 2, 3, 4, 6]
2
*/
// [3,2,1,5,6,4] 2
/*
3 i index 1
2 i 1 index 1 2 < 3 --> index--2 [3, 2, 1, 5, 6, 4]
1 i 2 index 2 1 < 3 --> index--3 [3, 2, 1, 5, 6, 4]
5 i 3 index 3 5 < 3
6 i 4 index 3 6 < 3
4 i 5 index 3 4 < 3
基准点交换 0 2 [1, 2, 3, 5, 6, 4] return 2
*/
复制代码
分割完就要进行递归操作了。递归要做的事情很简单,重复对数组进行分割。这一步要实现减治,首先我们需要知道第 k 个元素的下标,所以作为函数的参数传进来。然后根据第k个元素的下标在基准点的左边还是右边,进行二选一的递归操作。
function quickSort(arr, k, left = 0, right = arr.length) {
const 基准点下标 = 分割(arr, left, right)
// 这里是判断到底取基准点左边的区间还是基准点右边的区间,但是都不包括基准点本身
if (基准点下标 < k) {
quickSort(arr, k, left, 基准点下标 - 1)
} else if (基准点下标 > k) {
quickSort(arr, k, 基准点下标 + 1, right)
}
}
复制代码
同时为了处理递归终止的条件,我们需要 left < right,他们不能相等,至少区间内有两个元素。
以上就是快排的解析,完整代码如下。
function findKthLargest(nums, k) {
quickSort(nums, k - 1)
return nums[k - 1]
}
function quickSort(arr, k, left = 0, right = arr.length - 1) {
if (left < right) {
const pivotIdx = partition(arr, left, right)
if (pivotIdx > k) {
quickSort(arr, k, left, pivotIdx - 1)
} else if (pivotIdx < k) {
quickSort(arr, k, pivotIdx + 1, right)
}
}
return arr
}
// 升序降序的判断
const compare = (a, b) => b - a
function partition(arr, left, right) {
let pivotIdx = Math.floor(Math.random() * (right - left + 1)) + left
const pivot = arr[pivotIdx]
swap(arr, pivotIdx, left)
pivotIdx = left
left = left + 1
for (let i = left; i <= right; i++) {
if (compare(arr[i], pivot) < 0) {
swap(arr, i, left)
left++
}
}
swap(arr, pivotIdx, left - 1)
return left - 1
}
function swap(arr, n1, n2) {
;[arr[n1], arr[n2]] = [arr[n2], arr[n1]]
}
复制代码
堆
再次ps:堆在另一篇文章有详细的介绍。不想重复写大家跳过去看吧。
这道题完整代码如下:
function findKthLargest(nums, k) {
const heap = new Heap()
for(let i = 0; i < nums.length; i++) {
// 堆顶是最大堆里面最小的元素,比他还小就直接跳过
// 最小堆则相反
if(heap.size() === k && heap.data[0] > val ) {
continue
}
heap.push(nums[i])
if (heap.size() > k) {
heap.pop()
}
}
return heap.data[0]
}
class Heap {
constructor() {
this.data = []
this.compare = (a, b) => a - b
}
size() {
return this.data.length
}
swap(n1, n2) {
const { data } = this
const temp = data[n1]
data[n1] = data[n2]
data[n2] = temp
}
push(val) {
this.data.push(val)
this.bubblingUp(this.size() - 1)
}
pop() {
if (this.size() === 0) return null
const { data } = this
const discard = data[0]
const newMember = data.pop()
if (this.size() > 0) {
data[0] = newMember
this.bubblingDown(0)
}
return discard
}
bubblingUp(index) {
while (index > 0) {
const parent = (index - 1) >> 1
const { data } = this
if (this.compare(data[index], data[parent]) < 0) {
this.swap(parent, index)
index = parent
} else {
break
}
}
}
bubblingDown(index) {
const { data } = this
const last = this.size() - 1
while (true) {
const left = index * 2 + 1
const right = index * 2 + 2
let parent = index
if (left <= last && this.compare(data[left], data[parent]) < 0) {
parent = left
}
if (right <= last && this.compare(data[right], data[parent]) < 0) {
parent = right
}
if (index !== parent) {
this.swap(index, parent)
index = parent
} else {
break
}
}
}
}
复制代码