2
i
−
1
2^{i - 1}
2i−1个结点(
i
=
1
i >= 1
i>=1)。**
2.3 性质3
**在一棵深度为 k 的二叉树中,最多有
2
k
−
1
2^k - 1
2k−1 个结点。**
2.4 性质4
**具有 n 个结点的完全二叉树的深度为
⌊
log
2
n
⌋
1
\left \lfloor \log_{2}{n} \right\rfloor + 1
⌊log2n⌋+1。**
2.5 性质5
对一棵具有n个结点的完全二叉树从1开始按层序编号,则对于编号为
i
(
1
<
=
i
<
=
n
)
i(1 <= i <= n)
i(1<=i<=n)的结点
i
i
i,有:
**1. 如果
i
1
i > 1
i>1,则结点
i
i
i的父节点编号为
⌊
i
/
2
⌋
\left \lfloor i/ 2 \right\rfloor
⌊i/2⌋ ;否则结点
i
i
i 是根结点, 无双亲。**
**2. 如果
2
i
<
=
n
2i <= n
2i<=n,则结点的左孩子编号为
2
i
2i
2i ;否则结点无左孩子。**
== **3. 如果
2
i
<
=
n
1
2i <= n + 1
2i<=n+1,则结点的右孩子编号为
2
i
1
2i + 1
2i+1 ;否则结点无右孩子。**
3.二叉树的抽象数据类型定义
和树一样,在不同的情况下,二叉树的基本操作不尽相同。下面给出一些二叉树抽象数据类型定义的例子,包含了部分基本操作:
ADT BiTree
DataModel
二叉树由一个根节点和两棵互不相交的左右子树构成,二叉树中的结点具有层次关系
Operation
InitBiTree
输入:无
功能:初始化一棵二叉树
输出:一个空的二叉树
CreatBiTree
输入:n 个结点的数据信息
功能:建立一棵二叉树
输出:含有n个结点的二叉树
DestoryBiTree
输入:无
功能:销毁一棵二叉树
输出:释放二叉树所占的内存空间
PreOrder
输入:无
功能:前序遍历二叉树
输出:二叉树的前序遍历序列
InOrder
输入:无
功能:中序遍历二叉树
输出:二叉树的中序遍历序列
PostOrder
输入:无
功能:后序遍历二叉树
输出:二叉树的后序遍历序列
LeverOrder
输入:无
功能:层序遍历二叉树
输出:二叉树的层序遍历序列
endADT
需要注意的是,二叉树的实现方式可以有多种,如链式存储(使用节点对象和引用)或数组存储(使用数组来表示节点及其关系),具体的实现方式可以根据需求和语言特性进行选择。以上定义给出了二叉树的操作接口,在具体实现时可以根据需要进行适当的调整。
4.二叉树的遍历操作
二叉树的遍历是指按照一定的顺序访问二叉树的所有节点。常见的二叉树遍历方式有三种:先序遍历、中序遍历和后序遍历。这些遍历方式可以通过递归或迭代的方式实现。
以下是关于二叉树遍历的一些常见操作:
先序遍历(Preorder Traversal):
先序遍历的顺序是:根节点 -> 左子树 -> 右子树。
递归实现:
- 如果树为空,则返回。
- 访问当前节点的值。
- 递归地对左子树进行先序遍历。
- 递归地对右子树进行先序遍历。
迭代实现:
- 创建一个空栈,并将根节点压入栈。
- 当栈不为空时,执行以下步骤:
- 弹出栈顶节点,访问该节点的值。
- 如果存在右子节点,则将右子节点压入栈。
- 如果存在左子节点,则将左子节点压入栈。
中序遍历(Inorder Traversal):
中序遍历的顺序是:左子树 -> 根节点 -> 右子树。
递归实现:
- 如果树为空,则返回。
- 递归地对左子树进行中序遍历。
- 访问当前节点的值。
- 递归地对右子树进行中序遍历。
迭代实现:
- 创建一个空栈。
- 初始化当前节点为根节点。
- 当栈不为空或当前节点不为空时,执行以下步骤:
- 将当前节点及其所有左子节点依次压入栈,直到当前节点为空。
- 弹出栈顶节点,访问该节点的值。
- 将当前节点更新为弹出节点的右子节点。
后序遍历(Postorder Traversal):
后序遍历的顺序是:左子树 -> 右子树 -> 根节点。
递归实现:
- 如果树为空,则返回。
- 递归地对左子树进行后序遍历。
- 递归地对右子树进行后序遍历。
- 访问当前节点的值。
迭代实现:
- 创建一个空栈。
- 初始化当前节点为根节点。
- 当栈不为空或当前节点不为空时,执行以下步骤:
- 将当前节点及其所有左子节点依次压入栈,直到当前节点为空。
- 如果栈顶节点的右子节点存在且未被访问过,则将当前节点更新为右子节点。
- 否则,弹出栈顶节点,访问该节点的值,并将当前节点置为空。
层序遍历(Level Order Traversal):
按照层级顺序逐层访问二叉树节点的遍历方式。
层序遍历的步骤如下:
- 创建一个空队列(可以使用数组或链表实现)。
- 将根节点入队。
- 当队列不为空时,执行以下步骤:
- 弹出队头节点,并访问该节点的值。
- 如果弹出的节点有左子节点,将左子节点入队。
- 如果弹出的节点有右子节点,将右子节点入队。
层序遍历会按照从上到下、从左到右的顺序逐层访问二叉树的节点。这意味着在同一层级上,左边的节点会在右边的节点之前被访问到。
下面是一个层序遍历的示例:
A
/ \
B C
/ \ /
D E F
层序遍历的结果为:A, B, C, D, E, F。
在实际实现中,可以使用队列来辅助层序遍历。首先将根节点入队,然后每次从队列中取出一个节点,访问该节点的值,并将其左右子节点(如果存在)依次入队。重复这个过程,直到队列为空,即可完成层序遍历。
层序遍历常用于广度优先搜索(BFS)等算法中,可以用于按层级遍历或搜索树的结构。
四、二叉树的存储结构
1.顺序存储结构
二叉树的顺序存储结构是一种使用数组来表示二叉树的方法。在顺序存储结构中,树的节点按照从上到下、从左到右的顺序依次存储在数组中,通过数组的索引来表示节点之间的关系。
假设二叉树的顺序存储结构使用一个一维数组 arr
来表示,其中根节点存储在索引为 0 的位置,对于任意节点存储在索引 i
的位置上,它的左子节点存储在索引 2i+1
的位置上,右子节点存储在索引 2i+2
的位置上。
下图是一个示例二叉树的顺序存储结构表示:
A
/ \
B C
/ \ / \
D E F G
对应的顺序存储结构数组为:['A', 'B', 'C', 'D', 'E', 'F', 'G']
。
在顺序存储结构中,可以通过数组的索引计算出节点之间的关系,例如,索引为 i
的节点的父节点索引为 (i-1)//2
,左子节点索引为 2i+1
,右子节点索引为 2i+2
。
顺序存储结构的优点是简单、易于实现和访问,不需要额外的指针和动态内存分配。然而,它对于非完全二叉树会浪费一定的空间,因为可能存在一些数组元素未被使用。
需要注意的是,顺序存储结构适用于静态的二叉树,即在构建树后不会改变其结构。如果需要频繁地进行插入、删除节点操作,顺序存储结构可能不是一个高效的选择,因为需要频繁地调整数组大小和移动元素位置。在这种情况下,链式存储结构(使用节点对象和引用)更为常用和灵活。
下面给出二叉树顺序存储结构的定义:
#define MaxSize 100 //假设二叉树的最大编号
typedef char DataType; //定义二叉树的数据类型,假设为char
typedef struct
{
DataType data[MaxSize];
int biTreeNum; //结点个数
} SeqBiTree;
2.二叉链表
2.1 二叉链表的存储结构定义
二叉链表是一种使用节点对象和引用来表示二叉树的存储结构。在二叉链表中,每个节点包含一个数据元素和两个指针,分别指向其左子节点和右子节点(如果存在)。
下面给出二叉链表的存储结构定义:
typedef char DataType;
typedef struct BiNode
{
DataType data;
struct BiNode \* lchild, \* rchild;
} BiNode;
2.2 二叉链表的实现
遍历操作是用递归写的,非递归的版本将放在七、拓展与提高当中
2.2.1 前序遍历
由二叉树前序遍历的操作定义,容易写出前序遍历的递归算法,代码如下:
void PreOrder(BiNode\* root)
{
if (root == NULL) return; //递归结束条件
printf("%c", root->data); //访问根节点的数据域
PreOrder(root->lchild); //前序递归遍历root左子树
PreOrder(root->rchild); //前序递归遍历root右子树
}
2.2.2 中序遍历
由二叉树中序遍历的操作定义,容易写出中序遍历的递归算法,代码如下:
void InOrder(BiNode\* root)
{
if (root == NULL) return; //递归结束条件
InOrder(root->lchild); //中序递归遍历root左子树
printf("%c", root->data); //访问根节点的数据域
InOrder(root->rchild); //中序递归遍历root右子树
}
2.2.3 后序遍历
由二叉树后序遍历的操作定义,容易写出后序遍历的递归算法,代码如下:
void PostOrder(BiNode\* root)
{
if (root == NULL) return; //递归结束条件
PostOrder(root->lchild); //后序递归遍历root左子树
PostOrder(root->rchild); //后序递归遍历root右子树
printf("%c", root->data); //访问根节点的数据域
}
2.2.4 层序遍历
在进行层序遍历时,访问某一层的结点后,在对各个节点的左孩子和右孩子顺序访问,这样一层一层进行,先访问的节点其左右孩子也要先访问,这符合队列的操作特性。因此,在进行层序遍历时,设置一个队列存放已访问的结点,代码如下:
void LevelOrder(BiNode\* root)
{
BiNode\* q = NULL, \* Q[QMaxSize]; //顺序队列
int front, rear; //队头队尾
front = rear = -1; //初始化为-1
if (root == NULL) return; //如果二叉树为空,函数结束
Q[++rear] = root; //非空根节点入队
while (front != rear)
{
q = Q[++front];
printf("%c", q->data);
if (q->lchild != NULL) Q[++rear] = q->lchild;
if (q->rchild != NULL) Q[++rear] = q->rchild;
}
}
2.2.5 二叉树的创建
先理清楚一个概念:
拓展二叉树: 将二叉树中每个节点的空指针指向一个虚结点,其值为一个特定值以表示其为空,把这样处理后的二叉树原二叉树的拓展二叉树。
我们知道,对于任意一组序列,无论是前序,中序,后序,还是层序,我们无论知道哪一个序列,大多数情况下都无法直接构造出正确的二叉树。
但我们构造时,使用拓展二叉树,就一定可以得到唯一确定的一棵二叉树。
这里以拓展二叉树的前序序列举例,代码如下:
BiNode\* CreatBiTree(BiNode\* root)
{
char ch;
scanf("%c", &ch); //输入结点信息
if (ch == '#') root = NULL; //递归结束
else
{
root = (BiNode\*)malloc(sizeof BiNode); //生成新节点
root->data = ch; //新节点数据域
root->lchild = CreatBiTree(root->lchild); //递归建立左子树
root->rchild = CreatBiTree(root->rchild); //递归建立右子树
}
return root;
}
2.2.6 二叉树的销毁
二叉链表是动态分配内存,二叉链表的结点是在程序运行过程中动态申请的,在二叉树表变量退出作用域前,要释放二叉链表的存储空间。可以对二叉链表进行后序遍历,在访问结点时进行释放,代码如下:
void DestoryBiTree(BiNode\* root)
{
if (root == NULL) return;
DestoryBiTree(root->lchild);
DestoryBiTree(root->rchild);
free(root);
}
3.三叉链表
三叉链表是一种链表数据结构的扩展形式,它允许每个节点同时链接到前一个节点、后一个节点以及一个额外的父节点。这使得三叉链表在某些场景下比传统的单向或双向链表更加灵活和方便。
三叉链表的节点通常由三个指针组成:prev
、next
和parent
。这些指针分别指向前一个节点、后一个节点和父节点。
通过这种结构,三叉链表可以实现以下功能:
- 正向和反向遍历:由于每个节点都有指向前一个和后一个节点的指针,你可以从任意节点开始遍历整个链表,无论是正向还是反向。
- 父节点链接:每个节点还有一个指针指向其父节点。这对于从某个节点向上导航至其父节点非常有用,尤其是在树形结构中。
- 多叉树表示:通过使用三叉链表,可以轻松地将多叉树表示为一个链表结构。每个节点的父节点指针可以指向其树中的父节点,而后续节点的指针可以指向其子节点。
尽管三叉链表提供了更多的灵活性和功能,但它也带来了更多的空间开销和复杂性。在实际应用中,需要根据具体情况权衡使用三叉链表的优势和劣势。
五、森林
1.森林的逻辑结构
在数据结构中,森林是由多个独立的树(树结构)组成的集合。每棵树都是由节点和它们之间的关联关系(通常是父子关系)构成的。
森林可以通过多种方式表示,其中两种常见的方法是:
- 使用多个根节点:每棵树都有一个根节点,根节点之间没有父子关系。这种表示方法适用于树的根节点集合已知的情况,每个根节点代表一棵树。
- 使用指针或链接:每个节点包含指向其子节点的指针或链接。节点之间通过这些指针或链接建立关联关系。通过这种方式,可以在森林中方便地遍历树的节点。
森林数据结构常用于解决一些特定的问题,例如图的连通分量、无向图的生成树等。它提供了一种有效的方式来组织和操作多个独立的树结构。
2.树、森林与二叉树的转换
物理结构上看,数的孩子兄弟表示法和二叉树的二叉链表是相同的,树的孩子兄弟表示法的第一个孩子指针和右兄弟指针分别相当于二叉链表的左孩子指针和右孩子指针。换言之,给定一棵树,可以找到唯一的一棵二叉树与之对应。
2.1 树转换成二叉树
将一棵树转换成二叉树的常见方法是通过改变节点之间的链接关系,以便每个节点最多有两个子节点。这可以通过以下步骤实现:
- 选择树的根节点作为二叉树的根节点。
- 对于树中的每个节点,将其第一个子节点作为其左子节点,并将其右兄弟节点作为其右子节点。如果节点没有右兄弟节点,则将其右子节点设置为null。
- 对于每个节点的右子节点,将其第一个子节点作为其左子节点,并将其右兄弟节点作为其右子节点。如果节点没有右兄弟节点,则将其右子节点设置为null。
- 递归地应用步骤2和步骤3,直到所有节点都被处理。
通过这种转换,原始的树结构被转换成了二叉树结构,其中每个节点最多有左子节点和右子节点。这种转换允许使用二叉树的算法和技巧来处理原始树的问题。
2.2 森林转化成二叉树
将森林转换成二叉树可以通过逐个处理森林中的每棵树,并将每棵树转换为对应的二叉树。
以下是将森林转换为二叉树的步骤:
-
对于森林中的每棵树:
- 选择一棵树的根节点作为二叉树的根节点。
-
对于每棵树的节点:
- 如果节点有左子树(子节点),将其作为二叉树节点的左子节点。
- 如果节点有右兄弟节点,则将右兄弟节点作为二叉树节点的右子节点。
-
对于每棵树的根节点之间的链接关系:
- 选择一棵树的根节点作为二叉树的根节点。
- 将其他树的根节点作为前一棵树根节点的右子节点。
通过这些步骤,可以将森林转换为对应的二叉树。每个树的根节点成为二叉树的根节点,树的节点之间的链接关系转换为二叉树节点的左子节点和右子节点。
需要注意的是,如果森林中的每棵树都只有一个节点,则转换后的二叉树将只有一个节点。
2.3 二叉树转化成树或森林
将二叉树转换成树或森林可以通过修改节点之间的链接关系,以便每个节点可以有多个子节点。这可以通过以下步骤实现:
-
对于给定的二叉树,选择其中一个节点作为根节点。
-
对于每个节点:
- 如果节点有左子节点,将其作为父节点的子节点之一。
- 如果节点有右子节点,将其作为父节点的兄弟节点(即父节点的下一个子节点)。
-
对于二叉树中的每个节点,将其右子节点设置为null,以断开其与右兄弟节点的链接。
通过这些步骤,可以将二叉树转换成一棵树,其中每个节点可以有多个子节点。如果原始的二叉树包含多个独立的二叉树(森林),则可以为每个独立的二叉树重复上述步骤,从而将二叉树转换成森林。
需要注意的是,转换后的树或森林可能会失去原始二叉树中节点之间的某些顺序或关系。
六、 最优二叉树
1.哈夫曼算法
哈夫曼树(Huffman Tree),也称为最优二叉树(Optimal Binary Tree),是一种特殊的二叉树,常用于数据压缩和编码的算法中。它是一种带权路径长度最短的树,其中节点的权值表示其在编码中的频率或概率。
构建哈夫曼树的基本思想是通过合并权值较小的节点来逐步构建树,使得权值较大的节点位于树的较低层,从而实现最优的编码效果。
以下是构建哈夫曼树的步骤:
- 给定一组权值或频率,将每个权值或频率作为一个独立的节点。
- 从这些节点中选择权值最小的两个节点作为叶子节点,创建一个新的父节点,并将这两个节点设置为新节点的左右子节点。
- 将新节点的权值设置为两个子节点的权值之和。
- 将新节点插入到原节点集合中,并删除原来的两个子节点。
- 重复步骤2至4,直到只剩下一个节点,即构建完成的哈夫曼树的根节点。
在构建完成的哈夫曼树中,较频繁出现的字符或数据对应的节点位于较低层,而较不频繁出现的字符或数据对应的节点位于较高层。这样,可以使用较短的编码表示频繁出现的字符或数据,从而实现数据的高效压缩和编码。
哈夫曼树常用于哈夫曼编码算法中,该编码算法将字符或数据映射到哈夫曼树中的叶子节点,并通过叶子节点路径上的0和1来表示编码。编码后的数据可以实现高效的压缩和解压缩操作。
哈夫曼编码
哈夫曼编码(Huffman Coding)是一种变长编码方法,用于将字符或数据进行有效压缩。它基于哈夫曼树的概念,通过给出频率较高的字符较短的编码,从而实现压缩率的提高。
下面是哈夫曼编码的详细步骤:
- 统计字符或数据的频率:遍历待编码的字符或数据流,统计每个字符或数据的出现频率。
- 构建哈夫曼树:根据字符或数据的频率构建哈夫曼树。频率越高的字符或数据对应的节点位于树的较低层,频率越低的字符或数据对应的节点位于较高层。
- 分配编码:从哈夫曼树的根节点开始,遍历每个叶子节点,并分配编码。在遍历过程中,向左走的路径表示编码位0,向右走的路径表示编码位1。每个叶子节点所经过的路径即为其对应字符或数据的编码。
- 生成编码表:将每个字符或数据与其对应的编码存储在编码表中,以便后续的编码和解码操作。
- 进行编码:根据生成的编码表,将待编码的字符或数据流转换为对应的哈夫曼编码。
- 进行解码:使用相同的哈夫曼树和编码表,将哈夫曼编码解析为原始的字符或数据。
通过哈夫曼编码,频率较高的字符或数据被赋予较短的编码,而频率较低的字符或数据被赋予较长的编码。这样,可以实现对原始数据的高效压缩。
需要注意的是,为了确保编码的唯一解析性,哈夫曼编码要求没有任何一个字符或数据的编码是其他字符或数据编码的前缀。这被称为哈夫曼编码的前缀码性质,保证了编码的解码过程是无歧义的。
哈夫曼编码在数据压缩、通信传输和存储等领域有广泛应用,能够有效地减小数据的存储空间和传输带宽。
七、拓展与提高 (让我再学学)
1.二叉树遍历的非递归算法
2.线索二叉树
- 在某些教材或资料中,父节点也被称为"双亲结点"。这是因为在树结构中,每个节点可以有多个子节点,而每个子节点只有一个父节点,因此父节点也可以被称为双亲结点。这个术语的使用可能因教材或上下文而有所不同,但是概念是一致的,指的是连接到当前节点的直接上级节点。 ↩︎
为了做好运维面试路上的助攻手,特整理了上百道 【运维技术栈面试题集锦】 ,让你面试不慌心不跳,高薪offer怀里抱!
这次整理的面试题,小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。
本份面试集锦涵盖了
- 174 道运维工程师面试题
- 128道k8s面试题
- 108道shell脚本面试题
- 200道Linux面试题
- 51道docker面试题
- 35道Jenkis面试题
- 78道MongoDB面试题
- 17道ansible面试题
- 60道dubbo面试题
- 53道kafka面试
- 18道mysql面试题
- 40道nginx面试题
- 77道redis面试题
- 28道zookeeper
总计 1000+ 道面试题, 内容 又全含金量又高
- 174道运维工程师面试题
1、什么是运维?
2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?
3、现在给你三百台服务器,你怎么对他们进行管理?
4、简述raid0 raid1raid5二种工作模式的工作原理及特点
5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?
6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?
7、Tomcat和Resin有什么区别,工作中你怎么选择?
8、什么是中间件?什么是jdk?
9、讲述一下Tomcat8005、8009、8080三个端口的含义?
10、什么叫CDN?
11、什么叫网站灰度发布?
12、简述DNS进行域名解析的过程?
13、RabbitMQ是什么东西?
14、讲一下Keepalived的工作原理?
15、讲述一下LVS三种模式的工作过程?
16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?
17、如何重置mysql root密码?
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
goDB面试题**
- 17道ansible面试题
- 60道dubbo面试题
- 53道kafka面试
- 18道mysql面试题
- 40道nginx面试题
- 77道redis面试题
- 28道zookeeper
总计 1000+ 道面试题, 内容 又全含金量又高
- 174道运维工程师面试题
1、什么是运维?
2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?
3、现在给你三百台服务器,你怎么对他们进行管理?
4、简述raid0 raid1raid5二种工作模式的工作原理及特点
5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?
6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?
7、Tomcat和Resin有什么区别,工作中你怎么选择?
8、什么是中间件?什么是jdk?
9、讲述一下Tomcat8005、8009、8080三个端口的含义?
10、什么叫CDN?
11、什么叫网站灰度发布?
12、简述DNS进行域名解析的过程?
13、RabbitMQ是什么东西?
14、讲一下Keepalived的工作原理?
15、讲述一下LVS三种模式的工作过程?
16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?
17、如何重置mysql root密码?
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!