网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
i 的结点在二叉树中位置完全相同,则被称为 完全二叉树。
满二叉树一定是完全二叉树,而完全二叉树则不一定是满二叉树。
完全二叉树有如下几个特点:
1)叶子结点只能出现在最下面两层。
2)最下层的叶子结点一定是集中在左边的连续位置;倒数第二层如果有叶子结点,一定集中在右边的连续位置。
3)如果某个结点度为 1,则只有左子树,即 不存在只有右子树 的情况。
4)同样结点数的二叉树,完全二叉树的深度最小。
如下图所示,就不是一棵完全二叉树,因为 5 号结点没有右子树,但是 6 号结点是有左子树的,不满足上述第 2 点。
3、二叉树的性质
接下来我们来看下,二叉树有哪些重要的性质。
1)性质1
【性质1】二叉树的第
i
(
i
≥
1
)
i (i \ge 1)
i(i≥1) 层上至多有
2
i
−
1
2^{i-1}
2i−1 个结点。
既然是至多,就只需要考虑满二叉树的情况,对于满二叉树而言,当前层的结点数是上一层的两倍,第一层的结点数为 1,所以第
i
i
i 的结点数可以通过等比数列公式计算出来,为
2
i
−
1
2^{i-1}
2i−1。
2)性质2
【性质2】深度为
h
h
h 的二叉树至多有
2
h
−
1
2^{h}-1
2h−1 个结点。
对于任意一个深度为
h
h
h 的二叉树,满二叉树的结点数一定是最多的,所以我们可以拿满二叉树进行计算,它的每一层的结点数为
1
1
1、
2
2
2、
4
4
4、
8
8
8、…、
2
h
−
1
2^{h-1}
2h−1。
利用等比数列求和公式,得到总的结点数为:
1
2
4
.
.
.
2
h
−
1
=
2
h
−
1
1 + 2 + 4 + … + 2^{h-1} = 2^h - 1
1+2+4+…+2h−1=2h−1
3)性质3
【性质3】对于任意一棵二叉树
T
T
T,如果叶子结点数为
x
0
x_0
x0,度为 2 的结点数为
x
2
x_2
x2,则
x
0
=
x
2
1
x_0 = x_2 + 1
x0=x2+1
令
x
1
x_1
x1 代表度 为 1 的结点数,总的结点数为
n
n
n,则有:
n
=
x
0
x
1
x
2
n = x_0 + x_1 + x_2
n=x0+x1+x2
任意一个结点到它孩子结点的连线我们称为这棵树的一条边,对于任意一个非空树而言,边数等于结点数减一,令边数为
e
e
e,则有:
e
=
n
−
1
e = n-1
e=n−1
对于度为 1 的结点,可以提供 1 条边,如图中的黄色结点;对于度为 2 的结点,可以提供 2 条边,如图中的红色结点。所以边数又可以通过度为 1 和 2 的结点数计算得出:
e
=
x
1
2
x
2
e = x_1 + 2 x_2
e=x1+2x2 联立上述三个等式,得到:
e
=
n
−
1
=
x
0
x
1
x
2
−
1
=
x
1
2
x
2
e = n-1 = x_0+x_1+x_2 - 1 = x_1 + 2 x_2
e=n−1=x0+x1+x2−1=x1+2x2 化简后,得证:
x
0
=
x
2
1
x_0 = x_2 + 1
x0=x2+1
4)性质4
【性质4】具有
n
n
n 个结点的完全二叉树的深度为
⌊
l
o
g
2
n
⌋
1
\lfloor log_2n \rfloor + 1
⌊log2n⌋+1。
由【性质2】可得,深度为
h
h
h 的二叉树至多有
2
h
−
1
2^{h}-1
2h−1 个结点。所以,假设一棵树的深度为
h
h
h,它的结点数为
n
n
n,则必然满足:
n
≤
2
h
−
1
n \le 2^{h}-1
n≤2h−1 由于是完全二叉树,它一定比深度为
h
−
1
h-1
h−1 的结点数要多,即:
2
h
−
1
−
1
<
n
2^{h-1}-1 \lt n
2h−1−1<n 将上述两个不等式,稍加整理,得到:
2
h
−
1
≤
n
<
2
h
2^{h-1} \le n \lt 2^h
2h−1≤n<2h 然后,对不等式两边取以2为底的对数,得到:
h
−
1
≤
l
o
g
2
n
<
h
h-1 \le log_2n \lt h
h−1≤log2n<h 这里,由于
h
h
h 一定是整数,所以有:
h
=
⌊
l
o
g
2
n
⌋
1
h = \lfloor log_2n \rfloor + 1
h=⌊log2n⌋+1
二、二叉树的存储
1、顺序表存储
二叉树的顺序存储就是指利用数组对二叉树进行存储。结点的存储位置即数组下标,能够体现结点之间的逻辑关系,比如父结点和孩子结点之间的关系,左右兄弟结点之间的关系 等等。
1)完全二叉树
来看一棵完全二叉树,我们对它进行如下存储。
编号代表了数组下标的绝对位置,映射后如下:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
d
a
t
a
data
data |
−
− |
a
a
a |
b
b
b |
c
c
c |
d
d
d |
e
e
e |
f
f
f |
g
g
g |
h
h
h |
i
i
i |
j
j
j |
k
k
k |
l
l
l |
| 这里为了方便,我们把数组下标为 0 的位置给留空了。这样一来,当知道某个结点的下标
x
x
x,就可以知道它左右儿子的下标分别为
2
x
2x
2x 和
2
x
1
2x+1
2x+1;反之,当知道某个结点的下标
x
x
x,也能知道它父结点的下标为
⌊
x
2
⌋
\lfloor \frac x 2 \rfloor
⌊2x⌋。 | | | | | | | | | | | | | |
2)非完全二叉树
对于非完全二叉树,只需要将对应不存在的结点设置为空即可。
编号代表了数组下标的绝对位置,映射后如下:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
d
a
t
a
data
data |
−
− |
a
a
a |
b
b
b |
c
c
c |
d
d
d |
e
e
e |
f
f
f |
g
g
g |
−
− |
−
− |
−
− |
k
k
k |
l
l
l |
3)稀疏二叉树
对于较为稀疏的二叉树,就会有如下情况出现,这时候如果用这种方式进行存储,就比较浪费内存了。
编号代表了数组下标的绝对位置,映射后如下:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
d
a
t
a
data
data |
−
− |
a
a
a |
b
b
b |
c
c
c |
d
d
d |
−
− |
−
− |
g
g
g |
h
h
h |
−
− |
−
− |
−
− |
−
− |
| 于是,我们可以采取链表进行存储。 | | | | | | | | | | | | | |
2、链表存储
二叉树每个结点至多有两个孩子结点,所以对于每个结点,设置一个 数据域 和 两个 指针域 即可,指针域 分别指向 左孩子结点 和 右孩子结点。
typedef struct TreeNode {
DataType data;
struct TreeNode \*left; // (1)
struct TreeNode \*right; // (2)
}TreeNode;
- (
1
)
(1)
(1) left
指向左孩子结点;
- (
2
)
(2)
(2) right
指向右孩子结点;
三、二叉树的遍历
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点访问一次且仅被访问一次。
对于线性表的遍历,要么从头到尾,要么从尾到头,遍历方式较为单纯,但是树不一样,它的每个结点都有可能有两个孩子结点,所以遍历的顺序面临着不同的选择。
二叉树的常用遍历方法有以下四种:前序遍历、中序遍历、后序遍历、层序遍历。
我们用 void visit(TreeNode *root)
这个函数代表访问某个结点,这里为了简化问题,访问结点的过程就是打印对应数据域的过程。如下代码所示:
void visit(TreeNode \*root) {
printf("%c", root->data);
}
1、 前序遍历
1)算法描述
【前序遍历】如果二叉树为空,则直接返回。否则,先访问根结点,再递归前序遍历左子树,再递归前序遍历右子树。
前序遍历的结果如下:a
b
d
g
h
c
e
f
i
abdghcefi
abdghcefi。
2)源码详解
void preorder(TreeNode \*root) {
if(root == NULL) {
return ; // (1)
}
visit(root); // (2)
preorder(root->left); // (3)
preorder(root->right); // (4)
}
- (
1
)
(1)
(1) 待访问结点为空时,直接返回;
- (
2
)
(2)
(2) 先访问当前树的根;
- (
3
)
(3)
(3) 再前序遍历左子树;
- (
4
)
(4)
(4) 最后前序遍历右子树;
2、 中序遍历
1)算法描述
【中序遍历】如果二叉树为空,则直接返回。否则,先递归中序遍历左子树,再访问根结点,再递归中序遍历右子树。
中序遍历的结果如下:g
d
h
b
a
e
c
i
f
gdhbaecif
gdhbaecif。
2)源码详解
void inorder(TreeNode \*root) {
if(root == NULL) {
return ; // (1)
}
inorder(root->left); // (2)


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
s-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6Iux6ZuE5ZOq6YeM5Ye65p2l,size_13,color_FFFFFF,t_70,g_se,x_16#pic_center)
> 前序遍历的结果如下:
>
>
>
>
> a
>
>
> b
>
>
> d
>
>
> g
>
>
> h
>
>
> c
>
>
> e
>
>
> f
>
>
> i
>
>
>
> abdghcefi
>
>
> abdghcefi。
>
>
>
#### 2)源码详解
void preorder(TreeNode *root) {
if(root == NULL) {
return ; // (1)
}
visit(root); // (2)
preorder(root->left); // (3)
preorder(root->right); // (4)
}
* (
1
)
(1)
(1) 待访问结点为空时,直接返回;
* (
2
)
(2)
(2) 先访问当前树的根;
* (
3
)
(3)
(3) 再前序遍历左子树;
* (
4
)
(4)
(4) 最后前序遍历右子树;
### 2、 中序遍历
#### 1)算法描述
>
> 【中序遍历】如果二叉树为空,则直接返回。否则,先递归中序遍历左子树,再访问根结点,再递归中序遍历右子树。
> 
> 中序遍历的结果如下:
>
>
>
>
> g
>
>
> d
>
>
> h
>
>
> b
>
>
> a
>
>
> e
>
>
> c
>
>
> i
>
>
> f
>
>
>
> gdhbaecif
>
>
> gdhbaecif。
>
>
>
#### 2)源码详解
void inorder(TreeNode *root) {
if(root == NULL) {
return ; // (1)
}
inorder(root->left); // (2)
[外链图片转存中…(img-VntNvzTJ-1715693338393)]
[外链图片转存中…(img-LnoWiqKU-1715693338394)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!