(C语言数据结构)二叉树-概念-性质-存储结构-遍历-代码实现层层刨析
1. 树的基本概念
1.1 什么是树?
树是一种非线性数据结构,由n(n>0)个有限节点组成一个具有层次关系的集合。它看起来像一棵倒挂的树,根朝上,叶朝下。
树的核心特性:
有一个特殊的根节点,没有前驱节点
除根节点外,其余节点被分成M(M>0)个互不相交的集合T₁, T₂, ..., Tₘ
每个子集Tᵢ又是一棵结构与树类似的子树
注意:在树形结构中,⼦树之间不能有交集,否则就不是树形结构
非树形结构:
如果树中的节点没有子辈关系却相交了,则不为树结构,而是图
1.2 树的常用术语
父节点/双亲节点:含有子节点的节点(如A是B的父节点)
子节点/孩子节点:一个节点含有的子树的根节点(如B是A的孩子节点)
节点的度:节点拥有的子树个数(如A的度为6,B的度为0)
树的度:树中所有节点的度的最大值
叶节点:度为0的节点(如H、B、P)
分支节点:度不为0的节点
兄弟节点:具有相同父节点的节点(如B、C、D是兄弟节点)
节点的层次:从根开始,根为第1层,依次递增
树的高度:树中节点的最大层次
路径:从树中任意节点出发,沿父节点-子节点连接达到任意节点的序列
1.3 树的表示
我们通过上述的图片可以发现,树的逻辑结构很像链表,无非是多个节点通过链表相互连接,同时具有一定的关系,因此我们以下用链表来建造树
1.3.1 左孩子和右孩子表示法
typedef int BTDataType;typedef struct BinaryTreeNode {struct BinaryTreeNode* left; // 左孩子指针struct BinaryTreeNode* right; // 右孩子指针BTDataType data; // 数据域
} BTNode;
左孩子和右孩子顾名思义,即为一个节点的左下的节点和右下的节点
在上图中A的左孩子为B,右孩子为C
data为该节点存储的值
1.3.2 左孩子和右兄弟表示法
struct TreeNode
{ struct Node* leftchild; // 左边开始的第⼀个孩⼦结点struct Node* rightbrother; // 指向其右边的下⼀个兄弟结点int data;// 结点中的数据域
};
该表示法把右孩子改成了右兄弟,如B的右兄弟为C
2. 二叉树的核心概念
2.1 什么是二叉树?
二叉树是节点的有限集合,该集合:
或者为空
或者由一个根节点加上两棵分别称为左子树和右子树的二叉树组成
说白了就是二叉树中一个节点最多只能有两个分支,如果一个节点有三个分支那么就不算二叉树
注意:对于任意的⼆叉树都是由以下⼏种情况复合而成
2.2 特殊二叉树
2.2.1 满二叉树
每一层的节点数都达到最大值。如果层数为K,则节点总数为2ᴷ - 1。
可以看出,满二叉树中每个层级的每个位置必须得有节点,如果有一个空那么就不算满二叉树
例如这就不算满二叉树
2.2.2 完全二叉树
深度为K,有n个节点的二叉树,当且仅当其每一个节点都与深度为K的满二叉树中编号从1到n的节点一一对应。
完全二叉树中每个层级的节点必须得以从左到右分布,如果中间有空,而右边蹦出了一个节点就不算完全二叉树
例如这就不算完全二叉树
2.3 二叉树的重要性质
第i层最多有2ⁱ⁻¹个节点
深度为h的二叉树最多有2ʰ - 1个节点
对于任何二叉树:叶节点数n₀ = 度为2的节点数n₂ + 1
具有n个节点的完全二叉树深度为⌊log₂n⌋ + 1
以上性质其实很像高中数学学过的等比数列,第一层有1个节点,第二层有2个节点,第三层有4个节点,其实就算是以2为公比,1为首项的等比数列,性质都可以推导出来
2.3 二叉树存储结构
⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构。
2.3.1 顺序结构
顺序结构存储就是使⽤数组来存储,⼀般使用数组只适合表示完全⼆叉树,因为不是完全⼆叉树会有 空间的浪费,完全⼆叉树更适合使⽤顺序结构存储
因此可以使用数组来存储二叉树的节点
2.3.2 链式结构
⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。
在上面表示树的方法就讲到了两种方法,在二叉树中同样适应
3. 堆:特殊的完全二叉树
3.1 堆的概念
堆是一种特殊的完全二叉树,满足:
大堆:每个节点的值都大于或等于其子节点的值
小堆:每个节点的值都小于或等于其子节点的值
可以看出,堆一定是个完全二叉树。大堆可以看作从大到小排,但如果以每一层的从左到右看不一定都从大到小,只是在竖直方向上保持,即父亲节点的值一定大于儿子节点的值,但儿子节点之间的值不一定从左到右,从大到小排序。小堆即相反。
3.2 堆的实现
由于堆是个完全二叉树,我们可以使用顺序表来实现
那么如果把堆的节点存储在数组中?
可以从上到下,从左到右以序号来存储
我们把堆的根-A放在数组下标为0的位置,那么B和C理所当然分别为1和2,那么DEFG如何确定序号?
因为堆的性质可以利用等比数列来求,我们可以发现当根的序号为i时,两个子节点的序号分别为2i+1和2i+2,反过来也可也利用子节点的序号来推导根的序号
3.2.1 堆的创建与销毁
typedef int HPDataType;typedef struct Heap {HPDataType* data; // 存储数据的数组int size; // 当前元素个数int capacity; // 容量
} HP;// 堆的初始化
void HeapInit(HP* hp) {hp->data = NULL;hp->size = hp->capacity = 0;
}// 堆的销毁
void HeapDestroy(HP* hp) {free(hp->data);hp->data = NULL;hp->size = hp->capacity = 0;
}
void HPPush(HP* php, HPDataType x){assert(php);if (php->size == php->capacity){size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size-1);}//堆节点的引入
3.2.2 核心算法:向上调整
如果我们要创造一个小堆或者大堆,那么就必须对根和子节点进行交换,以保持从大到小或从小到大的序列
void AdjustUp(HPDataType* data, int child) {int parent = (child - 1) / 2;while (child > 0) {// 大堆:data[child] > data[parent]// 小堆:data[child] < data[parent]if (data[child] > data[parent]) {// 交换父子节点HPDataType temp = data[child];data[child] = data[parent];data[parent] = temp;// 向上继续调整child = parent;parent = (child - 1) / 2;} else {break;}}
}
在上述算法中,我们先选取一个子节点,找到他的根,随后进行大小的排序。
一个结构排完后,再继续向上排
3.2.3 核心算法:向下调整
删除堆是删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进⾏ 向下调整算法。
向下调整算法有⼀个前提:左右⼦树必须是⼀个堆,才能调整
void AdjustDown(HPDataType* data, int n, int parent) {int child = parent * 2 + 1; // 左孩子while (child < n) {// 选出左右孩子中较大的(大堆)if (child + 1 < n && data[child + 1] > data[child]) {child++;}// 如果孩子大于父亲,需要调整if (data[child] > data[parent]) {HPDataType temp = data[child];data[child] = data[parent];data[parent] = temp;parent = child;child = parent * 2 + 1;} else {break;}}
}
void HPPop(HP* php){assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);//删除堆顶数据时自动排序}
3.3 堆的应用
3.3.1 堆排序
当我们有一些无序的数据时,可以把他们放进堆中,制造一个小堆或者大堆,再对除最后一层外的所有节点进行向下调整算法,可以得到一个完全升序或者降序的堆
void HeapSort(int* arr, int n) {// 建堆:从最后一个非叶子节点开始向下调整for (int i = (n - 1 - 1) / 2; i >= 0; i--) {AdjustDown(arr, n, i);}// 排序:将堆顶元素与末尾交换,然后调整堆int end = n - 1;while (end > 0) {// 交换堆顶和末尾元素int temp = arr[0];arr[0] = arr[end];arr[end] = temp;// 调整堆AdjustDown(arr, end, 0);end--;}
}
该版本有⼀个前提,必须提供有现成的数据结构堆
3.3.2 TOP-K问题
假设有一串数据,我要你找到前K个最大或者前K个最小的数据?
我们可以建造一个含K个节点的小堆或者大堆,如果时前K个最大,可建造小堆,这时堆顶一定是堆中最小的数据,如果遇到了比更大的数据,那么把该数据置换到对顶中,以此到最后堆中一i的那个是前K个最大的数据。前K个最小则相反
void PrintTopK(int* arr, int n, int k) {// 用前K个元素建小堆for (int i = (k - 1 - 1) / 2; i >= 0; i--) {AdjustDown(arr, k, i);}// 遍历剩余元素for (int i = k; i < n; i++) {if (arr[i] > arr[0]) {arr[0] = arr[i];AdjustDown(arr, k, 0);}}// 输出前K个最大的元素for (int i = 0; i < k; i++) {printf("%d ", arr[i]);}
}
4. 实现链式结构二叉树
⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。通常的⽅法是链表中每个结点由三个域组 成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址, 其结构如下:
BTNode* CreateNode(BTDataType value) {BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));if (newNode == NULL) {perror("malloc fail");return NULL;}newNode->data = value;newNode->left = newNode->right = NULL;return newNode;
}// 手动创建一棵二叉树:
// 1
// / \
// 2 3
// / /
// 4 5
BTNode* CreateBinaryTree() {BTNode* n1 = CreateNode(1);BTNode* n2 = CreateNode(2);BTNode* n3 = CreateNode(3);BTNode* n4 = CreateNode(4);BTNode* n5 = CreateNode(5);n1->left = n2;n1->right = n3;n2->left = n4;n3->left = n5;return n1;
}
回顾⼆叉树的概念,⼆叉树分为空树和⾮空⼆叉树,⾮空⼆叉树由根结点、根结点的左⼦树、根结点 的右⼦树组成的
根结点的左⼦树和右⼦树分别⼜是由⼦树结点、⼦树结点的左⼦树、⼦树结点的右⼦树组成的,因此 ⼆叉树定义是递归式的,后序链式⼆叉树的操作中基本都是按照该概念实现的。
4.1 前中后序遍历
4.1.1 遍历规则
按照规则,⼆叉树的遍历有:前序/中序/后序的递归结构遍历:
1)前序遍历(PreorderTraversal亦称先序遍历):访问根结点的操作发⽣在遍历其左右⼦树之前 访 问顺序为:根结点、左⼦树、右⼦树
2)中序遍历(InorderTraversal):访问根结点的操作发⽣在遍历其左右⼦树之中(间) 访 问顺序为:左⼦树、根结点、右⼦树
3)后序遍历(PostorderTraversal):访问根结点的操作发⽣在遍历其左右⼦树之后 访 问顺序为:左⼦树、右⼦树、根结点
对于遍历,我们可采用函数递归的方法
前序遍历(根左右)
void PreOrder(BTNode* root) {if (root == NULL) {printf("NULL ");return;}printf("%d ", root->data); // 访问根节点PreOrder(root->left); // 遍历左子树PreOrder(root->right); // 遍历右子树
}
// 输出:1 2 4 NULL NULL NULL 3 5 NULL NULL NULL
在上述代码中,函数一开始会一直递归到左节点,如果碰到NULL,才会返回,同时又会打印根节点的数据。当左子树遍历完后才会开始遍历右子树
中序遍历(左根右)
void InOrder(BTNode* root) {if (root == NULL) {printf("NULL ");return;}InOrder(root->left); // 遍历左子树printf("%d ", root->data); // 访问根节点InOrder(root->right); // 遍历右子树
}
// 输出:NULL 4 NULL 2 NULL 1 NULL 5 NULL 3 NULL
后序遍历(左右根)
void InOrder(BTNode* root) {if (root == NULL) {printf("NULL ");return;}InOrder(root->left); // 遍历左子树printf("%d ", root->data); // 访问根节点InOrder(root->right); // 遍历右子树
}
// 输出:NULL 4 NULL 2 NULL 1 NULL 5 NULL 3 NULL
前序遍历结果:123456
中序遍历结果:321546
后序遍历结果:315641
4.2 层序遍历
层序遍历通俗而讲就是我们所说的从左到右,从上到下依次读取节点的数据,而不是按照根的左子树或者根的右子树遍历。
实现层序遍历需要额外借助数据结构:队列
我们一开始先把1存储在队列中,随后出队列,再把1的左节点和右节点存储在队列中
在下次中出列就会出2,然后再把2的左节点和右节点存储在队列中
因此反复当队列中的数据为空时则遍历完成
// 需要队列辅助实现
typedef struct QueueNode {BTNode* treeNode;struct QueueNode* next;
} QueueNode;typedef struct Queue {QueueNode* front;QueueNode* rear;
} Queue;void LevelOrder(BTNode* root) {if (root == NULL) return;Queue q;QueueInit(&q);QueuePush(&q, root);while (!QueueEmpty(&q)) {BTNode* front = QueueFront(&q);QueuePop(&q);printf("%d ", front->data);if (front->left) {QueuePush(&q, front->left);}if (front->right) {QueuePush(&q, front->right);}}QueueDestroy(&q);
}
// 输出:1 2 3 4 5
4.3 其他常用操作
计算节点个数
我们同样使用递归来实现,当根为空时,那么返回1,当不为空时返回左节点和右节点的合加1
int TreeSize(BTNode* root) {return root == NULL ? 0 : 1 + TreeSize(root->left) + TreeSize(root->right);
}
计算叶子节点
只有当左节点和右节点同时为空时才是叶子节点
int TreeLeafSize(BTNode* root) {if (root == NULL) return 0;if (root->left == NULL && root->right == NULL) return 1;return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
计算树的高度
我们可把树的高度拆分为左子树的高度和右子树的高度取较大值并加1
int TreeHeight(BTNode* root) {if (root == NULL) return 0;int leftHeight = TreeHeight(root->left);int rightHeight = TreeHeight(root->right);return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}
查找节点
我们依次从左子树和右子树查找对应值的节点,如果存在那么返回这个节点
BTNode* TreeFind(BTNode* root, BTDataType x) {if (root == NULL) return NULL;if (root->data == x) return root;BTNode* leftResult = TreeFind(root->left, x);if (leftResult != NULL) return leftResult;return TreeFind(root->right, x);
}
判断是否为完全二叉树
我们可用层序遍历来判断
当遇到空时,如果时完全二叉树,那么就不该存在剩余的节点了,队列中也不该存在数据
但如果存在,则不为完全二叉树
bool TreeComplete(BTNode* root) {if (root == NULL) return true;Queue q;QueueInit(&q);QueuePush(&q, root);// 层序遍历,直到遇到第一个NULLwhile (!QueueEmpty(&q)) {BTNode* front = QueueFront(&q);QueuePop(&q);if (front == NULL) {break;}QueuePush(&q, front->left);QueuePush(&q, front->right);}// 检查队列中剩余的节点是否都为NULLwhile (!QueueEmpty(&q)) {BTNode* front = QueueFront(&q);QueuePop(&q);if (front != NULL) {QueueDestroy(&q);return false;}}QueueDestroy(&q);return true;
}