二叉查找树 BST

二叉查找树 BST : https://blog.csdn.net/cj_286/article/details/90183298

二叉平衡树 AVL : https://blog.csdn.net/cj_286/article/details/90217072

红黑树 RBT : https://blog.csdn.net/cj_286/article/details/90245150

 

学习二叉树前需要先熟悉数组、链表、栈、队列等线性结构,还需要熟悉二叉树的先、中、后、层四种遍历。

JDK中用到的数据结构类
不可变数组String,动态数组StringBuilder
顺序表ArrayList,双链表LinkedList
队列Queue,栈Stack
哈希表HashMap,二叉树PriorityQueue
红黑树TreeMap

性质

二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树:
 1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
 2.若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
 3.它的左、右子树也分别为二叉排序树。
 4.中序遍历序列为升序

遍历算法 (先序,中序,后序,层序)
1.先(根)序遍历的递归算法
⑴ 访问根结点;
⑵ 遍历左子树;
⑶ 遍历右子树。
2.中(根)序遍历的递归算法
⑴遍历左子树;
⑵访问根结点;
⑶遍历右子树。
3.后(根)序遍历得递归算法
⑴遍历左子树;
⑵遍历右子树;
⑶访问根结点。
4.层序遍历
⑴首先访问第一层的树根节点
⑵然后从左到右访问第二层上的节点
⑶接着是第三层的节点,以此类推

反转二叉树

如图所示

               4                                              4
             /    \                                         /   \
            2      7                   -->                 7     2
          /  \   /  \                                     / \   /  \
         1    3  6   9                                   9   6 3    1

使用先序/中序/后续/层序交换所有结点的左右子树

public class TreeNode {
    int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int val) {
        this.val = val;
    }

    public TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
    public TreeNode() {
    }
}

public class InvertBinaryTree {
    /**
     *  Invert Binary Tree
     *  //先序遍历(根左右)
     *  先序遍历先从二叉树的根开始,然后到左子树,再到右子树
     * @param root
     * @return
     */
    public TreeNode invertTree1(TreeNode root){
        if (root != null) {
            TreeNode t = root.left;
            root.left = root.right;
            root.right = t;
            invertTree1(root.left);
            invertTree1(root.right);
            return root;
        }else {
            return null;
        }
    }

    /**
     *  Invert Binary Tree
     *  //后序遍历(左右根)
     *  后序遍历先从左子树开始,然后到右子树,再到根
     * @param root
     * @return
     */
    public TreeNode invertTree2(TreeNode root){
        if (root != null) {
            invertTree2(root.left);
            invertTree2(root.right);
            TreeNode t = root.left;
            root.left = root.right;
            root.right = t;
            return root;
        }else {
            return null;
        }
    }

    /**
     *  Invert Binary Tree
     *  //中序遍历(左根右)
     *  中序遍历先从左子树开始,然后到根,再到右子树
     * @param root
     * @return
     */
    public TreeNode invertTree3(TreeNode root){
        if (root != null) {
            invertTree3(root.left);
            TreeNode t = root.left;
            root.left = root.right;
            root.right = t;
            invertTree3(root.left);//root.left是原来的root.right
            return root;
        }else {
            return null;
        }
    }

    /**
     * Invert Binary Tree
     * 层序遍历 (一层一层遍历)
     *
     * @param root
     * @return
     */
    public TreeNode invertTree4(TreeNode root) {
        if (root == null) {
            return null;
        }else{
            Queue<TreeNode> queue = new LinkedList<>();
            queue.offer(root);
            while (!queue.isEmpty()){
                TreeNode node = queue.poll();
                TreeNode t = node.left;
                node.left = node.right;
                node.right = t;
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            return root;
         }
    }
}

插入

BST插入节点

首先定义一个Empty,用于存储键值对

/**
     * 存储key,value
     * @param <K>
     * @param <V>
     */
    public static class BSTEntry<K,V> implements Map.Entry<K,V> {
        private K key;
        private V value;
        private BSTEntry<K,V> left;
        private BSTEntry<K,V> right;

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V value) {
            this.value = value;
            return value;
        }

        public BSTEntry(K key) {
            this.key = key;
        }

