当前位置: 首页 > news >正文

数据结构:从堆中删除元素 (Deleting from a Heap)

目录

定义问题

一个直接但不靠谱的想法

从“结构属性”出发寻找解决方案

推导“下沉”操作来修复堆序

完善代码


定义问题

首先,我们必须明确“删除”的含义。在一个堆(特别是我们用作“优先队列”时),最有意义的删除操作是 “删除最大值”(对于大顶堆)。

因为堆这个结构的核心优势就是能以 O(1) 的时间复杂度访问到最大值。所以,我们通常说的“堆的删除”,指的就是 extractMax (取出最大值) 这个操作。

数据结构:堆(Heap)-CSDN博客

📌 目标:从堆中移除根节点(最大值),并让剩下的元素重新构成一个合法的堆,同时返回被移除的那个最大值。

一个直接但不靠谱的想法

最大值就在数组的 data[0] 位置。最简单的想法是:直接把它删掉。

[100, 90, 80, 70] -> [?, 90, 80, 70]

但这会留下一个“窟窿”在堆顶,整个结构被破坏了,它甚至不再是一棵连通的树。这个方法行不通。


从“结构属性”出发寻找解决方案

和插入操作一样,我们优先考虑如何维护 “结构属性”,也就是保证操作后它仍然是一棵完全二叉树

如果我们删除堆顶,树的形状就被破坏了。但如果我们删除的是最后一个元素呢?

[100, 90, 80, 70] -> [100, 90, 80] 剩下的部分 [100, 90, 80] 仍然完美地对应一棵完全二叉树。

💡 这个观察给了我们一个绝妙的思路来“修复”删除堆顶后留下的窟窿:

  1. 我们先把要删除的根节点的值(最大值)保存起来,因为最后要返回它。

  2. 我们把堆的最后一个元素(在数组的 data[size-1] 位置)拿出来。

  3. 用这个“最后的元素”去填补根节点的窟窿。

  4. 然后,把堆的 size 减一,逻辑上就相当于删除了最后一个元素。

这样操作之后,我们得到的树在形状上一定是正确的(仍然是完全二叉树),但堆序属性❌几乎肯定被破坏了。

举个例子,假设原来的堆是:

逻辑结构:

      100/   \90    80/70

数组表示: [100, 90, 80, 70] (size = 4)

我们要删除 100

  1. 保存 100 这个值。

  2. 拿出最后一个元素 70,放到根节点的位置。

  3. size 减为 3。

现在的数组变成了 [70, 90, 80]。 对应的逻辑结构是:

      70      <-- 堆序被破坏!/   \90    80

我们看到,根节点 70 比它的两个孩子 9080 都小,这严重违反了堆序属性。


推导“下沉”操作来修复堆序

现在的问题是,一个(可能很小的)元素被放到了堆顶,它需要找到自己“合适”的位置。这个过程和插入时的“上浮”正好相反,我们称之为 “下沉” (Sift Down)

下沉的逻辑推导:

  1. 当前的节点(一开始是根节点)的值,和它的两个孩子比较。

  2. 如果它的值比两个孩子都大(或等于),那么堆序属性在这一点得到了满足,下沉结束。

  3. 如果它的值比至少一个孩子小,为了满足堆序,它必须和值更大的那个孩子交换位置

  4. 交换位置后,这个节点来到了一个新的、更低的位置。它可能仍然比它的新孩子要小,所以我们必须重复第1步,继续向下比较和交换。

为什么是和更大的孩子交换

思考一下:如果和较小的孩子交换,那么那个更大的孩子仍然会比新的父节点(就是被换下来的那个较小孩子)要大,堆序属性依然是错的。所以,必须让最大的元素“上位”。

这个过程一直持续,直到这个节点“下沉”到了一个合适的位置(它比它的两个孩子都大),或者它自己变成了叶子节点(没有孩子了),就停止。

