【数据结构】--- 深入剖析二叉树(中篇)--- 认识堆堆排序Topk

 Welcome to 9ilk's Code World

       

(๑•́ ₃ •̀๑) 个人主页:        9ilk

(๑•́ ₃ •̀๑) 文章专栏:     数据结构之旅 


文章目录

🏠 初识堆

📒 堆的概念

📒 堆的性质

🏠 向上调整算法 && 向下调整算法

📒 向上调整算法

📒 向下调整算法

📒 向上调整 vs 向下调整

🏠 堆的应用场景

📒 堆排序

📒 Top K问题


上篇我们讲解了树以及二叉树,相信小伙伴们对二叉树有了初步的了解,本篇文章我们来了解下由二叉树延伸出来的堆以及堆排序,Top K问题。

🏠 初识堆

     我们知道二叉树的顺序结构适合于完全二叉树和满二叉树,而我们今天的主角也是个完全二叉树,因此它也是使用顺序结构的数组来存储

📒 堆的概念

堆的概念(来自度娘):

⚠️

  • 我们这里的堆是一种数据结构,而操作系统虚拟进程地址空间的堆区是操作系统中管理内存的一块区域分段
  • 堆分为大堆和小堆。大堆指的是双亲结点的值域大于孩子结点,小堆指的是双亲结点的值域小于孩子结点
  • 堆只规定了孩子和双亲的关系,并未规定兄弟间的大小关系
  • 堆在物理层面是数组,逻辑结构上是二叉树。

📒 堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值
     
  • 堆总是一棵完全二叉树
     

🏠 向上调整算法 && 向下调整算法

对于这样的一个小堆,我们要插入2这个数据,此时不满足小堆的要求,若要调整可能会影响祖先,那有什么方法能解决这个问题呢?这里就要介绍一个新的算法 --- 向上调整算法

📒 向上调整算法

我们先上个动图来感受下 ~ 

我们可以看到这个过程是针对某个结点而言的,若要满足小堆,依次拿这个结点向上与它的祖先比较,如果它比祖先小就交换,直到小于它的某个祖先或交换到根结点的位置。

⚠️  向上调整算法只能帮助我们使根结点的值域最小,而不能保证所有其他结点的大小关系!

  • 代码分析及实现

1.首先需要实现一个交换数组数据的Swap函数

2.如何找某个结点child的祖先parent,这里就要用到我们上节的知识:parent =(child - 1)/ 2;

3.何时不交换:当小于它的某个祖先时或交换到根结点的位置(child>0)

void Swap(Datatype& x,Datatype& y)
{Datatype temp = x;x = y;y = temp;
}void AdjustDown(int* arr,int child)
{int parent = (child - 1) / 2;//双亲结点while(child > 0){if(arr[child] < arr[parent]){Swap(arr[child],arr[parent]);child = parent;//更新孩子和双亲parent = (parent-1)/2;     }else{break;}}
}
  • 利用向上调整算法建堆

假设已知数组a[ ] = {1,5,3,8,7,6},如何把它建成一个大堆呢?这里就可以用到我们的向上调整算法。

整个过程就是,我们每次向新数组插入数据时,采用向上调整算法进行调整。

void HPInit(HP* php)
{assert(php);php->a = NULL;php->size = 0;php->capacity = 0;
}
void Swap(HPDataType* px, HPDataType* py)
{HPDataType tmp = *px;*px = *py;*py = tmp;
}void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;//while (parent >= 0)while(child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}
}void HPPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size-1);
}int main()
{int arr[] = {1,5,3,8,7,6};HP hp;HPInit(&hp);for(int i = 0; i < sizeof(arr)/sizeof(arr[0]);i++){HPPush(&hp,arr[i]);} return 0;
}

📒 向下调整算法

对于这样的一个小堆我们要删除堆顶数据10,应该怎么删呢?有的小伙伴认为直接循环覆盖不就行了,但这样做会出现两个问题:1.挪动覆盖时间复杂度是O(N) 2.堆结构破坏,父子兄弟间关系乱套。有什么解决方法呢?我们可以采取这样的一个方法