        public BSTEntry(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public BSTEntry(K key, V value, BSTEntry<K, V> left, BSTEntry<K, V> right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }

        @Override
        public String toString() {
            return "AVLEntry{" +
                    "key=" + key +
                    ", value=" + value +
                    '}';
        }
    }

比较关键字a和b的大小

public int compare(Object a, Object b) {
        if (comparator != null) {
            return comparator.compare((K)a,(K)b);//JDK中也是强转的
        }else{
            Comparable<K> t = (Comparable<K>) a;
            return t.compareTo((K)b);
        }
    }

添加元素

public V put(K key,V value) {
        if (root == null) {
            root = new BSTEntry<K,V>(key,value);
            size ++;
        }else{
            BSTEntry<K,V> p = root;
            while (p != null) {
                int cmp = compare(key,p.key);
                if (cmp < 0) {
                    if (p.left == null) {
                        p.left = new BSTEntry<K,V>(key,value);
                        size ++;
                        break;
                    }else{
                        p = p.left;//再次循环比较
                    }
                } else if (cmp > 0) {
                    if (p.right == null) {
                        p.right = new BSTEntry<K,V>(key,value);
                        size ++;
                        break;
                    }else{
                        p = p.right;
                    }
                }else{
                    p.setValue(value);//替换旧值
                    break;
                }
            }
        }
        //不管是插入的是新值还是重复值,都返回插入的值,这个和JDK TreeMap不一样
        return value;
    }

遍历

BST迭代器

利用中序遍历实现顺序迭代

方案1:递归添加进线性集合,迭代线性集合

/**
 * 递归实现iterator
 * 空间复杂度是O(n)
 */
public class BSTIterator {
    private Iterator<Integer> itr;

    public BSTIterator(TreeNode root) {
        //这个算法的空间复杂度是O(n)
        ArrayList<Integer> list = new ArrayList<>();
        inOrder(root,list);
        itr = list.iterator();
    }

    /**
     * 中序遍历将每个结点添加进集合
     * @param p
     * @param list
     */
    private void inOrder(TreeNode p,ArrayList<Integer> list) {
        if (p != null) {
            inOrder(p.left,list);
            list.add(p.val);
            inOrder(p.right,list);
        }
    }

    public boolean hasNext() {
        return itr.hasNext();
    }

    public int next() {
        return itr.next();
    }
}

方案2:使用的是非递归算法,使左路径节点压栈

           5
         /   \
        2     6
       / \     \
      1   4     7
         /
        3

1.从根节点依次遍历左节点,将其压栈 (Stack:1,2,5) (Out:)
2.压栈到1时,如果1没有左子树,将其出栈 (Stack:2,5) (Out:1)
3.如果1也没有右子树,再次弹栈,2出栈(Stack:5) (Out:1,2)
4.再看2是否有右子树,有,将2右节点4所在的节点左子树压栈(Stack:3,4,5) (Out:1,2)
5.3没有左子树,3弹栈(Stack:4,5) (Out:1,2,3)
6.3没有右子树,4弹栈(Stack:5) (Out:1,2,3,4)
7.4.没有右子树,5弹栈(Stack:) (Out:1,2,3,4,5)
8.5有右子树,将5右节点6所在的节点左子树压栈(Stack:6) (Out:1,2,3,4,5)
9.6没有左子树,6弹栈(Stack:) (Out:1,2,3,4,5,6)
10.6有右子树,将6右节点7所在的节点左子树压栈(Stack:7) (Out:1,2,3,4,5,6)
11.7没有左子树,7弹栈(Stack:) (Out:1,2,3,4,5,6,7)
12.7没有右子树,结束,这时输出的结果正好时顺序的(Out:1,2,3,4,5,6,7)

/**
     * 中序遍历实现迭代器
     * 这里实现引入Stack,而非递归实现
     * @param <K>
     * @param <V>
     */
    public class BSTIterator<K,V> implements Iterator<BSTEntry<K,V>> {
        private Stack<BSTEntry<K,V>> stack;
        public BSTIterator(BSTEntry<K, V> root) {
            stack = new Stack<>();
            addLeftPath(root);
        }

        private void addLeftPath(BSTEntry<K, V> p) {
            while (p != null) {
                stack.push(p);
                p = p.left;
            }
        }

        @Override
        public boolean hasNext() {
            return !stack.empty();
        }

        @Override
        public BSTEntry<K, V> next() {
            BSTEntry<K, V> entry = stack.pop();
            addLeftPath(entry.right);
            return entry;
        }
    }

查找

BST查找节点 

查找原理

查找与插入类似,假设查找关键字key
 1,若根结点的关键字值等于key,成功
 2,若key小于根结点的关键字,递归查找左子树
 3,若key大于根结点的关键字,递归查找右子树
 4,若子树为空,查找不会成功
时间复杂度O(logN)

