【数据结构】--- 深入剖析二叉树(中篇)--- 认识堆堆排序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,一经查实,立即删除!

相关文章

2024年5月营销日历,追热点做营销必备~

5月1日 劳动节 劳动节是致敬辛勤劳动者的节日&#xff0c;也是商家们争相推出优惠活动的黄金时期。以5.1元、51元或5.1折作为营销点&#xff0c;不仅能紧扣节日主题&#xff0c;还能吸引大量消费者。比如&#xff0c;推出抽奖活动&#xff0c;幸运者有机会享受全单5.1折的优惠…

【云原生】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;的基础标准&#…

【Numpy】一文向您详细介绍 np.linspace()

【Numpy】一文向您详细介绍 np.linspace() &#x1f308; 欢迎莅临我的个人主页&#x1f448; 这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博主简介&#xff1a;985高校的计算机专业人士&#xff0c;热衷于分享技术见…

idea中取消自动导包顺序

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

英语写作中“最后”finally、eventually、in the end、at last的用法

一、finally 是最通用的单词&#xff0c;它可以表示所有中文里“最后”的意思&#xff0c;例如&#xff1a; First do Task 1. Then do Task 2. Finally do Task 3.&#xff08;首先做任务1&#xff0c;再做任务2&#xff0c;最后做任务3。&#xff09; 上面是描述一个协议的…

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

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

实用的Chrome浏览器命令

Google Chrome 是一款广泛使用的网络浏览器&#xff0c;它提供了许多实用的快捷键和命令&#xff0c;可以帮助用户更高效地浏览网页。以下是一些常用的 Chrome 浏览器命令&#xff1a; 1. 新标签页: Ctrl T (Windows/Linux) 或 Command T (Mac) 2. 关闭当前标签: Ctrl W 或…

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

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

出海战略与技术:利用SOCKS5代理和代理IP加速跨界电商与游戏行业的全球拓展

在全球化的商业环境中&#xff0c;"出海"已成为中国企业扩展国际市场的重要战略。尤其是在跨界电商和游戏行业&#xff0c;企业不仅需要理解和适应多元文化的市场&#xff0c;还需要有效地克服技术和网络安全方面的挑战。在这种情况下&#xff0c;SOCKS5代理和代理IP…

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

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

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

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

《QT实用小工具·五十七》基于QT的语音识别

1、概述 源码放在文章末尾 该文章实现了简单的语音识别功能&#xff0c;首先&#xff0c;语音识别要做三件事情 &#xff1a; 1.记录用户的语音文件到本地 2.将用户语音编码 使用flac或者speex进行编码 3.使用第三方语音识别API或者SDK进行分析识别语音 目前做的比较简单就是使…

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

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

详细介绍Eclipse的安装过程

**Eclipse安装指南** 一、引言 Eclipse是一个广泛使用的集成开发环境&#xff08;IDE&#xff09;&#xff0c;主要用于Java语言的开发&#xff0c;但也支持其他多种编程语言。Eclipse以其强大的功能、灵活的插件系统和开源的特性&#xff0c;赢得了众多开发者的青睐。本文将…

android 启动优化方向跟踪

先简单带过framwork以上的流程&#xff0c;主要看framwrok里面的步骤 一 前期启动流程速览 1 kernel内核空间启动 负责启动 native层的init进程 具体可以参考linux内核&#xff08; Bootloader启动Kernel的swapper进程(pid0)&#xff0c;它是内核首个进程&#xff0c;用于初始…