[数据结构] 堆与堆排序

这篇文章使用 JavaScript 语言进行相关代码的编写。

数据结构——堆 heap

基本概念与性质

堆是一颗完全二叉树,根据父子节点之间值的大小关系可以分为:

  • 大根堆:每一个节点的值 大于或等于 其子节点的值;
  • 小根堆:每一个节点的值 小于或等于 其子节点的值;

image-20240727231821817

堆数据结构的底层通常使用顺序表进行存储。

通过索引的计算可以快速得到子节点、父节点的值:

以 0 作为根节点的索引:(下文使用这种标准)

Parent(i) = (i-1)/2;
LeftChild(i) = i*2+1;
RightChild(i) = i*2+2;

以 1 作为根节点的索引

Parent(i) = i/2;
LeftChild(i) = i*2;
RightChild(i) = i*2+1;

操作

一开始堆是一个空数组,最主要的两个API是插入和删除,这两个操作会在堆的头部和尾部进行操作,而这些操作会导致堆的性质丢失,因此每次进行插入和删除操作之后都需要重新维护堆的结构。

插入

插入操作将一个新的数据添加到堆的末尾,然后向上调整

图片来源:二叉堆 - OI Wiki (oi-wiki.org)

删除

删除操作将根节点摘除,然后将堆的末尾的节点移动到堆的顶部,最后向下调整

向上调整

对于某个节点(由一个明确的索引i指定),通过计算得到它的父节点的值,进行比较:

  • 如果二者大小不满足堆性质,则交换(即上升),上升后继续与新的父节点进行比较;
  • 如果二者大小满足堆性质,则终止。

时间复杂度与树的高度有关,这是完全二叉树,即O(log N)

function up(index){while(true){const parent = (index-1)>>1;if(this.arr[parent] < this.arr[index]){[this.arr[index], this.arr[parent]] = [this.arr[parent], this.arr[index]];index = parent;}else{break;}}
}
  • (index-1)>>1是通过位运算快速实现向下取整的除以2,有时候懒得写Math.floor...

向下调整

对于某个节点,通过索引计算得到左右两个子节点,分别进行比较:(以小根堆为例)

  • 如果当前节点的值均小于左右两个子节点的值,则说明满足堆结构,终止;
  • 如果左右两个子节点存在小于当前节点的值,那么找出最小值,最小的节点与当前节点进行交换(即下沉),下沉后继续与新的左右子节点进行比较。

时间复杂度:O(log N)

function down(index){while(true){let left = index*2+1;let right = index*2+2;let target = index;if(left<this.size() && this.arr[left]>this.arr[index]){target = left;}if(right<this.size() && this.arr[right]>this.arr[index]){target = right;if(left<this.size() && this.arr[left]>this.arr[right]){target = left;}}if(target!==index){[this.arr[index], this.arr[target]] = [this.arr[target], this.arr[index]];index = target;}else{break;}}
}

完整代码

class Heap {constructor(arr) {if(arr){this.arr = arr;for(let i=Math.floor((arr.length-1)/2); i>=0; i--){this._down(i);}}else{this.arr = [];}}size() {return this.arr.length;}push(val) {// 新的数据插入到尾部,然后向上调整this.arr.push(val);this._up(this.size() - 1);}top() {return this.arr[0];}pop() {// 没有数据了,返回nullif (this.size() == 0) return null;// 只剩下一个数据了,直接弹出返回if(this.size() == 1)return this.arr.pop();// 先记录堆顶数据let top = this.top();// 将最后一个数据放到堆顶,然后向下调整let last = this.arr.pop();this.arr[0] = last;this._down(0);return top;}_up(index){while(true){const parent = (index-1)>>1;if(this.arr[parent] < this.arr[index]){[this.arr[index], this.arr[parent]] = [this.arr[parent], this.arr[index]];index = parent;}else{break;}}}_down(index){while(true){let left = index*2+1;let right = index*2+2;let target = index;if(left<this.size() && this.arr[left]>this.arr[index]){target = left;}if(right<this.size() && this.arr[right]>this.arr[index]){target = right;if(left<this.size() && this.arr[left]>this.arr[right]){target = left;}}if(target!==index){[this.arr[index], this.arr[target]] = [this.arr[target], this.arr[index]];index = target;}else{break;}}}
}

堆排序

堆排序是建立在堆这种数据结构上的选择排序。

首先建立大根堆,每次从根节点获取最大值,将最大值移动到堆的尾部,然后对剩下的前n-1个节点进行维护大根堆的操作。

即:每次选择最大值移动至尾部,再继续在剩下的数中继续查找最大值。