 /**
     * @param key
     * @return
     */
    private BSTEntry<K,V> getEntry(Object key) {
        BSTEntry<K,V> p = root;//初始化指针p,指向根节点
        while (p != null) {
            int cmp = compare(key, p.key);//比较key与p.key的大小
            if (cmp < 0) {
                p = p.left;//key小于p.key,递归(循环)查找左子树
            } else if (cmp > 0) {
                p = p.right;//key大于p.key,递归(循环)查找右子树
            } else {
                return p;//key等于p.key,查找成功
            }
        }
        return null;//查找失败
    }

    @Override
    public V get(Object key) {
        BSTEntry<K, V> entry = getEntry(key);
        return entry != null ? entry.value : null;
    }

查询是否包含key

public boolean containsKey(Object key) {
        BSTEntry<K, V> entry = getEntry(key);
        return entry != null;
    }

查询是否包含value

/**
     * 遍历所有结点查看比较value
     * 这里直接使用的迭代器,一个一个比较(中序遍历)
     * 时间复杂度O(N)
     * @param value
     * @return
     */
    @Override
    public boolean containsValue(Object value) {
        Iterator<BSTEntry<K, V>> iterator = this.iterator();
        while (iterator.hasNext()) {
            if (iterator.next().equals(value)) {
                return true;
            }
        }
        return false;
    }

寻找最小节点

/**
     * 找到以p为根节点的最小结点(中序遍历的第一个结点)
     * @param p
     * @return
     */
    private BSTEntry<K, V> getFirstEntry(BSTEntry<K, V> p) {
        if(p == null)
            return null;
        while (p.left != null)//一直往左找,直到p没有左子树
            p = p.left;
        return p;//p是最左边且第一个没有左子树的结点
    }

寻找最大节点

/**
     * 找到以p为根节点的最大结点
     * @param p
     * @return
     */
    private BSTEntry<K, V> getLastEntry(BSTEntry<K, V> p) {
        if (p == null)
            return null;
        while (p.right != null)//一直往右找,直到p没有右子树
            p = p.right;
        return p;//p是最右边且第一个没有右子树的结点
    }

删除

BST的删除 

假设删除结点p,其父节点为f,分如下3中情况:

1,p是叶子结点,直接删除

            5
          /   \
         2     6
        / \     \            删除1-> 直接删除1
       1   4     7
           /
          3		

2,p只是左子树left(或右子树right),直接用p.left替换p

           5                                                     5
         /   \                                                 /   \
        2     6                                               2     6
       / \     \               删除4-> 3直接替换4             / \     \  
      1   4     7                                           1   3     7
         /
        3	

3,p既有左子树left,又有右子树right,找到右子树的最小结点rightMin(需要借助于getFirstEntry,或找到left的最大结点leftMax),用rightMin的值替换p的值,再根据以上两种情况删除rightMin