1.首尾(根结点和最后一个叶子结点)交换数据,删除尾部数据

2.对根结点采用向下调整算法恢复堆的结构

我们上动图 ~ 

由动图我们可以知道,向下调整大致是这样的一个流程:若要保证是小堆,先找出左右孩子中较小的那一个,如果调整节点比较小的那个要大,就两者交换。

  • 向下调整代码分析及实现

1.需要一个交换函数

2.选出左右孩子较小的那个(假设小堆),同时要保证有右孩子

3.调整后的更新 parent = child; child = 2*child + 1;

4.调整结束条件:调整结点比较小孩子结点小 或 调整到二叉树最后一层(child < n)

void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){// 假设法,选出左右孩子中小的那个孩子if (child+1 < n && a[child + 1] > a[child]){++child;}if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
  • 利用向下调整算法实现删除堆顶数据
void HPPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);//首尾交换php->size--;//删除尾部数据AdjustDown(php->a, php->size, 0);//向下调整
}
  • 利用向下调整建堆

利用向下调整建堆我们需要从倒数第一个非叶子结点开始,这与向上调整调整有所不同

从倒数第一个非叶子结点开始的原因:

1. 向下调整的前提结点的左右子树都是堆

如上图若要建小堆,27的右子树是小堆,但左子树不是小堆,若向下调整,会使原本应为根节点的15被忽视。

2.从倒数第一个非叶子结点开始的话,就可以先调整每个子树为小堆或大堆

动图 part ~

  • 代码分析以及实现

我们需要确定倒数第一个非叶子结点。

1.最后一个叶子结点下标为n-1

2.倒数第一个非叶子结点即为最后一个叶子结点的父亲

3.由于parent = (child - 1) / 2 , 因此倒数第一个非叶子结点下标为(n-1-1)/ 2

void HPInitArray(HP* php, HPDataType* a, int n)
{assert(php);php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);if (php->a == NULL){perror("malloc fail");return;}memcpy(php->a, a, sizeof(HPDataType) * n);php->capacity = php->size = n;// 向下调整for (int i = (php->size-1 - 1)/2; i >= 0; i--){AdjustDown(php->a, php->size, i);}
}

📒 向上调整 vs 向下调整

  • 时间复杂度 :向上调整 vs 向下调整

由于时间复杂度要考虑最坏的情况,所以二叉树中的结点最坏调整高度次,因此Push操作或向上调整算法的时间复杂度是O(logN);而对于Pop操作,我们一次向下调整最坏是从根结点开始调整,最坏要调整高度h次,因此Pop操作或向下调整算法的时间复杂度是O(logN)

结论: 

1.完全二叉树Pop和Push操作的时间复杂度都是O(logN)

2.向上调整算法和向下调整算法的时间复杂度都是O(logN)

向下调整和向上调整都是O(logN),我们是不是可以随意用呢?别急,我们分析建堆层面 ~ 


  • 时间复杂度: 向上调整建堆 vs 向下调整建堆

向上调整建堆

假设在最坏情况下,设树的高度为h,累计调整次数为f(h),f(h)为每个结点调整次数之和,由于每一层结点个数为2^(i-1),则有:

         f(h) = (2^1)*1 + (2^2)*2 + (2^3)*3 +... + (2^(h-1))*(h-1);

---> 2f(h) = (2^2)*1 + (2^3)*2 + (2^4)*3 +... + (2^(h-1))*(h-2) + (2^(h)*(h-1));

--->错位相减得  f(h) = (2^h)*(h-2)+2     (1)

由于 2^(h) - 1 = N -->  h = log(N + 1)    (2)

联立(1)  (2) 得  f(N) =  (N+1)(log(N+1) - 2) + 2 

由f(N)得   向上调整建堆的时间复杂度为O(N*logN)

向下调整建堆

假设在最坏情况下,设树的高度为h,累计调整次数为f(h),f(h)为每个结点调整次数之和,由于每一层结点个数为2^(i-1),则有:

         f(h) = (2^(h-2))*1 + (2^(h-3))*2 + (2^(h-4))*3 + ... + (2^0)*(h-1)

