一文了解堆的定义、优先队列、堆排序以及Python实现

介绍

本文是对bilibili视频的一个个人总结,方便后面复习
注意:本文使用的图片均来自于该视频,再次感谢UP主工程部老周
非常建议观看原视频,UP主讲得非常地透彻!

堆的定义

堆必须是一个完全二叉树

进一步的那什么又是完全二叉树呢?
为了完整,本文对满二叉树和完全二叉树进行解释

满二叉树的定义

如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。

如果一个二叉树为满二叉树,也可以说深度为k,有2^k+1个节点的二叉树

完全二叉树的定义

完全二叉树只允许最后一行不为满
且最后一行必须从左往右排序
最后一行元素之间不可以有间隔

其他定义:

在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。

举例
1)属于完全二叉树的情况:
在这里插入图片描述
2)不属于完全二叉树的情况:
在这里插入图片描述

堆序性

根据堆序性,堆可以被分为两类:

小根堆

每个父节点元素都要小于它的子节点元素

大根堆

每个父节点元素都要大于它的子节点元素

在这里插入图片描述
举例
在这里插入图片描述

堆的存储

1)按照层序遍历的顺利来给节点编号
2)把这些编号对应到一个一维数组的下标
3)把树的元素存入相应的下标里
因为堆是一个完全二叉树,所以每个下标和树的每个位置是一一对应的
数组中元素下标和树中节点的对应关系为:

节点下标为i
左子节点下标为2i+1
右子节点下标为2i+2

在这里插入图片描述

堆的基本操作

堆有两个基本操作(以生成一个大根堆为例):

上滤

将子节点与父节点进行比较,如果子节点大于父节点,则将子节点与父节点进行交换
一直交换到小于父节点,或到达树的根节点为止

Python实现:

def heapUp(arr, index):
    '''
    arr: 一维数组
    index: 一个子节点
    '''
    # 找到输入子节点的父节点
    father = (index - 1) // 2
    # 如果子节点大于父节点
    # 则将父节点与该节点进行交换
    if father >= 0 and arr[index] > arr[father]:
        arr[index], arr[father] = arr[father], arr[index]
        # 交换完后,输入子节点变为父节点
        # 它作为上面树的子节点继续上滤
        heapUp(arr, father)

下滤

将父节点与其最大子节点进行比较,如果父节点小于其最大子节点,则将父节点与其最大子节点进行交换
一直交换到大于全部子节点,或到达树的叶节点为止

Python实现:

def heapDown(arr, index):
    '''
    arr: 一维数组
    index: 一个父节点
    '''
    left = 2 * index + 1
    right = 2 * index + 2
    # 找到父节点、左节点和右节点中最大节点的下标largest
    largest = index
    if left < len(arr) and arr[left] > arr[largest]:
        largest = left
    if right < len(arr) and arr[right] > arr[largest]:
        largest = right
    # 如果最大节点的下标不是父节点
    # 则将父节点与最大节点进行交换
    if largest != index:
        arr[index], arr[largest] = arr[largest], arr[index]
        # 交换完后,该父节点变为子节点
        # 它作为下面树的父节点继续下滤
        heapDown(arr, largest)

两个操作的复杂度都为O(logN)

建堆

根据堆的基本操作,我们可以将任意一个一维数组构建为一个大根堆或者小根堆,使用的方式也可以是自顶向下或者自下而上两种方式
本文以自顶向下和自下而上两种方式构建一个大根堆为例

自顶向下

Python实现:

def buildMaxHeap_Up(arr):
    N = len(arr)
    # 跳过第一元素,因为它没有父节点
    for i in range(1, N):
        heapUp(arr, i)
        print(arr)

arr = [3, 4, 5, 6, 1, 7, 8]
print(arr)
buildMaxHeap_Up(arr)

输入结果:

[3, 4, 5, 6, 1, 7, 8] # 原始输入
[4, 3, 5, 6, 1, 7, 8] # 节点4: 4和3交换
[5, 3, 4, 6, 1, 7, 8] # 节点5: 5和4交换
[6, 5, 4, 3, 1, 7, 8] # 节点6: 6和3交换,再和5交换
[6, 5, 4, 3, 1, 7, 8] # 节点1: 不与任何元素交换
[7, 5, 6, 3, 1, 4, 8] # 节点7: 7和4交换,再和6交换
[8, 5, 7, 3, 1, 4, 6] # 节点8: 8和6交换,再和7交换

这种建堆的复杂度为O(NlogN)

