<数据结构与算法>堆的应用二叉树的链式实现

 

目录

前言

一、堆的应用

1. 堆排序

1.1 排升序,建大堆

1.2  时间复杂度计算

2. Top k问题

二、 二叉树的链式实现

1. 二叉树的遍历

2. 二叉树基础OJ

2.2     100. 相同的树

总结


前言

        学习完堆的数据结构,我们要清楚,它虽然实现了排序功能,但是真正的排序函数应当是在给定的数组内,将数组排序,如果我们要用堆排序,那么我们不可能手写堆的数据结构,在堆内排序后再复制给给定的数组,这样不仅很麻烦,而且还要开辟另外的空间。

        所以,我们又有了堆排序的方法。


一、堆的应用

1. 堆排序

        我们可以将给定的数组看为一个完全二叉树,但它此刻还不是堆,因为堆是有顺序的,所以我们可以使用向上或向下调整函数进行堆排序,模拟堆插入,这就是一个建堆的过程。

 堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1.建堆

  • 升序:建大堆
  • 降序:建小堆

2.利用堆删除思想进行排序

1.1 排升序,建大堆

        方法一:先利用向上调整建大堆(在给定的数组内从下标为1的数据开始进行向上调整),然后再进行向下调整,完成升序排序

        解释: 如果采用小堆,那么堆顶就是最小值,取走堆顶数据后,次小数上至堆顶,此时堆所表示的二叉树内父子兄弟关系就全乱了,不能再按顺序提出堆顶数据。

        所以,排升序还是需要用大堆

 //堆排升序 -- 建大堆
void HeapSort(int* a, int n)
{// 1.建堆 -- 向上调整建堆--模拟插入的过程for (int i = 1; i < n; ++i){AdjustUp(a, i);}// 2.利用堆删除思想进行排序int end = n - 1;while (end > 0){swap(&a[0], &a[end]);AdjustDown(a, end - 1, 0);end--;//end下标前移}}int main()
{int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3 }; // 对数组排序HeapSort(a, 10);return 0;
}

        方法二:先向下调整建大堆,再用向下调整完成升序排序

        注意: 在向下调整时,不能从堆顶开始,因为数组最初是无序的,而向下调整的前提是其左右子树是大堆或小堆,才可以向下调整,        

        所以我们先将最后一个叶子节点的父节点开始向下调整,完成调整后,从父节点开始向前向下调整,直到堆顶完成向下调整后结束。

综上分析,方法二效率比方法一高,只用写一个向下调整函数即可

 //堆排升序 -- 建大堆
void HeapSort(int* a, int n)
{//1.建堆 -- 向下调整建堆for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}// 2.利用堆删除思想进行排序int end = n - 1;while (end > 0){swap(&a[0], &a[end]);AdjustDown(a, end - 1, 0);end--;//end下标前移}}int main()
{int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3 }; // 对数组排序HeapSort(a, 10);return 0;
}

1.2  时间复杂度计算

        向下调整建堆的时间复杂度计算 O(N)

         向上调整建堆的时间复杂度计算 O(N*logN)

         最简单的解释:向下调整,节点最多的一层最坏情况只调整一次,节点最少的一层最坏情况调整h-1次,而向上调整相反,节点最少的一层最坏情况只调整一次,节点最多的一层最坏情况调整h-1次,显而易见,向上调整的时间复杂度大于向下调整.

2. Top k问题

         TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

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

        最佳的方式就是用堆来解决,基本思路下:

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

  • 前k个最大的元素,则建小堆(取要排序的前k个数据先建一个小堆,之后依次遍历数据,与堆顶数据比较大小,如果比堆顶数据大,就替代它进堆,然后向下调整)
  • 前k个最小的元素,则建大堆()

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

