树(Tree)

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

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

image.png

image.png

高度的概念:从下往上度量,计数起点是 0
深度的概念:从上往下度量,计数起点是 0
层数的概念:从根节点开始度量,计数起点是 1

二叉树(Binary Tree)

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
image.png
编号 2 的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫做满二叉树。
编号 3 的二叉树中,叶子节点都在最底下两层最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
image.png

如何存储一棵二叉树

基于指针或者引用的二叉链式存储法
每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的。
image.png
基于数组的顺序存储法
我们把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 i = 2 的位置,右子节点存储在 2 i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 i = 2 2 = 4 的位置,右子节点存储在 2 i + 1 = 2 2 + 1 = 5 的位置。
image.png
如果是一棵完全二叉树,用数组存储,仅仅浪费了一个下标为 0 的存储位置

image.png
如果不是一棵完全二叉树,用数组存储,会浪费比较多的空间

二叉树的遍历

如何将所有节点都遍历打印出来呢?
经典的方法有三种,前序遍历、中序遍历和后序遍历。
其中,前、中、后序,表示的是节点与它的左右子树节点遍历打印的先后顺序。

  • 前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
  • 中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
  • 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

image.png

  1. void preOrder(Node* root) {
  2. if (root == null) return;
  3. print root // 此处为伪代码,表示打印root节点
  4. preOrder(root->left);
  5. preOrder(root->right);
  6. }
  7. void inOrder(Node* root) {
  8. if (root == null) return;
  9. inOrder(root->left);
  10. print root // 此处为伪代码,表示打印root节点
  11. inOrder(root->right);
  12. }
  13. void postOrder(Node* root) {
  14. if (root == null) return;
  15. postOrder(root->left);
  16. postOrder(root->right);
  17. print root // 此处为伪代码,表示打印root节点
  18. }

二叉搜索树(Binary Search Tree)

二叉搜索树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
image.png

二叉搜索树的查找操作

我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。
image.png

  1. public class BinarySearchTree {
  2. private Node tree;
  3. public Node find(int data) {
  4. Node p = tree;
  5. while (p != null) {
  6. if (data < p.data) p = p.left;
  7. else if (data > p.data) p = p.right;
  8. else return p;
  9. }
  10. return null;
  11. }
  12. public static class Node {
  13. private int data;
  14. private Node left;
  15. private Node right;
  16. public Node(int data) {
  17. this.data = data;
  18. }
  19. }
  20. }

二叉搜索树的插入操作

新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。

如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。

image.png

  1. public void insert(int data) {
  2. if (tree == null) {
  3. tree = new Node(data);
  4. return;
  5. }
  6. Node p = tree;
  7. while (p != null) {
  8. if (data > p.data) {
  9. if (p.right == null) {
  10. p.right = new Node(data);
  11. return;
  12. }
  13. p = p.right;
  14. } else { // data < p.data
  15. if (p.left == null) {
  16. p.left = new Node(data);
  17. return;
  18. }
  19. p = p.left;
  20. }
  21. }
  22. }

二叉搜索树的删除操作

第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。

第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。

第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

image.png

  1. public void delete(int data) {
  2. Node p = tree; // p指向要删除的节点,初始化指向根节点
  3. Node pp = null; // pp记录的是p的父节点
  4. while (p != null && p.data != data) {
  5. pp = p;
  6. if (data > p.data) p = p.right;
  7. else p = p.left;
  8. }
  9. if (p == null) return; // 没有找到
  10. // 要删除的节点有两个子节点
  11. if (p.left != null && p.right != null) { // 查找右子树中最小节点
  12. Node minP = p.right;
  13. Node minPP = p; // minPP表示minP的父节点
  14. while (minP.left != null) {
  15. minPP = minP;
  16. minP = minP.left;
  17. }
  18. p.data = minP.data; // 将minP的数据替换到p中
  19. p = minP; // 下面就变成了删除minP了
  20. pp = minPP;
  21. }
  22. // 删除节点是叶子节点或者仅有一个子节点
  23. Node child; // p的子节点
  24. if (p.left != null) child = p.left;
  25. else if (p.right != null) child = p.right;
  26. else child = null;
  27. if (pp == null) tree = child; // 删除的是根节点
  28. else if (pp.left == p) pp.left = child;
  29. else pp.right = child;
  30. }

二叉搜索树的中序遍历

中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。