数据结构·二叉树(一)

news2024/11/27 8:42:04

1. 树概念及结构

1.1 树的概念

        树是一种非线性的数据结构,它是由n(n>=0)个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树,也就是说它是根朝上,树叶朝下的。

        有一个特殊的节点,称为根节点,根节点没有前驱节点。

        除了根节点外,其余节点被分成M(M>0)个互不相交的集合,T1、T2、T3······,其中每个集合Ti(1<=i<=m)又是一颗结构与树类似的子树。每颗子树的根节点有且只有一个前驱,可以有0个或多个后继。

        因此树是递归定义的。或者说,我们可以把一颗大树拆成一个根和若干子树,其中子树又可以拆成根和若干子树,以此类推。

        注意:树形结构中,子树之间不能有交集,否则就不是树形结构。

1.2 树的相关概念

        节点的度:一个节点含有的子树的个数称为节点的度。例如A的度为6

        叶节点或终端节点:度为0的节点称为叶节点,例如B、C、H、I······等节点为叶节点

        非终端节点或分支节点:度不为0的节点。例如D、E、F、G等节点为分支节点

        父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。A是B的父节点

        子节点:一个节点含有的子树的根节点称为该节点的子节点。B是A的子节点

        兄弟节点:具有相同父节点的节点互称为兄弟节点。如上图B、C是兄弟节点

        树的度:一棵树中最大的节点的度称为树的度。如上图树的度是6

        节点的层次:从根开始定义起,根为第一层,根的子节点为第二层,以此类推

        树的高度或深度:树中节点的最大层次。如上图树的高度是4

        堂兄弟节点:父节点在同一层的节点互为堂兄弟。例如H、I互为堂兄弟节点

        节点的祖先:从根到该节点所经分支上的所有节点。例如A是所有节点的祖先

        子孙:以某节点为根的子树中任意节点都称为该节点的子孙。例如所有节点都是A的子孙

        森林:由m(m>0)棵互不相交的树的集合称为森林

        

1.3 树的表示

        树的结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存节点和节点之间的关系,实际中树有很多表示形式如:双亲表示法、孩子表示法、孩子双亲表示法、以及孩子兄弟表示法等。

        这里就简单的了解其中最常用的孩子兄弟表示法(左孩子右兄弟表示法)

        父节点无论有几个孩子,都只指向它的大儿子,然后大儿子再指向它右边的兄弟,兄弟再指向兄弟,以此类推······

   

                

2. 二叉树概念及结构

2.1 概念

        一颗二叉树是节点的一个有限集合,该集合:

                1. 为空        

                2. 由一个根节点加上两颗别称为左子树和右子树的二叉树组成

        注意:

                1. 二叉树不存在度大于2的节点

                2. 二叉树的子树有左右之分,次序不能颠倒,因为二叉树是有序树

2.2 特殊的二叉树

        满二叉树:

                一个二叉树,如果每一个层的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且总结点数是 2^k-1 则他就是满二叉树

        完全二叉树:

                完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为k的,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号1至n的节点对应时称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树。

        简单点来说,满二叉树就是n-1层都是满的,只有最后一层(第n层)都是叶子节点;完全二叉树就是n-1层都是满的,最后一层不一定满,但是从左到右的节点是连续的

                        


2.3 二叉树的存储结构

        二叉树一般可以使用两种结构存储,顺序结构(数组),链式结构(链表)

2.3.1 顺序存储

        顺序存储只适合用于完全二叉树,因为如果不是的话会造成空间的浪费,而事实上,只有堆才使用顺序存储的方式。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

        我们分析左图,就可以看出来在存储时,父子节点下标之间是存在数学关系的

        左孩子 = 父节点 * 2 + 1

        右孩子 = 父节点 * 2 + 2

        父节点 = (孩子 - 1)/2

        这是顺序存储的优势,当然命中率高我们就不再提了

        而右图就是它的缺点,当不是完全二叉树时,就要把那些空节点的位置留出来,不然父子节点的数学关系就失效了

2.3.2 链式存储

        链式存储就是用链表直接模拟二叉树,因为孩子最多只有两个,所以我们不用担心空间浪费的问题,因此不必使用左孩子右兄弟法

        链式存储又有两种方式,二叉链表,三叉链表,很好理解,我画图示意一下

