Bootstrap

【数据结构初阶】初始二叉树 -- (二叉树基础概念+二叉树的顺序结构及实现)

一、树的概念及结构

1.1 树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限的结点组成一个具有层次关系的集合。

  • 根节点: 或者称为树根(root),每棵树有且仅有一个根结点,根结点没有前驱结点
  • 子树: 除根结点之外,其余结点被分为M(M>0)个互不相交的集合,其中每一个集合又是一颗结构与树相似的子树。每颗子树的根节点有且只有一个前驱,可以有0个或多个后继。
  • 每个元素称为节点( node )

在这里插入图片描述

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

1.2 树的相关概念

在这里插入图片描述

节点的度: 一个节点含有的子树的个数称为该节点的度; 如上图:A的为6

叶节点或终端节点: 度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点

非终端节点或分支节点: 度不为0的节点; 如上图:D、E、F、G…等节点为分支节点

双亲节点或父节点: 若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点

孩子节点或子节点: 一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点

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

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

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

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

堂兄弟节点: 双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点

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

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

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

上述就是树的一些相关概念,一些我们经常使用,常见的几个概念我已经标红。

1.3 树的表示

树有很多种表示方法,但由于树是非线性结构,所以要存储表示起来会比较麻烦,而最常用的就是孩子兄弟表示法

孩子兄弟表示法: 保存指向左边第一个孩子结点的指针,还有一个指针指向下一个兄弟结点

typedef int DataType;
struct Node
{
 struct Node* firstChild1; // 第一个孩子结点
 struct Node* pNextBrother; // 指向其下一个兄弟结点
 DataType data; // 结点中的数据域
};

如图:
在这里插入图片描述

二、二叉树的概念及结构

2.1 二叉树的概念

二叉树同样是结点的有限集合,该集合可为空,或 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
当集合为空时,称为空二叉树。在二叉树中,一个元素也称作一个结点。
如图:
在这里插入图片描述

注意:

  • 二叉树不存在度大于2的结点

每个结点可无子树或者一个子树,但最多不能超过2个

  • 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

2.2 特殊的二叉树

满二叉树: 一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k)-1 ,则它就是满二叉树。 如图:
在这里插入图片描述

完全二叉树: 完全二叉树的前 n-1 层为满二叉树,最后一层满或者不满都可以,但要求必须从左往右是连续的。如图:
在这里插入图片描述

满二叉树是一种特殊的完全二叉树

2.3 二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第 n 层上最多有 2^(n-1)个结点
  2. 若规定根节点的层数为1,则深度为 n 的二叉树的最大结点数是 (2^n) -1
  3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有n0 = n2 + 1
  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度为log(n+1),(ps:log(n+1)是以2为底,n+1为对数)
  5. 完全二叉树度为1的结点个数只有0和1两种可能

2.4 二叉树的存储结构

二叉树一般可以使用两种结构存储,一种是顺序结构,一种是链式结构

顺序存储结构

顺序结构存储就是使用数组来存储,从根结点开始从上往下从左往右依次有序存入数组。一般使用数组只适合表示完全二叉树

因为不是完全二叉树的话,有些缺少结点的树存入数组会造成空间的浪费,若不想浪费空间便不好规律地表示该树的结构。

而且由于的完全二叉树,还可以用数组下标来计算父子结点,如图:
在这里插入图片描述

注:
假设完全二叉树有n个结点,在序号为 i 的位置:
2i+1 >= n,则无左孩子
2i+2 >= n,则无右孩子

链式存储结构

二叉树的链式存储结构是指,用链表来表示一棵二叉树。通常的方法是链表中每个结点由三个域组成,数据域左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。

链式结构又分为二叉链三叉链,当前数据结构一般都是二叉链,高阶数据结构如红黑树等会用到三叉链。当前我们仅做了解

在这里插入图片描述

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
 struct BinTreeNode* pLeft; // 指向当前节点左孩子
 struct BinTreeNode* pRight; // 指向当前节点右孩子
 BTDataType data; // 当前节点值域
};

// 三叉链
struct BinaryTreeNode
{
 struct BinTreeNode* pParent; // 指向当前节点的双亲
 struct BinTreeNode* pLeft; // 指向当前节点左孩子
 struct BinTreeNode* pRight; // 指向当前节点右孩子
 BTDataType data; // 当前节点值域
};