         6                            6                              6
       /   \                        /   \                          /   \
      2     7                      3     7                        3     7
     / \     \  找到2的后继节点3   / \     \    已转化成情况1      / \     \    
    1   4     8       ->         1   4     8       ->           1   4     8      
       / \        将2替换成3         / \        直接删除3             \ 
      3	  5                         3  5                             5  
/**
     * 以p为根结点删除key
     * 递归查找删除
     * @param p
     * @param key
     * @return
     */
    private BSTEntry<K,V> deleteEntry(BSTEntry<K,V> p, Object key){
        if(p == null)
            return null;
        int cmp = compare(key, p.key);
        if (cmp == 0) {//找到删除的结点
            if (p.left == null && p.right == null) {//情况1
                p = null;
            } else if (p.left == null && p.right != null) {//情况2
                p = p.right;
            } else if (p.left != null && p.right == null) {//情况2
                p = p.left;
            } else {//情况3
                //为了达到平衡效果,随机执行以下两种情况
                if ((size & 1) == 0) {//找右子树中最小的来替换
                    BSTEntry<K, V> rightMin = getFirstEntry(p.right);
                    p.key = rightMin.key;
                    p.value = rightMin.value;
                    BSTEntry<K, V> newRight = deleteEntry(p.right, p.key);
                    p.right = newRight;
                }else {//找左子树中最大的来替代
                    BSTEntry<K, V> leftMax = getLastEntry(p.left);
                    p.key = leftMax.key;
                    p.value = leftMax.value;
                    BSTEntry<K, V> newLeft = deleteEntry(p.left, p.key);
                    p.left = newLeft;
                }
            }
        } else if (cmp < 0) {//要删除的结点在左边
            BSTEntry<K, V> newLeft = deleteEntry(p.left, key);//以左子树为根结点,递归重新找要删除的结点
            p.left = newLeft;//实现真正的删除功能(如果p.left是要删除的结点,并且p.left结点没有子节点了,那么newLeft必定返回null,也就实现了删除功能)
        } else /*cmp > 0*/{//要删除的结点在右边
            BSTEntry<K, V> newRight = deleteEntry(p.right, key);//以右子树为根结点,递归重新找要删除的结点
            p.right = newRight;//实现真正的删除功能(如果p.rigth是要删除的结点,并且p.right结点没有子节点了,那么newRight必定返回null,也就实现了删除功能)
        }
        return p;
    }
/**
     * 删除
     * @param key
     * @return
     */
    @Override
    public V remove(Object key) {
        BSTEntry<K, V> entry = getEntry(key);//查找是否有该结点
        if (entry == null) {
            return null;
        }
        V oldValue = entry.getValue();//获取value
        BSTEntry<K, V> newRoot = deleteEntry(root, key);//删除,并返回新的根节点(如果删除的是根节点,root就会变化)
        root = newRoot;
        return oldValue;
    }

层序遍历

便于查看BST树结构

/**
     * 层序遍历
     */
    public void levelOrder() {
        Queue<BSTEntry<K,V>> queue = new LinkedList<>();
        queue.offer(root);
        int preCount = 1;
        int pCount = 0;
        while (!queue.isEmpty()) {
            preCount --;
            BSTEntry<K,V> p = queue.poll();
            System.out.print(p + " ");
            if (p.left != null) {
                queue.offer(p.left);
                pCount ++;
            }
            if (p.right != null) {
                queue.offer(p.right);
                pCount ++;
            }
            if (preCount == 0) {
                preCount = pCount;
                pCount = 0;
                System.out.println();
            }
        }
    }

后续

寻找后续(前趋)节点(有parent指针)

情况1:t有右子树,和getFirstEntry完全一样

