🥳前端算法面试之堆排序-每日一练

今天分享一个非常热门的算法--堆排序。堆的运用非常的广泛,例如,Python中的heapq模块提供了堆排序算法,可以用于实现优先队列;Java中的PriorityQueue类实现了堆队列,可以用于实现优先级任务队列;C++中的优先队列容器适配器提供了基于堆的优先队列实现。

还有前端开发特别熟悉的React框架中也用到了,其中使用堆来管理组件的渲染优先级。在React中,组件的渲染优先级被抽象为一种堆结构,称为"Fiber堆"。Fiber堆中的每个节点代表一个组件,组件的优先级越高,在渲染时越优先。

什么是堆呢?

堆分为大根堆和小根堆,大根堆的每个结点的值都大于等于其子结点的值,即该结点是该子树中的最大值。小根堆的每个结点的值都小于等于其子结点的值,即该结点是该子树中的最小值。

他们都是一种特殊的完全二叉树,物理存储结构一般是一个连续的线性数组。并且节点的下标和左右子节点的下标之间存在一定的关系。假设节点的下标为 i,那么它的左子节点的下标为 2i,右子节点的下标为 2i + 1。相反地,如果一个节点的下标为 j,那么它的父节点的下标为 j/2(向下取整)。

那如何利用堆进行排序呢

以大根堆为例,就两步,建堆和堆化。

第一步先建堆,然后将堆顶和数组的最后一位更换位置,数组的最后一个位置就是最大值了。堆的大小减一。

第二步,再调整堆,使其再次满足大根堆的定义。

重复上面两步,直到堆的大小为1。

下面用代码实现这两个过程

建堆

javascript 复制代码
class Heap {
	constructor(data) {
		this.data = data;
	}

	build() {
		for (let i = 2; i < this.data.length; i++) {
			this.heapfyTop(i);
		}
	}

	heapfyTop(n) {
		while (n > 1 && this.data[n] > this.data[Math.floor(n / 2)]) {
			this.swap(n, Math.floor(n / 2));
			n = Math.floor(n / 2);
		}
	}

	swap(index1, index2) {
		const temp = this.data[index1];
		this.data[index1] = this.data[index2];
		this.data[index2] = temp;
	}
}

建堆有两种方法,这里先讲第一种。

建堆的过程有点像插入排序,假设第一个元素已经是一个大根堆,从第二个节点开始往后遍历,每个元素都往前面的大根堆中插入。直到遍历完整个数组的元素。完整的大根堆就建好了。

假设往大根堆中插入元素a,先将元素a放到数组的最后一个位置,然后比较a元素和其父元素的大小,如果大于父元素,就将两个元素的位置更换。这样a元素就有了新的父元素。然后继续比较a 元素和其父元素的大小。直到a元素小于等于父元素,或者a元素变成了大根堆的堆顶。

这个比较的过程,就是大根堆堆化的过程

上面代码中,build函数作用是从数组的第二个元素开始往后遍历,每遍历一个元素,就调用一次heapfyTop 函数。heapfyTop 函数的作用是调整大根堆。遍历完整个数组,堆也就建好了。

数组元素从下标 1 开始

测试代码

javascript 复制代码
const data = [-1, 21, 33, 5, 42, 123, 54, 65, 23, 33, 55];
const heap = new Heap(data);

heap.build();

console.log(heap.data);
// [
//   -1, 123, 55, 65, 33,
//   42,   5, 54, 21, 23,
//   33
// ]

新建一个 Heap 类,然后调用 build 方法,并且将堆的内容打印出来。打印数组确实满足大根堆定义,没有问题。

堆排序

javascript 复制代码
class Heap {

  //省略其他代码
  sort() {
    this.build2(); // 构建大顶堆
    let len = this.data.length - 1; // 数组长度减1,因为堆排序是从下标1开始
    while (len > 1) { // 当堆长度大于1时,继续排序
      this.swap(1, len); // 交换堆顶元素与堆尾元素
      len--; // 减小堆长度
      this.heapfyBelow(1, len); // 对新的堆顶元素进行调整
    }
  }

  heapfyBelow(n, end) { // 对下标为n的元素进行调整,使其满足大顶堆的性质,end为调整范围的上界
    // 是否是叶子节点
    while (n * 2 <= end) {
      let maxIndex = n; // 假设当前结点是最大值
      // 如果有左孩子,且左孩子的值比当前结点大,则将maxIndex更新为左孩子的下标
      if (n * 2 <= end && this.data[maxIndex] < this.data[n * 2]) maxIndex = n * 2;
      // 如果有右孩子,且右孩子的值比当前结点大,则将maxIndex更新为右孩子的下标
      if (n * 2 + 1 <= end && this.data[maxIndex] < this.data[n * 2 + 1]) maxIndex = n * 2 + 1;
      // 如果maxIndex没有发生变化,说明当前结点的值已经是最大值,调整结束
      if (maxIndex == n) break;
      // 否则,交换当前结点与maxIndex指向的结点
      this.swap(n, maxIndex);
      n = maxIndex; // 更新当前结点为新的maxIndex
    }
  }

}

