Top K 问题
问题描述
所谓 Top k 问题?简单来说就是在一组数据里面找到频率出现最高的前 k 个数,或前 k 大(当然也可以是前 k 小)的数。
解决方式 1
首先可以想到,用最简单的方式就是 sort 函数排序,返回前 k 个数。
ts
function inventoryManagement(stock: number[], k: number): number[] {
return stock.sort((a, b) => a - b).slice(0, k);
}
复杂度
时间复杂度:O(nlogn)
空间复杂度:O(logn)
解决方式 2
堆是一种非常常用的的数据结构。最大堆的性质:节点值大于子节点的值,堆顶元素是最大元素,利用这个性质,整体的算法流程如下:
- 创建大小为 k 的最大堆
- 将数组的前 k 个元素放入堆中
- 从下标的 k 继续开始依次遍历数组的剩余元素:
- 如果元素小于堆顶元素,那么取出堆顶元素,将当前元素入堆
- 如果元素大于/等于堆顶元素,不做操作
由于堆的大小是 k,空间复杂度是 O(k),时间复杂度是 O(nlogk)
js
const swap = (arr, i, j) => ([arr[i], arr[j]] = [arr[j], arr[i]]);
class MaxHeap {
constructor(arr = []) {
this.container = [];
if (Array.isArray(arr)) {
// 筛选最大值
arr.forEach(this.insert.bind(this));
}
}
// 插入元素排序 不是完整数组排序
insert(data) {
const { container } = this;
container.push(data);
// 从最后一个数值开始进行比较 执行后是最大堆
let index = container.length - 1;
while (index) {
// 找父元素
let parent = Math.floor((index - 1) / 2);
if (container[index] <= container[parent]) {
break;
}
swap(container, index, parent);
index = parent;
}
}
// 移除堆顶元素
extract() {
const { container } = this;
if (!container.length) {
return null;
}
swap(container, 0, container.length - 1);
const res = container.pop();
const length = container.length;
let index = 0, // 父节点
left = index * 2 + 1; // 左子节点
while (left < length) {
// 如果有右节点,并且右节点的值大于左节点的值
let right = index * 2 + 2;
if (right < length && container[right] > container[left]) {
left = right;
}
if (container[left] <= container[index]) {
break;
}
swap(container, left, index);
index = left;
left = index * 2 + 1;
}
return res;
}
// 获取堆顶元素
top() {
if (this.container.length) return this.container[0];
return null;
}
}
// 获取topk
const getLeastNumbers = (arr, k) => {
const length = arr.length;
if (k >= length) {
return arr;
}
// 创建大顶堆
const heap = new MaxHeap(arr.slice(0, k));
for (let i = k; i < length; ++i) {
if (heap.top() > arr[i]) {
heap.extract();
heap.insert(arr[i]);
}
}
return heap.container;
};
复杂度
时间复杂度:O(nlogk)
空间复杂度:O(k)
解决方式 3
利用快排的方式,主要是递归 + 分治的方式进行
代码讲解
快排首先找一个哨兵位,插入一个哨兵位置或者就是拿第一位位哨兵,所以比如一个数组 arr = [1,2,3,4,5] ,往队头塞一个数进去,让首位是个哨兵,然后后面的 (1, arr.length - 1) 才是真正的数据。
分为两个函数,一个是判断是否需要快排与基准位置,需要快排就执行左边与右边的划分递归函数,另一个是快排函数。
js
const inventoryManagement = function (stock, cnt) {
// 这里是放入哨兵的位置 后面的从1开始
stock.unshift(-1);
sorted(stock, 1, stock.length - 1);
return stock.slice(1, cnt + 1);
};
const quickSort = (arr, start, end) => {
// 设置哨兵
arr[0] = arr[start];
while (start < end) {
while (start < end && arr[end] >= arr[0]) {
end--;
}
// 如果小于哨兵,则赋值给start
arr[start] = arr[end];
while (start < end && arr[start] <= arr[0]) {
start++;
}
// 如果大于哨兵,则赋值给end
arr[end] = arr[start];
}
arr[start] = arr[0];
// 返回当前哨兵的位置
return start;
};
const sorted = (arr, strat, end) => {
if (start < end) {
const now = quickSort(arr, start, end);
sorted(arr, start, now - 1);
sorted(arr, now + 1, end);
}
};
复杂度
时间复杂度:O(nlogn)
空间复杂度:O(n)
TIP
综上第二种方式是最优解,时间复杂度最低,但是 1 和 3 的解决方式也更实用,根据实际需要选择最优解。