  • 普通的选择排序是在线性表上寻找最大值,时间复杂度为 \(O(n)\) ,要查找 \(n\) 个最大值,因此时间复杂度为 \(O(n^2)\).

  • 在大根堆上寻找最大值的时间复杂度是\(O(1)\),而获取删除最大值之后的向下调整操作时间复杂度为\(O(\log n)\),共寻找 \(n\) 个最大值,因此时间复杂度为 \(O(n\log n)\)

考虑到排序算法的输入是一个乱序的数组,我们首要的任务是将一个普通的数组转换成一个大根堆

方法:从 最后一个节点的父节点 ,逆序遍历数组,依次向下调整。

原理:自底向上,先保证子树满足堆性质,然后逐渐向上扩展直到整棵树都满足堆性质。

综上,堆排序总共分为两个步骤:

  1. 构造大根堆
  2. 选择排序 + 维护堆结构

代码

function heapSort(arr){const n = arr.length;// 从最后一个节点的父节点开始调整// 作用:构造大根堆for(let i=Math.floor(n/2)-1; i>=0; i--){heapify(arr, i, n);} // 将大根堆的根节点和最后一个节点交换,同时维护堆的性质// 作用:排序for(let i=n-1; i>0; i--){[arr[0], arr[i]] = [arr[i], arr[0]];heapify(arr, 0, i);}
}// 调整堆
function heapify(arr, index, n){let left = 2*index+1;let right = 2*index+2;while(true){let largest = index;// 找到左右子树中的最大值if(left<n && arr[left]>arr[largest]){largest = left;}if(right<n && arr[right]>arr[largest]){largest = right;}// 如果最大值不是根节点,则交换if(largest!==index){[arr[index], arr[largest]] = [arr[largest], arr[index]];// 更新下一轮的索引index = largest;left = 2*index+1;right = 2*index+2;}else{break;}}
}

堆的其它应用

优先队列

优先队列是一种特殊的队列,每个元素都有一个优先级,按优先级顺序出队。刷算法题的时候经常会用到的辅助类PriorityQueue

数组中的第 k 大(或小)元素

可以使用最小堆来高效找到第 \(k\) 大的元素。首先将前 \(k\) 个元素插入堆中,然后遍历数组的其余部分,更新堆,保持堆的大小为 \(k\)。最终堆顶元素就是第 \(k\) 大的元素。

合并多个有序链表

使用最小堆来维护每个有序列表的当前最小元素。每次从堆中取出最小元素,并将该元素所在列表的下一个元素插入堆中。重复该过程直到所有列表都被合并。

事件驱动模拟

Node.js中,多个计时器回调就是使用最小堆记录的,每次事件循环进行到timer阶段,就检查堆顶计时器的剩余时间是否已到达:

  • 如果未到达,则执行下一个阶段的回调任务;
  • 如果已到达,则执行本阶段的所有已到达的定时器回调任务;
    • 每个回调执行完成之后,如果是setInterval,则重新将任务添加到堆中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/772866.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

详解 Hough 变换(基本原理与直线检测)

Hough 变换原理与应用前言: 详细介绍了 Hough 变换的基本思想、基本原理和应用等。其中大多都是自己的理解,难免有偏差,仅供参考。文章目录Hough 变换原理与应用1. 基本概述1.1 一些基本问题 1.2 以例子说明1.2.1 例子1:直线 y = k x + b y = kx + by=kx+b​​​​​​​ 到…

致远AnalyticsCloud分析云任意文件读取漏洞复现

产品界面图:FOFA:"AnalyticsCloud分析云"GET请求payload即可读取文件内容paylaod: /.%252e/.%252e/c:/windows/win.ini/a/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252e/.%252…

分形之城 - 题解

分形之城时间限制:C/C++ 1000MS,其他语言 2000MS 内存限制:C/C++ 256MB,其他语言 512MB描述 城市的规划在城市建设中是个大问题。 不幸的是,很多城市在开始建设的时候并没有很好的规划,城市规模扩大之后规划不合理的问题就开始显现。 而这座名为 Fractal 的城市设想了这样…

开天辟地,环境安装(cangjie篇)

cangjie,尝鲜日记需求:安装好环境,输出hello world(程序员金字招牌,哈哈~)开整!1 下载SDK 由于官方关闭了开放下载入口,需要走申请、审核机制。然后gitcode平台,下载安装。我们这里就不方便提供下载地址了。目前,我安装的0.53.4b版本。如图:下载步骤就不赘述了,下一…

pycharm远程调试一直卡着(正在收集数据),查看变量时一直显示collecting data并报错Timeout waiting for response且看不到任何内容

1. 问题描述如题,在用PyCharm进行Python代码调试查看具体变量时,会随机遇到一直显示collecting data,到最后报错Timeout waiting for response,在界面中看不到变量内部的内容,如下图所示:2. 解决办法在PyCharm,打开Setting界面,在如下设置项中勾选“Gevent compatible”…

python错题记录:布尔运算与逻辑值检测

一 前言 环境:python 3.10 win10 二 布尔运算与逻辑值检测 1 案例 案例1如上,在布尔运算时,有些时候代码只会运算前面的一部分,剩下的部分根本不会运算。以前在练习算法代码时,就利用这个规则来减少代码的工作量 案例2如上,之前好长一段时间,上面的布尔运算总是让我感…

霍夫(Hough)直线变换(直线检测)

0 原理霍夫变换在检测各种形状的的技术中非常流行,如果你要检测的形状可以用数学表达式写出,你就可以是使用霍夫变换检测它。及时要检测的形状存在一点破坏或者扭曲也可以使用。我们下面就看看如何使用霍夫变换检测直线。 首先将一条直线用一个点表示,这样用一个点表示直线上…

使用浏览器开发人员工具抓取Windows聚焦桌面壁纸

最近发现Windows聚焦桌面壁纸质量都挺高的,比如下图,无奈图片不给保存。所有只有想办法把他爬下来。没有这个功能的可以在桌面右击鼠标,选择个性化,背景,Windows聚焦这样桌面上就出现一个图标右击这个图标,出现一个弹窗,选择“了解详情”就打开一个网页,我们按F12打开开…

编程语言之泛型困境

困境泛型不可能三角 泛型困境的本质是,关于泛型,你想要缓慢的程序员、缓慢的编译器和臃肿的二进制文件,还是缓慢的执行时间。简单来说就是:要么苦了程序员,要么苦了编绎器,要么降低运行时效率。 不同语言对泛型的考量 以C、C++和Java为例,它们在泛型的设计上有着不同考量…

CMake学习(一)

CMake学习(一) 1、简介CMake是一个强大的软件构建系统,可以用简单的语句来描述所有平台的安装(编译过程) 可以编译源代码、制作程序库、产生适配器(wrapper)、还可以用任意的顺序建构执行档https://cmake.org/2、构建基础项目 最基础的 CMake 项目是由单个源代码文件构建的…

[rCore学习笔记 018]实现特权级的切换

写在前面 本随笔是非常菜的菜鸡写的。如有问题请及时提出。 可以联系:1160712160@qq.com GitHhub:https://github.com/WindDevil (目前啥也没有 本节内容 因为risc-v存在硬件特权级机制,我们又要实现一个可以使得应用程序工作在用户级,使得操作系统工作在特权级.原因是要保证…

二分搜索

二分搜索 2024年7月25日 21:27正常二分思想 重点是遇到不同的数怎么定边界,怎么记录答案。 特殊情况:没有数字或者只有一个数,直接判断返回 先定一个ans=-1用于记录答案,l、r记录左右边界 看中点数值,比target小,说明比target的的数字在右边,l = mid+1 比target大,ans=…