/* topk 问题是一个经典的问题,我们维持一个大小为k的堆,如果要求最大的第三个数我们就维持小顶堆,反之维持大小为k的大顶堆 为什么?这有点反直觉,为什么求第k大的数要维持小顶堆,而不是大顶堆,这是因为第k大的数是数组中k个最大的数中最小的那一个, 用小顶堆来维持那么要求的第k个最大的数刚好在小顶堆的堆顶 */ const numbers = [5, 0, 8, 2, 1, 4, 7, 2, 5]; /** * * @param {number[]} nums * @param {number} k */ function topK(nums, k) { const heap = Array(k).fill(-Infinity); // 定义一个大小为k数组用来维护堆 for (let i = 0; i < nums.length; i++) { // 如果当前元素大于堆顶元素,替换堆顶元素然后下沉调整堆 if (nums[i] > heap[0]) { heap[0] = nums[i]; let cur = 0; // cur指向下沉元素的下标,一开始就在堆顶 while (true) { const left = cur * 2 + 1; const right = cur * 2 + 2; let smallest = cur; if (left < k && heap[left] < heap[smallest]) smallest = left; if (right < k && heap[right] < heap[smallest]) smallest = right; // 如果smallest 和cur相等,表示当前元素来到了合适的位置cur,结束下沉 if (smallest === cur) break; // 交换位置下沉 [heap[cur], heap[smallest]] = [heap[smallest], heap[cur]]; cur = smallest; } } } // 返回堆顶元素 return heap[0]; } /** * 使用小顶堆找出数组中第 K 大的数 * @param {number[]} nums * @param {number} k * @returns {number} */ function topKbest(nums, k) { const heap = nums.slice(0, k); // 先取前 k 个元素构建小顶堆 // 下沉函数(维护堆性质) function siftDown(heap, start, heapSize) { let cur = start; while (true) { const left = 2 * cur + 1; const right = 2 * cur + 2; let smallest = cur; if (left < heapSize && heap[left] < heap[smallest]) smallest = left; if (right < heapSize && heap[right] < heap[smallest]) smallest = right; if (smallest === cur) break; [heap[cur], heap[smallest]] = [heap[smallest], heap[cur]]; cur = smallest; } } // 构建小顶堆(自底向上建堆) for (let i = Math.floor(k / 2) - 1; i >= 0; i--) { siftDown(heap, i, k); } // 遍历剩余元素,维护一个大小为 k 的小顶堆 for (let i = k; i < nums.length; i++) { if (nums[i] > heap[0]) { heap[0] = nums[i]; siftDown(heap, 0, k); } } // 堆顶即为第 k 大元素 return heap[0]; } /* 若题目没有要求动态的维护这个topk,那么可以使用quick-select来实现,O(n)的时间复杂度完成这个题目。 思路:题目要求我们找出第k大的数,那么当整个数组排序之后,倒数第k个数不就是我们要求的吗?比如[5,4,3,2,1] k = 2,那么排序后[1,2,3,4,5]中 倒数第二个数的下标就是nums.length - k = 5 - 2 = 3 nums[3]恰好就是我们要找的这个数,我们没有必要对整个数组排序,我们只要找到排序后这个 数正确的位置即可,知道快速排序的话,我们可以使用partition来确定pivot,pivot在partition之后就已经确认,如果pivot的下标等于倒数第k个位置 那么pivot就是排序之后的倒数第k个数,也就是topk */ function f2(nums, k) { return quickSelect(nums, 0, nums.length - 1, nums.length - k); } function quickSelect(nums, left, right, n) { const p = partition(nums, left, right); if (p === n) return nums[n]; return p < n ? quickSelect(nums, p + 1, right, n) : quickSelect(nums, left, p - 1, n); } /** * 分区函数,返回pivot,pivot 左边的数都不大于,右边的数都不小于它 * @param {*} nums 要操作的数组 * @param {*} left 开始位置,左边界 * @param {*} right 结束位置, 右边界 * @returns number 返回的piivot */ function partition(arr, left, right) { const pivot = arr[right]; let p = left; for (let i = left; i < right; i++) { if (arr[i] <= pivot) { // [arr[i], arr[p]] = [arr[p], arr[i]]; swap(arr, i, p); p++; } } // [arr[p], arr[right]] = [arr[right], arr[p]]; swap(arr, p, right); return p; } function swap(nums, a, b) { const tmp = nums[a]; nums[a] = nums[b]; nums[b] = tmp; }