5 堆和图

文章目录


前言


一、堆 (简单)

【注】(并不难,不涉及递归、了解了堆化的思路就能写,可以说很简单—关键在于思路,代码没难度)

1 堆的基本概念

堆(heap)是一种满足特定条件的完全二叉树,主要可分为两种类型,如图所示。

  • 小顶堆(min heap):任意节点的值<=其子节点的值。
  • 大顶堆(max heap):任意节点的值>=其子节点的值。

在这里插入图片描述
由于堆是一种满足特定条件的完全二叉树,那么完全二叉树的性质堆也是满足的,因此堆有以下特性:

  • 最底层节点靠左填充,其他层的节点都被填满。
  • 我们将二叉树的根节点称为“堆顶”,将底层最靠右的节点称为“堆底”。
  • 对于大顶堆(小顶堆),堆顶元素(根节点)的值是最大(最小)的。

2 堆的常用操作(Python内置heapq最小堆模块)

需要指出的是,许多编程语言提供的是优先队列(priority queue),这是一种抽象的数据结构,定义为具有优先级排序的队列。

实际上,堆通常用于实现优先队列,大顶堆相当于元素按从大到小的顺序出队的优先队列。从使用角度来看,我们可以将“优先队列”和“堆”看作等价的数据结构。因此,本书对两者不做特别区分,统一称作“堆”。

另外Python 内置的 heapq 模块实现的是小顶堆。

(1)常用操作

堆的常见操作如下表,方法名要根据编程语言来确定

方法名 描述 时间复杂度
push() 元素入堆 O ( l o g n ) O(log n) O(logn)
pop() 堆顶元素出堆 O ( l o g n ) O(log n) O(logn)
peek() 访问堆顶元素 O ( 1 ) O(1) O(1)
size() 获取堆的元素数量 O ( 1 ) O(1) O(1)
isEmpty() 判断堆是否为空 O ( 1 ) O(1) O(1)

【注】:无论是元素入堆还是弹出堆顶元素,剩下的元素都会重新堆化,这样就能一直保持堆的性质

(2)Python内置heapq最小堆模块

heapq模块常见操作入下表

方法名 描述 具体说明
heapq.heapify(heap:List) 堆化 将列表堆化,原地操作;需要注意的是堆化后的数据类型还是list,list本来的性质哪些还是可以使用的
heapq.heappush(heap,item) 元素入堆 将元素item插入堆heap中;入堆后所有元素重新排列,保持最小堆的性质
heapq.heappop(heap) 堆顶元素出堆 弹出堆顶元素,剩下元素重新排列保持最小堆性质
heap[0] 访问堆顶元素 只访问,不弹出(其实这是保持了list的性质)
len(heap) 获取堆的元素数量 同样的直接使用Python内置的len函数即可
min_heap = [2,1,5,6,89,5,0,0,4,7,9,0,3,4,3]
# 将列表进行堆化
heapq.heapify(min_heap)
print(type(min_heap))    # <class 'list'>    可以看到列表堆化后还是list类型
print(min_heap)                                # [0, 0, 0, 1, 7, 2, 3, 6, 4, 89, 9, 5, 3, 4, 5]  堆化只是保证了最小堆性质(根节点一定最小,其余同济左右不要求),
                                                # 所以不保证堆化后直接打印小到大的,只是保证了根节点最小,必须要不断弹出根节点才能得到小到大的

print(min_heap[0])                             # 0  访问堆顶元素
length = len(min_heap)                         # 15
heapq.heappush(min_heap,-88)                     #  -88 入堆
for _ in range(length):
    cur = heapq.heappop(min_heap)
    print(cur,end=' ')               # -88 0 0 0 1 2 3 3 4 4 5 5 6 7 9 89  可以看到是 小到大的,每次弹出后剩下的元素会重新排列保存最小堆性质不变

另外,只要是封装了__lt__方法的自定义类型都是可以放进列表里面进行堆化操作的

