算法-遍历二分搜索树

9次阅读

共计 5625 个字符,预计需要花费 15 分钟才能阅读完成。

又是来自我的好朋友 EvilSay 的投稿,以下是原文:

1、基本定义

  • 二分搜索树的每个子节点最多有两个叶子节点
  • 二分搜索树的每个节点最多有一个根节点
  • 存储的元素必须具有可比较性
  • 二分搜索树每个子节点的值

    • 大于其左子节的所有节点的值
    • 小于其右子节点的所有节点的值
  • 二分搜索树不一定是满的

2、二分搜索树 Java 实现

/**
 * @Author: EvilSay
 * @Date: 2019/8/6 19:00
 */
public class BSTMain <E extends Comparable<E>> {
    private class Node {
        public E e;
        private Node left, right;

        public Node(E e) {
            this.e = e;
            left = null;
            right = null;
        }
    }

    // 根节点
    private Node root;
    private int size;

    public BSTMain() {
        root = null;
        size = 0;
    }

    public int size() {return size;}

    public boolean isEmpty() {return size == 0;}
    public void add(E e){root = add(this.root, e);

    }

    // 向 node 为根的二分搜索树中插入元素 E, 递归算法
    // 返回插入新节点后二分搜索树的根
    private Node add(Node node, E e){if (node == null){

            size ++;
            return new Node(e);
        }

        if (e.compareTo(node.e) < 0)
            node.left = add(node.left, e);
        else if (e.compareTo(node.e) > 0)
            node.right = add(node.right,e);
        return node;
    }

    // 看二分搜索树中是否包含元素 e
    public boolean contains(E e){return contains(root,e);
    }

    // 看以 node 为根的二分搜索树中是否包含元素 e, 递归算法
    private boolean contains(Node node, E e){if (node == null)
            return false;
        if (e.compareTo(node.e) == 0)
            return true;
        else if (e.compareTo(node.e) < 0)
            return contains(node.left, e);
        else
            return contains(node.right,e);
    }

    @Override
    public String toString() {StringBuilder res = new StringBuilder();
        generateBSTSString(root,0,res);
        return res.toString();}

    // 生成以 node 为根节点,深度为 depth 的描述二叉树的字符串
    private void generateBSTSString(Node root, int depth, StringBuilder res) {if (root == null){res.append(generateDepthString(depth) + "null\n");
            return;
        }
        res.append(generateDepthString(depth) + root.e + "\n");
        generateBSTSString(root.left, depth + 1 ,res);
        generateBSTSString(root.right, depth + 1, res);

    }

    private String generateDepthString(int depth) {StringBuilder res = new StringBuilder();
        for (int i = 0; i < depth; i++)
            res.append("--");
        return res.toString();}
}

3、图解二分搜索树

从图上我们看出二分搜索树每个节点的值大于其左子节的所有节点的值小于其右子节点的所有节点的值。

4、前序遍历

前序遍历也叫先序遍历,访问顺序是根左右,也就是先访问根节点,再到左子树,最后才到右子树。所以上图所示的访问顺序是 5、3、2、4、8、7、9。

二分搜索树前序遍历递归版与非递归版

    // 前序遍历以 node 为根的二分搜索树,递归算法
    private void preOrder(Node node){if (node == null)
            return;

        System.out.println(node.e);
        preOrder(node.left);
        preOrder(node.right);
    }
    
    // 二分搜索树的前序遍历递归调用
    public void preOrder(){preOrder(root);
    }
    
    // 二分搜索树的前序遍历非递归写法
    public void preOrderNG(){Stack<Node> stack = new Stack<>();
        // 根节点
        stack.push(root);

        while (!stack.isEmpty()){Node cur = stack.pop();

            System.out.println(cur.e);
            // 判断是否还有叶子节点
            if (cur.right != null)
                stack.push(cur.right);
            if (cur.left != null)
                stack.push(cur.left);
        }
    }

