一. 树(tree)
1. 树的定义
树是一种非线性的数据结构,因为它是一种一对多的结构,它由结点(node)和边(edge)组成。每个结点可以有零个或多个子结点,但只能有一个父结点(除了根结点)。根结点是树的顶端节点,没有父节点。
树的最大特点是它没有环路,即不存在从一个节点出发经过若干边又回到该节点的路径。
如下图,就是一棵树:
有三点需要注意:
- 子树是不相交的
- 除了根结点外,每个结点有且仅有一个父结点
- 一棵N个结点的树有N-1条边。
2. 树的一些概念
- 结点的度:一个结点含有的子树的个数称为该结点的度;如上图:A的为6
- 树的度:一棵树中,最大的结点的度称为树的度;如上图:树的度为6
- 叶子结点:度为0的结点称为叶结点;如上图:B、C、H、I...等结点为叶结点
- 根结点:树的顶层结点,即树的起始点;如上图:A
- 子结点(孩子结点):一个结点含有的子树的根结点称为该结点的子结点;如上图:B是A的子结点
- 父结点(双亲结点):若一个结点含有子结点,则这个结点称为其子结点的父结点;如上图:A是B的父结点
- 兄弟结点:具有相同父结点的结点互称为兄弟结点;如上图:B、C是兄弟结点
- 结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
- 树的高度(深度):树中结点的最大层次;如上图:树的高度为4
- 堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I为兄弟结点
- 结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先
- 子孙:以某结点为根的子树中任一结点都称为该结点的子孙;如上图:所有结点都是A的子孙
- 森林:由m(m>0)棵互不相交的树的集合称为森林;
3. 树的表示方法
树既可以采用顺序存储结构,又可以采用链式存储结构,通常会用三种不同的表示方法:双亲表示法,孩子表示法,孩子兄弟表示法。这里只介绍最常用的孩子兄弟表示法:
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
结点结构如图所示:
前面介绍了树,其实树的种类也有很多,比如:无序树(树的任意节点的子节点没有顺序关系),有序树(树的任意节点的子节点有顺序关系),二叉树(树的任意节点至多包含两棵子树,即度最大为 2 ),还有满二叉树,完全二叉树,霍夫曼树,二叉查找树,平衡二叉树,AVL树,红黑树...等等,下面要介绍的是二叉树:
二. 二叉树(binary tree)
1. 二叉树的定义
二叉树是一个特殊的树,是n(n >= 0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两颗互不相交,分别称为根结点的左子树和右子树的二叉树组成,它的度最大为 2 。
如下图所示:
1.1 二叉树的特点
二叉树是一种特殊的树结构,其特点包括:
-
每个结点最多有两个子结点,分别称为左子结点和右子结点,子结点的位置是固定的。
-
左子树和右子树是有顺序的,即使某个结点只有一个子结点,也需要确定是左子结点还是右子结点。
-
二叉树可以是空树,即没有任何节点。
-
二叉树的高度是根节点到叶子节点的最长路径的长度,深度是根节点到某个节点的路径长度。
三种特殊的二叉树
- 斜树:顾名思义,斜树中所有的结点都只有左子树的二叉树。所有结点都只有右子树的二叉树叫右斜树。他们统称为斜树。
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说如果一个二叉树的层数为K,且结点总数是,则它就是满二叉树。(每一层都是满的)
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有N个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至N的结点一 一对应时称之为完全二叉树。 有一点要注意:满二叉树是一种特殊的完全二叉树。(前K层是满的,但最后一层不一定满)
2. 二叉树的实现(链式结构)
二叉树也可以采用顺序存储结构或链式存储结构,但使用顺序存储结构的话较为不便。这里介绍较为容易实现理解的链式存储结构:
首先定义结点:
typedef int BTDataType;
typedef struct BinaryTreeNode //存放的数据,左子结点和右子结点
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
2.1 遍历二叉树
在二叉树的运算中,遍历是非常重要的一个运算,通过遍历,我们可以遍历二叉树中的所有节点,实现搜索、排序、输出等操作。
按照规则,二叉树的遍历有:前序,中序,后序的递归结构遍历。
顺便说一句,这里遍历要用到的递归思想:对于初学者来说,递归的代码总是简洁而又难懂,因为我们在写递归,包括读递归的代码的时候总是会去纠结这一层函数做了什么,它调用自身后的下一层函数又做了什么…然后脑袋就自然而然地宕机了:)
这是一个思维误区,一定要走出来。既然递归是一个反复调用自身的过程,这就说明它每一级的功能都是一样的,因此我们只需要关注一级递归的解决过程即可。或者可以自己动手画一下递归展开图,看看每一级是怎么样的。
- DLR 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前
// 根 左 右
void BinaryTreePrevOrder(BTNode* root) {
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
如图所示:
- LDR 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)
// 左 根 右
void BinaryTreeInOrder(BTNode* root) {
if (root == NULL)
{
printf("N ");
return;
}
BinaryTreePrevOrder(root->left);
printf("%d ", root->data);
BinaryTreePrevOrder(root->right);
}
如图所示:
- LRD 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
// 左 右 根
void BinaryTreePostOrder(BTNode* root) {
if (root == NULL)
{
printf("N ");
return;
}
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
printf("%d ", root->data);
}
如图所示:
2.2 二叉树的构建和销毁
可以根据一个用前序遍历数组来构建二叉树:
其中 “ # ”代表空
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi) {
if ((*pi) >= n || a[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL)
{
perror("malloc");
exit(-1);
}
root->data = a[(*pi)++];
root->left = BinaryTreeCreate(a, n, pi);
root->right = BinaryTreeCreate(a, n, pi);
return root;
}
它应该是这个样子的:
当还可以采用一种最直接的方法来构建你想要的二叉树:
//创建结点
BTNode* BuyTreeNode(int x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
assert(node);
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
//根据你想要的模样,创建一颗二叉树
BTNode* CreateTree()
{
BTNode* node1 = BuyTreeNode(1);
BTNode* node2 = BuyTreeNode(2);
BTNode* node3 = BuyTreeNode(3);
BTNode* node4 = BuyTreeNode(4);
BTNode* node5 = BuyTreeNode(5);
BTNode* node6 = BuyTreeNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1; //返回根结点
}
销毁 :
void BinaryTreeDestory(BTNode** root) {
if (*root == NULL)
return;
BinaryTreeDestory(&((*root)->left));
BinaryTreeDestory(&((*root)->right));
free(*root);
*root = NULL;
}
2.3 求结点个数
- 二叉树结点个数
int BinaryTreeSize(BTNode* root) {
if (root == NULL)
return 0;
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
- 二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root) {
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL) //必须两个结点的子结点都不为空才返回 1
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
- 二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k) {
if (root == NULL)
return 0;
if (k == 1) // 若k等于1,说明已经到了要查找的层数,或查找的层数是第一层
return 1;
return BinaryTreeLevelKSize(root->left, --k) + BinaryTreeLevelKSize(root->right, --k);
}
2.4 查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
if (root == NULL)
return NULL;
if (root->data == x) // 如果找到了,返回该结点的指针
return root;
BTNode* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
return ret1;
BTNode* ret2 = BinaryTreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}
下面是相关函数声明:
// 前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 中序遍历
void BinaryTreeInOrder(BTNode* root);
// 后序遍历
void BinaryTreePostOrder(BTNode* root);
// 构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树结点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
3. 二叉树的性质
- 若规定根结点的层数为1,则一棵非空二叉树的第 i 层上最多有个结点。
- 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是。
- 对任何一棵二叉树,如果度为0其叶结点个数为 ,度为2的分支结点个数为 ,则有 = +1。
- 若规定根结点的层数为1,具有n个结点的满二叉树的深度,h = 。
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为 i 的结点有:
- 若 i > 0,i 位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点。
- 若 2i+1 < n,左孩子序号:2i+1,2i+1>=n 否则无左孩子。
- 若 2i+2 < n,右孩子序号:2i+2,2i+2>=n 否则无右孩子。
三. 堆(heap)
1.堆的概念和结构
前面提到有三种特殊的二叉树,其中一种是完全二叉树,而下面要介绍的堆,就是一种完全二叉树。
堆总是满足下列性质:
-
堆中某个结点的值总是不大于或不小于其父结点的值;
-
堆总是一棵完全二叉树。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。本文要介绍的是小根堆
- 小根堆:父结点的值小于或等于子结点的值;
- 大根堆:父结点的值大于或等于子结点的值;
2.堆(小根堆)的实现
堆是一种采用顺序存储结构(即数组)的非线性数据结构,所以里面的成员包括一维数组,数组的容量,数组元素的个数。
定义结点:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
2.1 堆的向上调整和向下调整
堆的向上调整和向下调整算法可以说是堆数据结构中必不可少的两个算法,下面开始介绍。
由于是小根堆,所以我们要将小的数据往上送,所以要交换比目标结点大的位置:
- 向上调整:
void AdjustUp(HPDataType* arr, int child) {
int parent = (child - 1) / 2;
while (child > 0) // 调整到根结点位置
{
if (arr[parent] > arr[child])
{
swap(&arr[parent], &arr[child]); //若其父结点比它大则交换二者位置
child = (child - 1) / 2;
parent = (parent - 1) / 2; //更新父结点和当前结点的位置
}
else
break;
}
}
过程如下:
- 向下调整:
堆的向下调整算法不同于向上调整,它是有前提的:
若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
若想将其调整为大堆,那么根结点的左右子树必须都为大堆。
这是因为堆的堆顶元素必须是整个集合的最值,所以父结点和孩子结点比较,必须让孩子结点也是它这个集合中的最值,这样才能实现"堆顶是集合的最值"这个效果。
void AdjustDown(HPDataType* arr, int size, int parent) {
int child = parent * 2 + 1;
while (child < size) //如果下标越界,退出循环
{
if (arr[parent] > arr[child])
{
if (arr[child] > arr[child + 1] && child + 1 < size) //假设左孩子小,如果假设错了,更新一下
swap(&arr[parent], &arr[child + 1]);
else
swap(&arr[parent], &arr[child]);
child = child * 2 + 1;
parent = parent * 2 + 1;
}
else
break;
}
}
过程如下:
用到的 swap 函数 :
//交换
void swap(HPDataType* x, HPDataType* y) {
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
2.2 堆的构建,插入,删除
以上的操作都需要用到前面的调整算法。
- 堆的构建:
堆的构建过程一般会从最后一个非叶子结点(n - 2)/ 2开始,逐个对结点进行向下调整操作,直到将根结点调整到合适的位置。
void HeapCreate(Heap* hp, HPDataType* arr, int n) { // n是数组元素个数
hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
hp->size = hp->capacity = n; //初始化
for (int i = 0; i < n; i++)
hp->a[i] = arr[i];
for (int i = (n - 2) / 2; i >= 0; i--) //因为是下标,所以(n-1-1)/ 2
AdjustDown(hp->a, hp->size, i);
}
- 堆的插入:
在插入堆的时候,需要检查容量,再将结点的值放入并进行向上调整:
void HeapPush(Heap* hp, HPDataType x) {
assert(hp);
if (hp->capacity == hp->size) {
int newcapacity = hp->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(hp->a,sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, x);
}
过程如下:
- 堆的删除:
堆的删除,指的是删除堆顶的数据,但不是直接就删除,需要先将堆尾数据与堆顶数据交换位置,再删除:
void HeapPop(Heap* hp) {
assert(hp);
assert(hp->size);
swap(&hp->a[0],&hp->a[hp->size - 1]); // 交换堆顶结点和堆尾结点的位置
hp->size--; // 删除
AdjustDown(hp->a, hp->size, 0);
}
过程如下:
2.3 剩余操作
// 取堆顶的数据
HPDataType HeapTop(Heap* hp) {
assert(hp);
assert(hp->size);
return hp->a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp) {
assert(hp);
return hp->size;
}
// 堆的判空
int HeapEmpty(Heap* hp) {
assert(hp);
return hp->size == 0;
}
// 堆的销毁
void HeapDestory(Heap* hp) {
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
相关函数声明:
// 交换
void swap(HPDataType* x, HPDataType* y);
// 堆的向上调整
void AdjustUp(HPDataType* arr, int child);
// 堆的向下调整
void AdjustDown(HPDataType* arr, int size, int parent);
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* arr, int n);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);
以上就是全部内容。