Python - 深夜数据结构与算法之 Heap Binary Heap

news2024/9/28 16:12:21

目录

一.引言

二.堆与二叉堆介绍

1.Heap 堆

2.Binary Heap 二叉堆

3.HeapifyUp 添加节点

4.HeapifyDown 删除节点

5.Heap 时间复杂度

6.Insert & Delete 代码实现

三.经典算法实战

1.Smallest-K [M14]

2.Sliding-Window-Max [239]

3.Ugly-Number [264]

4.Top-K-Freq-Ele [347]

四.总结


一.引言

前面介绍了树和二叉树的概念,接下来介绍 Heap 堆和 Binary Heap 二叉堆,这里堆是一个抽象的数据结构,二叉堆只是其实现的一种形式,并不代表所有堆都使用二叉树实现。

二.堆与二叉堆介绍

1.Heap 堆

堆主要分为两种表现形式,最大堆 or 最小堆,也有叫大根堆 or 小根堆的,其可以在常数时间 o(1) 获取到最大 or 最小的元素,其一般包含 3 个基础 API:

◆ find-max - 寻找堆中的最大值

◆ delete-max - 删除堆中的最大值

◆ insert - 向堆中插入一个新元素

注意 📢 delete 和 insert 操作都会导致堆的结构破坏,所以需要重新调整父子节点位置从而维护堆的原始性质。

2.Binary Heap 二叉堆

二叉堆是基于二叉树实现的 Heap,不过有一点需要注意这里的二叉树是完全二叉树,假设其深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。也就是除了最后一层叶子 🍃 节点外,其余层的节点都是满的且没有 None。 同时二叉堆一般基于 List 实现,以下述列表为例:

[110, 100, 90, 40, 80, 20, 60, 10, 30, 50, 70]

完全二叉树可以看作是满二叉树从尾部叶子节点开始清退,即数组尾部清退,剩下的都是满足完全二叉树的,这样做主要是为了提高效率,如果过多节点为 None,二叉树的深度 h 增加,那么搜索的时间复杂度也会逐渐增加且浪费空间。 基于完全二叉树形态,其具备以下性质:

通过索引 i 我们可以获取对应索引的左右孩子节点和父节点的位置 [如果有的话],假设是 k 叉树的话,索引 i 的 k 个孩子索引为 k*i + 1、k*i + 2、k*i+3 以此类推,父节点索引则为 floor((i-1)/k)。 

3.HeapifyUp 添加节点

基于二叉堆 index 及其父子节点的关系,insert 操作首先将节点插入数组尾部,然后依次与父节点比较向上移动直到无法移动,增加元素结束。

◆ 插入 85 到 Heap

◆ 元素添加到队尾 

◆ 持续向上移动并交换

4.HeapifyDown 删除节点

删除元素首先将 root 节点移出,随后将尾节点替换至 root,与左右节点中较大的节点替换位置,如此循环下去,直到不能移动并恢复堆的性质。 

5.Heap 时间复杂度

再次重申下堆是一个抽象的结构,其有多种实现方式,就像是接口一样。这里最常见的实现方法为二叉堆,而随着实现方法的复杂,其 insert 和 delete 的复杂度也会更加友好。这里 Binaey Heap 的 find Min/Max 的时间复杂度为 o(1),其余时间复杂度为 o(logn)。

6.Insert & Delete 代码实现

◆ HeapifyUp

这里用了指针的方式,不断将较小的 Parent 节点与 Insert 节点互换,最终停止循环。

◆ HeapifyDown

增加元素只需要与 Parent 节点比较,但是删除节点需要和左右子节点比较,所以需要使用 maxChild 函数寻找更大的节点进行交换,这样才能满足堆得性质。 

三.经典算法实战

1.Smallest-K [M14]

最小的 K 个数: https://leetcode.cn/problems/smallest-k-lcci/

题目分析

既然是在堆的讲解下,所以我们可以直接使用 Heap,由于是最小的 k 个数,所以可以使用最小堆,每次返回堆顶元素并更新堆,执行 k 次即可。

小根堆

class Solution(object):
    def smallestK(self, arr, k):
        """
        :type arr: List[int]
        :type k: int
        :rtype: List[int]
        """

        heapq.heapify(arr)
        res = []
        for i in range(k):
            res.append(heapq.heappop(arr))
        return res

