优先队列与堆

优先队列是抽象数据类型,堆是具体的实现。在一些编程语言的实现中,二者不加区分。

优先队列

实现一个优先队列并不难。把所有的待排序的元素放在数组里,有以下两种方案实现优先队列的功能:

  • 有序数组:入队时维护有序性。出队的时候,以𝑂(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;
	}
}

最后

参考文章:

相关推荐
丫头,冲鸭!!!5 分钟前
B树(B-Tree)和B+树(B+ Tree)
笔记·算法
Re.不晚9 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
sszmvb123416 分钟前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺22 分钟前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉22 分钟前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船29 分钟前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
GIS程序媛—椰子41 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0011 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端