提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、什么是堆?
- 二、向上调整建堆
- 1. 算法思想
- 2. 步骤演示(大顶堆)
- 3. 代码实现(C语言)
- 4. 时间复杂度分析
- 三、向下调整建堆
- 1. 算法思想
- 2. 步骤演示(大顶堆)
- 3. 代码实现(C语言)
- 4. 时间复杂度分析
- 四、两种方法的对比与选择
- 五、在堆排序中的应用
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
在数据结构与算法中,堆(Heap)是一种非常重要的完全二叉树结构,常用于实现优先队列和堆排序。堆排序的核心在于如何高效地构建一个堆,而建堆过程主要分为两种策略:向上调整(Sift Up)建堆和向下调整(Sift Down)建堆。本文将详细解析这两种建堆方法的原理、步骤、时间复杂度以及适用场景,并通过代码示例帮助读者深入理解。
提示:以下是本篇文章正文内容,下面案例可供参考
一、什么是堆?
堆是一种特殊的完全二叉树,满足以下性质之一:
- 大顶堆(Max Heap):每个节点的值都大于或等于其子节点的值,根节点是最大值。
- 小顶堆(Min Heap):每个节点的值都小于或等于其子节点的值,根节点是最小值。
堆通常用数组来实现,对于数组中下标为i的节点:
(这里第一个元素的下标为0,有一些教材上是以1作为第一个元素的下标因此公式会不同)
- 父节点下标:
parent(i) = (i - 1) / 2 - 左子节点下标:
left(i) = 2 * i + 1 - 右子节点下标:
right(i) = 2 * i + 2
二、向上调整建堆
1. 算法思想
向上调整建堆采用增量插入的策略。假设初始时堆为空,我们依次将每个新元素插入到数组末尾,然后通过“向上调整”操作,使其满足堆的性质。
使用前提:如果要使用该算法建大堆,则必须要保证,该堆初始已经是大堆(小堆同理)。
向上调整:从当前节点开始,与其父节点比较。如果当前节点值大于父节点(大顶堆),则交换两者位置,然后继续向上比较,直到到达根节点或满足堆性质为止。
2. 步骤演示(大顶堆)
假设要构建的数组为[3, 9, 2, 1, 4, 5]:
- 插入 3:堆为
[3] - 插入 9:
[3, 9]→ 9 > 3,交换 →[9, 3] - 插入 2:
[9, 3, 2]→ 2 < 3(父节点),无需调整 - 插入 1:
[9, 3, 2, 1]→ 1 < 3,无需调整 - 插入 4:
[9, 3, 2, 1, 4]→ 4 > 3,交换 →[9, 4, 2, 1, 3]→ 4 < 9,停止 - 插入 5:
[9, 4, 2, 1, 3, 5]→ 5 > 2,交换 →[9, 4, 5, 1, 3, 2]→ 5 < 9,停止
最终大顶堆:[9, 4, 5, 1, 3, 2]
3. 代码实现(C语言)
// 向上调整(大顶堆)voidsiftUp(intheap[],intindex){while(index>0){intparent=(index-1)/2;//找出index的双亲节点if(heap[index]<=heap[parent]){break;}// 交换inttemp=heap[index];heap[index]=heap[parent];heap[parent]=temp;index=parent;}}// 向上调整建堆voidbuildHeapBySiftUp(intarr[],intn){//从第2个节点开始调用向上调整建堆,以确保再i之前的堆已经是大堆for(inti=1;i<n;i++){siftUp(arr,i);}}4. 时间复杂度分析
- 单个元素向上调整的最坏时间复杂度为 O(logn)
- 对 n 个元素建堆,总时间复杂度为O(n*logn)
三、向下调整建堆
1. 算法思想
向下调整建堆采用整体调整的策略。从最后一个非叶子节点开始,向前遍历每个节点,对每个节点执行“向下调整”操作,使其子树满足堆性质。
使用前提:如果要使用该算法建大堆,则在某个根节点后的所有子节点都必须已经是大堆(小堆同理)。
向下调整:从当前节点开始,与其左右子节点中较大(大顶堆)的那个比较。如果当前节点值小于该子节点,则交换两者位置,然后继续向下调整,直到到达叶子节点或满足堆性质为止。
2. 步骤演示(大顶堆)
同样以数组[3, 9, 2, 1, 4, 5]为例:
- 找到最后一个非叶子节点下标:
lastNonLeaf = (n/2) - 1 = 2 - 从下标 2(值为 2)开始向前调整:
- 节点 2:值 2,子节点为 5(下标 5),5 > 2,交换 →
[3, 9, 5, 1, 4, 2] - 节点 1:值 9,子节点为 1 和 4,9 最大,无需调整
- 节点 0:值 3,子节点为 9 和 5,9 最大,3 < 9,交换 →
[9, 3, 5, 1, 4, 2]- 继续向下调整节点 1(值 3):子节点为 1 和 4,4 最大,3 < 4,交换 →
[9, 4, 5, 1, 3, 2]
- 继续向下调整节点 1(值 3):子节点为 1 和 4,4 最大,3 < 4,交换 →
- 节点 2:值 2,子节点为 5(下标 5),5 > 2,交换 →
最终大顶堆:[9, 4, 5, 1, 3, 2](与向上调整结果相同)
3. 代码实现(C语言)
// 向下调整(大顶堆)voidsiftDown(intheap[],intn,intindex){intlargest=index;while(1){intleft=2*index+1;intright=2*index+2;if(left<n&&heap[left]>heap[largest]){largest=left;}if(right<n&&heap[right]>heap[largest]){largest=right;}if(largest==index){break;}// 交换inttemp=heap[index];heap[index]=heap[largest];heap[largest]=temp;index=largest;}}// 向下调整建堆(Floyd 建堆算法)voidbuildHeapBySiftDown(intarr[],intn){// 从最后一个非叶子节点开始向前调整,确保所有子节点都已经是大堆for(inti=n/2-1;i>=0;i--){siftDown(arr,n,i);}}4. 时间复杂度分析
- 单个元素向下调整的最坏时间复杂度为 O(logn)
- 向下调整建堆(Floyd算法)的总时间复杂度为O(n)
四、两种方法的对比与选择
| 特性 | 向上调整建堆 | 向下调整建堆 |
|---|---|---|
| 建堆策略 | 增量插入,从前往后 | 整体调整,从后往前 |
| 起始点 | 第二个元素开始 | 最后一个非叶子节点开始 |
| 时间复杂度 | O(n*logn) | O(n) |
| 空间复杂度 | O(1)(原地) | O(1)(原地) |
| 适用场景 | 流式数据、动态插入 | 静态数组、一次性建堆 |
| 优势 | 实现简单,适合动态维护 | 效率更高,适合批量构建 |
选择建议:
- 如果需要动态插入元素并随时保持堆性质(如优先队列),使用向上调整
- 如果已有完整数组需要一次性构建堆(如堆排序),使用向下调整(Floyd算法)
五、在堆排序中的应用
堆排序通常使用向下调整建堆,因为它的时间复杂度更低:
// 堆排序(升序,使用大顶堆)voidheapSort(intarr[],intn){// 1. 构建大顶堆(向下调整)buildHeapBySiftDown(arr,n);// 2. 逐个将堆顶元素移到末尾for(inti=n-1;i>0;i--){// 交换堆顶和当前末尾元素inttemp=arr[0];arr[0]=arr[i];arr[i]=temp;// 调整剩余元素,使其保持堆性质siftDown(arr,i,0);}}总结
提示:这里对文章进行总结:
向上调整建堆和向下调整建堆是构建堆的两种基本方法:
- 向上调整建堆(O(n*logn)):从第二个元素开始,逐个插入并向上调整,适合动态插入场景
- 向下调整建堆(O(n)):从最后一个非叶子节点开始向前调整,效率更高,适合静态数组的一次性构建
在实际应用中,堆排序和优先队列的实现通常优先选择向下调整建堆(Floyd算法)以获得更好的时间复杂度。