树(Tree)

这里面每个元素我们叫做“节点”;用来连接相邻节点之间的关系,我们叫做“父子关系”。

A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点。B、C、D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫做根节点,也就是图中的节点 E。我们把没有子节点的节点叫做叶子节点或者叶节点,比如图中的 G、H、I、J、K、L 都是叶子节点。


高度的概念:从下往上度量,计数起点是 0
深度的概念:从上往下度量,计数起点是 0
层数的概念:从根节点开始度量,计数起点是 1
二叉树(Binary Tree)
二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
编号 2 的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫做满二叉树。
编号 3 的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
如何存储一棵二叉树
基于指针或者引用的二叉链式存储法
每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的。
基于数组的顺序存储法
我们把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 i = 2 的位置,右子节点存储在 2 i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 i = 2 2 = 4 的位置,右子节点存储在 2 i + 1 = 2 2 + 1 = 5 的位置。
如果是一棵完全二叉树,用数组存储,仅仅浪费了一个下标为 0 的存储位置

如果不是一棵完全二叉树,用数组存储,会浪费比较多的空间
二叉树的遍历
如何将所有节点都遍历打印出来呢?
经典的方法有三种,前序遍历、中序遍历和后序遍历。
其中,前、中、后序,表示的是节点与它的左右子树节点遍历打印的先后顺序。
- 前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
- 中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
- 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

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节点}
二叉搜索树(Binary Search Tree)
二叉搜索树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
二叉搜索树的查找操作
我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。
public class BinarySearchTree {private Node tree;public Node find(int data) {Node p = tree;while (p != null) {if (data < p.data) p = p.left;else if (data > p.data) p = p.right;else return p;}return null;}public static class Node {private int data;private Node left;private Node right;public Node(int data) {this.data = data;}}}
二叉搜索树的插入操作
新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。
如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。

public void insert(int data) {if (tree == null) {tree = new Node(data);return;}Node p = tree;while (p != null) {if (data > p.data) {if (p.right == null) {p.right = new Node(data);return;}p = p.right;} else { // data < p.dataif (p.left == null) {p.left = new Node(data);return;}p = p.left;}}}
二叉搜索树的删除操作
第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。
第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。
第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

public void delete(int data) {Node p = tree; // p指向要删除的节点,初始化指向根节点Node pp = null; // pp记录的是p的父节点while (p != null && p.data != data) {pp = p;if (data > p.data) p = p.right;else p = p.left;}if (p == null) return; // 没有找到// 要删除的节点有两个子节点if (p.left != null && p.right != null) { // 查找右子树中最小节点Node minP = p.right;Node minPP = p; // minPP表示minP的父节点while (minP.left != null) {minPP = minP;minP = minP.left;}p.data = minP.data; // 将minP的数据替换到p中p = minP; // 下面就变成了删除minP了pp = minPP;}// 删除节点是叶子节点或者仅有一个子节点Node child; // p的子节点if (p.left != null) child = p.left;else if (p.right != null) child = p.right;else child = null;if (pp == null) tree = child; // 删除的是根节点else if (pp.left == p) pp.left = child;else pp.right = child;}
二叉搜索树的中序遍历
中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。
