二分搜索树

本篇文章,主要讲解二分搜索树结构,关于二分搜索树的理论概念,就不在复述因为网上有专业的理论:维基百科:二分查找树,需要熟悉链表的知识:数据结构中的表.
一个二叉树需要每个节点都会存储左右子树的节点和节点的值. 同时左子树一定会小于右子树.新建一个BST类来创建二叉树结构,泛型要继承Comparable 方便进行比较运算.具体代码如下:

  1. public class BST<T extends Comparable<T>>{
  2. //节点类
  3. public static class Node<T> {
  4. public T value;
  5. public Node<T> left;
  6. public Node<T> right;
  7. public Node(T value) {
  8. this.value = value;
  9. this.left = null;
  10. this.right = null;
  11. }
  12. }
  13. //根节点
  14. private Node<T> root;
  15. //大小
  16. private int size;
  17. public BST() {
  18. root = null;
  19. size = 0;
  20. }
  21. public boolean isEmpty() {
  22. return size == 0;
  23. }
  24. public int size() {
  25. return size;
  26. }
  27. }

常用操作之一:添加元素

如何添加元素呢? 已知我们需要比较添加的元素,如果小于则添加到左子树,如果大于则添加到右子树.但是我们并不知道二叉树中有多少个子树,所以我们需要遍历所有的子树来进行判断,最好的办法就是使用递归来实现. 我们需要将添加到元素与每个子树的元素进行比较,如果大于则比较右子树如果小于则比较左子树,如果遍历到某个节点为空,则new 节点,将该元素添加到节点中.

注意:写递归要先写终止条件

  1. public void add(T value){
  2. if(value == null) return;
  3. //如果根节点为空 则创建根节点
  4. if(root == null){
  5. root = new Node(value);
  6. size++;
  7. }else{
  8. add(root,value);
  9. }
  10. }
  11. private void add(Node<T> node,T value){
  12. //递归的终止条件 添加的元素重复
  13. if(value.equals(node.value)) return;
  14. //如果元素小于节点的值 且节点的左子树为空,则将该元素添加到左子树中
  15. else if (value.compareTo(node.value) < 0 && node.left == null) {
  16. node.left = new Node<>(value);
  17. size++;
  18. return;
  19. }
  20. //如果元素大于的节点的值,且节点的右子树为空,则将该元素添加到右子树中
  21. else if (value.compareTo(node.value) > 0 && node.right == null) {
  22. node.right = new Node<>(value);
  23. size++;
  24. return;
  25. }
  26. //递归调用
  27. if(value.compareTo(node.value)<0){//递归左子树
  28. add(node.left, value);
  29. }else {//添加的元素大于节点 递归右子树
  30. add(node.right, value);
  31. }
  32. }

思考:上述代码是否还可以优化.上述代码 if else太多了,我们可以进行优化,直接判断节点为空则new 节点.优化代码如下:

  1. public void add(T value) {
  2. root = add(root, value);
  3. }
  4. private Node<T> add(Node<T> node, T value) {
  5. if (node == null) {
  6. size++;
  7. return new Node<>(value);
  8. }
  9. if (value.compareTo(node.value) < 0) {
  10. node.left = add(node.left, value);
  11. } else if (value.compareTo(node.value) > 0) {
  12. node.right = add(node.right, value);
  13. }
  14. return node;
  15. }

测试添加元素的方法是否成立,覆写toString来打印出一个二叉树

  1. @Override
  2. public String toString() {
  3. StringBuilder res = new StringBuilder();
  4. generateBSTString(root, 0, res);
  5. return res.toString();
  6. }
  7. //生成以root 为跟节点,深入为depth的描述二叉树的字符串
  8. private void generateBSTString(Node<T> root, int depth, StringBuilder res) {
  9. if (root == null) {
  10. res.append(generateDepthString(depth) + "null\n");
  11. return;
  12. }
  13. res.append(generateDepthString(depth) + root.value + "\n");
  14. generateBSTString(root.left, depth + 1, res);
  15. generateBSTString(root.right, depth + 1, res);
  16. }
  17. private String generateDepthString(int depth) {
  18. StringBuilder res = new StringBuilder();
  19. for (int i = 0; i < depth; i++) {
  20. res.append("--");
  21. }
  22. return res.toString();
  23. }