自下而上

Python实现:

def buildMaxHeap_Down(arr):
    N = len(arr)
    # 为什么从(N-1-1)//2开始?
    # 我们的目标是找到最后一个元素对应的父节点
    # 最后一个元素的下标是N-1
    # 节点i的父节点是(i-1) // 2
    # 所以最后一个元素对应的父节点为: (N-1-1) // 2
    for i in range((N-1-1)//2, -1, -1):
        heapDown(arr, i)
        print(arr)

arr = [3, 4, 5, 6, 1, 7, 8]
print(arr)
buildMaxHeap_Down(arr)

输入结果:

[3, 4, 5, 6, 1, 7, 8] # 原始输入
[3, 4, 8, 6, 1, 7, 5] # 节点5: 5和8交换
[3, 6, 8, 4, 1, 7, 5] # 节点4: 4和6交换
[8, 6, 7, 4, 1, 3, 5] # 节点3: 3和8交换,再和7交换

这种建堆的复杂度为O(N)

优先队列

优先队列有两个操作:
1)插入队列
2)弹出最小(大)元素
如果是小根堆,弹出根节点就是弹出小元素
如果是大根堆,弹出根节点就是弹出大元素

插入队列

将新元素添加到数组尾部
再进行上滤操作

上滤的复杂度为O(logN),所以插入的复杂度也是O(logN)

弹出最小(大)元素

1)先构建一个小(大)根堆,弹出根节点即是最小(大)元素
2)将数组最后一个元素添加到根节点
3)再进行下滤操作,将剩余的元素重新构建成小(大)根堆

弹出的复杂度为O(logN)

堆排序

弹出最小元素

可以利用弹出最小元素的思想,依次弹出所有最小元素即可完成堆排序,但这样操作往往需要一个新空间来存储弹出的元素,所以这种方式往往不可取

大根堆

步骤:
1)构建一个大根堆
2)将跟节点,即未排序数组中的最大元素,与未排序数组中的最后一个元素进行交换
3)将交换后的根节点进行下滤操作,即构建新的大根堆
4)重复2,3步骤直到遍历完整个数组

Python实现:
注意这里将heapDown进行了一点改动,即增加了一个arrLen参数,该参数控制考虑的数组长度

def heapDown(arr, index, arrLen=None):
    '''
    arr: 一维数组
    index: 一个父节点
    '''
    left = 2 * index + 1
    right = 2 * index + 2
    if arrLen is None: arrLen = len(arr)
    # 找到父节点、左节点和右节点中最大节点的下标largest
    largest = index
    if left < arrLen and arr[left] > arr[largest]:
        largest = left
    if right < arrLen and arr[right] > arr[largest]:
        largest = right
    # 如果最大节点的下标不是父节点
    # 则将父节点与最大节点进行交换
    if largest != index:
        arr[index], arr[largest] = arr[largest], arr[index]
        # 交换完后,该父节点变为子节点
        # 它作为下面树的父节点继续下滤
        heapDown(arr, largest, arrLen)

def buildMaxHeap_Down(arr):
    N = len(arr)
    # 为什么从(N-1-1)//2开始?
    # 我们的目标是找到最后一个元素对应的父节点
    # 最后一个元素的下标是N-1
    # 节点i的父节点是(i-1) // 2
    # 所以最后一个元素对应的父节点为: (N-1-1) // 2
    for i in range((N-1-1)//2, -1, -1):
        heapDown(arr, i)
        # print(arr)

def heapSort(arr):
    # 构建一个大根堆
    buildMaxHeap_Down(arr)
    # 从最后一个元素遍历到第2个元素(排除根节点)
    for i in range(len(arr)-1, 0, -1):
        # 交换根节点和未排序数组的最后一个元素
        arr[i], arr[0] = arr[0], arr[i]
        # 将交换过后的堆进行下滤操作重新构建为大根堆
        # 注意:这里需要限制数组的长度为0到i,即只考虑未排序的数组
        # 而后面已经排好序的序列不再考虑
        heapDown(arr, 0, i)
    return arr

arr = [4, 1, 8, 6, 9, 0, 3, 7, 2, 5]
print(arr)

arr = heapSort(arr)
print(arr)

输入结果:

[4, 1, 8, 6, 9, 0, 3, 7, 2, 5]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

再次感谢bilibili视频
未经授权,禁止转载

猜你喜欢

转载自blog.csdn.net/qq_33757398/article/details/124276840
今日推荐