                  6
                /   \
               2     7
              / \     \            寻找2的后继节点,也就是3
             1   4     8
                / \
               3   5

情况2解法1,t没有右子树,向上回溯,找到第一个孩子是左子树孩子的父亲p

ch=5,p=4,ch是p的右孩子,将ch和p同时上移(ch=p,p=p.parent)

ch=4,p=2,ch是p的右孩子,将ch和p同时上移(ch=p,p=p.parent)

ch=2,p=6,ch是p的左孩子,则这时p(也就是6)是t(也就是5)的后继节点

情况2解法2,t没有右子树,向上回溯,找到第一个关键字比孩子大的父亲p

ch=5,p=4,p比ch小,将ch和p同时上移(ch=p,p=p.parent)

ch=4,p=2,p比ch小,将ch和p同时上移(ch=p,p=p.parent)

ch=2,p=6,p比ch大,则这时p(也就是6)是t(也就是5)的后继节点

JDK TreeMap寻找后继节点的算法情况1和情况2的解法1

static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {//寻找后继节点
        if (t == null)
            return null;
        else if (t.right != null) {//情况1:有右子树
            Entry<K,V> p = t.right;
            while (p.left != null)//找到右子树的最小节点,即右子树最左边的节点
                p = p.left;
            return p;
        } else {//情况2:没有右子树(解法1)
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {//找到第一个孩子是左孩子的父亲节点,或者根节点
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

JDK TreeMap寻找前趋节点的算法和后继节点是完全对称的

static <K,V> Entry<K,V> predecessor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.left != null) {
            Entry<K,V> p = t.left;
            while (p.right != null)
                p = p.right;
            return p;
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.left) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

寻找后续(前趋)节点(无parent指针)

情况1:p是最大节点,没有后继,返回null,或root为null,返回null
情况2:p有右子树,和getFirstEntry完全一样
情况3:p没有右子树,查找到节点p(同BST的查找),并将路径压栈,弹栈回溯的算法与有parent域的情况2类似

Stack实现:时间、空间复杂度小于O(logN),

过程是自底向上,它记录的是第一个遇到左孩子的父亲,则该左孩子的父亲就是t的后继节点  (情况3如下图所示)

bst-1-2-1

从根节点开始寻找,一直找到t(5),将寻找路径上的节点全部压栈,然后再出栈
5(ch=5)出栈(poll),指针向上回溯(peek),这时指针指向4(p=4),4(p=4)的右孩子是5(ch=5)
4(ch=4)出栈(poll),指针向上回溯(peek),这时指针指向2(p=2),2(p=2)的右孩子是4(ch=4)
2(ch=2)出栈(poll),指针向上回溯(peek),这时指针指向6(p=6),6(p=6)的左孩子是2(ch=2),所以6就是我们所找的结果t(5)的后继节点(6)

While实现(临时变量代替栈):时间复杂度O(logN)、空间复杂度O(1),

过程是自顶向下,它记录的是最后一个遇到比t大的父亲,该节点就是t的后继节点  (情况3如下图所示)

bst-1-2-2
public class InorderSuccessorInBST {
    /**
     * 根据root根结点,寻找p的后继结点
     * @param root
     * @param p
     * @return
     */
    public TreeNode inorderSuccessor(TreeNode root,TreeNode p) {
        if(root == null) return null;//1.如果root为null,直接返回null
        if(getLastEntry(root) == p) return null;//2.如果最大结点等于p结点,说明没有后继结点,直接返回null
        if(p.right != null) return getFirstEntry(p.right);//3.如果p有右孩子,直接返回以右孩子为根节点的最小结点
        //4.如果右孩子为空,可以有两种解法,一种是自顶向下,一种是自底向上
        //自顶向下:(需要用到while循环)找到的是最后一个父亲,也就是后续结点
        //自底向上:(需要用到栈Stack)找到的是第一个左孩子的父亲,
        //以下使用的是自顶向下的解法
        TreeNode parent = root;
        TreeNode tmp = root;
        while (parent != null) {
            if(parent == p) break;
            else if(p.val < parent.val){//小于说明在左孩子树上,说明parent有可能是p的后继节点,只要该parent是最后一个找到的(p.val < parent.val)的parent
                tmp = parent;
                parent = parent.left;
            }else //大于说明在右孩子树上
                parent = parent.right;
        }
        return tmp;
    }

    /**
     * 找到以p为根节点的最大结点
     * @param p
     * @return
     */
    private TreeNode getLastEntry(TreeNode p){
        while (p.right != null)
            p = p.right;
        return p;
    }

    /**
     * 找到以p为根节点的最小结点
     * @param p
     * @return
     */
    private TreeNode getFirstEntry(TreeNode p){
        while (p.left != null)
            p = p.left;
        return p;
    }
}

JDK TreeMap中的删除就使用到了后继节点

public V remove(Object key) {
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }
private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        //(BST的删除)情况3:p有两个孩子,找到p的后继节点s,用s的key和value替换p的key和value,并让p指向s
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;//s可能是叶子节点,可能是只有右子树的节点
        }

        // 如果p是s替换过来的,那么p只能有右孩子
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        //(BST的删除)情况2:p只有一个孩子,replacement要么是左孩子,要么是右孩子
        if (replacement != null) {
            // 这里需要将p的孩子节点replacement的parent和其父节点的left或right的索引重新赋值(因为p已被删除)
            replacement.parent = p.parent;//直接用p孩子替换p,让孩子节点的parent指向新的parent,也就是p的parent
            if (p.parent == null)
                root = replacement;//如果p.parent是null,则p的孩子就是新的根节点
            else if (p == p.parent.left)//p是左孩子
                p.parent.left  = replacement;// replacement重新指向p的父亲的左孩子
            else //p是右孩子
                p.parent.right = replacement;//replacement重新指向p的父亲的右孩子

            p.left = p.right = p.parent = null;//释放指针p(删除)

            // Fix replacement
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
        } else { // (BST的删除)情况1:p是叶子节点,直接删除
            if (p.color == BLACK)
                fixAfterDeletion(p);

            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;//释放掉p
                else if (p == p.parent.right)
                    p.parent.right = null;//释放掉p
                p.parent = null;//释放掉p
            }
        }
    }

源码:
https://github.com/xiaojinwei/java-learning/blob/master/src/com/cj/learn/tree/bst/BSTMap.java

参考:
https://www.bilibili.com/video/av23890827
https://github.com/kosoraYintai/TreeMapSourceAnalysis

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值