---> 2f(h) = (2^(h-1))*1 + (2^(h-2))*2 + (2^(h-3))*3 + ... + (2^0)*(h-2) + (2^0)*(h-1)

--->错位相减得  f(h) =  2^(h) + h - 2;   (1)

由于 2^(h) - 1 = N -->  h = log(N + 1)    (2)

联立(1)  (2)  得  f(N) = N + 1 + log(N+1) - 2;

由f(N)得  向上调整建堆的时间复杂度为O(N)

不同算法建堆差异的原因

我们发现向下调整建堆的时间复杂度小于向上调整建堆,效率较高,原因在于完全二叉树层数越大,该层结点数越多,而向下调整是先对倒数第一层的结点(可以说集合了这颗树的大部分结点)开始调整,且调整次数只有1次,也就是说向下调整对结点调整次数是多 x 少,对大部分结点的调整次数少 ;而向上调整建堆是从根结点开始调整,可以说是多 x 多,对大部分结点调整次数多。

因此对同样时间复杂度的算法,采用向下调整建堆的方法效率更高一些!


🏠 堆的应用场景​​​​​​​

对于堆,我们主要有两个应用场景堆排序和Top K问题

📒 堆排序

  • 第一种堆排序

我们前面实现了Pop操作,同时我们知道向上调整或算法可以使根结点为最大或最小,因此我们可以先对数组初始化建小堆,此时若要升序根节点值就是最小的再不断Pop操作就能实现排序

void HPInitArray(HP* php, HPDataType* a, int n)
{assert(php);php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);if (php->a == NULL){perror("malloc fail");return;}memcpy(php->a, a, sizeof(HPDataType) * n);php->capacity = php->size = n;for (int i = (php->size-1 - 1)/2; i >= 0; i--){AdjustDown(php->a, php->size, i);}
}void HPPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}void HeapSort(int* a, int n)
{HP hp;HPInitArray(&hp, a, n); //初始化堆int i = 0;while (!HPEmpty(&hp)){a[i++] = HPTop(&hp);//取堆顶数据HPPop(&hp);//删除数据}HPDestroy(&hp);
}

对于这种堆排序思路比较简单容易理解,但是存在两个问题:1.需要我们自己实现一个堆的数据结构 2.调用HpInitArray(),空间复杂度为O(N)

  • 第二种堆排序

若我们跟第一种堆排序一样升序建小堆而不申请空间原地操作,会有什么问题?

答案是这样我们虽然能得到最小的,但要得到次小的,要重新建堆O(N),重复下来整个过程的时间复杂度是N + N -1 + N - 2 + ... + 1 --> O(N^2) 这个效率是大大不行的,有什么解决之法?那我们就反着来,升序建大堆看看 ~ 

大概流程是:

1.若要升序初始化建大堆

2.首尾交换数据,缩小范围

3.向下调整根结点 循环往复(2)(3) 直到范围缩小为0