理解非递归的实现逻辑、推导出前序递归的实现

  • 创建一个堆栈,我们把根节点 5 推入栈中,接下来我们就要访问 5 这个根节点了,所有把 5 从栈中推出。
  • 推出的元素有 {5},栈中的元素有 []。
  • 在推入 5 的子节点就是 3,8,我们先入后出,先推入 8 再推入 3,现在堆栈的元素有 [8,3],栈顶的 3 就是我们下一次要访问的节点所以把 3 推出。
  • 推出的元素有 {5,3},栈中的元素有 [8]。
  • 在推入 3 的子节点就是 2,4 继续先入后出,先推入 4 再推入 2,现在堆栈的元素有 [8,4,2],栈顶的 2 就是我们下一次要访问的节点所以把 2 推出。
  • 推出的元素有 {5,3,2},栈中的元素有 [8,4]。
  • 访问栈顶 4,由于 2 和 4 没有子节点。所以我们直接把栈顶中的 4 推出。
  • 推出的元素有 {5,3,2,4},栈中的元素有 [8]。
  • 访问栈顶 8 把 8 推出栈堆,再把 8 的子节点 7、9 推入栈中,先推入 9 后推入 7。
  • 推出的元素有 {5,3,2,4,8},栈中的元素有 [9,7]。
  • 访问 7,没有子节点,推出。
  • 推出的元素有 {5,3,2,4,8,7},栈中的元素有 [9]。
  • 访问 9,没有子节点,推出。
  • 推出的元素有 {5,3,2,4,8,7,9},栈中的元素有 []。

5、中序遍历

中序遍历,访问顺序是左根右,也就是先访问左子树,再到根节点,最后才到右子树。所以上图所示的访问顺序是 2、3、4、5、7、8、9。

