二叉树入门
树的基本定义
树是我们计算机非常重要的一种数据结构,同时使用树这种结构,可以描述现世生活中的很多事物。
树是由$n(n>=1)$个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是跟朝上,而叶朝下的。
树具有以下特点:
- 每个结点有零个或多个子节点;
- 没有父结点的结点为根结点;
- 每一个非根结点只有一个父结点;
- 每个结点及其后代结点整体上可以看做是一棵树,称为当前结点的父结点的一个子树;
树的相关术语
结点的度:
一个结点含有的子树的个数称为该结点的度;
叶结点:
度为0的结点称为叶节点,也可以叫做终端结点;
分支结点:
度不为0的结点称为分支结点,也可以叫做非终端结点;
结点的层次:
从根结点开始,根结点的层次为1,根的直接后继层次为2,以此类推;
结点的层序编号:
将树中的结点,按照从上层到下层,同层从左往右的次序排列成一个线性序列,把他们编成连续的自然数;
树的度:
树中所有结点的度的最大值;
树的深度(高度):
树中结点的最大层次;
森林:
$m(m>=0)$个互不相交的树的集合,将一棵非空树的根结点删去,树就变成了一个森林;给森林增加一个统一的根结点,森林就会变成一棵树;
孩子结点:
一个结点的直接后继结点称为该结点的孩子结点;
双亲结点(父结点):
一个结点的直接前驱称为该结点的双亲结点;
兄弟结点:
同一双亲结点的孩子结点间互称为兄弟结点;
二叉树的基本定义
二叉树就是度不超过2的树(每个结点最多有两个子结点)
满二叉树:
一个二叉树,如果每一层的结点树都到达最大值,则这个二叉树就是满二叉树。
完全二叉树:
叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
二叉查找树的创建
结点类的API设计:
类名 | Node\ |
---|---|
构造方法 | Node(Key key, Value value, Node left, Node right) |
成员变量 | public Node left |
public Node right | |
public Key key | |
public Value value |
结点类的代码实现:
1 | private class Node<Key,Value>{ |
二叉查找树的API设计:
类名 | BinaryTree\ |
---|---|
构造方法 | BinaryTree() |
成员变量 | private Node root |
private int N | |
成员方法 | public void put(Key key, Value value) |
public Node put(Node x, Key key, Value val) | |
public Value get(Key key) | |
private Value get(Node x, Key key) | |
public void delete(Key key) | |
private Node delete(Node x, Key key) | |
public int size() |
二叉查找树的代码实现:
- 插入方法put实现思想:
- 如果当前树中没有任何一个结点,则直接把新结点当做根结点使用
- 如果当前树不为空,则从根结点开始:
- 如果新结点的key小于当前结点的key,则继续找当前结点的左子结点;
- 如果新结点的key大于当前结点的key,则继续找当前结点的右子结点;
- 如果新结点的key等于当前结点的key,则树中已经存在这样的结点,替换该结点的value值即可。
- 查询方法get实现思想:
- 从根结点开始:
- 如果要查询的key小于当前结点的key,则继续查找当前结点的左子结点;
- 如果要查询的key大于当前结点的key,则继续查找当前结点的右子结点;
- 如果要查询的key等于当前结点的key,则树中返回当前结点的value;
- 从根结点开始:
- 删除方法delete实现思想:
- 找到被删除结点;
- 找到被删除结点右子树中的最小结点minNode;
- 删除右子树中的最小结点;
- 让被删除结点的左子树称为最小结点minNode的左子树,让被删除结点的右子树称为最小结点minNode的右子树;
- 让被删除结点的父结点指向最小结点minNode。
1 | public class BinaryTree<Key extends Comparable<Key>, Value>{ |
二叉查找树其他便捷方法
查找二叉树中最小的键:
public Key min() | 找出树中最小的键 |
---|---|
private Node min(Node x) | 找出指定树x中,最小键所在的结点 |
1 | public Key min(){ |
查找二叉树中最大的键:
Public Key max() | 找出树中最大的键 |
---|---|
public Node max(Node x) | 找出指定树x中,最大键所在的结点 |
1 | public Key max(){ |
二叉树的基础遍历
很多情况下,我们可能需要像遍历数组一样,遍历树,从而拿出树中存储的每一个元素,由于树状结构和线性结构不一样,它没有办法从头开始依次向后遍历,所以存在如何遍历,也就是按照什么样的搜索路径进行遍历的问题。
二叉树的遍历分为以下三种方式:
- 前序遍历:先访问根结点,然后再访问左子树,最后访问右子树
- 中序遍历:先访问左子树,中间访问根结点,最后访问右子树
- 后序遍历:先访问左子树,在访问右子树,最后访问根结点
前序遍历
public Queue\ |
使用前序遍历,获取整个树中的所有键 |
---|---|
private void preErgodic(Node x, Queue\ |
使用前序遍历,获取指定树x中的所有键 |
实现步骤:
- 把当前结点的key放入到队列中;
- 找到当前结点的左子树,如果不为空,递归遍历左子树;
- 找到当前结点的右子树,如果不为空,递归调用遍历右子树。
实现代码:
1 | public Queue<Key> preErgodic(){ |
1 | public class BinaryTreeTest{ |
中序遍历
public Queue\ |
使用中序遍历,获取整个树的所有键 |
---|---|
private void midErgodic(Node x, Queue\ |
使用中序遍历,获取指定树x中的所有键 |
实现步骤:
- 找到当前结点的左子树,如果不为空,递归遍历左子树;
- 把当前结点的key放入到队列中;
- 找到当前结点的右子树,如果不为空,递归遍历右子树。
代码实现:
1 | public Queue<Key> midErgodic(){ |
后序遍历
public Queue\ |
使用后序遍历,获取整个树中的所有键 |
---|---|
private void afterErgodic(Node x, Queue\ |
使用后序遍历,获取指定树x的所有键 |
实现步骤:
- 找到当前结点的左子树,如果不为空,递归遍历左子树;
- 找到当前结点的右子树,如果不为空,递归遍历右子树;
- 把当前结点的key放入到队列中。
实现代码:
1 | public Queue<Key> afterErgodic(){ |
二叉树的层序遍历
所谓的层序遍历,就是从根结点开始,依次向下,获取每一层所有结点的值。
层序遍历的API:
public Queue\ |
使用层序遍历,获取整个树中的所有键 |
---|---|
实现步骤:
- 创建队列,存储每一层的结点;
- 使用循环从队列中弹出一个结点:
- 获取当前结点的key;
- 如果当前结点的左子结点不为空,则把左子结点放入到队列中;
- 如果当前结点的右子结点不为空,则把右子结点放入到队列中;
实现代码:
1 | public Queue<Key> layerErgodic(){ |
二叉树的最大深度问题
需求:
给定一棵树,请计算出树的最大深度。
最大深度的API:
public int maxDepth() | 计算整个树的最大深度 |
---|---|
private int maxDepth(Node x) | 计算指定树x的最大深度 |
实现步骤:
- 如果根结点为空,则最大深度为0;
- 计算左子树的最大深度;
- 计算右子树的最大深度;
- 当前树的最大深度=左子树的最大深度和右子树的最大深度的较大者 + 1
代码实现:
1 | public int maxDepth(){ |
折纸问题
需求:
把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折一次,压出折痕后展开。此时,折痕是凹下去的,即折痕突起的方向指向纸条的背面,如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。
给定一个输入参数N,代表纸条从下边向上方对折N次,请从上到下打印所有折痕的方向;例如:N=1,打印:down;N=2,打印:donw,donw,up。
分析:
我们把对折后的纸张翻过来,这时把第一次对折产生的折痕看做是根结点,那第二次对折产生的下折痕就是该结点的左子结点,而第二次对折的上折痕就是该结点的右子结点,这样我们就可以使用树型数据结构来描述对折后产生的折痕。
这棵树有这样的特点:
- 根结点为下折痕;
- 每一个结点的左子结点为下折痕;
- 每一个结点的右子结点为上折痕;
构建深度为N的折痕树:
- 第一次对折,只有一条折痕,创建根结点;
- 如果不是第一次对折,则使用队列保存根结点;
- 循环遍历队列:
- 从队列中拿出一个结点
- 如果这个结点的左子结点不为空,则把这个左子结点添加到队列中
- 如果这个结点的右子结点不为空,则把这个右子结点添加到队列中;
- 判断当前结点的左子结点和右子结点都不为空,如果是,则需要为当前结点创建一个值为down的左子结点,一个值为up的右子结点。
代码的实现:
1 | public class PagerFoldingTest{ |