###### 堆里面节点是任何东西,链表都行,不过必须要求能够比较大小(必须实现__lt__方法)
class ListNode:
    def __init__(self,val = 0, next = None):
        self.val: int = val
        self.next: Union[ListNode,None] = next

    def __lt__(self, other):
        # 堆里面节点(元素)必须要能够比较大小才行
        return self.val < other.val
node1 = ListNode(3)
node2 = ListNode(1)
node3 = ListNode(4)
node4 = ListNode(2)

min_node_heap = [node1,node2,node3,node4]
heapq.heapify(min_node_heap)
print(min_node_heap)            # [<__main__.ListNode object at 0x000001F3D3D3D4C0>, <__main__.ListNode object at 0x000001F3D3D3D490>,
                                            # <__main__.ListNode object at 0x000001F3D3D3D460>, <__main__.ListNode object at 0x000001F3D3D3D430>]
len = len(min_node_heap)

for node in min_node_heap:
    print(node.val,end=' ')        # 1 2 4 3  可以看到直接遍历堆化后的顺序,不是小到大的,只是保证了根节点最小,必须要不断弹出根节点才能得到小到大的


for _ in range(len):
    cur = heapq.heappop(min_node_heap)
    print(cur.val,end=' ')      # 1 2 3 4  可以看到是 小到大的,每次弹出后剩下的元素会重新排列保存最小堆性质不变

3 堆的实现

上面已经介绍了堆的常用方法和Python内置的最小堆模块,为了更加深入的理解堆这种数据结构,我们对最大堆来进行一个粗略的实现。

(1)堆的存储与表示

“二叉树”章节讲过,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,因此我们将采用数组来存储堆。

当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。节点指针通过索引映射公式来实现。

如图所示,给定索引 i i i,其左子节点的索引为 2 i + 1 2i+1 2i+1 ,右子节点的索引为 2 i + 2 2i+2 2i+2 ,父节点的索引为 ( i − 1 ) / 2 (i-1)/2 (i1)/2(向下整除)。当索引越界时,表示空节点或节点不存在(这个越界问题会在具体的代码中进行处理,不用担心)

【注】:在二叉树那一节专门讲过完全二叉树的数组表示有一点特别
值得说明的是,完全二叉树非常适合使用数组来表示。回顾完全二叉树的定义,None 只出现在最底层且靠右的位置,因此所有 None 一定出现在层序遍历序列的末尾。这意味着使用数组表示完全二叉树时,可以省略存储所有 None

在这里插入图片描述
我们可以将索引映射公式封装成函数,方便后续使用:

def left(self, i: int):
    '''获取左子节点索引'''
    return 2*i + 1
def right(self, i: int):
    '''获取右子节点索引'''
    return 2* + 2
def parent(self, i :int):
    '''获取父节点索引'''
    return (i - 1)//2
    # 向下整除

(2)访问堆顶元素

堆顶元素即为二叉树的根节点,也就是列表的首个元素:

def peek(self) -> int:
    '''访问堆顶元素'''
    return self.max_heap[0]

(3)元素入堆(入堆堆化)

给定元素 val ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏,因此需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为堆化(heapify)。

【注】:入堆和栈顶元素出堆的堆化过程是不一样的过程,本质上入堆和栈顶元素出堆都是堆化的过程。

考虑从入堆节点开始,从底至顶执行堆化。如下图所示(hello算法上面动图更加清晰),我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

设节点总数为 n n n,则树的高度为 l o g n logn logn。由此可知,堆化操作的循环轮数最多为 O ( l o g n ) O(logn) O(logn),元素入堆操作的时间复杂度为
O ( l o g n ) O(logn) O(logn)。代码如下所示:

def swap(self, i: int, j: int):
    '''交换元素'''
    self.max_heap[i], self.max_heap[j] = self.max_heap[j], self.max_heap[i]
    
def push(self, val: int):
    '''元素入堆'''
    # 添加节点
    self.max_heap.append(val)
    # 自底向上堆化
    self.shft_up(len(self.max_heap) - 1)

def shft_up(self, i: int):
    '''从节点索引i开始,自底向上堆化'''
    while True:
        parent_index = self.parent(i)
        if parent_index < 0 or self.max_heap[i] <= self.max_heap[parent_index]:
            break
        self.swap(i,parent_index)
        i = parent_index

