42. 盘点那些必问的数据结构算法题之二叉堆

简介: 42. 盘点那些必问的数据结构算法题之二叉堆

42. 盘点那些必问的数据结构算法题之二叉堆


0 概述

本文要描述的堆是二叉堆。二叉堆是一种数组对象,可以被视为一棵完全二叉树,树中每个结点和数组中存放该结点值的那个元素对应。树的每一层都是填满的,最后一层除外。二叉堆可以用于实现堆排序,优先级队列等。本文代码地址在 这里。

https://github.com/shishujuan/dsalg/tree/master/code/ds/heap

1 二叉堆定义

使用数组来实现二叉堆,二叉堆两个属性,其中 LENGTH(A) 表示数组 A 的长度,而 HEAP_SIZE(A) 则表示存放在A中的堆的元素个数,其中 LENGTH(A) <= HEAP_SIZE(A),也就是说虽然 A[0,1,…N-1] 都可以包含有效值,但是 A[HEAP_SIZE(A)-1] 之后的元素不属于相应的堆。

二叉堆对应的树的根为 A[0],给定某个结点的下标 i ,可以很容易计算它的父亲结点和儿子结点。注意在后面的示例图中我们标注元素是从1开始计数的,而实现代码中是从0开始计数。

#define PARENT(i) ( i > 0 ? (i-1)/2 : 0)
#define LEFT(i) (2 * i + 1)
#define RIGHT(i) (2 * i + 2)

:堆对应的树每一层都是满的,所以一个高度为 h 的堆中,元素数目最多为 1+2+22+…2h = 2^(h+1) - 1(满二叉树),元素数目最少为 1+2+…+2^(h-1) + 1 = 2^h。

由于元素数目 2^h <= n <= 2^(h+1) -1,所以 h <= lgn < h+1,因此 h = lgn 。即一个包含n个元素的二叉堆高度为 lgn 。

2 保持堆的性质

本文主要建立一个最大堆,最小堆原理类似。为了保持堆的性质,maxHeapify(int A[], int i) 函数让堆数组 A 在最大堆中下降,使得以 i 为根的子树成为最大堆。

void maxHeapify(int A[], int i, int heapSize)
{
    int l = LEFT(i);
    int r = RIGHT(i);
    int largest = i;
    if (l <= heapSize-1 && A[l] > A[i]) {
        largest = l;
    }
    if (r <= heapSize-1 && A[r] > A[largest]) {
        largest = r;
    }
    if (largest != i) { // 最大值不是i,则需要交换i和largest的元素,并递归调用maxHeapify。
        swapInt(A, i, largest);
        maxHeapify(A, largest, heapSize);
    }
}

在算法每一步里,从元素 A[i] 和 A[left] 以及 A[right] 中选出最大的,将其下标存在 largest 中。如果 A[i] 最大,则以 i 为根的子树已经是最大堆,程序结束。

否则,i 的某个子结点有最大元素,将 A[i] 与 A[largest] 交换,从而使i及其子女满足最大堆性质。此外,下标为 largest 的结点在交换后值变为 A[i],以该结点为根的子树又有可能违反最大堆的性质,所以要对该子树递归调用maxHeapify()函数。

当 maxHeapify() 函数作用在一棵以 i 为根结点的、大小为 n 的子树上时,运行时间为调整A[i]、A[left]、A[right] 的时间 O(1),加上对以 i 为某个子结点为根的子树递归调用 maxHeapify 的时间。i 结点为根的子树大小最多为 2n/3(最底层刚好半满的时候),所以可以推得 T(N) <= T(2N/3) + O(1),所以 T(N)=O(lgN)。

下图是一个运行 maxHeapify(heap, 2) 的例子。A[] = {16, 4, 10, 14, 7, 9, 3, 2, 8, 1},堆大小为 10。

3 建立最大堆

我们可以知道,数组 A[0, 1, …, N-1] 中,A[N/2, …, N-1] 的元素都是树的叶结点。如上面图中的 6-10 的结点都是叶结点。每个叶子结点可以看作是只含一个元素的最大堆,因此我们只需要对其他的结点调用 maxHeapify() 函数即可。

void buildMaxHeap(int A[], int n)
{
    int i;
    for (i = n/2-1; i >= 0; i--) {
        maxHeapify(A, i, n);
    }
}

之所以这个函数是正确的,我们需要来证明一下,可以使用循环不变式来证明。

