# 前 K 个高频元素 (opens new window)
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
输入: nums = [1], k = 1 输出: [1]
- 1 <= nums.length <= 105
- k 的取值范围是 [1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
注意:你所设计算法的时间复杂度 必须 优于 O(n*log n)
,其中 n 是数组大小。
# 思路分析
首先,题目要求出现频率,那么,我们自然会想到建立哈希表,来统计每个整数出现的次数,在这儿,至少会有个遍历,那么这儿的时间复杂度是 O(n)
,空间复杂度 O(n)
;
接下来就是比较关键的问题了,频率求出来了,我们要求 TOP K,有个问题就是最快的排序算法至少是O(n*log n)
,所以通过排序求肯定是无法满足性能要求的。那么,在这种想有序,但是又不能排序的场景下,我们自然而然会想到堆
,但是是采用最大堆还是最小堆呢,我们要取的是前面 K 个,堆的大小是比数组小的,那必须是最小堆,因为这样你才能知道当前元素是否应该入堆。
假设当前堆的内容还没有达到 K,随便加入内容,如果达到了,要判断一下当前堆顶的元素是比当前元素大还是小,如果比当前元素小,说明当前元素才应该进去,堆顶的元素应该丢弃,否则保持堆不动。
因为堆内的元素在最终输出的时候是能保证有序性的,所以说,我们就遍历这个记录次数的对象(最坏情况是每个元素都只出现一次),因此外层循环是 O(N)
,堆在插入的时候,建堆的操作是基于二分查找的,所以其时间复杂度是 O(logK)
,因为最多 K 个元素,因此空间复杂度是O(K)
,因为两个套在一起使用的,所以时间复杂度是 O(N*logK)
。
最后再将堆里面的内容全部输出,这儿需要牵涉到堆的调整,已知数组能够在线性的时间复杂度内调整成堆,那么这儿的时间复杂度是O(K)
;
总的时间复杂度就是O(K)
+ O(N*logK)
,去掉小的O(K)
,时间复杂度是O(N*logK)
; 空间复杂度是: O(K)
+O(N)
,去掉小的O(K)
,空间复杂度是O(N)
。
# 算法实现
/*
* 抽象堆
*/
class Heap {
/**
* 定义一个存储数据的内存空间
*/
_data = [];
/**
* 比较函数
*/
_compare = null;
get size() {
return this._data.length;
}
constructor(...nums) {
// 初始化数组元素
nums.forEach((v) => {
this._data.push(v);
});
this.buildHeap();
}
_selfCompare = (compareVal, currentVal) => {
if(typeof this._compare !== 'function') {
throw `cannot compare without a compare function`;
}
return this._compare(compareVal, currentVal);
};
/**
* 设置比较函数
* @param {(compareVal, currentVal) => boolean} compareFunc
*/
setCompare(compareFunc) {
this._compare = compareFunc;
}
/**
* 获取堆顶的元素
*/
getTop() {
return this._data[0];
}
/**
* 按断堆是否是空
*/
isEmpty() {
return this._data.length === 0;
}
/**
* 向堆中插入一个合法值
* @param {number} val
*/
insert(val) {
// 如果当前数组没有元素,直接插入即可
if (this._data.length === 0) {
this._data[0] = val;
} else {
// 让i指向当前新位置,因为没有哨兵的关系,最后一个元素是length - 1,所以新位置就是length
let i = this.size;
while (this._selfCompare(val, this._data[Math.floor(i / 2)]) && i > 0) {
this._data[i] = this._data[Math.floor(i / 2)];
i = Math.floor(i / 2);
}
// 在合适的位置插入新的值
this._data[i] = val;
}
}
/**
* 获取堆中的最小元素
* @returns
*/
deleteMin() {
if (this.isEmpty()) {
console.warn("can not delete max from empty heap");
return;
}
// 取出堆顶的元素
let minVal = this._data[0];
let temp = this._data[this.size - 1];
this._data[0] = temp;
// JavaScript语言需要进行这一步,让数组的规模缩小,释放空间
this._data.length--;
// 如果当前堆里面还存在元素的话,将
this.percDown(0);
return minVal;
}
/**
* 下滤:将堆中以堆data[p]为根的子堆调整为最小堆
* @param {number} p 根节点索引
*/
percDown(p) {
// 如果当前堆元素小于1个,就不执行调整操作
if (this.size <= 1) {
return;
}
let parent, child;
/* 取出根结点存放的值 */
let temp = this._data[p];
for (parent = p; parent * 2 < this.size; parent = child) {
child = parent * 2;
if (
child + 1 < this.size &&
this._selfCompare(this._data[child + 1], this._data[child])
) {
child++; /* child指向左右子结点的较大者 最大堆 较小者 最小堆 */
}
/* 找到了合适位置 */
// 注意这儿一定要取得等号 temp <= this.#data[child] 最小堆 temp >= this.#data[child] 最大堆
if (this._selfCompare(temp, this._data[child])) {
break;
} else {
/* 下滤X */
this._data[parent] = this._data[child];
}
}
this._data[parent] = temp;
}
/**
* 构建最小堆
*/
buildHeap() {
/* 调整data中的元素,使满足最小堆的有序性 */
/* 这里所有size个元素已经存在data[]中 */
/* 从最后一个结点的父节点开始,到根结点1 */
for (let i = Math.floor(this.size / 2); i > 0; i--) {
this.percDown(i);
}
}
}
/**
* 求TOP K个高频元素
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function (nums, k) {
const map = Object.create(null);
// 建立出现频率的哈希表
nums.forEach((num) => {
if (typeof map[num] === "undefined") {
map[num] = 1;
} else {
map[num]++;
}
});
// 将哈希表转成出现频率数组
let frequentNumbers = [];
Object.entries(map).forEach(([prop, value]) => {
frequentNumbers.push({
prop: Number.parseInt(prop),
value,
});
});
let minHeap = new Heap();
minHeap.setCompare((compareVal, currentVal) => {
return currentVal.frequent - compareVal.frequent >=0;
});
// 遍历频率建立哈希
frequentNumbers.forEach(({ prop, value: frequent }) => {
if (minHeap.size < k) {
minHeap.insert({
prop,
frequent,
});
} else {
// 获取堆顶的最小值
let { frequent: minFrequent } = minHeap.getTop();
// 如果堆顶的小,说明当前元素才应该进堆,堆顶元素应该丢弃
if (minFrequent < frequent) {
minHeap.deleteMin();
minHeap.insert({
prop,
frequent,
});
}
}
});
// 依次输出堆顶元素
let results = [];
while (!minHeap.isEmpty()) {
let { prop } = minHeap.deleteMin();
results.push(prop);
}
return results;
};
# 算法复杂度
时间复杂度:O(N*logK)
;
空间复杂度:O(N)