树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树有一个特殊的结点,称为根结点,除根节点没有前驱结点除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间
的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法
等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
树比较常见的应用就是用目录的结构,比如Linux的树状目录结构;
一棵二叉树是结点的一个有限集合,该集合为空或者由一个根节点加上两棵别称为左子树和右子树的二叉树组成
通过概念和图解我们可以发现:
注意:对于任意的二叉树都是由以下几种情况复合而成的
现实生活中的二叉树图片:
若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
若2i+1=n否则无左孩子
若2i+2=n否则无右孩子
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* pLeft; // 指向当前节点左孩子
struct BinTreeNode* pRight; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
}
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
声明一下,堆的实现我们不会先讲解如何建堆,而是先讲解调整算法,因为建堆时会用到调整算法。
还有两条性质:
//堆的结构声明
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
size_t size;//已经存储的数据个数
size_t capacity;//容量
}Heap;
首先再次声明:堆的实现中,会多次交换两个数据,这里我们将其封装成一个函数:
//交换两个数据
void swap(HPDataType* e1, HPDataType* e2)
{
HPDataType tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
向下调整的思路:
//从上往下调整
void AdjustDown(HPDataType* a, size_t size, size_t parent)
{
size_t child = (parent * 2) + 1;//找到左孩子
while (child < size)
{
if (child + 1 < size && a[child] < a[child + 1])//找两个孩子中大的那一个
{
child = child + 1;
}
if (a[parent] < a[child])//满足条件则交换
{
swap(&a[child], &a[parent]);
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
}
思路:
//向上调整
void AdjustUp(HPDataType* a, size_t child)
{
size_t parent = (child - 1) / 2;//找父亲结点
while (child > 0)
{
if (a[parent] < a[child])//满足条件则交换
{
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
思路:
现在假设有数组arr={2,7,26,25,19,17,1,90,3,36};
我们将其构建成大堆,请看图解
图解:
动图来自算法可视化网站VisuAlgo:https://visualgo.net/zh
注:本篇博客的可视化算法都是来自该网站。
代码实现:
void HeapCreate(Heap* php, HPDataType* a, int n)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
exit(-1);
}
memcpy(php->a, a, sizeof(HPDataType) * n);//拷贝数组
php->size = php->capacity = n;
// 从下往上建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->a, n, i);//才用向下调整算法
}
}
Heap.h
//头文件包含
#include
#include
#include
#include
#include
//结构声明
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
size_t size;
size_t capacity;
}Heap;
//函数声明
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
void HeapInit(Heap* php);
//堆的初始化
void HeapInit(Heap* php);
// 堆的销毁
void HeapDestory(Heap* php);
//堆的插入
void HeapPush(Heap* php, HPDataType n);
//堆的删除(删头)
void HeapPop(Heap* php);
//返回堆顶元素
HPDataType HeapTop(Heap* php);
//返回堆的数据个数
size_t HeapSize(Heap* php);
//判断堆是否为空
bool HeapEmpty(Heap* php);
Heap.c
#include "Heap.h"
// 堆的构建
void AdjustDown(HPDataType* php, size_t size, size_t parent);
void HeapCreate(Heap* php, HPDataType* a, int n)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
if (php->a == NULL)
{
perror("malloc fail");
exit(-1);
}
memcpy(php->a, a, sizeof(HPDataType) * n);
php->size = php->capacity = n;
// 从下往上建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->a, n, i);
}
}
//堆的初始化
void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
// 堆的销毁
void HeapDestory(Heap* php)
{
assert(php);
free(php->a);
php->capacity = php->size = 0;
}
//交换两个数据
void swap(HPDataType* e1, HPDataType* e2)
{
HPDataType tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//向上调整,保证堆的成立
void AdjustUp(HPDataType* a, size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (a[parent] < a[child])
{
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入
void HeapPush(Heap* php, HPDataType n)
{
assert(php);
//如果堆为空或者满了就扩容
if (php->capacity == php->size)
{
size_t newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc failed");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = n;
php->size++;
AdjustUp(php->a, php->size - 1);
}
//从上往下调整
void AdjustDown(HPDataType* a, size_t size, size_t parent)
{
size_t child = (parent * 2) + 1;
while (child < size)
{
if (child + 1 < size && a[child] < a[child + 1])
{
child = child + 1;
}
if (a[parent] < a[child])
{
swap(&a[child], &a[parent]);
parent = child;
child = (parent * 2) + 1;
}
else
{
break;
}
}
}
//堆的删除(删头)
void HeapPop(Heap* php)
{
assert(php);
assert(php->size > 0);
swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
//返回堆顶元素
HPDataType HeapTop(Heap* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
//返回堆的数据个数
size_t HeapSize(Heap* php)
{
assert(php);
return php->size;
}
//判断堆是否为空
bool HeapEmpty(Heap* php)
{
assert(php);
return HeapSize(php) == 0;
}
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于我们现在对二
叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树
操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
具体操作的代码如下:
#include
#include
#include
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
//申请一个新结点
BTNode* BuyNode(BTDataType x)
{
BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
if (newNode == NULL)
{
perror("malloc failed");
exit(-1);
}
newNode->data = x;
newNode->left = newNode->right = NULL;
return newNode;
}
void test1()
{
BTNode* root = BuyNode('A');
BTNode* n2 = BuyNode('B');
BTNode* n3 = BuyNode('C');
BTNode* n4 = BuyNode('D');
BTNode* n5 = BuyNode('E');
BTNode* n6 = BuyNode('F');
root->left = n2;
root->right = n3;
n2->left = n4;
n2->right = n5;
n3->right = n6;
}
int main()
{
test1();
return 0;
}
接下来我们就将通过在这棵树来学习二叉树的遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
代码实现:
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");//不想打印NULL的话可以去掉
return;
}
printf("%c ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreeInOrder(root->left);
printf("%c ", root->data);
BinaryTreeInOrder(root->right);
}
后序遍历代码:
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c ", root->data);
}
顾名思义,层序遍历就是一层一层的遍历。层序遍历需要用到队列这个数据结构,该结构在我之前的博客有讲到
思路如下:
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root != NULL)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%c ", front->data);
//出队并且将左右子树入队
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestroy(&q);
}
通过前序遍历的数组"ABD##E#H##CF##"构建二叉树
这一部分需要注意的就是数组数据的存放顺序需要按照前序遍历的顺序来存放,其中#表示为空
思路和前序遍历差不多,只是把打印换成了创建二叉树而以,直接上代码
//通过前序遍历的数组"ABD##E#H##CF##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
assert(a);
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = BuyNode(a[*pi]);
(*pi)++;
root->left = BinaryTreeCreate(a, pi);
root->right = BinaryTreeCreate(a, pi);
return root;
}
这样就可以直接构建出如下二叉树:
至此,我们以后构建二叉树就可以按照这种方法构建了。
二叉树的销毁需要采用后序遍历的顺序进行销毁。
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
return;
BinaryTreeDestory(&(*root)->left);
(*root)->left = NULL;
BinaryTreeDestory(&(*root)->right);
(*root)->right = NULL;
free(*root);
*root = NULL;
}
通过以上的学习,我们已经对二叉树的构建,销毁和遍历有了一定掌握了,接下来我们一起进行二叉树的其他操作。
两者都需要用到递归的思想。
求节点个数的思路:
从根节点开始,我们要知道该二叉树的节点个数,我们就需要知道其左子树和右子树的节点个数然后加上根节点即可,
按照这个逻辑递归。
图解:
代码实现:
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
//方法1
//if (root == NULL)
//return 0;
//int ret = 1;
//ret += BinaryTreeSize(root->left);
//ret += BinaryTreeSize(root->right);
//return ret;
//方法2
return root == NULL ? 0 :
BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
这里提供了两种写法,一个思路清晰,一个代码简介。
求叶子节点个数:
思路基本一样,只是不需要额外算上根节点自己了。直接上代码:
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
//方法1
//int ret = 0;
//ret += BinaryTreeLeafSize(root->left);
//ret += BinaryTreeLeafSize(root->right);
//return ret;
//方法2
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
同样也是提供了两种方法
同样也是递归思想,还是以我们之前构建的二叉树为例,要求第三层的节点个数,我们从根出发,就是求第三层的节点个数,相对于求根的左子树的第二层节点个数和根的右子树的第二层的节点个数…依次递归,即求k > 1 子树的k-1层结点个数相加。
图解:
如果觉得没有讲述清楚,我们换个方式理解:A的第3层,相对于B和C来说是第2层,而B和C的第2层,相对于D、E和F来说是第一层,而第一层的节点个数要么为0,要么为1
代码实现:
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
// k > 1 子树的k-1层结点个数相加
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
思路很简单,遍历一遍树来查找就行了。这里我们以前序遍历为例,上代码:
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* rleft = BinaryTreeFind(root->left, x);
if (rleft)
return rleft;
BTNode* rright = BinaryTreeFind(root->right, x);
if (rright)
return rright;
return NULL;
}
BinaryTree.h
#include
#include
#include
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
#include "Queue.h"
//申请一个新结点
BTNode* BuyNode(BTDataType x);
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树的深度
int TreeHeight(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
#include "Queue.h"
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
BinaryTree.c
#include "BinaryTree.h"
//申请一个新结点
BTNode* BuyNode(BTDataType x)
{
BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
if (newNode == NULL)
{
perror("malloc failed");
exit(-1);
}
newNode->data = x;
newNode->left = newNode->right = NULL;
return newNode;
}
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
assert(a);
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = BuyNode(a[*pi]);
(*pi)++;
root->left = BinaryTreeCreate(a, pi);
root->right = BinaryTreeCreate(a, pi);
return root;
}
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
return;
BinaryTreeDestory(&(*root)->left);
(*root)->left = NULL;
BinaryTreeDestory(&(*root)->right);
(*root)->right = NULL;
free(*root);
*root = NULL;
}
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreeInOrder(root->left);
printf("%c ", root->data);
BinaryTreeInOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c ", root->data);
}
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
//方法1
//if (root == NULL)
//return 0;
//int ret = 1;
//ret += BinaryTreeSize(root->left);
//ret += BinaryTreeSize(root->right);
//return ret;
//杭哥方法
return root == NULL ? 0 :
BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
//方法1
//int ret = 0;
//ret += BinaryTreeLeafSize(root->left);
//ret += BinaryTreeLeafSize(root->right);
//return ret;
//杭哥方法
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
//二叉树的深度
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;
}
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
// k > 1 子树的k-1层结点个数相加
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* rleft = BinaryTreeFind(root->left, x);
if (rleft)
return rleft;
BTNode* rright = BinaryTreeFind(root->right, x);
if (rright)
return rright;
return NULL;
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root != NULL)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%c ", front->data);
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestroy(&q);
}
二叉树的内容是比较多的,而且很多算法的思路都需要用到递归的思想,需要大家多花时间思考和画图分析。希望本篇博客对大家的数据结构有帮助。后面两期的博客我应该会讲一下几个比较常见的排序算法和Linux的入门。
还有一点注意的是:本篇博客用到的可视化视频是在VisuAlgo网站截取的,如果大家有需要也可以去该网站学习。
那我们下一期博客再见吧!