1. 二叉树概念及结构
1.1概念
- 一棵二叉树是结点的一个有限集合,该集合或者为空
- 二叉树由一个根节点加上两棵别称为左子树和右子树的二叉树组成
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
1.2特殊的二叉树:
-
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是
说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。 -
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K
的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对
应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
1.3 二叉树的性质
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从 0 开始编号,则对
于序号为i的结点有:
1. 若i>0,i 位置节点的双亲序号:(i-1)/2;i=0,i 为根节点编号,无双亲节点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
1.4二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
(1、顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空
间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺
序存储在物理上是一个数组,在逻辑上是一颗二叉树。
(2、链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。 链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
2.二叉树的顺序结构及实现
2.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。
而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
数组下标计算父子关系公式:
leftchild = parent*2+1 // 奇数
rightchild = parent*2+2 // 偶数
parent = (child-1)/2
2.2 堆的概念及结构
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
2.3 堆的实现
2.2.1 堆向下调整算法
将一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整
成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
// 小堆
void AdjustDown( HPDataType* arr, int size , int parent ){
assert(arr);
// 找出 parent节点的 两个child节点中较小的那个
int minchild = 2 * parent + 1;
if (minchild + 1 < size && arr[minchild + 1] < arr[minchild] )
minchild = minchild + 1;
while (minchild < size) {
if (arr[minchild] > arr[parent]) // 当较小的那个child节点 都 大于parent节点 即结束
break;
Swap(&arr[minchild], &arr[parent]); // 较小的那个child节点的值 与 parent节点的值交换
parent = minchild;
// 找出新 parent 节点的 两个 child 节点中较小的那个
minchild = 2 * parent + 1;
if (minchild + 1 < size && arr[minchild] > arr[minchild + 1])
minchild = minchild + 1;
}
}
2.2.2堆的创建
int a[] = {1,5,3,8,7,6};
int n = sizeof(a)/sizeof(int);
这里我们从倒数的第一个非叶子节点的子树(最后一个节点的父亲)开始调整,一直调整到根节点的树,就可以调整成堆。
// n-1 是最后一个非叶子节点在数组中的下标, (n-1-1)/2 是他的父亲节点。
for(int i = (n-1-1)/2; i >= 0; --i ){
AdjustDown(a,n,i);
}
2.2.3 建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的
就是近似值,多几个节点不影响最终结果):
推算得:向下插入建堆的时间复杂度为O(N)。
2.2.4 堆的插入
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
2.2.5 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调
整算法。
2.2.6 堆的代码实现
typedef int HPDataType;
typedef struct Heap{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
2.4 堆的应用
2.4.1 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
升序:建大堆。找出最大的放到最后,将size–。再在前 n-1 个中找出次大的…
降序:建小堆。… - 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
代码实现:
void HeapSort(int* a, int n) {
for (int i = (n-1-1)/2; i >=0; i--) { // 建堆
AdjustDown(a, n, i); // 向下调整建堆
}
for (int i = n-1; i >0; i--) {
Swap(&a[0], &a[i]); // 将堆顶换到最后
AdjustDown(a, i , 0); // 将新的堆顶向下调整,形成新的堆
}
/*for (int i =0; i <n; i++) {
printf("%d ", a[i]);
}*/
}
2.4.2 TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
数据都不能一下子全部加载到内存中。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
前k个最大 的元素,则建 小堆
前k个最小 的元素,则建 大堆 - 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
int* FindTopK(int* arr, int n, int k) {
Heap hp;
HeapCreate(&hp);
int* newkarr = new int[k];
for (int i = 0; i < k; i++) {
newkarr[i] = arr[i];
AdjustUp(newkarr, i);// 向上调整建堆
}
// 用剩余的元素依次与堆顶元素来比较,不满足则替换堆顶元素,对比完之后,堆中剩余的元素即为所求
for (int i = k; i < n; i++) {
if (arr[i] > newkarr[0]) {
newkarr[0] = arr[i];
AdjustDown(newkarr, k, 0); //
}
}
return newkarr;// 返回存放 TopK 的数组
}
3、二叉树的链式结构的实现
3.1 二叉树的遍历
3.1.1 前序、中序以及后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉
树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历
是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
-
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
void BinaryTreePrevOrder(BTNode* root){ if (root == NULL) { printf("NULL "); return; } printf("%d ", root->data); BinaryTreePrevOrder(root->left); BinaryTreePrevOrder(root->right); }
-
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
void BinaryTreeInOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } BinaryTreeInOrder(root->left); printf("%d ", root->data); BinaryTreeInOrder(root->right); }
-
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
void BinaryTreePostOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } BinaryTreePostOrder(root->left); BinaryTreePostOrder(root->right); printf("%d ", root->data); }
对于下面的二叉树:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y3mUI1Zd-1660481193274)(E:\比特\数据结构\数据结构与算法\数据结构初阶.assets\1660480440085.png)]
前序遍历结果: 1 2 3 4 5 6
中序遍历结果: 3 2 1 5 4 6
后序遍历结果: 3 2 5 6 4 1
3.1.2 层序遍历
层序遍历:设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
// 层序遍历
// 利用队列的先进先出。将头节点入队列,当一个节点出队时,将他的左右孩子节点入队。直到为空。
void BinaryTreeLevelOrder(BTNode* root) {
queue<BTNode*> q;
if (root)
q.push(root);
while (!q.empty()) {
BTNode* front = q.front();
printf("%d ", front->data);
q.pop();
if (front->left != NULL) // 将NULL跳过
q.push(front->left);
if (front->right != NULL)
q.push(front->right);
}
}
3.2 节点个数以及高度等
二叉树的深度
int TreeDeep(BTNode* root) {
if (root == NULL)
return 0;
return TreeDeep(root->left) > TreeDeep(root->right) ?
1+TreeDeep(root->left) : 1+TreeDeep(root->right);
}
二叉树节点个数
int BinaryTreeSize(BTNode* root) {
if (root == NULL)
return 0;
return 1+BinaryTreeSize(root->left)+BinaryTreeSize(root->right);
}
二叉树叶子节点个数
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root) {
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k) {
if (root == NULL)
return 0;
if (k == 1)
return 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*Lret=BinaryTreeFind(root->left, x);
if (Lret)
return Lret;
BTNode* Rret = BinaryTreeFind(root->left, x);
if (Rret)
return Rret;
return NULL;
}
3.3 二叉树的创建和销毁
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi) {
if (*pi == '#')
return NULL;
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->data = a[*pi];
(*pi)++;
root->left = BinaryTreeCreate(a, n, pi);
root->right = BinaryTreeCreate(a, n, pi);
return root;
}
// 二叉树销毁
BTNode* BinaryTreeDestory(BTNode** root) {
BTNode* cur = *root;
if (cur->left == NULL && cur->right == NULL) {
//free(cur);
return NULL;
}
else{
cur->left = BinaryTreeDestory(&cur->left);
cur->right = BinaryTreeDestory(&cur->right);
}
}