数据结构: 实现二分搜索树

242 阅读5分钟

二分搜索树, Binary Search Tree

特点

每个树节点的值:

  • 必须大于左子树的所有节点的值,
  • 必须小于右子树的所有节点的值。
  • 树的叶子节点的左右子节点都为null(为了便于理解我这里这样假设。有些文章将这个null作为叶子节点。)

储存的数据:

  • 必须具有可比较性(具体表现在K需要实现Comparable接口, 当然如果我们用hashcode方法,这个条件可以不要)

实现代码

    public class BST<K extends Comparable<E>, V> {
        private class Node {
            // 定义私有类,对用户屏蔽
            public K key;
            public V value;
            // 左右两个节点
            public Node left; 
            public Node right;
            
            // 定义我们的构造方法
            public Node(K key, V value, Node left, Node right) {
                this.key = key;
                this.value = value;
                this.left = left;
                this.right = right;
            }
            public Node(K key, V value) {
                // 默认构造左右子节点都为空 
                this(key, value, null, null)
            }
        }
        
        // 树根节点
        private Node root;
        // size 树的大小
        private int size;
        
        // 树的构造方法
        public BST() {
            root = null;
            size = 0;
        }
        
        // 一些基本的方法
        public int getSize() {
            return size;
        }
        public boolean isEmpty() {
            return size == 0;
        }
        
        // 添加元素
        public void add(K key, V value) {
            // 树结构有着天然的递归性质, 我们使用递归去添加元素
            root = add(root, key, value);
        }
        private Node add(Node node, K key, V value) {
            if (node == null) { 
                // 递归结束条件, 已经遍历到了叶子节点的子节点了。在叶子节点的后面添加我们的新增节点。不要忘了维护size
                size++;
                return new Node(key, value);
            }
            if (key.compareTo(node.key) > 0) {
                // 我们添加的key比我们当前找到的node的key 要大(注意这里的可比较性 -- > key > node.key) , 说明我们要添加在当前找到的node的右边, 递归继续查找
                node.right = add(node.right, key, value)
            }else if (key.compareTo(node.key) < 0) {
                 node.left = add(node.left, key, value)
            }else {
                // 相等的情况,修改值。 当然其他处理方式也可以,比如放左放右或者
                node.value = value;
            }
            
            // 最后不要忘记把我们的node return出去
            return node;
        }
        
        // 删除节点
        public V remove(K key) {
            // 先查找出来,在删除
            Node retNode = getNode(key);
            if (retNode != null) {
                root = remove(root, K key);
                
                // 返回查找到的 node 的值
                return retNode.value;    
            }
            // 没找到返回null
            return null;
        }
        private Node remove(Node node, K key) {
            if (node == null) { 
                // 递归结束还没找到这个key的话,说明没有这个node
                return null;    
            }
            
            if (key.compareTo(node.key) > 0) {
                node.right = remove(node.right, key)
            }else if (key.compareTo(node.key) < 0) {
                node.left = remove(node.left, key)
            }else {
                // 如果找到了这个key
                // 分两种情况:
                // 1、 找到的这个node不是是叶子节点,但是是左子节点或者有子节点有一个是null, 直接删除就好了,然后把不是null的子节点返回个出去就好了
                // 2、 找到的这个node不是叶子节点,而且左子和右子节点都不为空, 这个时候, 我们可以找到待删除的node节点的后继(或者前驱)节点替代当前节点位置,在返回出去
                        这里有必要说一下后继和前驱节点的意思, 后继节点就是当前节点右子树所有节点当中的最小的节点
                        前驱节点就是当前节点左子树所有节点当中的最大的节点
                
                // 实现代码
                if (node.left == null) {
                    Node ret = node.right;
                    // 释放空间
                    node.right = null;
                    size--;
                    return ret;
                }else if (node.right == null) {
                    Node ret = node.left;
                    // 释放空间
                    node.left = null;
                    size--;
                    return ret;
                }else {
                     // 找到当前节点的后继节点
                    Node succeedNode = minimum(node.right);
                    succeedNode.left = node.left;
                    // 我们把删除node的后继节点之后的node.right作为这个后继节点的右子节点
                    succeedNode.right = remove(node.right, succeedNode.key)
                    node.left = node.right = null;
                    return succeedNode;   
                }
                
            }
        }
        
        // 查找某个node, 整体和add很相似
        private Node getNode(K key) {
            if (key.compareTo(node.key) > 0) {
                return getNode(node.right, key)
            }else if (key.compareTo(node.key) < 0) {
                return getNode(node.left, key)
            }else {
                return node;
            }
        }
        
        // 找到节点的后继节点
        private Node minimum(Node node) {
            while (node.left != null) {
                node = node.left;
            }
            return node;
        }
        
        // 查看是否包含某个key
        public boolean contains(K key) {
            return contains(root, key)
        }
        private boolean contains(Node node, K key) {
            if (node == null) {
                return false;
            }
            if (key.compareTo(node.key) > 0) {
                return contains(node.right, key)
            }else if (key.compareTo(node.key) < 0) {
                return contains(node.left, key)
            }else {
                return true;
            }
        }
    }
  • 可以看到, 我们上面的代码已经实现了二分搜索树的添加和删除,为了不使代码变得很沉长难读, 我把接下来的遍历方法写在了下面
  • 对于树的遍历, 通常分为两种:
    • 广度优先遍历: 广度优先遍历也称为层序遍历,是从树根节点逐层向下遍历,是对树的一个横向遍历操作
    • 深度优先遍历:深度优先遍历分为 前序遍历, 中序遍历, 后序遍历

