【数据结构】二叉树
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、pandas是什么?
- 二、使用步骤
- 1.引入库
- 2.读入数据
- 总结
前言
我们前面了解4种线性表,这里我们来了解一个非线性结构:树。本文重点了解树中的二叉树。
一、树的概念及结构
1.1 树的概念
树是一种非线性的数据结构,它是由n(n >= 0)个有限节点组成一个具有层次关系的有限集合。把它叫做数是因为它看起来像一颗倒挂的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的节点称为根节点,根节点没有前驱结点。
- 除了根节点外,其余节点被分成M(M > 0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1 <= i <= m)又是一个结构与树类似的子树,每棵子树的根节点有且只有一个前驱,可以有0个或者多个后继。
- 因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构。
上面三种都不是树形结构。
树的特点:
- 子树不能相交
- 除了根节点外,其他节点有且只有一个父节点(前驱结点)
- 一棵N个结点的树拥有N-1条边
1.2 树的相关概念
节点的度:一个节点含有的子树的个数称为该节点的度;如上图:1的度为3
叶节点或终端节点:度为0的节点称为叶节点;如上图:6,7,9……等节点为叶节点
非终端节点或分支节点:度不为0的节点;如上图:1,2,3,4……等节点为分支节点
双亲结点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如上图:1是2的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为称为该节点的子节点;如上图:2是1的子节点
兄弟节点:具有相同的父节点的节点称为兄弟节点;如上图:2,3,4是兄弟节点
树的度:一棵树中,最大的节点的度就是树的度;如上图,这棵树的度为3
节点的层次:从根开始定义起,跟为第一层,根的子节点为第二层,以此类推
树的高度或深度:树中节点的最大层次;如上图:该树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟节点;如上图:6和9互为堂兄弟节点
节点的祖先:从根到该节点所经分支上上的所有节点;如上图:1是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙;如上图:所有节点都是1的子孙
森林:由m(m > 0)棵互不相交的树的集合称为森林
1.3 树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存节点和节点之间的关系,实际中树有很多种表示方式,如:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{struct Node* _firstChild1; // 第一个孩子结点struct Node* _pNextBrother; // 指向其下一个兄弟结点DataType _data; // 结点中的数据域
};
1.4 树在实际中的运用
在Linux中表示文件系统的目录就是树结构
二、二叉树的概念及结构
2.1 二叉树的概念
一棵二叉树是节点的有限集合,该集合:
- 或者为空
- 有一个根节点加上加上两个别称为左子树和右子树组成
由上图可以看出:
- 二叉树不存在度大于2的节点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是有以下几种情况复合组成的:
2.2 特殊的二叉树
- 满二叉树:一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且节点总数是2的K次方 - 1,则他就是满二叉树。
- 完全二叉树:完全二叉树是一种效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为K的,有n个结点的二叉树,当且仅当每一个节点都与深度为K的满二叉树中编号从1至n的节点一一对应时称为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树。
2.3 二叉树的性质
1. 若规定根节点的层数为1,则一颗非空二叉树的第i层上最多可以有2的(i - 1)次方个结点。
2. 若规定根节点的层数为1,则深度为h的二叉树的最大节点数为2的h次方 - 1。
3. 对任何一棵二叉树,如果度为0其叶节点数为n,度为2的节点个数为m,则n = m + 1。
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度 h = log2(n+1).(log以2为底,n+1为 对数)。
5. 对于具有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则无右孩子
2.4 二叉树的存储结构
二叉树一般可以使用两种结构存储:一种是顺序存储,另一种是链式存储。
2.4.1 顺序存储
顺序结构存储就是使用数组存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间浪费。而现实中使用只有堆才会使用来存储,堆在后面讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一棵二叉树。
2.4.2 链式存储
二叉树的连式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左孩子和右孩子所在的的链节点的存储地址。链式结构又能分为二叉链和三叉链,这里讲解二叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{struct BinTreeNode* _pLeft; // 指向当前节点左孩子struct BinTreeNode* _pRight; // 指向当前节点右孩子BTDataType _data; // 当前节点值域
};
三、二叉树顺序结构及实现
3.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程空间里的对是两个概念,一个是数据结构,一个是操作系统管理内存的一段区域分段。
3.2 堆的概念及结构
如果有一个关键码的集合K = { k0,k1,k2,……,kn-1},把它所有元素按照完全二叉树的顺序存储方式存储在一个一维数组里,并满足:Ki <= K2*i+1且Ki <= K2*i+2(Ki >= K2*i+1且Ki >= K2*i+2)i=0,1,2,……,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
根的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值
- 堆总是一颗完全二叉树
3.3 堆的实现
3.3.1 堆向下调整算法
现在我们给出一个数组,逻辑上看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把他调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[11] = { 9, 1, 8, 2, 3, 10, 11, 4, 5, 6, 7 };
3.3.2 堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆,根节点左右子树不是堆,我们应该如何调整?这里我们从倒数第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
int array[] = { 1, 5, 9, 8, 10, 16 };
3.3.3 堆的插入
在插入时,我们是使用向上调整算法插入,这里插入一个10为例:
3.3.4 堆的删除
删除堆是删除对顶的数据,将堆顶的数据与最后一个数据交换,然后删除数组最后一个数据,再对堆顶数据进行向下调整算法
3.3.5 堆的代码实现
堆的结构定义:
typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;
先介绍向下调整算法和向上调整算法(实现小根堆),下面的初始化和插入删除都要用到:
// 交换两个数据,下面两个算法会用到
void Swap(HPDataType* px, HPDataType* py)
{HPDataType tmp = *px;*px = *py;*py = tmp;
}// 向上调整算法,这个是调整为小根堆
void AdjustUp(HPDataType* a, int child)
{// child和parent都是数组下标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 + 1] < a[child]){++child;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
堆的初始化:
// 初始化为空堆
void HPInit(HP* php)
{assert(php);php->a = NULL;php->size = 0;php->capacity = 0;
}// 将传入的数组初始化为堆
void HPInitArray(HP* php, HPDataType* a, int n)
{assert(php);php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);if (php->a == NULL){perror("malloc fail");return;}memcpy(php->a, a, sizeof(HPDataType) * n);php->capacity = php->size = n;// 向上调整,建堆 O(N*logN)//for (int i = 1; i < php->size; i++)//{// AdjustUp(php->a, i);//}// 向下调整,建堆 O(N)for (int i = (php->size - 1 - 1) / 2; i >= 0; i--){AdjustDown(php->a, php->size, i);}
}
堆的销毁:
void HPDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->capacity = 0;php->size = 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);
}
堆的删除:
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); // 堆顶数据进行向下调整
}
取堆顶数据:
HPDataType HPTop(HP* php)
{assert(php);return php->a[0];
}
堆的判空:
bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}
四、二叉树链式结构及实现
4.1 二叉树的遍历
4.2.1 前序、中序以及后序遍历
学习二叉树的结构,最简单的方式就是遍历,所谓二叉树的遍历就是按照某种特定的规则,依次对二叉树的节点进行相应操作,并且每个节点只操作一次。访问节点所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的操作之一,也是二叉树进行其他运算的基础。
按照规则,二叉树的遍历有:前序、中序和后序的递归结构遍历:
- 前序遍历(先序遍历):访问根节点的操作发生在遍历其左右子树之前
- 中序遍历:访问根节点的操作发生在遍历左子树之后,遍历右子树之前
- 后序遍历:访问根节点的操作发生在遍历左右子树之后
由于被访问的节点必是某子树的根,所以N(Node), L(Left subtree), R(Right subtree) 又可解释为根,根的左子树,根的右子树。NLR,LNR,LRN分别称为前序遍历,中序遍历和后序遍历。
前序遍历的实现代码:
void PreOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}printf("%d ", root->val);PreOrder(root->left);PreOrder(root->right);
}
中序遍历的实现代码:
void InOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}InOrder(root->left);printf("%d ", root->val);InOrder(root->right);
}
后序遍历的实现代码:
void PostOrder(BTNode* root)
{if (root == NULL){printf("N ");return;}InOrder(root->left);InOrder(root->right);printf("%d ", root->val);
}
下面主要分析前序递归遍历,中序与后序图解类似。
前序遍历递归图解:
- 进入前序遍历函数,输出当前根的数据:1
- 通过递归调用,进入当前根的左子树
- 以当前节点为根作为一个子树,输出当前跟的数据:2,然后进入当前根的左子树
- 以当前节点为根作为一个子树,输出当前跟的数据:3,然后进入当前根的左子树
- 发现左子树为空,回退到上一个函数中
- 回退到了根的数据为3的节点中,然后进入当前根的右子树,发现同样为空,在向上回退
- 回退到了根的数据为2的节点中,然后进入当前跟的右子树,发现为空,再向上回退
- 回退到了根节点,然后进入根节点的右子树中,下面操作与进入左子树一样
当我们对上面的二叉树进行遍历的结构如下:
- 前序遍历:1 2 3 4 5 6
- 中序遍历:3 2 1 5 4 6
- 后序遍历:3 2 5 6 4 1
4.2.2 层序遍历
层序遍历:除了前序遍历,中序遍历和后序遍历外,还可以对二叉树进行层序遍历。设二叉树根节点所在层数为1,层序遍历就是从二叉树所在的根节点出发,首先访问树的根节点,然后从左到右访问第二层上的节点,接着是第三层上的节点,以此类推。从上至下,从左至右访问树的节点就是层序遍历。
层序遍历的实现代码:
void LevelOrder(BTNode* root)
{// 层序遍历需要用到队列Queue q;QueueInit(&q);// 先插入根节点QueuePush(&q, root);int num = 1;// 队列中没有元素说明该树已经层序遍历完成while (!QueueEmpty(&q)) {int i = num;num = 0;// 循环获取一层中所有的节点for (int j = 0; j < i; j++) {BTNode* tmp = QueueFront(&q);QueuePop(&q);// 该节点有子节点则将子节点插入到队列中if (tmp->left) QueuePush(&q, tmp->left), num++;if(tmp->right) QueuePush(&q, tmp->right), num++;printf("%d ", tmp->val);}printf("\n");}
}
4.2 二叉树节点个数
获得节点个数的实现:
// 通过传参返回节点个数
void TreeSize(BTNode* root, int* psize)
{if (root == NULL)return 0;else++(*psize);TreeSize(root->left, psize);TreeSize(root->right, psize);
}
// 通过返回值返回节点个数
int TreeSize(BTNode* root)
{return root == NULL ? 0 :TreeSize(root->left) + TreeSize(root->right) + 1;
}
二叉树第k层节点个数:
// 求二叉树第k层的节点个数
int TreeKLevel(BTNode* root, int k)
{assert(k > 0);if (NULL == root)return 0;if (k == 1)return 1;// 不等于空,且k > 1说明第k层的节点在子树里面,转换成子问题求解return TreeKLevel(root->left, k - 1)+ TreeKLevel(root->right, k - 1);
}
总结
本文我们了解到树的基本概念,然后重点讲解二叉树的概念,结构,性质和存储结构。之后我们详细了解堆的基本操作的实现,最后讲解了二叉树的遍历操作及其实现,希望对大家有所帮助。