void PrintTopK(const char* file, int k)
{// 1. 建堆--用a中前k个元素建小堆int* topk = (int*)malloc(sizeof(int) * k);assert(topk);FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen error");return;}// 读出前k个数据建小堆for(int i = 0; i < k; ++i){fscanf(fout, "%d", &topk[i]);}for (int i = (k-2)/2; i >= 0; --i){AdjustDown(topk, k, i);}// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换int val = 0;int ret = fscanf(fout, "%d", &val);while (ret != EOF){if (val > topk[0]){topk[0] = val;AdjustDown(topk, k, 0);}ret = fscanf(fout, "%d", &val);}for (int i = 0; i < k; i++){printf("%d ", topk[i]);}printf("\n");free(topk);fclose(fout);
}void CreateNDate()
{// 造数据int n = 10000000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (size_t i = 0; i < n; ++i){int x = rand() % 10000;fprintf(fin, "%d\n", x);}fclose(fin);
}int main()
{//int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3}; // 对数组排序//HeapSort(a, 10);CreateNDate();PrintTopK("data.txt", 10);return 0;
}

二、 二叉树的链式实现

        二叉树本身增删查改没有什么实际意义,但当加上了一个特性例如:左子树小于根,右子树大于根后,即搜索二叉树,它就有了实际意义。

在看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:

1. 空树

2. 非空:根节点,根节点的左子树、根节点的右子树组成的。

1. 二叉树的遍历

        把每一个二叉树都分为三部分:根、左子树、右子树

                                        理解函数栈帧,画图就可理解递归调用过程 

 二叉树结构体:

typedef int BTDataType;
typedef struct BinaryTreeNode
{BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;

1.1 前序遍历根、左子树、右子树  (先访问根,遇到每一个都作为根)

1  2  3  NULL  NULL  NULL  4  5  NULL  NULL  6  NULL  NULL

//前序遍历
void PreOrder(BTNode* root) {if (root == NULL) {printf("NULL ");return;}printf("%d ", root->data);PreOrder(root->left);PreOrder(root->right);
}

1.2 中序遍历:左子树、根、右子树    (遇到每一个都先访问左子树,直到NULL,所以访问的第一个一定为NULL)

NULL  3  NULL  2  NULL  1  NULL  5 NULL  4  NULL  6  NULL

//中序遍历
void InOrder(BTNode* root) {if (root == NULL) {printf("NULL ");return;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}

1.3 后序遍历:左子树、右子树、根     (左、右、根)

NULL  NULL  3  NULL  2  NULL  NULL 5  NULL  NULL 6  4  1

//后序遍历
void PostOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}PostOrder(root->left);PostOrder(root->right);printf("%d ", root->data);
}

1.4 层序遍历:一层从左往右

1  2  4  3  5  6

用队列实现,每出队一个根节点就把它的孩子入队,实现出上一层带入下一层

#include"Queue.h"
//队列的实现,并把data的类型改为树结点指针类型void LevelOrder(BTNode* root)
{Queue q;QueueInit(&q);if (root)QueuePush(&q, root);while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);//用树节点的指针类型接收队头数据(因为队头数据类型为树节点指针类型)QueuePop(&q);printf("%d ", front->data);//如果左孩子不为空就入队if(front->left)QueuePush(&q, front->left);//如果右孩子不为空就入队if (front->right)QueuePush(&q, front->right);}QueueDestroy(&q);
}

1.计算二叉树内结点个数

注意:

  1. 如果想要计算二叉树内节点个数,可以在传参时加上一个参数psize指针
  2. 不要在函数内使用static静态局部变量,因为它指挥在第一次调用时才执行,之后调用都不会再执行,也就是说第一次计算二叉树内节点个数是正确的,而再次计算时,size不能初始化为0,所以计算结果就会出错。
  3. 也尽量不使用全局变量,这样的话我们要在每一次调用计算函数前,自己手动初始化size全局变量为0,并不是很方便。

        最简形式:

int TreeSize(BTNode* root)
{return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

2.计算树的高度

        看山不是山,不考虑太多递归过程,看结果

注意:

  1. 一定要记录递归结果,因为如果不记录结果,那么程序就会递归两大遍次数
int TreeHeight(BTNode* root)
{if (root == NULL)return 0;int leftHeight = TreeHeight(root->left);int rightHeight = TreeHeight(root->right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

3.计算第k层节点数

        k--直到k为1,就找到了要计算的第k层,如果此时root不为NULL,则return 1,如果为NULL,则返回0。这样就计算出了左右子树的第k-1层节点数,最后相加。

int TreeKLevel(BTNode* root, int k)
{assert(k > 0);if (root == NULL)return 0;if (k == 1)return 1;return TreeKLevel(root->left, k - 1)+ TreeKLevel(root->right, k - 1);
}

4.二叉树查找值为x的结点

注意:

        在函数多层调用时,返回root是不能直接返回到最外面的,只能返回上一个函数调用处

//二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL)return NULL;if (root->data == x)return root;BTNode* lret = BinaryTreeFind(root->left, x);if (lret)return lret;BTNode* rret = BinaryTreeFind(root->right, x);if (rret)return rret;return NULL;
}

2. 二叉树基础OJ

2.1      965. 单值二叉树

bool isUnivalTree(struct TreeNode* root)
{if(root == NULL)return true;if(root->left && root->val != root->left->val)return false;if(root->right && root->val != root->right->val)return false;return isUnivalTree(root->left) && isUnivalTree(root->right);
}

        在写递归时,要写递归的出口!顾名思义,是在递归过程中特殊的结果,写能阻断递归的逻辑表达式。例如此题:第二三个if,只有在值不相等的时候返回false,终止递归,不能在判断中写相等的情况,因为那样的判断没有任何用处 。


2.2     100. 相同的树

   bool isSameTree(TreeNode* p, TreeNode* q){if(p == NULL && q == NULL)return true;if(p == NULL || q == NULL)return false;if(p->val != q->val)return false;return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);}

  

2.3      144. 二叉树的前序遍历

        由于要返回数组,所以要在函数内开辟一个数组空间,而开辟多少字节空间就要我们计算二叉树的结点数 

int TreeSize(struct TreeNode* root)
{return root == NULL? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}void preorder(struct TreeNode* root, int*arr, int* pi)
{if(root == NULL)return;a[(*pi)++] = root->val;//错误,因为i的值不会及时更新preorder(root->left,arr,pi);preorder(root->right,arr,pi);
}int* preorderTraversal(struct TreeNode* root, int* returnSize)
{*returnSize = TreeSize(root);int* arr = (int*)malloc(sizeof(int)*(*returnSize)));int i = 0;preorder(root,arr,&i);
}

注意前序遍历函数内数组下标是*pi,因为如果只传整形 i 是无法在递归中及时更新i的值的!

2.4      572. 另一棵树的子树

         使左边的每一颗子树与右边的树比较,可以利用上相同的树函数

bool isSameTree(TreeNode* p, TreeNode* q){//两个都为空if(p == NULL && q == NULL)return true;//其中一个为空if(p == NULL || q == NULL)return false;//可以退出的情况if(p->val != q->val)return false;return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);}bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{if(root == NULL)return false;if(isSameTree(root,subRoot))return true;//递归一定要记录,不然就白递归了//比如://isSubtree(root->left,subRoot);//isSubtree(root->right,subRoot);//return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);//递归了两大遍,是错误写法return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
}

 

2.5       KY11 二叉树遍历

struct TreeNode
{struct TreeNode* left;struct TreeNode* right;char val;
};void InOrder(struct TreeNode* root)
{if (root == NULL)return;InOeder(root->left);printf("%c ", root->val);InOeder(root->right);
}struct TreeNode* CreateTree(char* a, int* pi)
{if (a[*pi] == '#'){(*pi)++;return NULL;}struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode));root->val = a[(*pi)++];root->left = CreateTree(a, pi);root->right = CreateTree(a, pi);return root;
}int main()
{char a[100];scanf("%s", a);int i = 0;InOrder(CreateTree(a, &i));return 0;
}