让我们用上面的例子来走一遍“下沉”流程:

      70      <-- 堆序被破坏!/   \90    80
  1. 初始状态: [70, 90, 80],当前节点是 70 (下标0)。

  2. 第一次比较: 70 的孩子是 90 (下标1) 和 80 (下标2)。70 小于它们。

  3. 选择交换对象: 两个孩子中,90 是更大的那个。

  4. 交换: 7090 交换位置。数组变为 [90, 70, 80]

  5. 更新位置: 70 现在来到了下标 1 的位置。

  6. 继续下沉: 检查下标为 1 的节点 70。它有孩子吗?根据 size=3,它的左孩子下标 2*1+1=3 已经超出了范围,所以它现在是叶子节点,没有孩子了。

  7. 下沉结束。最终数组为 [90, 70, 80],它代表的树重新满足了堆序。


完善代码

首先,我们需要一个 siftDown 的辅助函数。这个函数非常重要,是堆的核心操作之一。

// (之前已经有 swap 函数)// siftDown (下沉) 操作
// h: 堆的指针
// index: 从哪个位置开始进行下沉操作
void siftDown(Heap* h, int index) {int currentIndex = index;// 循环条件:只要当前节点有左孩子(有左必有右或无右,保证不是叶子节点)while (leftChild(currentIndex) < h->size) {int largerChildIndex = leftChild(currentIndex);int rightChildIndex = rightChild(currentIndex);// 检查是否存在右孩子,并且右孩子比左孩子更大if (rightChildIndex < h->size && h->data[rightChildIndex] > h->data[largerChildIndex]) {largerChildIndex = rightChildIndex;}// 如果当前节点已经比它最大的孩子要大,说明位置正确,停止下沉if (h->data[currentIndex] >= h->data[largerChildIndex]) {break;}// 否则,和更大的孩子交换swap(&h->data[currentIndex], &h->data[largerChildIndex]);// 更新当前索引,继续向下检查currentIndex = largerChildIndex;}
}
// extractMax (删除并返回最大值) 函数
int extractMax(Heap* h) {// 检查堆是否为空if (h->size <= 0) {std::cout << "错误:堆为空。\n";return -1; // 返回一个错误码}// 1. 保存根节点的值(最大值)int maxValue = h->data[0];// 2. 将最后一个元素移动到根节点h->data[0] = h->data[h->size - 1];// 3. 堆的大小减一h->size--;// 4. 从根节点开始执行“下沉”操作,以恢复堆序siftDown(h, 0);// 5. 返回最大值return maxValue;
}

复杂度分析: “下沉”操作的路径也是从根到底,所以它的时间复杂度也是由树高决定的,即 O(logN)。

http://www.xdnf.cn/news/1378081.html

相关文章:

  • 微服务-30.配置管理-动态路由
  • 3 无重复字符的最长子串
  • 第二阶段Winfrom-8:特性和反射,加密和解密,单例模式
  • Gopher URL协议与SSRF二三事
  • 入门概念|Thymeleaf与Vue
  • 路由基础(二):路由表和FIB表
  • Day7--HOT100--54. 螺旋矩阵,48. 旋转图像,240. 搜索二维矩阵 II
  • 【JAVA实现websocket】
  • Java设计模式之《外观模式》
  • 大模型安全概述、LlamaFirewall
  • 深度学习---卷积神经网络CNN
  • Git-远程操作
  • AI-Agent 深度科普:从概念到架构、应用与未来趋势
  • JVM之【Java对象在内存中的结构】
  • Linux--->网络编程(TCP并发服务器构建:[ 多进程、多线程、select ])
  • Linux 系统调优与CPU-IO-网络内核参数调优
  • MySQL InnoDB vs MyISAM
  • 深度学习——卷积神经网络CNN(原理:基本结构流程、卷积层、池化层、全连接层等)
  • LeetCode - 反转链表 / K 个一组翻转链表
  • day2_softmax回归的实现 李沐动手学深度学习pytorch记录
  • 神经网络学习笔记12——高效卷积神经网络架构MobileNet
  • PLC_博图系列☞基本指令”S_ODT:分配接通延时定时器参数并启动“
  • leecode-三数之和
  • 如何防御安全标识符 (SID) 历史记录注入
  • 【Linux实时内核机制】ww_rt_mutex 的contending_lock异常问题
  • wireshark解析FLV插件分享
  • Unity Shader unity文档学习笔记(二十一):几种草体的实现方式(透明度剔除,GPU Instaning, 曲面细分+几何着色器实现)
  • HTML5超详细学习内容
  • GPIO推挽和开漏的名称由来和本质含义
  • FactoryBean接口作用