数据结构之二叉树

目录

目录

一、树的概念及结构

1.1树的基本概念

1.2树的相关概念

二、二叉树的基本概念及结构

2.1二叉树概念

2.2现实生活中的二叉树

2.3特殊二叉树(完全二叉树和满二叉树)

2.4满二叉树的性质

2.5习题

2.5二叉树的存储结构

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

3.1二叉树的顺序结构

3.2堆的概念及结构

3.3堆的实现

3.3.1堆向下调整算法

3.3.2堆的创建

3.3.3 建堆时间复杂度

3.3.4堆的插入

 3.3.5堆的删除

3.3.6堆的代码实现

 3.4堆的应用

3.4.1堆排序

过程概述(以排序降序为例,默认已建好堆):

为什么排升序要建大堆,排降序要建小堆:

3.4.2 TOP-k问题

四·二叉树链式结构的实现

4.1 前置说明

4.2二叉树的遍历

4.2.1前序·中序·后序遍历

4.2.2层序遍历

五、一些二叉树的相关习题

总结


一、树的概念及结构

1.1树的基本概念

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

有一个特殊的结点,称为根结点,根节点没有前驱结点 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合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
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林;

二、二叉树的基本概念及结构

2.1二叉树概念

一棵二叉树是结点的一个有限集合,该集合:

1. 或者为空

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

从上图可以看出:

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

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

注意:对于任意的二叉树都是由以下几种情况复合而成的:

2.2现实生活中的二叉树

2.3特殊二叉树(完全二叉树和满二叉树)


1.满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2.完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

2.4满二叉树的性质


1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(k-1)个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1.
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n, 度为2的分支结点个数为m,则有n=m+1.
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log(n+1)(ps:是log以2为底,n+1为对数)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对 于序号为i的结点有:

    5.1 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
    5.2 若(2i+1)<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
    5.3若(2i+2)<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

2.5习题

1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199

2.下列数据结构中,不适合采用顺序存储结构的是( )
A 非完全二叉树
B 堆
C 队列
D 栈

3.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2

4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )
A 11
B 10
C 8
D 12

5.一个具有767个节点的完全二叉树,其叶子节点个数为()
A 383
B 384
C 385
D 386



答案:1.B 2.A 3.A 4.B 5.B

2.5二叉树的存储结构

1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空 间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面会专门讲解。二叉树顺 序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 :链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。

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

3.1二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

3.2堆的概念及结构

大根堆(大堆):根节点最大的堆
小根堆(小堆):根节点最小的堆
堆的性质
(1)子节点一定大于(小堆)或小于(大堆)根节点
(2)堆一定是一颗完全二叉树

3.3堆的实现

3.3.1堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

3.3.2堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算 法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的 子树开始调整,一直调整到根节点的树,就可以调整成堆。

3.3.3 建堆时间复杂度

此处建堆的方式为向下调整建堆

3.3.4堆的插入

先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。

 3.3.5堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

3.3.6堆的代码实现

#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable:6031)
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType* arry;
	int size;
	int capacity;
}Heap;

//要防止数据过大,出现栈溢出的情况
//要注意这种交换方法不支持,传入两个相同的数据
void Swep_data(HeapDataType *a,HeapDataType *b)
{
	*a = *a ^ *b;
	*b = *a ^ *b;
	*a = *a ^ *b;
}

//初始化
void Heap_Init(Heap *obj)
{
	obj->arry = (HeapDataType*)malloc(sizeof(HeapDataType)*10);
	obj->size = 0;
	obj->capacity = 10;

}

//插入数据
void Heap_push(Heap *obj,HeapDataType x)
{
	if (obj->size == obj->capacity - 1)
	{
		obj->capacity *= 2;
		obj->arry = (HeapDataType*)realloc(obj->arry, sizeof(HeapDataType) * obj->capacity);
	}
	obj->arry[obj->size] = x;
	obj->size++;
}

void Heap_print(Heap *obj)
{
	if (obj->size == 0)
	{
		return;
	}
	for (int i = 0;i < obj->size;i++)
	{
		printf("%d ", obj->arry[i]);
	}
}
//向上调整(这里以小根堆为例),堆的一种创建形式
void adjust_up(HeapDataType *arry,int child_index,int size)
{
	assert(arry);
	if (child_index<0 || child_index>size)
	{
		printf("程序已结束,请确认下标输入是否合理: %d ,堆容量:%d\n", child_index,size);
		exit(-1);
	}

	int parent = (child_index - 1) / 2;
	while (child_index > 0)
	{
		if (arry[parent] > arry[child_index])   ///这里是‘>’还是‘<’是不确定的,要根据要建立大堆还是小堆自行判断
		{
			Swep_data(&arry[parent], &arry[child_index]);
		}
		if (arry[parent] < arry[child_index])
		{
			child_index = parent;
			parent = (child_index - 1) / 2;
		}
		else
		{
			break;
		}
	}

}