测试用例如下:

  1. BST<Integer> bst = new BST<>();
  2. int[] nums = {5, 3, 6, 8, 4, 2};
  3. for (int num : nums) {
  4. bst.add(num);
  5. }
  6. System.out.println(bst);

打印结果如下:

  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8
  6. 5
  7. --3
  8. ----2
  9. ------null
  10. ------null
  11. ----4
  12. ------null
  13. ------null
  14. --6
  15. ----null
  16. ----8
  17. ------null
  18. ------null

上述打印结构很直观的返回给我们,添加元素的方法成立的. 我们还可以更直观的来输出二叉树那就是二叉树的遍历.

前序遍历

什么是二叉树的前序遍历? 我通过下面的图来演示

  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8
  6. 我们可以把每个节点看成三个部分:值 左子树 右子树. 前序遍历的允许就是遍历每个节点的:值 -> 左子树 -> 右子树
  7. 我们看上图的二叉树:先遍历值5输出,然后遍历左子树.左子树节点先遍历值3,然后遍历左子树.左子树线遍历值2,然后遍历左子树,左子树为空,然后遍历右子树,右子树为空然后遍历节点3的右子树.右子树节点4,先遍历值4输出,然后遍历左子树为空,遍历右子树为空.这时候节点3已经遍历完毕.然后遍历节点5的右子树.节点6先遍历值6输出,然后遍历左子树为空,遍历右子树8节点,节点6遍历完成,先遍历值8输出,左子树和右子树为空.到此整个二叉树前序遍历完成.
  8. 遍历的结果:5 3 2 4 6 8

如何代码实现前序遍历呢? 其实很简单我们可以通过递归就可以实现.先输出值然后递归左子树,然后递归右子树.代码如下,递归可以更能让我们理解思路.

  1. //前序遍历
  2. public void preOrder() {
  3. preOrder(root);
  4. }
  5. //前序遍历递归实现
  6. private void preOrder(Node<T> node) {
  7. if (node == null)
  8. return;
  9. System.out.println("node = [" + node.value + "]");
  10. preOrder(node.left);//遍历左子树 不为空就会一直遍历
  11. preOrder(node.right);//遍历右子树 不为空就会一直遍历
  12. }

进阶: 如何非递归的实现前序遍历.
我们都知道栈结构,把根节点放入栈中,while循环所有节点.先取出根节点,然后压入右子树和左子树.栈是先入后出结构.这样我们就会先取出左子树节点,然后压入左子树节点的右子树. 这样我们就能然着前序遍历的顺序取出:值 -> 左子树 -> 右子树.
这样的实现就是,深度优先遍历就是栈结构.
来看下图:

  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8
  6. 初始化栈:先将节点5压入栈中.
  7. | |
  8. | |
  9. | |
  10. |5 |
  11. |__|
  12. 取出节点5,压入右子树和左子树:
  13. | | 5
  14. | |
  15. |3 |
  16. |6 |
  17. |__|
  18. 取出节点3,压入右子树和左子树:
  19. | | 5 3
  20. |2 |
  21. |4 |
  22. |6 |
  23. |__|
  24. 取出节点2,左子树和右子树为空不用压入栈:
  25. | | 5 3 2
  26. | |
  27. |4 |
  28. |6 |
  29. |__|
  30. 取出节点4,左子树和右子树为空不用压入栈:
  31. | | 5 3 2 4
  32. | |
  33. | |
  34. |6 |
  35. |__|
  36. 取出节点6,左子树为空不用压入栈 右子树不为空压入栈:
  37. | | 5 3 2 4 6
  38. | |
  39. | |
  40. |8 |
  41. |__|
  42. 取出节点8,左子树和右子树为空,最终栈中没有数据,整个树遍历完成:
  43. | | 5 3 2 4 6 8
  44. | |
  45. | |
  46. | |
  47. |__|