3. 堆

3.1 堆的概念及结构

        简单来讲,堆就是把数据按完全二叉树的逻辑存储到一个一维数组中。同时我们将 任意父节点<=孩子 的结构称作小根堆(小堆); 将 任意父节点>=孩子 的结构称作大根堆(大堆)

                        小根堆(小堆)                                                        大根堆(大堆)

        根据小堆、大堆的特点,堆可以用作堆排序和TOP-K问题

3.2 堆的实现

        前面已经讲过很多次实现数据结构的流程了,之后便不再赘述。

        刚刚我们已经确定好了堆的实现方法——数组

                

3.2.1 初始化和销毁

3.2.2 插入数据   O(N*logN)

        这里我们实现一个小根堆,如果要弄大根堆的话道理是一样的,只需要在插入数据时略微改动即可

        插入数据的话肯定是尾插数组,那么思考一下尾插数组相当于在二叉树逻辑的什么位置插入数据,答案显然是下一个位置,哈哈好吧这是句废话

        观察上图,我们要插入一个数据50,那么就是插在数组下标是6的位置,同时二叉树逻辑中也是插在6的位置,它们的对应关系现在显而易见了吧

        现在来到下一步,我们插入了之后发现现在的二叉树不满足小根堆了,因为56大于了它的右儿子50,所以我们要进行调整。

        此时用到向上调整算法:就是说50这个数据的插入只影响它的祖先,并不会影响兄弟或者叔叔什么的,因此我们只对比它的父亲,如果50小于它的父亲,就将它们换位置,如此交换到50的父亲终于小于它为止。

        那么父节点的下标规律我们之前也已经总结过了,现在实现插入数据:

        向上调整算法:O(logN)

        

        

        这里如果我们想要得到大根堆,只需要把交换两个数据的判断条件改成大于号> 就行了,这样就会在孩子大于父亲的时候进行交换,如此形成在同一条线上的上大下小结构

3.2.3 取堆顶

        取堆顶就是取到最小或者最大值

                ​​​​​​​        

        现在问题来了,如果我们想取次小的数据,或者取次次小的数据怎么办,我们可以保证堆顶是最小的,次小的出现在第二层,但是次次小的我们无法保证是出现在第二层还是第三层。

        这样下去次次次小的怎么取,次次次次小的又怎么取,依次类推,那么这个问题的解决办法就是删去堆顶的数据,让次小的数据来到堆顶,并且这个上来的过程不能破坏堆的结构

3.2.4 删除堆顶的数据 O(logN)

        首先错误示范:直接将数组首元素删去,其他元素向前提,这种方法是删掉了堆顶元素,但是仅此而已,这么做破坏了堆的结构,兄弟变父子,叔侄变兄弟,简直倒反天罡!堆的大小结构全都乱套了,那么我们可以将这个堆重新放进堆中再整理一次,就像第一次把数据录进堆中一样。但是这种方式的时间复杂度是 O(N) 不好。

        正确方法:

                1. 收尾数据交换,删除尾部数据

                2. 向下调整算法 O(logN)

        ​​​​​​​        

        这么做不会破坏原堆的结构,同时大大缩减时间复杂度

        那么向下调整算法是怎么弄?

        首先根它的两个儿子比较,与较小的交换位置,如此循环下去。那么怎么判断该停了呢,是比自己的两个儿子都小,或者是换到了叶子位置,那么又如何判断到没到叶子位呢,就是判断自己还有没有左儿子,没有左儿子就说明自己没儿子了

        

        

        这里参数n是用来判断有没有向下调整到了叶子,如果到了就不用再调了,从参数parent开始向下调整

3.2.5 判断是否是空堆

        ​​​​​​​        ​​​​​​​        ​​​​​​​

        到此如何建立一个堆及其各种功能的实现就算是完成了

3.2.6 完整代码

        Heap.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

void HPInit(HP* php);
void HPDestroy(HP* php);

void HPPush(HP* php, HPDataType x);

//取堆顶
HPDataType HPTop(HP* php);

//删除堆顶的数据
void HPPop(HP* php);