总结

    二叉树这一数据结构包含了诸多的递归函数,本节学习了二叉树的遍历、计算二叉树的高度、第k层结点数、总结点数、如何在二叉树内查找数以及一些二叉树的OJ

    如何正确书写递归函数,如何正确使用递归函数来计算二叉树的各项属性是学习二叉树的关键。

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

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

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

相关文章

售后服务管理软件怎么选择?售后服务管理系统有什么用?

随着企业信息化发展&#xff0c;越来越多的企业纷纷选择售后服务管理软件来服务客户和进行内部人员管理。借助这款软件&#xff0c;企业能够高效地满足客户提出的需求&#xff0c;并提高客户对售后服务的满意度。售后服务通常涉及客户、客服、维修师傅和服务管理人员等各种角色…

用C++/JS/Python/Java代码描述秋天的味道

前言 秋天是一个充满诗意和浪漫的季节&#xff0c;它带来了清新、芬芳和美食的味道。让我们一起探索如何用编程语言来写出秋天味道的代码吧&#xff01;无论是C、JavaScript、Python还是Java&#xff0c;以下是几个简单的步骤来帮助你创造出充满秋天味道的代码&#xff1a; …

Redis缓存问题(穿透, 击穿, 雪崩, 污染, 一致性)

目录 1.什么是Redis缓存问题&#xff1f; 2.缓存穿透 3.缓存击穿 4.缓存雪崩 5.缓存污染&#xff08;或满了&#xff09; 5.1 最大缓存设置多大 5.2 缓存淘汰策略 6.数据库和缓存一致性 6.1 4种相关模式 6.2 方案&#xff1a;队列重试机制 6.3 方案&#xff1a;异步更新缓…

[C++11]

文章目录 1. 自动类型推导1.1 auto1.1.1 推导规则1.1.2 auto的限制1.1.3 auto的应用1.1.4 范围for 1.2 decltype1.2.1 推导规则1.2.2 decltype的应用 1.3 返回类型后置 2.可调用对象包装器、绑定器2.1 可调用对象包装器2.1.1 基本用法2.1.2 作为回调函数使用 2.2 绑定器 3. usi…

idea创建javaweb项目,jboss下没有web application

看看下图这个地方有没有web application

C++_模板进阶_非类型模板参数_模板特化_分离编译

一、非类型模板参数 模板参数&#xff0c;分为类型形参和非类型形参。 类型形参就是在模板中跟在typename和class之后的参数类型名称&#xff0c;非类型形参就是用一个常量作为类模板或者函数模板的一个参数&#xff0c;在类模板和函数模板中&#xff0c;可以将该参数当作常量…

【JUC系列-01】深入理解JMM内存模型的底层实现原理

一&#xff0c;深入理解JMM内存模型 1&#xff0c;什么是可见性 在谈jmm的内存模型之前&#xff0c;先了解一下并发并发编程的三大特性&#xff0c;分别是&#xff1a;可见性&#xff0c;原子性&#xff0c;有序性。可见性指的就是当一个线程修改某个变量的值之后&#xff0c…

卷积神经网络全解:(AlexNet/VGG/ GoogLeNet/LeNet/ResNet/卷积/激活/池化/全连接)、现代卷积神经网络、经典卷积神经网络

CNN&#xff0c;卷积神经网络&#xff0c;Convolution Neural Network 卷积计算公式&#xff1a;N &#xff08;W-F2p&#xff09;/s1 这个公式每次都得看看&#xff0c;不能忘 1 经典网络 按照时间顺序 1.1 LeNet LeNet是 Yann LeCun在1998年提出&#xff0c;用于解决手…

Lua 数据结构

一、Lua 中的数据结构 Lua 中并没有像 java、kotlin 语言有专门的数组、列表、集合等数据结构的类&#xff0c;而只提供 table 类型&#xff0c;但他很强大而且也很灵活&#xff0c;而且也在很多场景中天然的就解决了如何对接和使用的问题&#xff08;例如模块的引入&#xff…