循环不变式:在for循环开始前,结点 i+1、i+2…N-1 都是一个最大堆的根。

初始化:for循环开始迭代前,i = N/2-1, 结点 N/2, N/2+1, …, N-1都是叶结点,也都是最大堆的根。

保持:因为结点 i 的子结点标号都比 i 大,根据循环不变式的定义,这些子结点都是最大堆的根,所以调用 maxHeapify() 后,i 成为了最大堆的根,而 i+1, i+2, …, N-1仍然保持最大堆的性质。

终止:过程终止时,i=0,因此结点 0, 1, 2, …, N-1都是最大堆的根,特别的,结点0就是一个最大堆的根。

虽然每次调用 maxHeapify() 时间为 O(lgN),共有 O(N) 次调用,但是说运行时间是 O(NlgN) 是不确切的,准确的来说,运行时间为 O(N),这里就不证明了,具体证明过程参见《算法导论》。

4 堆排序

开始用 buildMaxHeap() 函数创建一个最大堆,因为数组最大元素在 A[0],通过直接将它与A[N-1] 互换来达到最终正确位置。去掉 A[N-1],堆的大小 heapSize 减1,调用maxHeapify(heap, 0, --heapSize) 保持最大堆的性质,直到堆的大小由N减到1。

void heapSort(int A[], int n)
{
    buildMaxHeap(A, n);
    int heapSize = n;
    int i;
    for (i = n-1; i >= 1; i--) {
        swapInt(A, 0, i);
        maxHeapify(A, 0, --heapSize);
    }
}

5 优先级队列

最后实现一个最大优先级队列,主要有四种操作,分别如下所示:

**insert(PQ, key):**将 key 插入到队列中。

**maximum(PQ):**返回队列中最大关键字的元素

**extractMax(PQ):**去掉并返回队列中最大关键字的元素

**increaseKey(PQ, i, key):**将队列 i 处的关键字的值增加到 key

这里定义一个结构体 PriorityQueue 便于操作。

typedef struct PriorityQueue {
    int capacity;
    int size;
    int elems[];
} PQ;

最终优先级队列的操作实现代码如下:

/**
 * 从数组创建优先级队列
 */
PQ *newPQ(int A[], int n)
{
    PQ *pq = (PQ *)malloc(sizeof(PQ) + sizeof(int) * n);
    pq->size = 0;
    pq->capacity = n;
    int i;
    for (i = 0; i < pq->capacity; i++) {
        pq->elems[i] = A[i];
        pq->size++;
    }
    buildMaxHeap(pq->elems, pq->size);
    return pq;
}
int maximum(PQ *pq)
{
    return pq->elems[0];
}
int extractMax(PQ *pq)
{
    int max = pq->elems[0];
    pq->elems[0] = pq->elems[--pq->size];
    maxHeapify(pq->elems, 0, pq->size);
    return max;
}
PQ *insert(PQ *pq, int key)
{
    int newSize = ++pq->size;
    if (newSize > pq->capacity) {
        pq->capacity = newSize * 2;
        pq = (PQ *)realloc(pq, sizeof(PQ) + sizeof(int) * pq->capacity);
    }
    pq->elems[newSize-1] = INT_MIN;
    increaseKey(pq, newSize-1, key);
    return pq;
}
void increaseKey(PQ *pq, int i, int key)
{
    int *elems = pq->elems;
    elems[i] = key;
    while (i > 0 && elems[PARENT(i)] < elems[i]) {
        swapInt(elems, PARENT(i), i);
        i = PARENT(i);
    }
}

参考资料

算法导论第6章《堆排序》

目录
相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
68 1
|
3月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
112 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
12天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
37 2
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
131 23
|
2月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
76 20
|
2月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
81 0
|
3月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
61 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
2月前
|
算法 vr&ar 计算机视觉
数据结构之洪水填充算法(DFS)
洪水填充算法是一种基于深度优先搜索(DFS)的图像处理技术,主要用于区域填充和图像分割。通过递归或栈的方式探索图像中的连通区域并进行颜色替换。本文介绍了算法的基本原理、数据结构设计(如链表和栈)、核心代码实现及应用实例,展示了算法在图像编辑等领域的高效性和灵活性。同时,文中也讨论了算法的优缺点,如实现简单但可能存在堆栈溢出的风险等。
84 0

热门文章

最新文章