二分搜索树中序遍历递归版与非递归版

    // 递归调用
    public void inOrder(){inOrder(root);
    }
    // 二分搜索树的中序遍历递归写法
    private void inOrder(Node root){if (root == null)
            return;

        inOrder(root.left);
        System.out.println(root.e);
        inOrder(root.right);
    }
    // 二分搜索树中序遍历给递归写法
    public void preInOrderNG(){
        // 创建栈, 和前序遍历类似
        Stack<Node> stack = new Stack<>();
        
        Node node = root;
        // 添加暂时完毕,开始 pop 元素
        while(node!=null || stack.size()>0 ){while(node!=null){stack.push(node);
                node = node.left;
            }
            // 一边 pop 并且一边进行判断,右结点不会 null 的,右子树,继续按照添加方法,将最左结点全部添加进去
            if(stack.size()>0){Node pop = stack.pop();
                System.out.print(pop.e+" ");
                if(pop.right!=null){node = pop.right;}
            }
        }

理解非递归的实现逻辑、推导出中序递归的实现

  • 首先我们把 5 这个节点推入栈中,再把 5 的左子节点 3 推入,再把 3 的 左子节点 2 推入,再把 2 的左子节点推入 (此时 2 的左子节点为空,node==null while 循环退出)。
  • 推出的元素有 {},栈中的元素有 [5,3,2]。
  • node 为空,但我们栈中还有元素,访问栈顶元素 2,并查看 2 是否有右子节点。没有则推出栈并结束循环。
  • 推出的元素有 {2},栈中的元素有 [5,3]。
  • 访问栈顶元素 3,把 3 推出栈中,并把 3 的右子节点 4 推入栈中,结束循环。
  • 推出的元素有 {2,3},栈中的元素有 [5]。
  • 访问栈顶元素 5,把 5 推出栈中。把 5 的右子节点 8 推入栈中,并把 8 的左子节点 7 推入栈中, 结束循环。
  • 推出的元素有 {2,3,5},栈中的元素有 [8,7]
  • 访问栈顶元素 7,并查看 2 是否有右子节点。没有则推出栈并结束循环。
  • 推出的元素有 {2,3,5,7},栈中的元素有 [8]。
  • 访问栈顶元素 8,把 8 推出栈中。把 8 的右子节点 9 推入栈中
  • 推出的元素有 {2,3,5,7,8},栈中的元素有 [9]。
  • 访问栈顶元素 9,并查看 2 是否有右子节点。没有则推出栈并结束循环。
  • 推出的元素有 {2,3,4,5,7,8,9},栈中的元素有 []。

6、后序遍历

中序遍历,访问顺序是左右根,也就是先访问左子树,再到右子树,最后才到根节点。所以上图所示的访问顺序是 2、4、3、7、9、8、5。

二分搜索树后序遍历递归版与非递归版

// 递归调用
public void postOrder() {postOrder(root);
} 

// 二分搜索树的后序遍历递归方法
private void postOrder(Node node){if (node == null)
        return;

    postOrder(node.left);
    postOrder(node.right);
    System.out.println(node.e);
} 

public void postOrderNG(){Stack<Node> stack = new Stack<>();
    // 利用一个 list 集合记录已将被遍历过的根节点,防止产生死循环
    ArrayList<Node> list = new ArrayList<>();
    Node node = root;
    Node proud; 
    int flag; 

    // 首页检查完树的左子树,再右子数,最后将根节点输出
    while (node != null || stack.size() > 0){
        // 将最左子树添加完毕
        while (node != null){stack.push(node);

            node = node.left;
        } 

        // 和中序遍历相似,为先输出左子节点,但是做节点输出完毕之后,不能直接将根节点弹出,而是必须先将右节点弹出,// 最后再将根节点弹出来,就会牵扯到一个根节点的访问状态的问题,是否已经被遍历过了
        if (stack.size() > 0){Node peek = stack.peek();
            if (peek.right != null){boolean con = list.contains(peek);
                if (con){Node pop = stack.pop();
                    System.out.println(pop.e);
                }else{list.add(peek);
                    node = peek.right;
                }
            }else {Node pop = stack.pop();
                System.out.println(pop.e);
            }

        }

    }
}

理解非递归的实现逻辑、推导出后序递归的实现

  • 把 5 这个节点推入栈中,再把 5 的左子节点 3 推入,再把 3 的左子节点 2 推入,再把 2 的左子节点推入 (此时 2 的左子节点为空,node==null while 循环退出)。
  • 推出的元素有 {},栈中的元素有 [5,3,2]。
  • node 为空但我们栈中还有元素,访问栈顶元素 2,并查看 2 是否有左子节点。没有则推出栈并结束循环。
  • 推出的元素有 {2},栈中的元素有 [5,3]。
  • 访问栈顶元素 3,3 的右子节为 4,判断 list 中是否有 3,没有则把 3 放入 list 中并把 node 赋值为 4 结束循环。
  • 推出的元素有 {2},栈中的元素有 [5,3]。
  • node 为 4,把 4 推入栈中,并访问栈顶元素 4,并查看 4 是否有右子节点。没有则推出栈并结束循环。
  • 推出的元素有 {2,4},栈中的元素有 [5,3]。
  • 访问栈顶元素 3,list 中有 3,把 3 的推出栈中并结束循环。
  • 推出的元素有 {2,4,3},栈中的元素有 [5]。
  • 访问栈顶元素 5,5 的右子节为 8,判断 lis t 中是否有 8,没有则把 5 放入 list 中并把 node 赋值为 8 结束循环。
  • 推出的元素有 {2,4,3},栈中的元素有 [5]。
  • node 为 8,把 8 推入栈中,并访问栈顶元 素 8,8 有左子节点为 7。把 7 推入栈中。
  • 推出的元素有 {2,4,3},栈中的元素有 [5,8,7]。
  • 访问栈顶元素 7,并查看 7 是否有右子节点。没有则推出栈并结束循环。
  • 推出的元素有 {2,4,3,7},栈中的元素有 [5,8]。
  • 访问栈顶元素 8,8 的右子节点为 9。判断 list 中是否有 9,没有则把 8 放入 list 中并把 node 并把 node 赋值为 9 结束循环。
  • 推出的元素有 {2,4,3,7},栈中的元素有 [5,8]。
  • node 为 9,把 9 推入栈中,并访问栈顶元素 9,并查看 9 是否有右子节点。没有则推出栈并结束循环。
  • 推出的元素有 {2,4,3,7,9},栈中的元素有 [5,8]。
  • 访问栈顶元素 8,list 中有 8,把 8 的推出栈中并结束循环。
  • 推出的元素有 {2,4,3,7,9,8},栈中的元素有 [5]。
  • node 为空栈中还有元素,访问栈顶元素 5,list 中有 5,把 5 的推出栈中并结束循环。
  • 推出的元素有 {2,4,3,7,9,8,5},栈中的元素有 []。

推荐阅读:

1、java | 什么是动态代理

2、SpringBoot | 启动原理

3、SpringBoot | 自动配置原理

正文完
 0