//判断是否是空堆
bool HPEmpty(HP* php);

        Heap.c

#include"heap.h"

void HPInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}


void HPDestroy(HP* php)
{
	assert(php);

	free(php->a);
	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 (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void HPPush(HP* php, HPDataType x)
{
	assert(php);
	
	if (php->capacity == php->size)
	{
		size_t newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;

		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);
}



//取堆顶
HPDataType HPTop(HP* php)
{
	assert(php);

	return php->a[0];
}



//向下调整
void AdjustDown(HPDataType* a,int n,int parent)
{
	//假设法,选出小的孩子
	int child = parent * 2 + 1;//假设左孩子小
	while (child < n)//当child>=size时出数组了说明是叶子
	{
		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);
}



//判断是否是空堆
bool HPEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

3.2.7 向下调整算法处理数组 O(N)

        这里是说如果给你一整个无序的数组,让你整理成堆备用,我们该怎么处理。

        比较容易想到的方案就是用 HPPop() 将数组中的元素一个一个的插入到堆中去,这个插入的过程中,我们使用了向上调整算法,整个方法的时间复杂度是O(N*logN)

        在此我提供一种新的处理方案,使用向下调整算法:

        我们直接将数组视为一个无序的堆,然后从最后一个非叶子节点开始向下调整,这个节点调整完后再调整前一个节点,直到所有节点全部调整完毕,或者说调到根,根再向下调整,根调整完,整个堆也就调整完了

        那么为什么要这么调整呢,然后一个无序的堆我们是无法进行向上调整的,向上调整的要求是:该节点上面的堆是有序的(不包括该节点)。因此我们只能考虑向下调整,向下调整的要求是:该节点下面的堆是有序的(不包括该节点)。叶子节点们下面已经没有堆了,所以不需要再进行调整,因此我们从最后一个非叶子节点开始调整,而最后一个非叶子节点就是最后一个节点的父亲。

        ​​​​​​​        ​​​​​​​        

        上代码:

        最后我们还可以计算出这种方案的时间复杂度是 O(N)

        我们回头看这两种处理方法,它们的做法是相似的,那为什么时间复杂度有差距,其原因在于满二叉树中一半的节点都会存储在最后一层中,而向下调整算法正好不用处理最下一层,倒数第二层中的每个节点也都最多只用处理一次,这样形成了一个 多数据*少调整次数 的结构。反观向上调整算法,是 多数据*多调整次数 的结构。

3.3 堆的应用

3.3.1 堆排序 O(N*logN)

        堆排序即利用堆的思想来进行排序,要注意:

                        排升序:建大堆

                        排降序:建小堆

        这里我们用排降序举例子

        可能有的同学觉得,排降序不应该排大根堆,然后找到数组中最大的数,放到第一位,之后删除它,找到第二大的数,如此下去,但是我们要注意,当我们将最大的数放到第一位的时候它处在堆根或者说数组的首元素,此时我们将它删除那堆的结构就被破坏了,那处理后面的数据又要重新建堆,每删一个数就要重新建一次堆,时间复杂度极大

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

        因此我们要转换思路,排小根堆,将找到的最小值与数组尾部的值交换,再删除,之后向下调整找到第二小的值,再交换,如此反复,处理整个数组。说白了就是排小根堆,每次找到最小值落底。

                                

        代码如下:

        ​​​​​​​        

        这里我要提醒一下,排大根堆还是排小根堆取决于 AdjustDown() 函数中的条件,这里面的条件我们按需调整

        ​​​​​​​        

3.3.2 TOP-K 问题

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

        对于这类问题我们第一思路就是排序,但是由于数据量非常大,排序就不太可能了(可能数据都不能一下子全部加载到内存中)。所以最佳的方式就是用堆来解决,基本思路如下:

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

                前K个最大元素,则建大堆

                前K个最小元素,则建小堆

        2. 用剩余的N-K个元素依次与堆顶元素来比较,判断是否需要替换堆顶元素

        简单点来讲,这个方案就是先挑出K个元素,之后继续扫描数据,判断是否要替换掉之前那K个元素中最大(或最小)的元素,最后结果就是剩下所有数据中的top-k个数据        

        首先我们先搞一堆数据(10000个)写在文件里

        ​​​​​​​        

        再实现我们上面的思路

                

        最后我们来测试一下能不能跑通

        ​​​​​​​        

        

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

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

相关文章

嵌入式系统工程师错题总结

笔者来介绍一下嵌入式系统工程师考试的一些易错题目 题目介绍  流水线指令计算公式&#xff1a;一条指令总时间max&#xff08;单个指令执行时间&#xff09;*&#xff08;指令数-1&#xff09;  平均故障间隔时间  ICMP协议&#xff1a;传送通信问题相关的消息。 …

sqlserver中将csv非空间数据(带点坐标)转为空间数据

1、导入csv数据 2、修改字段shape为空间字段 ALTER TABLE FJPOIHB66 ALTER COLUMN shape geometry;3、空间字段转字符串 UPDATE FJPOIHB66 SET shape geometry::STGeomFromText(CONVERT(nvarchar(254),shape), 4326);4、设置主键字段 5、即可

141 Linux 系统编程18,线程,ps –Lf 进程 查看LWP,线程间共享数据,优缺点,编译加-lpthread,

一 线程概念 什么是线程 LWP&#xff1a;light weight process 轻量级的进程&#xff0c;本质仍是进程(在Linux环境下) 进程&#xff1a;独立地址空间&#xff0c;拥有PCB 线程&#xff1a;有独立的PCB&#xff0c;但没有独立的地址空间(共享) 区别&#xff1a;在于是否共…

C++vector的模拟实现

文章目录 模拟实现1. 迭代器2. 容量操作&#xff08;1&#xff09;size和capacity&#xff08;2&#xff09;reserve&#xff08;3&#xff09; resize 3. 元素访问&#xff08;1&#xff09;下标 [ ] 4. 修改操作&#xff08;1&#xff09;push_back&#xff08;2&#xff09…

【死磕Elasticsearch】从实战中来,到实战中去

文章目录 写在前面&#xff1a;1、索引阻塞的种类2、什么时候使用阻塞&#xff1f;场景1&#xff1a;进行系统维护场景。场景2&#xff1a;保护数据不被随意更改场景。场景3&#xff1a;优化资源使用的场景。场景4&#xff1a;遵守安全规则场景。 3、添加索引阻塞API4、解除设置…

使用jquery的autocomplete属性实现联想补全操作

平时使用百度&#xff0c;淘宝等软件搜索时&#xff0c;常见一个搜索框联想提示&#xff0c;感觉确实好用但没有研究过原理&#xff0c;最近恰巧工作中遇到一个同样的场景&#xff0c;不同于大厂使用高端的Python&#xff0c;这次需要使用jQuery的autocomplete属性来自动联想补…

git撤回代码提交commit或者修改commit提交注释

执行commit后&#xff0c;还没执行push时&#xff0c;想要撤销之前的提交commit 撤销提交 使用命令&#xff1a; git reset --soft HEAD^命令详解&#xff1a; HEAD^ 表示上一个版本&#xff0c;即上一次的commit&#xff0c;也可以写成HEAD~1 如果进行两次的commit&#xf…

Singularity(二)| 安装singularity工具

Singularity&#xff08;二&#xff09;| 安装singularity工具 以默认安装 Ubuntu 22.04 (jammy) 发行版的 WSL 2 (Windows Subsystem for Linux 2) 为例&#xff1a; 参考&#xff1a;官方快速安装向导 安装系统依赖项 首先在主机上安装开发工具和库&#xff0c;在基于 debian…

【黑马程序员】python函数

文章目录 函数什么是函数为什么学习函数函数定义函数的传入参数函数的返回值返回值基础None返回值 函数说明文档函数的嵌套调用定义代码示例 全局变量和局部变量全局变量global变量局部变量 函数综合案例 函数 什么是函数 组织好的&#xff0c;可重复使用的、用来实现特定功能…

5.Java并发编程—JUC线程池架构

JUC线程池架构 在Java开发中&#xff0c;线程的创建和销毁对系统性能有一定的开销&#xff0c;需要JVM和操作系统的配合完成大量的工作。 JVM对线程的创建和销毁&#xff1a; 线程的创建需要JVM分配内存、初始化线程栈和线程上下文等资源&#xff0c;这些操作会带来一定的时间和…

【C语言步行梯】自定义函数、函数递归详谈

&#x1f3af;每日努力一点点&#xff0c;技术进步看得见 &#x1f3e0;专栏介绍&#xff1a;【C语言步行梯】专栏用于介绍C语言相关内容&#xff0c;每篇文章将通过图片代码片段网络相关题目的方式编写&#xff0c;欢迎订阅~~ 文章目录 什么是函数库函数自定义函数函数执行示例…

数据结构 第2章:线性表

文章目录 2.1 线性表的定义和操作2.1.1 线性表的基本概念2.1.2 线性表的基本操作 2.2. 顺序表2.2.1. 顺序表的基本概念2.2.2. 顺序表的实现2.2.3. 顺序表的基本操作 2.3 链表2.3.1 单链表的基本概念2.3.2 单链表的实现2.3.3 单链表的插入2.3.4. 单链表的删除2.3.5. 单链表的查找…

VB 数据质量诊断软件(分析数据的完整性,合理性,准确性)-139-(代码+程序说明)

转载地址http://www.3q2008.com/soft/search.asp?keyword139 前言: 为何口出狂言,作任何VB和ASP的系统, 这个就是很好的一个证明 :) 又有些狂了... 数据库操作谁都会,接触的多了也没什么难的,VB编程难在哪?算法上,这个是一个算法题的毕业设计 哈哈忙活了足足有一○小时, …

2024年最新阿里云和腾讯云云服务器价格租用对比

2024年阿里云服务器和腾讯云服务器价格战已经打响&#xff0c;阿里云服务器优惠61元一年起&#xff0c;腾讯云服务器61元一年&#xff0c;2核2G3M、2核4G、4核8G、4核16G、8核16G、16核32G、16核64G等配置价格对比&#xff0c;阿腾云atengyun.com整理阿里云和腾讯云服务器详细配…

C语言 ——关键字

关键字&#xff1a;在C语言中被赋予了特定含义的英文单词&#xff0c;一共有32个关键字 * 关键字全部小写 * 在特定的编译器中&#xff0c;关键字是高亮显示的 vs&#xff1a;蓝色或者紫色 vs&#xff1a;蓝色 下图圈起来的都是关键字 c auto break case char const con…

Kafka消费者重平衡

「&#xff08;重平衡&#xff09;Rebalance本质上是一种协议&#xff0c;规定了一个Consumer Group下的所有Consumer如何达成一致&#xff0c;来分配订阅Topic的每个分区」。 比如某个Group下有20个Consumer实例&#xff0c;它订阅了一个具有100个分区的Topic。 正常情况下&…

【C语言】如何规避野指针

✨✨ 欢迎大家来到莉莉的博文✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 目录 一、概念&#xff1a; 二、野指针成因&#xff1a; 2.1. 指针未初始化 2.2 指针越界访问 3. 指针指向的空间释放 三、如何规避野指针 3.…

MT6771 android13 自定义背光曲线

一. Android系统源码中的参数配置 MTK6771平台MTK重写了背光曲线的参数&#xff0c;路径在s0_vnd/vendor/mediatek/proprietary/packages/overlay/vendor/FrameworkResOverlayExt/brightness_adaptive_support/res/values/config.xml 不过MTK的其他平台可能不是在这个路径 来看…

C/C++语言学习基础版(一)

目录 一and二、C语言说明 注释&#xff1a; 1、声明语句 2、输出函数 3、return 语句 三、C语言的数据结构 1、常量与变量 2、基本数据结构 3、关键字 练习&#xff1a;进制转换 四、基本输入输出 1、字符输出函数putchar 2、字符输入函数getchar 3、格式化输出函…

JavaParser的快速介绍

开发的工作主要是写代码&#xff0c; 有考虑过使用代码写代码&#xff0c; 使用代码分析和改进代码吗&#xff1f; JavaParser 就可以帮你用来处理Java 代码的这些功能。 Java Parser 的介绍 Java Parser是一个用于解析和分析Java源代码的开源工具。它提供了一个API接口&…