void AdjustDown(HPDataType* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){// 假设法,选出左右孩子中大的那个孩子if (child+1 < n && a[child + 1] > a[child]){++child;}if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}void HeapSort(int* a, int n)
{// a数组直接建堆 O(N)for (int i = (n-1-1)/2; i >= 0; --i){AdjustDown(a, n, i);}// O(N*logN)int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}

这种堆排序主要是升序建大堆,再利用堆删除数据的思想,时间复杂度是O(NlogN)

📒 Top K问题

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决。

基本思路如下:

1. 用数据集合中前K个元素来建堆

要前k个最大的元素,则先对前k个元素建小堆;要前k个最小的元素,则先对前k个数据建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

说明:若要前k个最大,建小堆就能保证前k大的能通过向下调整沉进这个“三角形”里,同时前k大之外的数据能不断被剔除出去,因为会往上“浮动”

void topk()
{printf("请输入k:>");int k = 0;scanf("%d", &k);const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen error");return;}//申请空间准备建堆int val = 0;int* minheap = (int*)malloc(sizeof(int) * k);if (minheap == NULL){perror("malloc error");return;}for (int i = 0; i < k; i++){fscanf(fout, "%d", &minheap[i]);}// 建k个数据的小堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(minheap, k, i);}//判断调整int x = 0;while (fscanf(fout, "%d", &x) != EOF){// 读取剩余数据,比堆顶的值大,就替换他进堆if (x > minheap[0]){minheap[0] = x;AdjustDown(minheap, k, 0);}}for (int i = 0; i < k; i++){printf("%d ", minheap[i]);}fclose(fout);}

本次分享到这里就结束啦,下篇我们将讲解二叉树结构及其遍历和相关oj题,记得三连呀 ~ 

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

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

相关文章

【云原生】Pod 的生命周期(一)

【云原生】Pod 的生命周期&#xff08;一&#xff09;【云原生】Pod 的生命周期&#xff08;二&#xff09; Pod 的生命周期&#xff08;一&#xff09; 1.Pod 生命期2.Pod 阶段3.容器状态3.1 Waiting &#xff08;等待&#xff09;3.2 Running&#xff08;运行中&#xff09;3…

《Python编程从入门到实践》day20

#尝试在python3.11文件夹和pycharm中site-packages文件夹中安装&#xff0c;最终在scripts文件夹中新建py文件成功导入pygame运行程序 #今日知识点学习 import sysimport pygameclass AlienInvasion:"""管理游戏资源和行为的类"""def __init__(…

memory consistency

memory consistency model 定义了对于programmer和implementor来说&#xff0c;访问shared memory system的行为&#xff1b; 对于programmer而言&#xff0c;他知道期望值是什么&#xff0c; 知道会返回什么样的数据&#xff1b;&#xff1b; 对于implementro而言&#xff0c;…

微信小程序原生代码实现小鱼早晚安打卡小程序

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 小鱼早晚安打卡小程序&#xff1a;开启健康生活&#xff0c;共享正能量 在这个快节奏的时代&#xff0c;我们常常被各种琐事和压力所困扰&#xff0c;以至于忽略了对健康生活方式的追求。然…

【探秘地球宝藏】矿产资源知多少?

当我们仰望高楼林立的城市&#xff0c;乘坐便捷的交通工具&#xff0c;享受各种现代生活的便利时&#xff0c;你是否曾想过这一切背后的支撑力量&#xff1f;答案就藏在我们脚下——矿产资源&#xff0c;这些大自然赋予的宝贵财富&#xff0c;正是现代社会发展的基石。今天&…

OpenHarmony 实战开发——ABI

OpenHarmony系统支持丰富的设备形态&#xff0c;支持多种架构指令集&#xff0c;支持多种操作系统内核&#xff1b;为了应用在各种OpenHarmony设备上的兼容性&#xff0c;本文定义了"OHOS" ABI&#xff08;Application Binary Interface&#xff09;的基础标准&#…

idea中取消自动导包顺序

1、取消自动导入 2、取消导包顺序设置

Python学习笔记------处理数据和生成折线图

给定数据&#xff1a; jsonp_1629344292311_69436({"status":0,"msg":"success","data":[{"name":"美国","trend":{"updateDate":["2.22","2.23","2.24",&qu…

奶爸预备 |《P.E.T.父母效能训练:让亲子沟通如此高效而简单:21世纪版》 / 托马斯·戈登——读书笔记

目录 引出致中国读者译序前言第1章 父母总是被指责&#xff0c;而非受训练第2章 父母是人&#xff0c;不是神第3章 如何听&#xff0c;孩子才会说&#xff1a;接纳性语言第4章 让积极倾听发挥作用第5章 如何倾听不会说话的婴幼儿第6章 如何听&#xff0c;孩子才肯听第8章 通过改…

保研面试408复习 3——操作系统

文章目录 1、操作系统一、进程有哪几种状态&#xff0c;状态之间的转换、二、调度策略a.处理机调度分为三级&#xff1a;b.调度算法 标记文字记忆&#xff0c;加粗文字注意&#xff0c;普通文字理解。 为什么越写越少&#xff1f; 问就是在打瓦。(bushi) 1、操作系统 一、进程…

设计模式Java实现-建造者模式

楔子 小七在2019年的时候&#xff0c;就想写一个关于设计模式的专栏&#xff0c;但是最终却半途而废了。粗略一想&#xff0c;如果做完一件事要100分钟&#xff0c;小七用3分钟热情做的事&#xff0c;最少也能完成10件事情了。所以这一次&#xff0c;一定要把他做完&#xff0…

Windows常用快捷键与CMD常用命令

1.win系列快捷键使用 WinD&#xff0c;快速进入桌面 WinE&#xff0c;打开我的电脑&#xff08;文件资源管理器&#xff09; WinI&#xff0c;打开设置界面 WinL&#xff0c;快速锁屏 WinM&#xff0c;最小化所有窗口 WinShiftM&#xff0c;还原最小化的窗口 WinV&#…

为什么下载卡在idealTree:NodeJS: sill idealTree buildDeps

可能使用的是npm config set registry https://registry.npm.taobao.org而这个镜像文件已经过期了 解决方法如下&#xff1a; 先使用 npm cache clean --force 清除缓存 再切换镜像源 再使用npm config get registry 进行查看是否换源成功 再使用 npm install -g vue/cli 就…

【算法】滑动窗口——长度最小的子数组

本篇文章是用一个实例来介绍常用算法之一“滑动窗口”的相关概念&#xff0c;有需要借鉴即可。 目录 1.题目2.暴力求解2.1暴力求解思路&#xff1a;2.2时间复杂度是多少&#xff1f; 3.暴力求解的优化3.1固定left的情况下&#xff0c;优化right的次数。3.2sum求值优化3.3不同组…

商城数据库88张表结构完整示意图81~88及总览图(十六)

八十一&#xff1a; 八十二&#xff1a; 八十三&#xff1a; 八十四&#xff1a; 八十五&#xff1a; 八十六&#xff1a; 八十七&#xff1a; 八十八&#xff1a; 总览图&#xff1a;

Redis开源社区持续壮大,华为云为Valkey项目注入新的活力

背景 今年3月21日&#xff0c;Redis Labs宣布从Redis 7.4版本开始&#xff0c;将原先比较宽松的BSD源码使用协议修改为RSAv2和SSPLv1协议&#xff0c;意味着 Redis在OSI&#xff08;开放源代码促进会&#xff09;定义下不再是严格的开源产品。Redis官方表示&#xff0c;开发者…

websevere服务器从零搭建到上线(四)|muduo网络库的基本原理和使用

文章目录 muduo源码编译安装muduo框架讲解muduo库编写服务器代码示例代码解析用户连接的创建和断开回调函数用户读写事件回调 使用vscode编译程序配置c_cpp_properties.json配置tasks.json配置launch.json编译 总结 muduo源码编译安装 muduo依赖Boost库&#xff0c;所以我们应…

webpack与vite

webpack 使用步骤&#xff1a; 初始化项目 pnpm init -y安装依赖webpack、webpack-cli在项目中创建src目录&#xff0c;然后编写代码&#xff08;index.js&#xff09;执行pnpm weboack来对代码进行打包&#xff08;打包后观察dist文件夹&#xff09; 配置古文件&#xff08;w…

使用ThemeRoller快速实现前端页面风格美化

使用ThemeRoller快速实现前端页面风格美化 文章目录 使用ThemeRoller快速实现前端页面风格美化一、ThemeRoller二、使用方法1.基本操作面板介绍2.直接用现成的配色风格——Gallery画廊3.自定义风格——Roll Your Own4.下载风格包并应用到页面 一、ThemeRoller ThemeRoller是jQ…

基于java的CRM客户关系管理系统的设计与实现(论文 + 源码 )

【免费】基于Java的CRM客户关系管理系统的设计和实现.zip资源-CSDN文库https://download.csdn.net/download/JW_559/89273409 基于Java的CRM客户关系管理系统的设计与实现 摘 要 随着互联网的高速发展&#xff0c;市场经济的信息化&#xff0c;让企业之间的竞争变得&#xff0…