可以看到这个堆化的过程并不难,清楚了思路就能写出来。

(4)堆顶元素出堆(堆顶元素出堆堆化)

堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化进行修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。

  • (1)交换堆顶元素与堆底元素(交换根节点与最右叶节点)。
  • (2)交换完成后,将堆底从列表中删除(注意,由于已经交换,因此实际上删除的是原来的堆顶元素)。
  • (3)从根节点开始,从顶至底执行堆化

如下图所示,“从顶至底堆化”的操作方向与“从底至顶堆化”相反,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 O ( l o g n ) O(logn) O(logn) 。代码如下所示:

def pop(self):
    '''元素出堆'''
    # 判空处理
    if not self.max_heap:
        return IndexError('堆为空')
    # 交换堆顶和堆底元素
    self.swap(0, self.size() - 1)
    # 删除堆底元素
    cur = self.max_heap.pop()
    # 从顶到底进行堆化
    self.shft_down(0)
    # 返回堆顶元素
    return cur

def shft_down(self, i: int):
    '''从节点 i索引开始,从顶向底堆化'''
    while True:
        # 判断节点索引 i, l, r 中最大节点索引,记为ma
        l, r, ma = self.left(i), self.right(i), i
        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:
            ma = l
        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:
            ma = r
        # 判断 节点 i 最大或 l,r越界,退出循环
        if (l > self.size() -1 and r > self.size() -1) or ma == i:
            break
        self.swap(i,ma)
        i = ma

(5)建堆操作(对列表进行堆化)

在某些情况下,我们希望使用一个列表的所有元素来构建一个堆,这个过程被称为“建堆操作”。
有两种建堆操作,一种比较容易想到,另一种需要再脑袋里面想一想(其实想一下就明白了,也不难)。

----借助入堆操作实现

我们首先创建一个空堆,然后遍历列表,依次对每个元素执行“入堆操作”,即先将元素添加至堆的尾部,再对该元素执行“从底至顶”堆化。
每当一个元素入堆,堆的长度就加一。由于节点是从顶到底依次被添加进二叉树的,因此堆是“自上而下”构建的。
设元素数量为 n n n ,每个元素的入堆操作使用 O ( l o g n ) O(logn) O(logn)时间,因此该建堆方法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

很显然,这种建堆操作很自然就能想到。

----通过遍历堆化实现