代码实现:

    public class BST<K extends Comparable<E>, V> {
        // 广度优先遍历, 我们直接使用java的队列实现,  就非常的简单
        Queue<Node> queue = new LinkedList<>();
        queue.add(root);
        while(!queue.isEmpty) {
            Node node = queue.remove()
            System.out.printLn(node.value)
            if (node.left != null) {
                queue.add(node.left)
            }
            if (node.right != null) {
                queue.add(node.right)
            }
        }
        
        // 深度优先遍历,无论是前序还是中序后序,其实都是同一种遍历方式, 只是在我们操作元素的时间段不同
        // 当我们使用递归去遍历的时候, 任意一个节点都会经过三个过程,
        // 1、刚开始遍历到当前节点(这个时候操作当前节点-->前序遍历)
        // 2、遍历完当前节点的左子节点后回到当前节点 (这个时候操作当前节点-->中序遍历)
        // 3、遍历完当前节点的右子节点后回到当前节点, 结束遍历。(这个时候操作当前节点-->后序遍历)
        // 大家会发现中序遍历, 就是一个对key的排序过程
        
        // 前序遍历
        public void preOrder() {
            preOrder(root);
        }
        private void preOrder(Node node) {
            if (node == null) {
                return;
            }
            Sysout.out.printLn(node.key);
            preOrder(node.left);
            preOrder(node.right);
        }
    }
    
        // 中序遍历
        public void inOrder() {
            inOrder(root);
        }
        private void inOrder(Node node) {
            if (node == null) {
                return;
            }
            inOrder(node.left);
            Sysout.out.printLn(node.key);
            inOrder(node.right);
        }
    }
    
        // 后序遍历
        public void postOrder() {
            postOrder(root);
        }
        private void postOrder(Node node) {
            if (node == null) {
                return;
            }
            postOrder(node.left);
            postOrder(node.right);
            Sysout.out.printLn(node.key);
        }
        
        
    }
  • 这样我们就实现了一个简单的二分搜索树, 增删改查操作,均摊复杂度为O(log n) 级别。 log底为2。
  • 但是我们的树在一些极端得情况下,比如我们故意插入的是一组有序的数据时 我们的树就会退化成一个链表,复杂度会变为O(n) 级别
  • 在下一篇, 我们将会给它添加自平衡的能力, 使他成为一颗简单的AVL树