//向下调整(这里以小根堆为例),堆的一种创建形式
void adjust_down(HeapDataType *arry,int parent_index,int size)
{
	assert(arry);
	if (parent_index<0 || parent_index>size)
	{
		printf("程序已结束,请确认下标输入是否合理: % d, 堆容量: % d\n", parent_index, size);
		exit(-1);
	}
	int child_index = parent_index*2+1 ;
	while (child_index<size)//循环结束条件要注意,并且多加思考
	{
        //要考虑数组越界问题
		if (child_index+1<size&&arry[child_index + 1] < arry[child_index])
		{
			child_index++;
		}
		if (arry[parent_index] > arry[child_index])//这里的判断条件要注意
		{
			Swep_data(&arry[parent_index], &arry[child_index]);
			parent_index = child_index;
			child_index = parent_index * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

//向上调整建堆
void Creat_Heap_up(HeapDataType *arry,int size_arry)
{
	for (int i = 0;i < size_arry;i++)
	{
		adjust_up(arry, i, size_arry);
	}

}

//向下调整建堆
void Creat_Heap_down(HeapDataType* arry, int size_arry)
{
	for (int i = (size_arry-1-1) / 2;i > 0;i--)
	{
		adjust_down(arry, i, size_arry);
	}
}

//排降序(排升序时,建堆时要建大堆:排降序时,要建小堆)
void Heap_sort(HeapDataType *arry,int size_arry)
{
	Creat_Heap_down(arry, size_arry);//使用 Creat_Heap_up 建堆也可以

	for (int i = size_arry - 1;i > 0;i--)
	{
		Swep_data(&arry[0], &arry[i]);
		adjust_down(arry, 0, i);
	}
}
int main()
{
	Heap obj;
	Heap_Init(&obj);
	for (int i = 0;i < 20;i++)
	{
		Heap_push(&obj, i % 10);//这里只是把数据压入了数组,因此时并不能算是堆
	}
	Heap_print(&obj);
	printf("\n");
	Heap_sort(obj.arry, obj.size);
	Heap_print(&obj);
}

运行结果如图所示: 

 3.4堆的应用

3.4.1堆排序

过程概述(以排序降序为例,默认已建好堆):

若要进行降序,则应该建小堆(原因稍后讲解)。故此时根节点的值一定小于其左右子树中的任一节点,那么此时将这一根节点(数组的零偏移处)与堆末尾(也就是数组末尾)交换,这时堆最后一个元素即为最小,即有序。这时再将堆的大小(容量)减去一,以保证有序的元素不在参与调整,那么再重复上述过程,会令堆倒数第二个元素有序。重复元素总个数减一次后,堆便有序。

为什么排升序要建大堆,排降序要建小堆:

 我们假设排升序建小堆(也就是根节点小于其左右子树),那么试想,如何才能在小堆中找到最大值呢,那么不可避免地要去遍历树的最后一层,然后选出最大的值,甚至你在最后一层找到的并非最大值,如图所示:

所以,在小堆中找到最大值是十分困难的一件事,但在大堆中,找最大值,显然要容易的多,在排降序的时候要建小堆也是同样的道理。

堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

(在3.3.6堆代码的实现中,已经实现过,这里就不在实现代码了)

3.4.2 TOP-k问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

其实在上文3.3.6中,我们早已带入了这个思想,由于我们在建立数组的时候,是通过for循环,输入的有规律的数据,其必然不能算作是一个堆,但是我们后来通过向上或向下调整利用已经产生的数据创建了一个堆,这个过程实际上就是解决TOP-K问题的第一步,再然后,我们通过排一个降序,然后控制输出最大的k个值,自然而然的找出最大的K个值了。(这里就不与实现,希望读者能自行思考、尝试,稍加改动3.3.6代码就可以完成这一问题)

四·二叉树链式结构的实现

4.1 前置说明

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

 注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。再看二叉树基本操作前,再回顾下二叉树的概念,

二叉树是:
1. 空树

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

4.2二叉树的遍历

4.2.1前序·中序·后序遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中间。

3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
 

 代码实现:

#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable:6031)
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>


typedef char BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType val;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
    }
	printf("%d ", root->val);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%d ", root->val);
	BinaryTreeInOrder(root->right);
}

// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%d", root->val);
}

4.2.2层序遍历

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

 这里还是要解释以下层序遍历的逻辑的:

首先,我们可以想一想链式结构这一特殊的结构,其可以轻易的找到其后边元素,那么试想一下,一个以链式结构连接起来的二叉树,如上图所示,以队列的形式存储每一个节点的地址,当这一节点要离开时,先通过链式结构将其左子树的根节点存入队列,而后将其右子树的根节点存入队列,然后打印这一节点存储的数据值,最后,再让要离开的节点离开。这样就实现了层序遍历。以上面的树为例,我们最开始将A节点的地址压入队列,而后我们可以在这时打印其对应的值(只要在节点离开之前打印都可),而后我们将A左子树的根节点,也就是B节点压入队列后,在将其右子树的根节点C压入队列后,就可以让A节点出队列了,而后对B、C节点进行相同的操作,知道没有元素可以出队列为止。

#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable:6031)
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>


typedef char BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType val;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;
	int size;
}Que;

bool QueueEmpty(Que* pq)
{
	assert(pq);

	return pq->head == NULL;
}

void QueueInit(Que* pq)
{
	assert(pq);

	pq->head = pq->tail = NULL;
	pq->size = 0;
}

void QueuePush(Que* pq, QDataType x)
{
	assert(pq);

	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;

	if (pq->tail == NULL)
	{
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}

	pq->size++;
}
void QueuePop(Que* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	if (pq->head->next == NULL)
	{
		free(pq->head);
		pq->head = pq->tail = NULL;
	}
	else
	{
		QNode* next = pq->head->next;
		free(pq->head);
		pq->head = next;
	}

	pq->size--;
}

QDataType QueueFront(Que* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->head->data;
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	Que q;
	QueueInit(&q);
	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		printf("%d", front->val);
		if(front->left)
		QueuePush(&q, root->left);
		if(front->right)
		QueuePush(&q, root->right);
		QueuePop(&q);
	}

}

五、一些二叉树的相关习题

  1.单值二叉树

2.检查两棵树是否相同

3.对称二叉树

4.另一棵树的子树


总结

本篇文章到这里就结束了,文章编译人力有限,时间仓促,难免纰漏,如有问题,欢迎在评论区下斧正,感谢您的留言讨论。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木鱼不是木鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值