在讲二叉树和堆之前我们要先讲一讲它们的“祖宗”:树。也就是说它们都是树的一种,在本质上是一样的。
树
树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。你可以把它看成一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根节点没有前驱结点;
除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。也就是说一个树可以再分树枝,我们可以再将树枝看成新的树,对于根而言,在根上可以有多条树枝(>=0),但是对于树枝而言,每条树枝都只对应一个树根,因此树是递归定义的。
值得注意的是:在树形结构中,子树之间不能有交集,也就是不能有环形结构,如下图,否则就不是树形结构。在我们现实生活中的树也是一样的,树枝和树枝不能长在一起,一个树枝不可能对应多个根。
树的相关常识
我们以下图为例来对树的一些基本概念进行讲解。
1.节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
2.叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
3.非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
4.兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
5.树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
6.节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
7.树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
8.堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
9.节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
10.子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
11.森林:由m(m>0)棵互不相交的树的集合称为森林;
树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
也就是我们平时使用的目录结构。
在了解完树的一些知识后,我们不难发现,树有很多缺点,不说其他的,就单单说实现复杂度吧,和顺序表、链表等数据结构相比,树明显的实现复杂度更高,不仅是因为树要维护父子关系,还有一些其他问题,那么这时我们直接用顺序表、链表……它不香吗?
所以在实际生活我们一般使用的都是一些树的“变种”,也就是一些特殊形式,比如二叉树、完全二叉树、满二叉树、B树……而今天我们讲的二叉树和堆其实就是它们中的两种。
二叉树
二叉树的概念及特殊二叉树
二叉树是每个结点最多有两个子结点的树结构,通常子结点被称为“左结节点”和“右结节点”。子结点可以为空,另外对于任意的二叉树都是由以下几种情况复合而成的:
而在上面我们也说过树是递归定义的,所以我们又可以对二叉树进行下面的划分,不难看出其实就是上面几种情况进行的组合。
而对于二叉树而言,我们又可以划分出一些更加特殊的二叉树,比如:
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树;
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树,而后面我们要讲的堆就是完全二叉树。
二叉树的性质
对于二叉树我们具有以下性质:
1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i- 1)个结点;
2.若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1;
3.对任何一棵二叉树,如果度为0的结点(叶结点)个数为n0,度为2的分支结点个数为n2,则有n0=n2 + 1;
4.若规定根节点的层数为1,具有n个结点的满二叉树的深度h=log2(n + 1)(log2(n + 1)是log以2为底,n+1为对数);
5.对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
a.若i>0, i位置节点的双亲序号: (i-1)/2; i=0, i为根节点编号,无双亲节点;
b.若2i+1<n, 左孩子序号: 2i+1, 2i+1>=n否则无左孩子;
c.若2i+2<n,右孩子序号: 2i+2, 2i+2>=n否则无右孩子。
二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树,因此我们先来讲讲堆。
堆的性质
对于堆我们不讲那些复杂的概念,我们只用了解其两点性质:
一. 堆中某个节点的值总是不大于或不小于其父节点的值,另外我们将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆;
二. 堆总是一棵完全二叉树。
堆的主要操作
1.堆的构建
2.堆的销毁
3.堆的插入
4.堆的删除5.取堆顶的数据
6.堆的数据个数7.堆的判空
堆实现
头文件:
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<string.h>
typedef int HPDataType;
typedef struct HP
{
HPDataType* a; //数组
int size; //有效元素个数
int capacity; //容量
}HP;
// 堆的构建
void HeapInit(HP* hp);
// 堆的销毁
void HeapDestory(HP* hp);
//交换函数
void Swap(HPDataType* a, HPDataType* b);
//向上调整算法
void AdjustUp(HPDataType* a, int child);
//向下调整算法
void AdjustDown(HPDataType* a, int n, int parent);
// 堆的插入
void HeapPush(HP* hp, HPDataType x);
// 堆的删除
void HeapPop(HP* hp);
// 取堆顶的数据
HPDataType HeapTop(HP* hp);
// 堆的数据个数
int HeapSize(HP* hp);
// 堆的判空
bool HeapEmpty(HP* hp);
函数实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include"HP.h"
// 堆的构建
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->size = 0;
hp->capacity = 0;
}
// 堆的销毁
void HeapDestory(HP* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = 0;
hp->capacity = 0;
}
//因为需要多处使用,所以我们将其封装成函数
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *b;
*b = *a;
*a = 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 = (parent - 1) / 2;
}
else
{
break;
}
}
}
//向下调整算法
void AdjustDown(HPDataType* a, int n,int parent)
{
//根据二叉树的性质可以由父亲节点的下标得出孩子节点的下标,同时假设左孩子<=右孩子
int child = parent * 2 + 1;
while (child < n)
{
//判断右孩子是否存在及假设是否成立
if (child + 1 < n && a[child] > a[child + 1])
{
child++;
}
//小堆,如果孩子的值小于父亲则交换,否则跳出
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
//调整孩子节点和父亲节点
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 堆的插入
void HeapPush(HP* hp, HPDataType x)
{
assert(hp);
//扩容函数
if (hp->size == hp->capacity)
{
int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
HPDataType* tmp = realloc(hp->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
//每次插入后都要使用向上调整算法,确保插入后依旧是小堆
AdjustUp(hp->a, hp->size - 1);
}
// 堆的删除
void HeapPop(HP* hp)
{
assert(hp);
assert(!(HeapEmpty(hp)));
//本质是数组,所以我们可以将数组的首尾交换后删除
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
//向下调整算法,确保是删除后依旧是小堆
AdjustDown(hp->a, hp->size, 0);
}
// 取堆顶的数据
HPDataType HeapTop(HP* hp)
{
assert(hp);
assert(!(HeapEmpty(hp)));
return hp->a[0];
}
// 堆的数据个数
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
// 堆的判空
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
链式结构
二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
二叉树的性质已经在上面讲解过了,所以这里讲讲二叉树的遍历吧。
二叉树的遍历
二叉树的遍历是指按照某种规则访问二叉树中的所有节点,使得每个节点被访问且仅被访问一次。常见的二叉树遍历方式有四种:前序遍历、中序遍历、后序遍历和层次遍历。
前序遍历:前序遍历的访问顺序是:根节点 -> 左子树 -> 右子树。
中序遍历:中序遍历的访问顺序是:左子树 -> 根节点 -> 右子树。
后序遍历:后序遍历的访问顺序是:左子树 -> 右子树 -> 根节点。
层次遍历:层次遍历是按照树的层次,从根节点开始,逐层访问每个节点。
不难看出,对于前序遍历、中序遍历和后序遍历而言,我们一般是采用递归实现,而对于层次遍历我们通常需要使用队列来实现。
二叉树的主要操作
对于二叉树我们主要有以下操作:
1.构建二叉树 2.二叉树销毁 3.二叉树节点个数 4.二叉树叶子节点个数 5.二叉树第k层节点个数 6.二叉树查找值为x的节点 7.二叉树前序遍历 8.二叉树中序遍历 9.二叉树后序遍历 10.层序遍历 11.判断二叉树是否是完全二叉树
二叉树实现
头文件:
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include "queue.h" //参考之前的代码
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data; //数据
struct BinaryTreeNode* left; //左孩子
struct BinaryTreeNode* right;//右孩子
}BTNode;
// 构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
//二叉树高度
int BTHeight(BTNode* root)
// 二叉树叶子节点个数
int BinaryTreeLeafSize(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);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
函数实现:
#include "BTree.h"
//二叉树的构建
BTNode *BinaryTreeCreate(BTDataType * src, int n, int* pi)
{
//插入为空(#)或插入完时返回
if (*pi >= n || src[*pi] == '#')
{
(*pi)++;
return NULL;
}
//创建节点
BTNode * cur = (BTNode *)malloc(sizeof(BTNode));
cur->_data = src[*pi];
(*pi)++;
//递归,分别插入进左孩子,右孩子
cur->left = BinaryTreeCreate(src, n, pi);
cur->right = BinaryTreeCreate(src, n, pi);
//返回根节点
return cur;
}
//二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
//递归销毁全部节点
if (*root)
{
BinaryTreeDestory(&(*root)->left);
BinaryTreeDestory(&(*root)->right);
free(*root);
*root = NULL;
}
}
// 二叉树节点个数
int BTSize(BTNode* root)
{
//根节点为空时返回
if (root == NULL)
{
return 0;
}
//后序遍历递归返回节点个数
return BTSize(root->left) + BTSize(root->right) + 1;
}
//二叉树高度
int BTHeight(BTNode* root)
{
//根节点为空时返回
if (root == NULL)
{
return 0;
}
//递归分别计算左子树和右子树高度
int left = BTHeight(root->left) + 1;
int right = BTHeight(root->right) + 1;
//哪个大就返回哪个
if (left > right)
return left;
return right;
}
// 二叉树叶子节点个数
int BTLeafSize(BTNode* root)
{
//根节点为空时返回
if (root == NULL)
{
return 0;
}
//左右孩子都为空时即为叶子节点,递归加一
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BTLeafSize(root->left) +
BTLeafSize(root->right);
}
// 二叉树第k层节点个数
int BTLevelKSize(BTNode* root, int k)
{
//根节点为空时返回
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k - 1);
}
// 二叉树查找值为x的节点
BTNode* BTFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
//先从左子树找,若有就返回节点位置
BTNode* ret1 = BTFind(root->left, x);
if (ret1)
return ret1;
BTNode* ret2 = BTFind(root->right, x);
//若右子树找到返回节点位置,否则就是没有,返回空
return ret2;
}
//前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
//根节点为空时跳出
if (root)
{
//先访问根节点,再访问左子树,最后访问右子树
putchar(root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
}
//中序遍历
void BinaryTreeInOrder(BTNode* root)
{
//根节点为空时跳出
if (root)
{
//先访问左子树,再访问根节点,最后访问右子树
BinaryTreeInOrder(root->left);
putchar(root->data);
BinaryTreeInOrder(root->right);
}
}
//后序遍历
void BinaryTreePostOrder(BTNode* root)
{
//根节点为空时跳出
if (root)
{
//先访问左子树,再访问右子树,最后访问根节点
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
putchar(root->data);
}
}
//层次遍历
void BinaryTreeLevelOrder(BTNode* root)
{
//使用队列来实现层次遍历
Queue qu;
BTNode * cur;
//初始化队列
QueueInit(&qu);
//插入根节点
QueuePush(&qu, root);
//当队列为空时遍历完成
while (!QueueIsEmpty(&qu))
{
cur = QueueTop(&qu);
putchar(cur->data);
//遍历完根节点,再使左右孩子节点入队
if (cur->left)
{
QueuePush(&qu, cur->left);
}
if (cur->right)
{
QueuePush(&qu, cur->right);
}
//使根节点出队
QueuePop(&qu);
}
//使用完队列记得销毁
QueueDestory(&qu);
}