python 的 heapq 默认为最小堆,直接将元素通过 o(n) 复杂度构建一个 Heap,随后 pop k 次堆顶的元素即可。当然 heapq 提供 nsmallest 和 nlarggest (k, nums) 的函数可以直接获取前 k 个最小、最大的元素,其等价于 nums.sort()[:k]。

2.Sliding-Window-Max [239]

最大滑动窗口: https://leetcode.cn/problems/sliding-window-maximum/description/

题目分析

之前 ArrayList 部门我们通过 dequeue 双端队列实现了 k-window-max 的算法,一个技巧就是我们需要记录每个元素的索引,从而判断是否在窗口中。这里 python 默认的 heapq 为小根堆,通过 -1 * nums 转换为大根堆,随后遍历 k 窗口,取出堆顶最大值即可,注意在堆的维护过程中,排除索引 index 在 k-window 之外的元素。

大根堆

class Solution(object):
    def maxSlidingWindow(self, nums, k):

        n = len(nums)

        # 构造最大堆
        q = [(-nums[i], i) for i in range(k)]
        heapq.heapify(q)

        # 先获取前k个元素的 max
        ans = [-q[0][0]]

        for i in range(k, n):
            # 向堆添加新的元素
            heapq.heappush(q, (-nums[i], i))

            # 堆顶为窗口外元素则去除该元素
            while q[0][1] <= i - k:
                heapq.heappop(q)
            
            # pop 后仍然保持堆的结构,弹出堆顶最大值
            ans.append(-q[0][0])
        
        return ans

对于最大、最小 k 个数的题目,我们都可以想到通过 heap 来实现,工程环境下直接使用对应的库即可。

3.Ugly-Number [264]

丑数: https://leetcode.cn/problems/ugly-number-ii/description/

题目分析 

这个题目主要是理解比较困难,感觉是记忆性题目。1 是丑数,假设 x 是丑数,则 2x、3x、5x 也是丑数,所以我们依次加入堆中,第 n 次出队时,对应元素即为第 n 个丑数。

最小堆

#!/usr/bin/python
# -*- coding: UTF-8 -*-

class Solution(object):

    def nthUglyNumber(self, n):
        """
        :type n: int
        :rtype: int
        """

        nums = [2, 3, 5]

        # 去重
        appeared = {1}

        # 将初始化丑数放到队列里
        pq = [1]

        # 每次从队列取最小值,然后将对应数 2x 3x 5x 入队
        for i in range(1, n+1):
            x = heapq.heappop(pq)
            if i == n:
                return x
            for num in nums:
                t = num * x
                if t not in appeared:
                    appeared.add(t)
                    heapq.heappush(pq, t)
    
        return -1

三指针

class Solution(object):
    def nthUglyNumber(self, n):
        if n < 0:
            return 0
        dp = [1] * n
        # 维护3个指针
        index2, index3, index5 = 0, 0, 0
        for i in range(1, n):
            # 每次看哪个位置乘出来的丑数最小
            dp[i] = min(2 * dp[index2], 3 * dp[index3], 5 * dp[index5])
            # 用过的索引 += 1
            if dp[i] == 2 * dp[index2]: index2 += 1
            if dp[i] == 3 * dp[index3]: index3 += 1
            if dp[i] == 5 * dp[index5]: index5 += 1
        return dp[n - 1]

4.Top-K-Freq-Ele [347]

前 k 个高频元素: https://leetcode.cn/problems/top-k-frequent-elements/description/

题目分析 

这题找 k 个最大,还是使用堆即可,只不过需要预先做一次 word_count 统计词频,这里如果是 scala 可以直接用 tuple-2 再 sortBy(-_.2) 即可,python 的话就直接使用 heqpq.nlargest 即可。

最大堆 API