三、二叉树的顺序结构及实现

3.1 二叉树的顺序结构

前面已经说过,普通二叉树不适用与数组存储,会造成空间的浪费。而完全二叉树更适合使用顺序结构存储。我们通常把(一种完全二叉树) 使用顺序结构的数组来存储

注意:这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

3.2 堆的概念及结构

若n个元素的集合{k0,k1,k2,…,kn },把它的所有元素按完全二叉树的顺序存储方式存放到数组中,当且仅当满足以下条件时,称之为堆。
Ki <= K2i+1 && Ki <= K2i+2 Ki >= K2i+1 && Ki >= K2i+2

堆的性质:

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

堆的分类:
在这里插入图片描述

3.3 堆的实现

堆的结构定义

由于堆是用数组来存储的,所以定义的结构与顺序表相似,逻辑上是一个完全二叉树,物理上是一个数组的形式。

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;
堆的初始化

堆的初始化和顺序表一样,只要把指针置空,容量和数据个数置为0即可。

代码实现:

//初始化	
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;
}
堆的插入

堆的插入就是在数组末尾进行插入数据,除了需要考虑扩容外,还由于插入的数值不确定,此时堆的形态可能遭到破坏,当然该结点只会影响该结点到根结点所在路径上的所有结点,所以就需要考虑向上调整:一直交换到满足堆的形态即可。

//插入x,并继续保持堆形态
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//考虑是否需要扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;

		HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;

	//需要判断插入的数据,是否能保持堆形态
	AdjustUp(php->a, php->size - 1);
}
堆向上调整算法

请添加图片描述
向上调整算法,从插入数据的位置开始,一直向上找父节点,当满足子节点比父节点小或者大的就交换,直到调整到根节点或不满足条件停止

