一、二叉搜索树

1. 二叉树搜索基本实现

  1. public class BST {
  2. private Node root;
  3. private int count;
  4. public BST() {
  5. root == null;
  6. count = 0;
  7. }
  8. public int size() {
  9. return count;
  10. }
  11. public boolean isEmpty() {
  12. return count == 0;
  13. }
  14. class Node {
  15. private int key;
  16. private int value;
  17. private Node left;
  18. private Node right;
  19. public Node(int key, int value) {
  20. this.key = key;
  21. this.value = value;
  22. this.left = null;
  23. this.right = null;
  24. }
  25. }
  26. }

2. 二叉搜索树结点插入

思路:我们应该充分利用二叉搜索树的特点来完成 insert 操作的编写。

  1. /**
  2. *
  3. * @param key
  4. * @param value
  5. */
  6. public void insert(K key, V value) {
  7. root = insert(root, key, value);
  8. }
  9. /**
  10. * 在以 node 为结点的二分搜索树中插入结点(key, value)
  11. * 返回插入(key, value) 的二分搜索树的根
  12. * (1) count++ 别忘了
  13. * (2) 要把 node 返回回去
  14. */
  15. private Node insert(Node node, K key, V value) {
  16. if (node == null) {
  17. count++;
  18. return new Node(key, value);
  19. } else if (node.key.compareTo(key) == 0) {
  20. node.value = value;
  21. return node;
  22. } else if (key.compareTo(node.key) > 0) {
  23. node.right = insert(node.right, key, value);
  24. return node;
  25. } else {
  26. node.left = insert(node.left, key, value);
  27. return node;
  28. }
  29. }

3. 二叉搜索树的查找

查找专注于找到这个元素。我们思考一下,在 search 这个方法中,我们应该将什么返回回去,一个好的数据结构应该将内部的数据对外隐藏。

二分查找树的包含 contain(返回 true 或者 false) 和查找 search (返回相应的 value 值)同质。

考虑查找成功和失败这两种情况。

3.1 contain 函数

  1. /**
  2. * 在整个二叉树中是否包含 key
  3. */
  4. public boolean contain(int key) {
  5. return contain(root, key);
  6. }
  7. public boolean contain(Node node, int key) {
  8. if (node == null) {
  9. return false;
  10. }
  11. if (key == node.key) {
  12. return true;
  13. } else if (key < root.key) {
  14. return contain(node.left, key);
  15. } else {
  16. return contain(node.right, key);
  17. }
  18. }

3.2 search 函数

  1. // serach 的返回值是什么,这是一个设计问题
  2. // 可以返回 Node,返回 value
  3. public int search(int key) {
  4. return search(root, key);
  5. }
  6. /**
  7. * 在以 node 为根的二叉搜索树中查找 key 所对应的 value
  8. */
  9. private Integer search(Node node, int key) {
  10. if (node == null) {
  11. return null;
  12. }
  13. if (key == node.key) {
  14. return node.value;
  15. } else if (key < node.key) {
  16. return search(node.left, key);
  17. } else {
  18. return search(node.right, key);
  19. }
  20. }

二、树的遍历

1. 前、中与后序遍历

1.1 递归实现

  1. // 对以 node 为根的二叉树搜索树进行前序遍历
  2. public void preOrder(Node node) {
  3. if (node != null) {
  4. System.out.println(node.key);
  5. preOrder(node.left);
  6. preOrder(node.right);
  7. }
  8. }
  9. // 对以 node 为根的二叉搜索树进行中序遍历
  10. public void inOrder(Node node) {
  11. if (node != null) {
  12. inOrder(node.left);
  13. System.out.println(node.key);
  14. inOrder(node.right);
  15. }
  16. }
  17. // 对以 node 为根的二叉搜索树进行后序遍历
  18. public void postOrder(Node node) {
  19. if (node != null) {
  20. postOrder(node.left);
  21. postOrder(node.right);
  22. System.out.println(node.key);
  23. }
  24. }

2. 层序遍历(广度优先)