代码实现如下:
代码的逻辑很简单,取出栈顶元素 输出,先压入右子树,再压入左子树

  1. //前序遍历非递归实现 通过栈结构深度优先遍历
  2. public void preOrderNR() {
  3. if (root != null) {
  4. //新建一个栈
  5. Stack<Node<T>> stack = new Stack<>();
  6. stack.push(root);
  7. //遍历栈 直到栈元素为空为止
  8. while (!stack.empty()) {
  9. //取出栈顶元素 然后判断该元素是否有左子树或者右子树
  10. Node<T> cur = stack.pop();
  11. System.out.println(cur.value);
  12. //先压入右子树
  13. if (cur.right != null) {
  14. stack.push(cur.right);
  15. }
  16. //再压入左子树
  17. if (cur.left != null) {
  18. stack.push(cur.left);
  19. }
  20. }
  21. }
  22. }

进阶: 既然栈结构能够显示二叉树的深度优先遍历,那么和栈结构差不多的队列结构能否实现呢? 答案是肯定的. 那就是广度优先遍历.只不过广度优先遍历不是二叉树的前序遍历.

先回忆一下队列的结构:先入先出. 继续通过画图来思考,

  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8
  6. 初始化队列:先将节点5添加到队列中.
  7. | 5|
  8. | |
  9. | |
  10. | |
  11. | |
  12. | 3| 5
  13. | 6|
  14. | |
  15. | |
  16. | |
  17. | 6| 5 3
  18. | 2|
  19. | 4|
  20. | |
  21. | |
  22. | 2| 5 3 6
  23. | 4|
  24. | 8|
  25. | |
  26. | |
  27. | 4| 5 3 6 2
  28. | 8|
  29. | |
  30. | |
  31. | |
  32. | 8| 5 3 6 2 4
  33. | |
  34. | |
  35. | |
  36. | |
  37. | | 5 3 6 2 4 8
  38. | |
  39. | |
  40. | |
  41. | |

代码实现如下:

  1. //广度优先遍历又叫做层序遍历
  2. public void lastOrder() {
  3. Queue<Node<T>> q = new LinkedList<>();
  4. q.add(root);
  5. while (!q.isEmpty()) {
  6. Node<T> cur = q.remove();
  7. System.out.println(cur.value);
  8. if (cur.left != null) {
  9. q.add(cur.left);
  10. }
  11. if (cur.right != null) {
  12. q.add(cur.right);
  13. }
  14. }
  15. }

中序遍历

中序遍历顺序:左子树 -> 值 -> 右子树.实现很简单这里就不再详细讲解了,只要读懂了前序遍历其他遍历的实现都是一样的原理.
代码如下:

  1. //中序遍历 二分搜索树排序的结果 是顺序排列的
  2. public void inOrder() {
  3. inOrder(root);
  4. }
  5. private void inOrder(Node<T> node) {
  6. if (node == null)
  7. return;
  8. inOrder(node.left);
  9. System.out.println("node = [" + node.value + "]");
  10. inOrder(node.right);
  11. }
  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8
  6. 中序遍历:2 3 4 5 6 8

后序遍历

后序遍历:左子树 -> 右子树 -> 值

  1. //后序遍历
  2. public void postOrder() {
  3. postOrder(root);
  4. }
  5. private void postOrder(Node<T> node) {
  6. if (node == null)
  7. return;
  8. postOrder(node.left);
  9. postOrder(node.right);
  10. //遍历完左右节点才会进行操作
  11. System.out.println("node = [" + node.value + "]");
  12. }
  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8
  6. 后序遍历:2 4 3 8 6 5

查找最小节点和最大节点

查找最小节点和最大节点很简单,最小节点就是一直遍历左子树,最大节点就是一直遍历右子树.

  1. //查找最小节点 一直查找左子树
  2. public T minimum() {
  3. if (size == 0)
  4. throw new IllegalArgumentException("BST is empty");
  5. return minimum(root).value;
  6. }
  7. private Node<T> minimum(Node<T> node) {
  8. if (node.left == null)
  9. return node;
  10. return minimum(node.left);
  11. }
  12. //查找二分搜索树最大节点 一直查找右子树
  13. public T maximum() {
  14. if (size == 0) {
  15. throw new IllegalArgumentException("BST is empty");
  16. }
  17. return maximum(root).value;
  18. }
  19. private Node<T> maximum(Node<T> node) {
  20. if (node.right == null) {
  21. return node;
  22. }
  23. return maximum(node.right);
  24. }

