什么是树
树这种结构很像我们生活中的树,这里的每个元素叫做节点,用线连接的节点,我们叫做父子关系
比如下面这个图,A节点就是B节点的父节点,B节点是A节点的子节点,BCD三个节点的父节点是同一个父节点,他们互相称为兄弟节点,我们把没有父节点的称为根节点,比如E,我们把没有子节点的叫做叶子节点或叶节点比如图中的G H I J K L都是叶子节点
树还有几个相似的概念,高度,深度,层,他们的定义是这样的
节点的高度=节点到叶子节点的最长路径(边数)
节点的深度=根节点到这个节点经历边的个数
节点的层数=节点的深度+1
树的高度=根节点的高度
例如:
高度一般是从下向上计算,深度是从上到下计算,层数跟深度的计算相似,不过计数起点为1
二叉树
二叉树顾名思义,每个节点最多有俩个叉,分别是左子节点和右子节点,二叉树不要求每个节点都有俩个叉,可以只有一个左子节点,也可以只有一个右子节点
上面这张图中有来个节点比较特殊,分别是2和3号
编号2的二叉树,叶子节点都在最底层,除了叶子节点之外,其他节点都有俩个叉,这种二叉树叫做满二叉树
编号3的二叉树,叶子节点在最下俩层,左后一层的叶子节点都靠左排列,并且除了最后一层,其它层节点个数都要达到最大,这种叫做完全二叉树
如何储存二叉树
储存二叉树有俩种方法,一是基于指针和引用的二叉链式储存法,一种是基于数组的顺序储存法
1 二叉链式储存法
这个比较简单,我们从图中可以看出,每个节点除了储存数据,还储存了左右子节点的指针,我们只要拿到根节点,就可以把整个树串起来
顺序储存法
我们把根节点储存在下标i=1的位置,那么左子节点储存在下标2 * i = 2的位置,右子节点储存在下标2 * i + 1 = 3的位置,由此类推,B的左子节点储存在2 * i = 2 * 2 = 4的位置,B的右子节点储存在2 * i + 1 = 2 * 2 + 1 = 5的位置
总结一下 ,如果X储存在i位置,那么左子节点储存在i2的位置,右子节点储存在2i+1的位置,反过来i/2就是他父节点的位置,这种情况我们只要知道根节点的位置(一般根节点储存在下标1的位置,方便计算),就可以串起整个树
上方的例子是一个完全二叉树,所以只浪费了一个0的位置,不过如果不是完全二叉树,就会浪费比较多的空间
所以完全二叉树,做好的储存方式就是数组,因为数组不需要储存左右子节点的引用,这也是为什么完全二叉树会单独提出来,也是为什么完全二叉树最后一层叶子节点要靠左边的原因
二叉树的遍历
二叉树有三种遍历方式,前序遍历,中序遍历,后续遍历
前序遍历:对于树中任意节点来说,先打印这个节点,然后打印他的左子节点,最后打印他的右子节点
中序遍历:对于树中任一节点来说,先打印他的左子节点,然后打印他本身,最后打印他的右子节点
后序遍历:对于树中的任意节点来说,先打印他的左子节点,然后打印他的右子节点,最后打印它本身
实际上,二叉树的前中后遍历就是一个递归的过程,写递归代码的关键在于,是否可以写出递归公式
前序遍历的递推公式:
preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)
中序遍历的递推公式:
inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)
后序遍历的递推公式:
postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r
翻译成代码
void preOrder(Node* root) {
if (root == null) return;
print root // 此处为伪代码,表示打印 root 节点
preOrder(root->left);
preOrder(root->right);
}
void inOrder(Node* root) {
if (root == null) return;
inOrder(root->left);
print root // 此处为伪代码,表示打印 root 节点
inOrder(root->right);
}
void postOrder(Node* root) {
if (root == null) return;
postOrder(root->left);
postOrder(root->right);
print root // 此处为伪代码,表示打印 root 节点
}
二叉树遍历的时间复杂度
从上边的图中可以看到,每个节点被访问了俩次,所有时间复杂度伪O(n)