基于队列实现

  1. public void levelOrder() {
  2. Queue<Node> queue = new ArrayDeque<>();
  3. while (!queue.isEmpty()) {
  4. Node node = queue.poll();
  5. System.out.println(node.key);
  6. if (node.left != null) {
  7. queue.add(node.left);
  8. }
  9. if (node.right != null) {
  10. queue.add(node.right);
  11. }
  12. }
  13. }

三、二叉搜索树的删除

1. 删除最大/小值

1.1 删除最小值

  1. /**
  2. * 寻找最小的 key
  3. */
  4. public int minimum() {
  5. assert count != 0;
  6. Node minimum = minimum(root);
  7. return minimum.key;
  8. }
  9. public Node minimum(Node node) {
  10. if (node.left == null) {
  11. return node;
  12. }
  13. return minimum(node.left);
  14. }
  15. /**
  16. * 从二分搜索树中删除最小 key 所在的结点
  17. */
  18. public void removeMin() {
  19. if (root != null) {
  20. root = removeMin(root);
  21. }
  22. }
  23. /**
  24. * 删除以 node 为根的二分搜索树中的最小的结点
  25. * 返回删除结点后新的二分搜索树的根
  26. *
  27. * @param node
  28. * @return
  29. */
  30. private Node removeMin(Node node) {
  31. if (node.left == null) {
  32. // 就是删除这个结点 node
  33. Node rightNode = node.right;
  34. node.right = null;
  35. count--;
  36. return rightNode;
  37. }
  38. node.left = removeMin(node.left);
  39. return node;
  40. }

1.2 删除最大值

  1. /**
  2. * 寻找最大的 key
  3. */
  4. public int maximum() {
  5. assert count != 0;
  6. Node maximum = maximum(root);
  7. return maximum.key;
  8. }
  9. public Node maximum(Node node) {
  10. assert count != 0;
  11. // 只要是递归就要处理递归到底的情况
  12. if (node.right == null) {
  13. return node;
  14. }
  15. return maximum(node.right);
  16. }
  17. public void removeMax() {
  18. if (root != null) {
  19. root = removeMax(root);
  20. }
  21. }
  22. public Node removeMax(Node node) {
  23. if (node.right == null) {
  24. Node leftNode = node.left;
  25. node.left = null;
  26. count--;
  27. return leftNode;
  28. } else {
  29. node.right = removeMax(node.right);
  30. return node;
  31. }
  32. }

2. 删除任意结点

四、二叉搜索树的顺序性与局限性

1. 顺序性

二分搜索树当做查找表的一种实现。我们使用二分搜索树的目的是通过查找 key 马上得到 value。

2. 局限性

二分查找树的性能。二分查找树在一些极端情况下性能并不好。
我们首先要认识下面一个事实:同样的数据,可以对应不同的二分搜索树。看下面的例子。

image-20201016000521573.png

二分搜索树可以退化为链表。此时时间复杂度变成了 O(n)。
极端测试:如果把 key 排序好以后,依次插入到二分搜索树中,此时二分搜索树的高度就会变得非常高。

解决方案:平衡二叉树,使用红黑树(红黑树是一种平衡二叉树的实现,其它平衡二叉树的实现还有 2-3 tree,AVL tree,Splay tree,平衡二叉树和堆的结合:Treap)。左右两棵子树的高度差不会超过1。

解决方案:平衡二叉树,使用红黑树(红黑树是一种平衡二叉树的实现,其他平衡二叉树的实现还有 2-3 tree,AVL tree,Splay tree,平衡二叉树和堆的结合:Treap)。左右两颗子树的高度差不会超过 1。

trie。使用 trie 统计词频。

image-20201016000945053.png

五、树形问题和更多树

解题的时候要利用好树的特性,

首先几个遍历一定要掌握好,基本上成功的遍历可以将二叉树压成链表来看,复杂度进一步降低。除此之外,二叉搜索树的特性也要考虑到。

此外,有些问题虽然没有直接创建树。但是却是和数离不开的。

递归方法天然地具有递归的性质。

归并排序法和快速排序法的思想它们像极了对一棵树进行后序遍历和前序遍历。

  • 搜索问题;
  • 一条龙游戏;
  • 8 数码;
  • 8 皇后;
  • 数独;
  • 搬运工;
  • 人工智能:搬运工,树形搜索;
  • 机器学习;
  • 更多树:KD 树,区间树,哈夫曼树