这题其实只是要先计数,剩下的只是排序,与 LeetCode215 数组中的第K个最大元素 类似。
// javascript
var topKFrequent = function(nums, k) {
let res = new Array();
const occurrences = new Map();
for (const n of nums) {
occurrences.set(n, (occurrences.get(n) || 0) + 1);
}
// 桶排序
// 将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
const list = new Array(nums.length + 1);
for (const [num, count] of occurrences.entries()) {
// 获取出现的次数作为下标
if (list[count] === undefined) list[count] = new Array();
list[count].push(num);
}
// 倒序遍历数组获取出现顺序从大到小的排列
for (let i = list.length - 1; i >= 0 && res.length < k; i--) {
if (list[i] !== undefined) {
res = res.concat(list[i]); // res 有被改动,不能声明为 const
}
}
return res;
};
// javascript
var topKFrequent = function(nums, k) {
const occurrences = new Map();
for (const n of nums) {
occurrences.set(n, (occurrences.get(n) || 0) + 1);
}
const queue = new MinPriorityQueue();
for (let [currNum, currPriority] of occurrences.entries()) {
if (queue.size() < k) {
queue.enqueue(currNum, currPriority);
}
else {
let frontEntry = queue.front();
if (frontEntry.priority < currPriority) {
queue.dequeue();
queue.enqueue(currNum, currPriority);
}
}
}
return queue.toArray().map(el => el.element);
};
调用了 JS 的小顶堆 API,详细用法见:priority-queue/README.md
手写小顶堆:
// javascript
var topKFrequent = function(nums, k) {
const occurrences = new Map();
for (const n of nums) {
occurrences.set(n, (occurrences.get(n) || 0) + 1);
}
const heap = Array.from(occurrences);
// 构建大小为k的小顶堆
buildMinHeap(heap, k);
for (let i = k; i < heap.length; i++) {
if (heap[i][1] > heap[0][1]) {
swap(heap, i, 0);
minHeapify(heap, 0, k);
}
}
return heap.slice(0, k).map(el => el[0]);
};
// 原地建堆,从后往前,自上而下式建小顶堆
const buildMinHeap = (heap, size) => {
// 从最后一个非叶子节点开始,自下而上堆化
for(let i = (size >> 1) - 1; i >= 0; i--) {
minHeapify(heap, i, size);
}
}
// 堆化
const minHeapify = (heap, i, size) => {
let minIndex = i;
const leftIndex = (i << 1) + 1, rightIndex = (i << 1) + 2;
if (leftIndex < size && heap[leftIndex][1] < heap[minIndex][1]) minIndex = leftIndex;
if (rightIndex < size && heap[rightIndex][1] < heap[minIndex][1]) minIndex = rightIndex;
if (minIndex !== i) {
swap(heap, minIndex, i);
minHeapify(heap, minIndex, size);
}
};
// 交换
const swap = (heap, i , j) => {
[heap[i], heap[j]] = [heap[j], heap[i]];
}
或者封装成一个类:
// javascript
class MinHeap {
constructor() {this.heap = [];}
swap(i, j) {[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];} // 交换节点
getParentIndex(i) {return ((i - 1) >> 1);} // 获取父节点, 2进制操作,取商: Math.floor((i - 1) / 2)
getLeftIndex(i) {return (i << 1) + 1;}
getRightIndex(i) {return (i << 1) + 2;}
shiftUp(index) { // 上移
if (index == 0) return;
const parentIndex = this.getParentIndex(index);
// 父节点大于当前节点
if (this.heap[parentIndex] && this.heap[parentIndex].priority > this.heap[index].priority) {
this.swap(parentIndex, index);
this.shiftUp(parentIndex);
}
}
shiftDown(index) { // 下移
let minIndex = index;
const leftIndex = this.getLeftIndex(index);
const rightIndex = this.getRightIndex(index);
if (this.heap[leftIndex] && this.heap[leftIndex].priority < this.heap[minIndex].priority) {
minIndex = leftIndex;
}
if (this.heap[rightIndex] && this.heap[rightIndex].priority < this.heap[minIndex].priority) {
minIndex = rightIndex;
}
if (minIndex !== index) {
this.swap(minIndex, index);
this.shiftDown(minIndex);
}
}
// 将值插入堆的底部,即数组的尾部。
// 然后_上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值
// 大小为k的堆中插入元素的时间复杂度为O(logK)
enqueue(value) {
this.heap.push(value);
this.shiftUp(this.size() - 1);
}
// 删除堆顶
// 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)。
// 然后下移: 将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶。
// 大小为k的堆中删除堆顶的时间复杂度为O(logk)。
dequeue() {
// 因为下面的操作会先 enqueue 再 dequeue 所以不会出现 heap 大小为 1 的情况
this.heap[0] = this.heap.pop();
this.shiftDown(0);
}
front() {return this.heap[0];}
size() {return this.heap.length;}
}
var topKFrequent = function(nums, k) {
const occurrences = new Map();
for (const n of nums) {
occurrences.set(n, (occurrences.get(n) || 0) + 1);
}
const h = new MinHeap();
for (const [element, priority] of occurrences.entries()) {
h.enqueue({priority, element});
if (h.size() > k) {
h.dequeue();
}
}
return h.heap.map(el => el.element);
};
// javascript
var topKFrequent = function(nums, k) {
const occurrences = new Map();
for (const n of nums) {
occurrences.set(n, (occurrences.get(n) || 0) + 1);
}
const list = Array.from(occurrences);
buildMaxHeap(list);
for (let i = list.length - 1; i >= list.length - k; i--) {
swap(list, 0, i);
maxHeapify(list, 0, i);
}
return list.slice(list.length - k).map(element => element[0]);
};
const buildMaxHeap = (list) => {
for (let i = (list.length >> 1) - 1; i >= 0; i--) {
maxHeapify(list, i, list.length);
}
};
const maxHeapify = (list, i, size) => {
let maxIndex = i, l = (i << 1) + 1, r = (i << 1) + 2;
if (l < size && list[l][1] > list[maxIndex][1]) maxIndex = l;
if (r < size && list[r][1] > list[maxIndex][1]) maxIndex = r;
if (maxIndex !== i) {
swap(list, i, maxIndex);
maxHeapify(list, maxIndex, size);
}
};
const swap = (list, i, j) => {
[list[i], list[j]] = [list[j], list[i]];
};
时间复杂度: O ( k l o g n ) O(klogn) O(klogn),n 是数组中不同数字的数量,堆中有 n 个元素,移除堆顶的时间是 logn,重复操作了 k 次。
空间复杂度: O ( n ) O(n) O(n),n 是数组中不同数字的数量,哈希表和堆的空间。
// javascript
var topKFrequent = function (nums, k) {
// 统计出现次数
const occurrences = new Map();
for (const n of nums) {
occurrences.set(n, (occurrences.get(n) || 0) + 1);
}
const list = Array.from(occurrences);
quickSelect(list, 0, list.length - 1, k);
return list.slice(0, k).map(element => element[0]);
};
var partition = function(list, left, right) {
// Math.random() * (right - left + 1) => [0, right - left + 1)
// Math.floor([0, right - left + 1)) => 0, 1, 2, ..., right - left
// left + Math.floor([0, right - left + 1)) => left, left + 1, ..., right
const pivot = list[left];
let i = left, j = right;
while (i < j) {
while (i < j && list[j][1] <= pivot[1]) j--; // 从后往前,找到比pivot更大的数
list[i] = list[j]; // 将更大的数放入左边
while (i < j && list[i][1] >= pivot[1]) i++; // 从前往后,找到比pivot更小的数
list[j] = list[i]; // 将更小的数放入右边
}
list[i] = pivot; // 将pivot放入最终位置
return i;
};
var quickSelect = function(list, left, right, k) {
if (left >= right) return;
let index = partition(list, left, right);
// index - left = k - 1 时,因为题目说可以按任意顺序返回答案,所以不需要再排序
// index - left > k - 1 时,前 k 个高频元素在左边
if (index - left > k - 1) quickSelect(list, left, index - 1, k);
// index - left < k - 1 时,前 index - left + 1 个高频元素在左边
// 要去右边寻找剩下的前 k - (index - left + 1) 个高频元素
if (index - left < k - 1) quickSelect(list, index + 1, right, k - (index - left + 1));
};