实际上,我们可以实现一种更为高效的建堆方法,共分为两步。

  • 将列表所有元素原封不动地添加到堆中,此时堆的性质尚未得到满足。
  • 倒序遍历堆(层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。

【注】:理解 倒序遍历堆(层序遍历的倒序)这个里面层序遍历的倒序结合下面代码这个才是关键,能理解这句话这个就没问题了

每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆。而由于是倒序遍历,因此堆是“自下而上”构建的。
之所以选择倒序遍历,是因为这样能够保证当前节点之下的子树已经是合法的子堆,这样堆化当前节点才是有效的。
值得说明的是,由于叶节点没有子节点,因此它们天然就是合法的子堆,无须堆化。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化:

class MaxHeap:
    def __init__(self, heap: List[int]):
        self.max_heap = heap

        # 列表堆化操作
        for i in range(self.parent(self.size()-1),-1,-1):
            self.shft_down(i)

【注】:在hello算法上面还写了很多推导出这个建堆操作的时间复杂度为 O ( n ) O(n) O(n),比第一种不断入堆的复杂度要低

----堆排序

趁热打铁,蹭着前面的建堆、自顶向下堆化这些还没忘记,赶紧将堆排序一起看了,不然以后再单独看堆排序又要回来看这个很麻烦的
堆排序

(6)代码汇总

from typing import Union,List
class TreeNode:
    def __init__(self,val = 0, left = None, right = None):
        self.val: int = val
        self.left: Union[TreeNode,None] = left
        self.right: Union[TreeNode,None] = right

#############################################   打印树和堆结构区   ########################################################

class Trunk:
    def __init__(self, prev, string: Union[str, None] = None):
        self.prev = prev
        self.str = string
def show_trunks(p: Union[Trunk, None]):
    if p is None:
        return
    show_trunks(p.prev)
    print(p.str, end="")

def print_tree(
    root: Union[TreeNode, None], prev: Union[Trunk, None] = None, is_right: bool = False
):
    """
    打印二叉树
    This tree printer is borrowed from TECHIE DELIGHT
    https://www.techiedelight.com/c-program-print-binary-tree/
    """
    if root is None:
        return

    prev_str = "    "
    trunk = Trunk(prev, prev_str)
    print_tree(root.right, trunk, True)

    if prev is None:
        trunk.str = "———"
    elif is_right:
        trunk.str = "/———"
        prev_str = "   |"
    else:
        trunk.str = "\———"
        prev.str = prev_str

    show_trunks(trunk)
    print(" " + str(root.val))
    if prev:
        prev.str = prev_str
    trunk.str = "   |"
    print_tree(root.left, trunk, False)

def print_heap(heap: List[int]):
    """打印堆"""
    print("堆的数组表示:", heap)
    print("堆的树状表示:")
    root: Union[TreeNode, None] = list_to_tree(heap)
    print_tree(root)

def list_to_tree(arr: List[int]) -> Union[TreeNode, None]:
    """将列表反序列化为二叉树"""
    return list_to_tree_dfs(arr, 0)

def list_to_tree_dfs(arr: List[int], i: int) -> Union[TreeNode, None]:
    """将列表反序列化为二叉树:递归"""
    # 如果索引超出数组长度,或者对应的元素为 None ,则返回 None
    if i < 0 or i >= len(arr) or arr[i] is None:
        return None
    # 构建当前节点
    root = TreeNode(arr[i])
    # 递归构建左右子树
    root.left = list_to_tree_dfs(arr, 2 * i + 1)
    root.right = list_to_tree_dfs(arr, 2 * i + 2)
    return root
#############################################   打印树结构区   ########################################################

class MaxHeap:
    def __init__(self, heap: List[int]):
        self.max_heap = heap

        # 列表堆化操作
        for i in range(self.parent(self.size()-1),-1,-1):
            self.shft_down(i)

    def left(self, i: int):
        '''获取左子节点索引'''
        return 2*i + 1
    def right(self, i: int):
        '''获取右子节点索引'''
        return 2* + 2
    def parent(self, i :int):
        '''获取父节点索引'''
        return (i - 1)//2
        # 向下整除

    def swap(self, i: int, j: int):
        '''交换元素'''
        self.max_heap[i], self.max_heap[j] = self.max_heap[j], self.max_heap[i]

    def size(self) -> int:
        '''获取堆大小'''
        return len(self.max_heap)

    def is_empty(self) -> bool:
        return self.size() == 0

    def peek(self) -> int:
        '''访问堆顶元素'''
        return self.max_heap[0]

    def push(self, val: int):
        '''元素入堆'''
        # 添加节点
        self.max_heap.append(val)
        # 自底向上堆化
        self.shft_up(len(self.max_heap) - 1)

    def shft_up(self, i: int):
        '''从节点索引i开始,自底向上堆化'''
        while True:
            parent_index = self.parent(i)
            if parent_index < 0 or self.max_heap[i] <= self.max_heap[parent_index]:
                break
            self.swap(i,parent_index)
            i = parent_index

    def pop(self):
        '''元素出堆'''
        # 判空处理
        if not self.max_heap:
            return IndexError('堆为空')
        # 交换堆顶和堆底元素
        self.swap(0, self.size() - 1)
        # 删除堆底元素
        cur = self.max_heap.pop()
        # 从顶到底进行堆化
        self.shft_down(0)
        # 返回堆顶元素
        return cur

    def shft_down(self, i: int):
        '''从节点 i索引开始,从顶向底堆化'''
        while True:
            # 判断节点索引 i, l, r 中最大节点索引,记为ma
            l, r, ma = self.left(i), self.right(i), i
            if l < self.size() and self.max_heap[l] > self.max_heap[ma]:
                ma = l
            if r < self.size() and self.max_heap[</
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值