将堆顶元素和最后一个元素更换位置之后,堆的大小减一,并且需要重新调整堆的大小,所以代码中 len--,并且调用了this.heapfyBelow(1, len)。这也是一个堆调整的代码,与之前不同的是,这个代码是从上往下调整堆。不断地比较当前元素和子元素,如果有子元素比当前元素还大的,就更换位置。直到遍历到叶子节点,或者没有比当前元素更大的子节点。

为了方便调用者,sort 函数中直接调用了 build 函数,完成建堆的步骤。

测试代码

javascript 复制代码
const data = [-1, 21, 33, 5, 42, 123, 54, 65, 23, 33, 55];
const heap = new Heap(data);
heap.sort();
console.log(heap.data);
// [
//    -1,  5, 21, 23, 33,
//    33, 42, 54, 55, 65,
//   123
// ]

打印的数组有序,代码正确

完整代码

javascript 复制代码
class Heap {
	constructor(data) {
		this.data = data;
	}

	build() {
		for (let i = 2; i < this.data.length; i++) {
			this.heapfyTop(i);
		}
	}

	sort() {
		this.build2();
		let len = this.data.length - 1;
		while (len > 1) {
			this.swap(1, len);
			len--;
			this.heapfyBelow(1, len);
		}
	}

	heapfyBelow(n, end) {
		// 是否是叶子节点
		while (n * 2 <= end) {
			let maxIndex = n;
			// 是否有左孩子
			if (n * 2 <= end && this.data[maxIndex] < this.data[n * 2]) maxIndex = n * 2;
			// 是否有右孩子
			if (n * 2 + 1 <= end && this.data[maxIndex] < this.data[n * 2 + 1]) maxIndex = n * 2 + 1;
			if (maxIndex == n) break;
			this.swap(n, maxIndex);
			n = maxIndex;
		}
	}
  
	heapfyTop(n) {
		while (n > 1 && this.data[n] > this.data[Math.floor(n / 2)]) {
			this.swap(n, Math.floor(n / 2));
			n = Math.floor(n / 2);
		}
	}
  
	swap(index1, index2) {
		const temp = this.data[index1];
		this.data[index1] = this.data[index2];
		this.data[index2] = temp;
	}
}

const data = [-1, 21, 33, 5, 42, 123, 54, 65, 23, 33, 55];
const heap = new Heap(data);

heap.sort();

console.log(heap.data);

这是堆排序的完整代码,大家可以直接 copy 下来在本地跑一跑

总结

这篇文章分享了堆排序的概念讲解以及 JS 代码实现。堆排序是一种高效的排序算法,利用堆的特性进行排序。它的时间复杂度为O(nlogn),通过建堆和堆化的过程,可以将一个无序的数组转化为有序的数组。堆排序在实际应用中有广泛的应用,特别是在需要维护优先级队列的场景中非常有用。

下篇文章来分享建堆的另一种方式,以及堆的元素如何删除,并且分析堆排序的时间复杂度

什么问题可以评论区留言哦。我每天都会分享一篇算法小练习,喜欢就点赞+关注吧

相关推荐
薛定猫AI42 分钟前
【深度解析】从 Antigravity 更新看 Agent IDE 的工程化演进:权限、沙盒、MCP 与模型治理
前端·javascript·ide
漂流瓶jz7 小时前
总结CSS组件化演进之路:命名规范/CSS Modules/CSS in JS/原子化CSS
前端·javascript·css
z200509307 小时前
每日简单算法题——————跟着卡尔
算法
踩着两条虫8 小时前
「AI + 低代码」的可视化设计器
开发语言·前端·低代码·设计模式·架构
Jagger_8 小时前
项目上线忙碌结束之后,为什么总想找点事做?
前端
GalenZhang8888 小时前
OpenClaw 配置多个飞书账号实战指南
前端·chrome·飞书·openclaw
️是788 小时前
信息奥赛一本通—编程启蒙(3395:练68.3 车牌问题)
数据结构·c++·算法
Liangwei Lin9 小时前
LeetCode 118. 杨辉三角
算法·leetcode·职场和发展
计算机安禾9 小时前
【c++面向对象编程】第24篇:类型转换运算符:自定义隐式转换与explicit
java·c++·算法
鼠鼠我(‘-ωก̀ )好困9 小时前
leetGPU
算法