MYSQL完全卸载、安装与账号创建、权限控制

一、卸载mysql CentOS 卸载 MySQL 1. 查看安装情况 使用以下命令查看当前安装mysql情况&#xff0c;查找以前是否装有mysql rpm -qa|grep -i mysql这里显示我安装的 MySQL 服务有有&#xff1a; 2. 停止 mysql 服务、删除之前安装的 mysql 删除命令&#xff1a;rpm -e –n…

C++笔记之条件变量(Condition Variable)与cv.wait 和 cv.wait_for的使用

C笔记之条件变量&#xff08;Condition Variable&#xff09;与cv.wait 和 cv.wait_for的使用 参考博客&#xff1a;C笔记之各种sleep方法总结 code review! 文章目录 C笔记之条件变量&#xff08;Condition Variable&#xff09;与cv.wait 和 cv.wait_for的使用1.条件变量&…

v8引擎编译全过程

环境vs2019 cmd 命令行需要设置成为代理模式 set http_proxyhttp://127.0.0.1:10809 set https_proxyhttp://127.0.0.1:10809 这个必须带上&#xff0c;不然报错&#xff0c;告诉编译器win系统的模式 set DEPOT_TOOLS_WIN_TOOLCHAIN0 源码 GitHub: GitHub - v8/v8: The…

Eclipse如何设置快捷键

在eclopse设置注释行和取消注释行 // 打开eclipse&#xff0c;依次打开&#xff1a;Window -> Preferences -> General -> Key&#xff0c;

solidwords(6)

从右视图开始&#xff0c;分上下两部分 标题 这里的薄壁要留意一下怎么算的&#xff08;单向&#xff1a;默认向内&#xff1b;如果想向外记得选反向&#xff09;

【Spring专题】Spring之Bean的生命周期源码解析——阶段二(二)(IOC之属性填充/依赖注入)

目录 前言阅读准备阅读指引阅读建议 课程内容一、依赖注入方式&#xff08;前置知识&#xff09;1.1 手动注入1.2 自动注入1.2.1 XML的autowire自动注入1.2.1.1 byType&#xff1a;按照类型进行注入1.2.1.2 byName&#xff1a;按照名称进行注入1.2.1.3 constructor&#xff1a;…

idea 新建servlet 访问提示404 WebServlet注解找不到包 报错

检查访问路径是否设置正确 如果设置为name “/testServlet”&#xff0c;则会404 WebServlet注解报错找不到包 检查是否引入了tomcat依赖包

线性代数的学习和整理8: 方阵和行列式相关(草稿-----未完成)

1.4.1 方阵 矩阵里&#xff0c;行数列数的矩阵叫做方阵方阵有很多很好的特殊属性 1.4.2 行列式 行列式是方阵的一种特殊运算如果矩阵行数列数相等&#xff0c;那么这个矩阵是方阵。行列数的计算方式和矩阵的不同只有方阵才有行列式行列式其实是&#xff0c;矩阵变化的一个面…

超越函数界限:探索JavaScript函数的无限可能

&#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 &#x1f4da; 前言 &#x1f4d8; 1. 函数的基本概念 &#x1f4df; 1.1 函数的定义和调用 &#x1f4df; 1.2 …

动态内存管理

目录 为什么要用动态内存开辟 动态内存有关函数 void* malloc (size_t size); void free (void* ptr); void* calloc (size_t num, size_t size); void* realloc (void* ptr, size_t size); C/C程序的内存开辟 柔性数组 特点&#xff1a; 柔性数组的使用&#xff1a; 为什么要用…

【nodejs】用Node.js实现简单的壁纸网站爬虫

1. 简介 在这个博客中&#xff0c;我们将学习如何使用Node.js编写一个简单的爬虫来从壁纸网站获取图片并将其下载到本地。我们将使用Axios和Cheerio库来处理HTTP请求和HTML解析。 2. 设置项目 首先&#xff0c;确保你已经安装了Node.js环境。然后&#xff0c;我们将创建一个…