删除最小节点和最大节点

删除最小节点:要删除最小节点,我们需要找到最小节点,上述我们已经实现了查找最小节点的方法minimum.然后我们需要保存最小节点的右子树,然后将右子树赋给最小节点的父节点的左子树节点.

  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8

代码如下:

  1. //删除二分搜索树最小节点
  2. public T removeMin() {
  3. //找到最小值
  4. T minimum = minimum();
  5. //删除最小值
  6. root = removeMin(root);
  7. return minimum;
  8. }
  9. private Node<T> removeMin(Node<T> node) {
  10. //先写递归的终止条件
  11. //如果递归的节点的左子树为null那么就查找到最小的节点
  12. if (node.left == null) {
  13. //保存右子树
  14. Node<T> rightNode = node.right;
  15. node.right = null;
  16. size--;
  17. return rightNode;
  18. }
  19. //然后将最小的节点进行赋值
  20. node.left = removeMin(node.left);
  21. return node;
  22. }

删除最大节点如下:

  1. //删除二分搜索树中最大的节点
  2. public T removeMax() {
  3. T max = maximum();
  4. root = removeMax(root);
  5. return max;
  6. }
  7. private Node<T> removeMax(Node<T> node) {
  8. if (node.right == null) {
  9. Node<T> leftNode = node.left;
  10. node.left = null;
  11. size--;
  12. return leftNode;
  13. }
  14. node.right = removeMax(node.right);
  15. return node;
  16. }

删除任意一个元素

首先,我们需要找到要删除元素的节点,然后判断删除的节点是否有左子树和右子树,如果要删除的节点只有左子树或者右子树,则将子树赋给要删除的节点.如果要删除的节点左右子树都存在,我们可以找到该节点左子树的最大节点替换删除的节点或者找到删除节点右子树的最小节点替换删除的节点. 我们可以使用上述代码的删除最小节点和最大节点操作

  1. // 5
  2. // / \
  3. // 3 6
  4. // / \ \
  5. // 2 4 8

代码如下:

  1. private Node<T> remove(Node<T> node, T e) {
  2. if (node == null)
  3. return null;
  4. //查找节点
  5. //如果查找的节点大于当前的节点值 则查找当前节点的右子树
  6. if (e.compareTo(node.value) > 0) {
  7. node.right = remove(node.right, e);
  8. return node;
  9. } else if (e.compareTo(node.value) < 0) {//小于 查找左子树
  10. node.left = remove(node.left, e);
  11. return node;
  12. } else {//e == root.node
  13. //要删除的节点只有右子树
  14. if (node.left == null) {
  15. return moveRight((Node<T>) node);
  16. }
  17. //要删除的节点只有左子树
  18. if (node.right == null) {
  19. return moveLeft((Node<T>) node);
  20. }
  21. //左右子树都存在的情况
  22. //后继方式 找到该节点右子树最小的节点
  23. // Node<T> s = minimum(node.right);
  24. //
  25. // //将s节点从删除节点的右子树中移除,然后将s节点指向右子树指向删除节点的右子树
  26. // s.right = removeMin(node.right);//removeMin中已经将size-1了
  27. // //s节点的左子树=删除节点的左子树
  28. // s.left = node.left;
  29. // //前驱方式 找到该节点左子树最大的节点
  30. Node<T> p = maximum(node.left);
  31. //先将p节点移除在做其他操作
  32. p.left = removeMax(node.left);
  33. p.right = node.right;
  34. //将删除节点置空
  35. node.right = null;
  36. node.left = null;
  37. return p;
  38. }
  39. }
  40. private Node<T> moveLeft(Node<T> node) {
  41. Node<T> leftNode = node.left;
  42. node.left = null;
  43. size--;
  44. return leftNode;
  45. }
  46. private Node<T> moveRight(Node<T> node) {
  47. Node<T> rightNode = node.right;
  48. node.right = null;
  49. size--;
  50. return rightNode;
  51. }

OK,至此我们了解了二分搜索树的基本结构实现,二叉树的探索绝对不止这些,大家可以试着实现中序遍历和后序遍历的非递归实现以及leetcode的练习.

我会尽量已清晰的方式来讲解复杂的算法.