//向上调整
void AdjustUp(HPDataType* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;

	while (child > 0)
	{	
		//小堆
		if (a[child] < a[parent])   //if (a[child] > a[parent])//大堆
		{
			Swap(&a[child], &a[parent]);//进行互换 -- 互换代码将在最后完整代码内写出
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

}
堆的删除

堆的删除,一般是删除堆顶元素,但并不是像顺序表一样,简单的把数组元素向前移动,这样会严重破坏堆的结构。
应该先将堆顶元素与最后一个元素进行交换(即数组首元素和尾元素进行交换),然后进行尾删再进行向下调整。

无论是大堆还是小堆,交换删除后,都需要进行向下调整
若是大堆,交换后数据比子节点小,向下调整
若是小堆,交换后数据比子节点大,向下调整

// 删除堆顶元素,并保持堆形态
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	//向下调整
	AdjustDown(php->a, php->size, 0);
}

堆的向下调整算法

把尾元素换到堆顶,必然会改变堆的性质,但根结点的左右子树还是保持原有的性质。所以,只需要将堆顶元素逐步向下调整:将根结点与其较小(大)的子结点进行交换,只要满足父结点比其二者子结点中任意一个大或小的条件,就让其与子结点进行交换,直到交换到叶结点或不满足条件为止。
请添加图片描述

//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	//先求出根节点下的次小的孩子结点
	int minChild = parent * 2 + 1;
	while (minChild < n)
	{
		//if (minChild + 1 < n && a[minChild + 1] > a[minChild])//大堆	
		if (minChild + 1 < n && a[minChild + 1] < a[minChild]) 
		{
			//找出小的哪个结点
			minChild++;
		}

		if (a[minChild] < a[parent])   //if (a[minChild] > a[parent])//大堆
		{
			Swap(&a[minChild], &a[parent]);
			parent = minChild;
			minChild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
堆的创建

对于给定的一个数组,在逻辑上可以看作是完全二叉树,但它不一定是堆,那我们需要如何把它构建成一个大堆或者小堆呢?

方法1: 向下调整

建堆若采用向下调整,我们可以采用堆的删除思想,在我们删除过程中,首尾元素会发生互换,互换后就不会满足堆的性质,所以需要进行向下调整。

那我们在进行堆的创建过程中,采用向下调整该如何操作呢?

向下调整,我们需要从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。

因为对于一个完全二叉树来说它不能保证它是一个堆,它其中任意的左右子树也不能保证是堆,所以我们才需要从倒数的第一个非叶子节点的子树开始调整
请添加图片描述
向下调整,从倒数第一个非叶子结点的子树开始调整,这个过程可以理解为 “建完再调”

void HeapCreate(HPDataType* a, int size)
{
	assert(a);
	for (int i = (size - 1 - 1) / 2; i >= 0; --i)//从最后一个叶结点的父结点开始到根结点
	{
		//向下调整
		AdjustDown(a,size, i);
	}
}
方法2: 向上调整

建堆若采用向上调整,我们可以采用堆的插入思想。从根节点即数组首元素开始,依次将数组的元素插入堆中,每插入一个,就调整一次,最后就会形成堆的形态
请添加图片描述
每插入一个元素,就调用AdjustUp一次,从插入位置开始到根节点进行向上调整。这个过程可以理解为 “边建边调”

void HeapCreate(HPDataType* a, int size)
{
	assert(a);
	for (int i = 1; i < size; ++i)//从第二个结点开始遍历
	{
		//向上调整
		AdjustUp(a, i);
	}
}
建堆的时间复杂度

1.向下调整建堆时间复杂度:
在这里插入图片描述
同理,我们按照同样方法可以算出,向上调整建堆的时间复杂度为O(N*logN)

取堆顶的元素

取堆顶元素,直接返回第一个元素即可,但要判断堆是否为空

// 取堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	return php->a[0];
}
判断堆是否为空

当size==0时,堆为空

//判空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}
堆中元素个数

直接返回size即可

//返回堆中元素个数 
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}
堆的销毁

和顺序表类似

//销毁
void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

完整代码

Heap.h


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

typedef int HPDataType;

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

//建堆
void HeapCreate(HPDataType* a, int size);
//初始化
void HeapInit(HP* php);
//销毁
void HeapDestroy(HP* php);
//插入x并保持堆
void HeapPush(HP* php, HPDataType x);
// 删除堆顶元素,并保持堆形态
void HeapPop(HP* php);
// 取堆顶元素
HPDataType HeapTop(HP* php);
//判空
bool HeapEmpty(HP* php);
//返回堆中元素个数 
int HeapSize(HP* php);

//打印堆
void HeapPrint(HP* php);
//交换
void Swap(HPDataType* p1, HPDataType* p2);
//向上调整
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int n, int parent);

Heap.c

#include "Heap.h"


//打印堆
void HeapPrint(HP* php)
{
	assert(php);
	for (int i = 0; i < php->size; ++i)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}
//初始化	
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;                
}
//销毁
void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}
//交换操作
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整
void AdjustUp(HPDataType* a, int child)
{
	assert(a);

	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 AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	//先求出根节点下的次小的孩子结点
	int minChild = parent * 2 + 1;//先假设左孩子是次小的
	while (minChild < n)
	{
		//小根堆
		if (minChild + 1 < n && a[minChild + 1] < a[minChild])
		{
			//如果右孩子比左孩子还小,则++
			minChild++;
		}

		if (a[minChild] < a[parent])
		{
			Swap(&a[minChild], &a[parent]);
			parent = minChild;
			minChild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//建堆
//void HeapCreate(HPDataType* a, int size)
//{
//	assert(a);
//	for (int i = 1; i < size; ++i)//从第二个结点开始遍历
//	{
//		//向上调整
//		AdjustUp(a, i);
//	}
//}
void HeapCreate(HPDataType* a, int size)
{
	assert(a);
	for (int i = (size - 1 - 1) / 2; i >= 0; --i)//从最后一个叶结点的父结点开始到根结点
	{
		//向下调整
		AdjustDown(a,size, i);
	}
}

//插入x,并继续保持堆形态
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//考虑是否需要扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;

		HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;

	//需要判断插入的数据,是否能保持堆形态
	AdjustUp(php->a, php->size - 1);
}


// 删除堆顶元素,并保持堆形态
//每次找次大的或次小的 -- 所以时间复杂度为 O(logN)
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	//向下调整
	AdjustDown(php->a, php->size, 0);
}
// 取堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	return php->a[0];
}
//判空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}
//返回堆中元素个数 
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

;