优先队列是抽象数据类型,堆是具体的实现。在一些编程语言的实现中,二者不加区分。
优先队列
实现一个优先队列并不难。把所有的待排序的元素放在数组里,有以下两种方案实现优先队列的功能:
- 有序数组:入队时维护有序性。出队的时候,以𝑂(1)的时间复杂度找到优先级最高的元素;
- 无序数组:不维护优先队列的顺序,在出队的时候遍历数组(时间复杂度𝑂(𝑁)),选出优先级最高的元素。
优先队列实现 | 入队 | 出队 |
---|---|---|
有序数组 | O(N) | O(1) |
无序数组 | O(1) | O(N) |
如果不改变线性的数据存储的方式,就不能突破时间复杂度为 𝑂(𝑁) 的限制 。因此考虑「堆」这种更高效的数据结构来平衡「入队」和「出队」的时间复杂度。
完全二叉树和满二叉树
完全二叉树首先是一棵二叉树(每个结点最多有 2 棵子树)。「完全」的意思是:在二叉树的基础上,从上至下、从左至右没有空结点。
如果 完全二叉树 的最后一层没有空结点,此时完全二叉树称为 满二叉树 。将满二叉树每一层结点的个数依次排列开来,是一个 等比数列。
数组表示完全二叉树
完全二叉树有个重要的性质:它可以使用数组表示。这是因为按照从上至下、从左至右的顺序给完全二叉树编号,任意结点的父亲结点和子结点的下标是有规律的:
- 根据父亲结点的下标,可以访问到它的两个子结点;
- 根据子结点的下标,可以访问到它的唯一的父亲结点。
使用数组存放二叉树的优点是:不用维护左右子结点、父结点的引用关系。
从下标 0 开始存储数据
从下标 1 开始存储数据
堆有序
「最大堆」是完全二叉树,任意某个结点的值,不大于其父亲结点的值。这是一种特殊意义下的 有序 的树形结构,称之为「堆有序」。在这种定义下,树根结点这个元素一定是这棵二叉树中值最大的结点。
同理,可以定义「最小堆」:
最大堆的抽象数据类型
方法名 | 描述 |
---|---|
MaxHeap() | 初始化优先队列 |
isEmpty() | 队列是否为空 |
size() | 返回优先队列中元素的个数 |
offer(x) | 向队列添加一个元素 |
poll() | 将一个元素出队 |
peek() | 返回队首元素 |
replace(x); | 将当前队首元素替换成为 x |
实现
js
class MaxHeap {
constructor() {
this.size = 0;
this.data = [];
}
offer(item) {
this.size++;
this.data[this.size] = item;
this.siftUp(this.size);
}
poll() {
if (this.isEmpty()) return false;
const result = this.data[1];
this.data[1] = this.data[this.size];
this.siftDown(1);
return result;
}
replace(item) {
this.data[1] = item;
this.siftDown(1);
}
siftUp(k) {
while (k > 1 && this.data[Math.floor(k / 2)] > this.data[k]) {
let j = Math.floor(k / 2);
const temp = this.data[k];
this.data[k] = this.data[j];
this.data[j] = temp;
k = j;
}
}
siftDown(k) {
const temp = this.data[k];
while (2 * k <= this.size) {
let j = 2 * k;
if (j + 1 <= this.size && this.data[j] < this.data[j + 1]) {
j++;
}
if (temp > this.data[j]) break;
this.data[k] = this.data[j];
k = j;
}
this.data[k] = temp;
}
peek() {
if (this.isEmpty()) return false;
return this.data[1];
}
size() {
return this.size;
}
isEmpty() {
return this.size === 0;
}
}
将数组整理成堆
自顶向下
从这个二叉堆的第 22 个结点开始,依次执行 siftUp
操作即可。
自底向上
自底向上依次执行 siftDown 操作,向下调整。一下子把整棵树一半以上的元素都跳过 ,从第1 个非叶子结点处开始执行 siftDown。
从执行「上浮」或者「下沉」的次数来看,自底向上少了很多操作。在完全二叉树中,叶子结点的个数几乎占了整棵树结点总数的一半。因此 有一半以上的结点数都不用操作。叶子结点不操作,并不意味着它们的位置不会调整,它们会在以后的 siftDown 过程中逐渐调整位置;
Tips: 从 1 开始编号的堆最后一个非叶子结点的下标是 size / 2
练习
排序数组 - 中等
js
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function (nums) {
let len = nums.length;
heapify(nums);
for (let i = len - 1; i >= 1; ) {
swap(nums, 0, i);
i--;
siftDown(nums, 0, i);
}
return nums.reverse();
};
function swap(arr, idx1, idx2) {
const temp = arr[idx1];
arr[idx1] = arr[idx2];
arr[idx2] = temp;
}
function heapify(arr) {
for (let i = Math.floor((arr.length - 1) / 2); i >= 0; i--) {
siftDown(arr, i, arr.length - 1);
}
}
function siftDown(arr, k, end) {
while (2 * k + 1 <= end) {
let j = 2 * k + 1;
if (j + 1 <= end && arr[j + 1] < arr[j]) {
j++;
}
if (arr[k] <= arr[j]) {
break;
}
swap(arr, k, j);
k = j;
}
}
合并 K 个排序链表 - 困难
js
/*
* @lc app=leetcode.cn id=23 lang=javascript
*
* [23] 合并 K 个升序链表
*/
// @lc code=start
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
class MinHeap {
constructor() {
this.size = 0;
this.data = new Array();
}
// 交换两个元素位置
swap(idx1, idx2) {
const temp = this.data[idx1];
this.data[idx1] = this.data[idx2];
this.data[idx2] = temp;
}
// 入队
offer(item) {
this.size++;
this.data[this.size] = item;
this.siftUp(this.size);
}
siftUp(k) {
while (k > 1 && this.data[k].val < this.data[Math.floor(k / 2)].val) {
this.swap(k, Math.floor(k / 2));
k = Math.floor(k / 2);
}
}
// 出队
poll() {
if (this.isEmpty()) return false;
const result = this.data[1];
this.data[1] = this.data[this.size];
this.size--;
this.siftDown(1);
return result;
}
siftDown(k) {
while (2 * k <= this.size) {
let j = 2 * k;
if (j + 1 <= this.size && this.data[j + 1].val < this.data[j].val) {
j++;
}
if (this.data[k].val <= this.data[j].val) {
break;
}
this.swap(k, j);
k = j;
}
}
isEmpty() {
return !this.size;
}
}
/**
* @param {ListNode[]} lists
* @return {ListNode}
*/
var mergeKLists = function (lists) {
const queue = new MinHeap();
for (const list of lists) {
let curNode = list;
while (curNode !== null) {
queue.offer(new ListNode(curNode.val));
curNode = curNode.next;
}
}
let dummyHead = new ListNode(0);
let current = dummyHead;
while (!queue.isEmpty()) {
current.next = queue.poll();
current = current.next;
}
return dummyHead.next;
};
// @lc code=end
数组中的第K个最大元素 - 中等
js
/*
* @lc app=leetcode.cn id=215 lang=javascript
*
* [215] 数组中的第K个最大元素
*/
// @lc code=start
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function (nums, k) {
const len = nums.length;
if (len === 1) return nums[0];
const heap = new maxHeap();
let i = 0;
while (i < len) {
heap.offer(nums[i]);
i++;
}
let j = k;
let result;
while (j > 0) {
result = heap.poll();
j--;
}
return result;
};
class maxHeap {
constructor() {
this.size = 0;
this.data = new Array();
}
offer(item) {
this.data[this.size + 1] = item;
this.size++;
this.siftUp(this.size);
}
poll() {
if (this.isEmpty()) return false;
const result = this.data[1];
this.data[1] = this.data[this.size];
this.size--;
this.siftDown(1);
return result;
}
isEmpty() {
return this.size === 0;
}
siftUp(k) {
while (k > 1 && this.data[k] > this.data[Math.floor(k / 2)]) {
const temp = this.data[k];
this.data[k] = this.data[Math.floor(k / 2)];
this.data[Math.floor(k / 2)] = temp;
k = Math.floor(k / 2);
}
}
siftDown(k) {
const temp = this.data[k];
while (2 * k <= this.size) {
let j = 2 * k;
if (j + 1 <= this.size && this.data[j] < this.data[j + 1]) {
j++;
}
if (temp > this.data[j]) break;
this.data[k] = this.data[j];
k = j;
}
this.data[k] = temp;
}
}
前K个高频元素 - 中等
js
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function (nums, k) {
const map = new Map();
nums.forEach((el) => {
if (map.get(el)) {
map.set(el, map.get(el) + 1);
} else {
map.set(el, 1);
}
});
const heap = new MinHeap(map);
map.forEach((value, key) => {
if (heap.size < k) {
heap.offer(key);
} else if (value > map.get(heap.peek())) {
heap.poll();
heap.offer(key);
}
});
const res = new Array();
while (!heap.isEmpty()) {
res.push(heap.poll());
}
return res;
};
class MinHeap {
constructor(map) {
this.size = 0;
this.data = new Array();
this.map = map;
}
offer(item) {
this.data[this.size + 1] = item;
this.size++;
this.siftUp(this.size);
}
poll() {
if (this.isEmpty()) return false;
const result = this.data[1];
this.data[1] = this.data[this.size];
this.size--;
this.siftDown(1);
return result;
}
isEmpty() {
return this.size === 0;
}
siftUp(k) {
while (
k > 1 &&
this.map.get(this.data[k]) < this.map.get(this.data[Math.floor(k / 2)])
) {
const temp = this.data[k];
this.data[k] = this.data[Math.floor(k / 2)];
this.data[Math.floor(k / 2)] = temp;
k = Math.floor(k / 2);
}
}
siftDown(k) {
const temp = this.data[k];
while (2 * k <= this.size) {
let j = 2 * k;
if (
j + 1 <= this.size &&
this.map.get(this.data[j]) > this.map.get(this.data[j + 1])
) {
j++;
}
if (this.map.get(temp) < this.map.get(this.data[j])) break;
this.data[k] = this.data[j];
k = j;
}
this.data[k] = temp;
}
peek() {
if (this.isEmpty()) return false;
return this.data[1];
}
}
数据流的中位数 - 困难
js
var MedianFinder = function () {
this.minHeap = new Heap(0);
this.maxHeap = new Heap(1);
this.size = 0;
};
/**
* @param {number} num
* @return {void}
*/
MedianFinder.prototype.addNum = function (num) {
// 奇数
if (this.size % 2) {
if (this.maxHeap.size > 0) {
if (this.maxHeap.peek() > num) {
this.minHeap.offer(this.maxHeap.poll());
this.maxHeap.offer(num);
this.size++;
return;
}
}
this.minHeap.offer(num);
// 偶数
} else {
if (this.minHeap.size > 0) {
if (this.minHeap.peek() < num) {
this.maxHeap.offer(this.minHeap.poll());
this.minHeap.offer(num);
this.size++;
return;
}
}
this.maxHeap.offer(num);
}
this.size++;
};
/**
* @return {number}
*/
MedianFinder.prototype.findMedian = function () {
if (this.size === 1) return this.maxHeap.peek();
// 奇数
if (this.size % 2) {
return this.maxHeap.peek();
// 偶数
} else {
return ((this.maxHeap.peek() + this.minHeap.peek()) / 2).toFixed(1);
}
};
class Heap {
// type=0为最小堆,type=1为最大堆
constructor(type) {
this.size = 0;
this.data = [];
this.type = type;
}
offer(item) {
this.size++;
this.data[this.size] = item;
this.siftUp(this.size);
}
poll() {
if (this.isEmpty()) return false;
const result = this.data[1];
this.data[1] = this.data[this.size];
this.siftDown(1);
this.size--;
return result;
}
siftUp(k) {
if (this.type) {
while (k > 0 && this.data[Math.floor(k / 2)] < this.data[k]) {
this.swap(Math.floor(k / 2), k);
k = Math.floor(k / 2);
}
} else {
while (k > 0 && this.data[Math.floor(k / 2)] > this.data[k]) {
this.swap(Math.floor(k / 2), k);
k = Math.floor(k / 2);
}
}
}
siftDown(k) {
const temp = this.data[k];
while (2 * k <= this.size) {
let j = 2 * k;
if (this.type) {
if (j + 1 <= this.size && this.data[j] < this.data[j + 1]) {
j++;
}
if (temp > this.data[j]) break;
} else {
if (j + 1 <= this.size && this.data[j] > this.data[j + 1]) {
j++;
}
if (temp < this.data[j]) break;
}
this.data[k] = this.data[j];
k = j;
}
this.data[k] = temp;
}
swap(idx1, idx2) {
const temp = this.data[idx1];
this.data[idx1] = this.data[idx2];
this.data[idx2] = temp;
}
peek() {
if (this.isEmpty()) return false;
return this.data[1];
}
isEmpty() {
return this.size === 0;
}
}
根据字符出现频率排序 - 中等
js
/**
* @param {string} s
* @return {string}
*/
var frequencySort = function (s) {
const sMap = new Map();
for (let item of s) {
const el = sMap.get(item);
if (el) {
sMap.set(item, el + 1);
} else {
sMap.set(item, 1);
}
}
const heap = new MaxHeap();
for (let item of sMap) {
heap.offer(item);
}
let result = "";
while (heap.size) {
const item = heap.poll();
while (item[1]) {
result += item[0];
item[1]--;
}
}
return result;
};
class MaxHeap {
constructor() {
this.size = 0;
this.data = [];
}
swap(idx1, idx2) {
const temp = this.data[idx1];
this.data[idx1] = this.data[idx2];
this.data[idx2] = temp;
}
offer(item) {
this.size++;
this.data[this.size] = item;
this.siftUp(this.size);
}
siftUp(k) {
while (k > 1 && this.data[Math.floor(k / 2)][1] < this.data[k][1]) {
this.swap(k, Math.floor(k / 2));
k = Math.floor(k / 2);
}
}
poll() {
const result = this.data[1];
this.data[1] = this.data[this.size];
this.size--;
this.siftDown(1);
return result;
}
siftDown(k) {
const temp = this.data[k];
while (2 * k <= this.size) {
let j = 2 * k;
if (j + 1 <= this.size && this.data[j][1] < this.data[j + 1][1]) {
j++;
}
if (temp[1] > this.data[j][1]) break;
this.data[k] = this.data[j];
k = j;
}
this.data[k] = temp;
}
}
接近原点的 K 个点 - 中等
js
/**
* @param {number[][]} points
* @param {number} k
* @return {number[][]}
*/
var kClosest = function (points, k) {
const heap = new MinHeap();
for (let item of points) {
const [x, y] = [Math.abs(item[0]), Math.abs(item[1])];
const distance = Math.sqrt(x * x + y * y);
heap.offer({ key: item, value: distance });
}
let i = k;
const result = [];
while (i > 0) {
result.push(heap.poll().key);
i--;
}
return result;
};
class MinHeap {
constructor() {
this.size = 0;
this.data = [];
}
offer(item) {
this.size++;
this.data[this.size] = item;
this.siftUp(this.size);
}
siftUp(k) {
while (k > 1 && this.data[Math.floor(k / 2)].value > this.data[k].value) {
const j = Math.floor(k / 2);
const temp = this.data[j];
this.data[j] = this.data[k];
this.data[k] = temp;
k = j;
}
}
poll() {
const result = this.data[1];
this.data[1] = this.data[this.size];
this.size--;
this.siftDown();
return result;
}
siftDown() {
let k = 1;
const temp = this.data[k];
while (2 * k <= this.size) {
let j = 2 * k;
if (j + 1 <= this.size && this.data[j].value > this.data[j + 1].value) {
j++;
}
if (temp.value < this.data[j].value) break;
this.data[k] = this.data[j];
k = j;
}
this.data[k] = temp;
}
}
最后
参考文章: