From 7eb11b0136eb12ff7409ee086dd7de3f18c46ec1 Mon Sep 17 00:00:00 2001 From: LouisFonda Date: Sun, 25 May 2025 21:33:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A0=86=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E7=9B=B8=E5=85=B3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- heap/76数组中第k个最大的元素.js | 41 ++++++++++++++++ heap/base/topk问题.js | 86 +++++++++++++++++++++++++++++++++ heap/base/原地构建.js | 75 ++++++++++++++++++++++++++++ heap/base/逐个插入.js | 35 ++++++++++++++ 4 files changed, 237 insertions(+) create mode 100644 heap/76数组中第k个最大的元素.js create mode 100644 heap/base/topk问题.js create mode 100644 heap/base/原地构建.js create mode 100644 heap/base/逐个插入.js diff --git a/heap/76数组中第k个最大的元素.js b/heap/76数组中第k个最大的元素.js new file mode 100644 index 0000000..1126bbd --- /dev/null +++ b/heap/76数组中第k个最大的元素.js @@ -0,0 +1,41 @@ +/** + * @param {number[]} nums + * @param {number} k + * @return {number} + */ +const findKthLargest = function (nums, k) { + // 将数组的前k个元素作为堆,然后将其堆化 + const heap = nums.slice(0, k); + // 从最后一个非叶子节点遍历,自底向上下沉堆化 + for (let i = Math.floor(k / 2 - 1); i >= 0; i--) { + siftDown(heap, i, k); + } + // 处理剩余元素 + for (let i = k; i < nums.length; i++) { + // 如果剩余元素大于堆顶元素,替换然后下沉维护堆 + if (nums[i] > heap[0]) { + heap[0] = nums[i]; + siftDown(heap, 0, k); + } + } + + return heap[0]; +}; + +// 下沉函数(维护堆性质) +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; + } +} diff --git a/heap/base/topk问题.js b/heap/base/topk问题.js new file mode 100644 index 0000000..a983205 --- /dev/null +++ b/heap/base/topk问题.js @@ -0,0 +1,86 @@ +/* +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]; +} diff --git a/heap/base/原地构建.js b/heap/base/原地构建.js new file mode 100644 index 0000000..5128a4a --- /dev/null +++ b/heap/base/原地构建.js @@ -0,0 +1,75 @@ +const numbers = [5, 0, 8, 2, 1, 4, 7, 2, 5, -1]; // 测试用例 + +const vaild = [-1, 0, 4, 2, 1, 8, 7, 5, 5, 2]; // 验证用例 + +/** + * 自底向上下沉 + * @param {*} nums + */ +function buildHeap(nums) { + const n = nums.length; + // 寻找最后一个非叶子节点,从这里开始倒序遍历(自底向上) + for (let i = Math.floor(n / 2 - 1); i >= 0; i--) { + // 取当前节点左右节点较小的 + let min = i * 2 + 1; + if (i * 2 + 2 < n && nums[i * 2 + 2] < nums[min]) min = i * 2 + 2; + let cur = i; // 表示当前节点下沉的位置 + + // 如果父节点大于子节点中较小的哪一个那么就交换位置,如果交换位置之后还是大于当前位置的子节点小的那个,继续下沉 + while (min < n && nums[cur] > nums[min]) { + [nums[cur], nums[min]] = [nums[min], nums[cur]]; + cur = min; + min = cur * 2 + 1; + if (cur * 2 + 2 < n && nums[cur * 2 + 2] < nums[min]) min = cur * 2 + 2; + } + } +} + +/** + * 原地构建小顶堆(heapify),使用下沉(sift down)法 + * @param {number[]} nums 完全二叉树的层序遍历(原始数组) + * @returns {number[]} 构建完成的小顶堆(原地修改 nums 并返回) + */ +function buildHeapBest(nums) { + const n = nums.length; + + // 从最后一个非叶子节点开始,依次向前,对每个节点执行下沉 + for (let i = Math.floor(n / 2) - 1; i >= 0; i--) { + siftDown(nums, i, n); + } + + return nums; +} + +/** + * 对 nums[i] 进行下沉调整,确保以 i 为根的子树满足小顶堆性质 + * @param {number[]} nums 堆数组 + * @param {number} i 当前要下沉的节点索引 + * @param {number} n 数组长度(限制范围)yiw + */ +function siftDown(nums, i, n) { + let cur = i; + + while (true) { + const left = 2 * cur + 1; + const right = 2 * cur + 2; + let smallest = cur; + + // 与左孩子比较 + if (left < n && nums[left] < nums[smallest]) { + smallest = left; + } + + // 与右孩子比较 + if (right < n && nums[right] < nums[smallest]) { + smallest = right; + } + + // 如果当前节点已经是最小的,堆性质满足,退出循环 + if (smallest === cur) break; + + // 否则交换并继续下沉 + [nums[cur], nums[smallest]] = [nums[smallest], nums[cur]]; + cur = smallest; + } +} diff --git a/heap/base/逐个插入.js b/heap/base/逐个插入.js new file mode 100644 index 0000000..5370500 --- /dev/null +++ b/heap/base/逐个插入.js @@ -0,0 +1,35 @@ +const numbers = [5, 0, 8, 2, 1, 4, 7, 2, 5, -1]; // 测试用例 + +const vaild = [-1, 0, 4, 2, 1, 8, 7, 5, 5, 2]; // 验证用例 + +/** + * 使用逐个插入法构造小顶堆 + * @param {number[]} nums 需要处理的完全二叉树,用数组表示(层序遍历) + * @returns {number[]} 返回一个小顶堆的层序遍历 + */ +function buildHeap(nums) { + const heap = []; + + for (let i = 0; i < nums.length; i++) { + heap.push(nums[i]); + let cur = heap.length - 1; + let parent = Math.floor((cur - 1) / 2); + + while (cur > 0 && heap[cur] < heap[parent]) { + [heap[cur], heap[parent]] = [heap[parent], heap[cur]]; + cur = parent; + parent = Math.floor((cur - 1) / 2); + } + } + + return heap; +} + +function test(nums1, nums2) { + for (let i = 0; i < nums1; i++) { + if (nums1[i] !== nums2[i]) return false; + } + return true; +} + +console.log(test(buildeap(numbers), vaild));