class Solution(object):
    def topKFrequent(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: List[int]
        """

        if k == 0:
            return []

        # 1.构造 word count 计数
        count = {}
        for i in nums:
            if i not in count:
                count[i] = 0
            count[i] += 1

        # 2.构造 Heap
        heap = [(val, key) for key, val in count.items()]

        # 3.取前 k 个元素
        return [item[1] for item in heapq.nlargest(k, heap)]

这里使用 nlargest 获取前 k 个元素,但是我们构造时使用了全部数据,下面我们自己维护 k 个元素实现 heap。

K-最大堆

class Solution(object):
    def topKFrequent(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: List[int]
        """

        if k == 0:
            return []

        # 1.构造 word count 计数
        count = {}
        for i in nums:
            if i not in count:
                count[i] = 0
            count[i] += 1

        # 2.构造 Heap
        re = []
        heapq.heapify(re)
        for key, val in count.items():
            # 添加元素
            heapq.heappush(re, (val, key))

            # 超过 k 就把堆顶最小的拿走,最后剩下 K 个最大的
            while len(re) > k:
                heapq.heappop(re)
        
        return [i[1] for i in re]

 因为我们只维护了 k 大小的堆,遍历数组是 o(n),而堆内排序是 o(logk),所以时间快一些。

四.总结

本文结合上一篇文章的二叉树介绍了堆的概念以及常用的堆的功能应用,取前 k 个最大 or 最小的问题适合用堆实现,其次常用的二叉堆我们应该记住父子节点之间的关系。最后是常用堆的实现代码,这里我们都是调用 heapq 库,如果想要自己了解可以参考下述链接: 

Heap Sort – Data Structures and Algorithms Tutorials

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1333801.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习或深度学习的数据读取工作(大数据处理)

机器学习或深度学习的数据读取工作&#xff08;大数据处理&#xff09;主要是.split和re.findall和glob.glob运用。 读取文件的路径&#xff08;为了获得文件内容&#xff09;和提取文件路径中感兴趣的东西(标签) 1&#xff0c;“glob.glob”用于读取文件路径 2&#xff0c;“.…

靠谱免费的MAC苹果电脑杀毒软件CleanMyMac X2024

您是否曾经为Mac电脑的性能下降、存储空间不足而烦恼&#xff1f;是否希望有一个简单而高效的解决方案来优化您的Mac系统&#xff1f;那么&#xff0c;我向您介绍一款非常出色的工具&#xff1a;CleanMyMac X。它能够轻松处理这些问题&#xff0c;并让您的Mac恢复到最佳状态。 …

新版IDEA中Git的使用(三)

说明&#xff1a;前面介绍了在新版IDEA中Git的基本操作、分支操作&#xff0c;本文介绍一下在新版IDEA中&#xff0c;如何回滚代码&#xff1b; 分以下三个阶段来介绍&#xff1a; 未Commit的文件&#xff1b; 已经Commit&#xff0c;但未Push的文件&#xff1b; 已经Push的…

常见的Ubuntu命令30条(二)

Ubuntu命令是指在Ubuntu操作系统中用于执行各种任务和操作的命令行指令。这些命令可以用于管理系统、配置网络、安装软件、浏览文件等。Ubuntu命令通常在终端&#xff08;Terminal&#xff09;应用程序中输入并执行。 history&#xff1a;显示命令行历史记录。grep&#xff1a…

Spark编程语言选择:Scala、Java和Python

在大数据处理和分析领域&#xff0c;Apache Spark已经成为一种非常流行的工具。它提供了丰富的API和强大的性能&#xff0c;同时支持多种编程语言&#xff0c;包括Scala、Java和Python。选择合适的编程语言可以直接影响Spark应用程序的性能、可维护性和开发效率。在本文中&…

jvm_下篇_补充:浅堆深堆与内存泄露

笔记来源&#xff1a;尚硅谷 JVM 全套教程&#xff0c;百万播放&#xff0c;全网巅峰&#xff08;宋红康详解 java 虚拟机&#xff09; 同步更新&#xff1a;https://gitee.com/vectorx/NOTE_JVM https://codechina.csdn.net/qq_35925558/NOTE_JVM https://github.com/uxiahnan…

shell 如何调用多个脚本

简介 这篇文章主要描述如何通过主脚本去调用其他脚本中的方法&#xff0c;调用的过程中可能出现哪些坑&#xff0c;如何避免。 目录 1. 主脚本调用其他脚本的方法 1.1. bash方法 1.2. source方法 2. 避坑技巧 2.1. 路径配置无效 2.2. source变量冲突 3. 总结 1. 主脚本调…

工具系列:TensorFlow决策森林_(5)使用文本和神经网络特征

文章目录 设置使用原始文本作为特征使用预训练的文本嵌入同时训练决策树和神经网络构建模型训练和评估模型 欢迎来到 TensorFlow决策森林&#xff08; TF-DF&#xff09;的 中级教程。 在本文中&#xff0c;您将学习有关 TF-DF的一些更高级的功能&#xff0c;包括如何处理自…

SQL进阶理论篇(二十一):基于SQLMap的自动化SQL注入

文章目录 简介获取当前数据库和用户信息获取MySQL中的所有数据库名称查询wucai数据库中的所有数据表查看heros数据表中的所有字段查询heros表中的英雄信息总结参考文献 简介 从上一小节&#xff0c;可以发现&#xff0c;如果我们编写的代码存在着SQL注入的漏洞&#xff0c;后果…

HarmonyOS的装饰器之BuilderParam 理解

BuilderParam 装饰器 使用时间&#xff1a;当定义了一个子组件&#xff0c;并且子组件的build()中有一个布局在不同的父组件&#xff0c;实现效果不一样的时候&#xff0c;可以在子组件中用这个BuilderParam装饰器&#xff0c; 在父组件用Builder 装饰器进行实现&#xff0c;然…

Lua的垃圾回收机制详解

Lua 是一种轻量级的编程语言&#xff0c;广泛用于嵌入到其他应用程序中&#xff0c;尤其是在游戏开发领域。Lua 的内存管理机制采用了自动垃圾收集&#xff08;Garbage Collection&#xff09;的方法。以下是Lua内存管理的一些关键方面&#xff1a; 垃圾收集原理概述 Lua 使用…

我的软考之路

缘起 2016年&#xff0c;入职了一家业务相对稳定的公司。技术栈的切换使得刚入职的时光格外忙碌。然而当所有工作所需技术逐步掌握并渐渐精通&#xff0c;摸鱼的时间也相对多了起来。 这样的日子一多&#xff0c;危机感开始蔓延&#xff0c;毕竟35是谁都绕不过的一道坎。程序猿…

SQL实践篇(一):使用WebSQL在H5中存储一个本地数据库

文章目录 简介本地存储都有哪些&#xff1f;如何使用WebSQL打开数据库事务操作SQL执行 在浏览器端做一个英雄的查询页面如何删除本地存储参考文献 简介 WebSQL是一种操作本地数据库的网页API接口&#xff0c;通过它&#xff0c;我们可以操作客户端的本地存储。 WebSQL曾经是H…

【C++练级之路】【Lv.5】动态内存管理(都2023年了,不会有人还不知道new吧?)

目录 一、C/C内存分布二、new和delete的使用方式2.1 C语言内存管理2.2 C内存管理2.2.1 new和delete操作内置类型2.2.2 new和delete操作自定义类型 三、new和delete的底层原理3.1 operator new与operator delete函数3.2 原理总结3.2.1 内置类型3.2.2 自定义类型 四、定位new表达…

OpenAI开发者大会简介

文章目录 GPT-4 Turbo 昨天晚上 OpenAI的首届开发者大会召开 Sam Altman也做了公开演讲&#xff0c;应该说 这是继今年春天发布GPT-4之后 OpenAI在AI行业又创造的一个不眠夜 过去一年 ChatGPT绝对是整个科技领域最热的词汇 OpenAI 也依靠ChatGPT取得了惊人的成绩 ChatG…

模拟生物自然进化的基因遗传算法

基因遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09;是一种通过模拟生物进化过程来寻找最优解的优化算法。它是一种常见的启发式搜索算法&#xff0c;常用于优化、搜索和机器学习等领域。 生物基因遗传 生物的基因遗传是指父母通过基因传递给子代的过程。基因…

基于STM32的DS1302实时时钟模块应用及原理介绍

在嵌入式系统中&#xff0c;实时时钟模块是一个常见的功能模块&#xff0c;用于记录和管理系统的时间信息。DS1302是一款低功耗、具有多种功能的实时时钟芯片&#xff0c;被广泛应用于各种电子产品中。本文将介绍基于STM32微控制器的DS1302实时时钟模块的应用及原理&#xff0c…

C++类的继承

目录 什么是继承&#xff1f; 父类与子类对象的赋值转换 继承中的作用域问题 子类的默认成员函数问题 如何使一个类不能被继承&#xff1f; 父类的友元和静态成员变量 多重继承与菱形继承 继承和组合 什么是继承&#xff1f; 继承 (inheritance) 机制是面向对象程序设…

基于FPGA的图像Robert变换实现,包括tb测试文件和MATLAB辅助验证

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 fpga的结果导入到matlab显示&#xff1a; 2.算法运行软件版本 vivado2019.2 matlab2022a 3.部分核心程序 ..................................…

obsidian使用分享

ob对比其他软件 上文提到obsidian&#xff0c;这里对obsidian做一个简要的总结 优点&#xff1a;对比notion&#xff0c;语雀这些软件&#xff0c;内容存储在应用商的服务器上。它是存在本地的。 对比思源笔记。说一下思源笔记的不足。思源是块来控制的&#xff0c;回车就是一…