• 树及二叉树笔记


    数据结构之树(Tree)

    笔者本来是准备看JDK HashMap源码的,结果深陷红黑树中不能自拔,特撰此篇以加深对树的理解

    定义

    首先来看树的定义:

    (Tree)是n(n≥0)个节点的有限集。n = 0 时称为空树。在任意一棵非空树中:1、有且仅有一个特定的节点称为根(Root)的节点。2、当n > 1时,其余节点可分为m(m > 0)个互不相交的有限集T1、T2、T3、……Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)。
    树
    节点的度:节点拥有子树数称为节点的度。(也就是该节点拥有的子节点数)度为0的节点称为非终端节点或分支节点,除根节点外,分支节点也称为内部节点,树的度是树内各节点度的最大值。
    节点的度
    节点的层次与深度:节点的层次(Level)从根开始定义,根为第一层,根的孩子为第二层。若A节点在第l层,则其子树的根就在第l+1层(即A节点的子节点)。其双亲在同一层的节点互为堂兄弟。树中节点的最大层次称为树的深度(Depth)或高度。

    树的存储结构

    简单的顺序存储不能满足树的实现,需要结合顺序存储和链式存储实现。
    三种表示方法:

    • 双亲表示法
    • 孩子表示法
    • 孩子兄弟表示法
    • 孩子双亲表示法

    双亲表示法:

    在每个节点中,附设一个指示器,指示其双亲节点在链表中的位置。
    单个
    树结构
    缺点:找父节点容易,找子节点难。

    孩子表示法:

    方案一:
    孩子表示法
    缺点:大量空指针造成浪费
    方案二:
    孩子表示法二
    缺点:维护困难,不易实现。
    也未解决找父节点容易,找子节点难的问题。

    孩子兄弟表示法

    任意一棵树,他的结点的第一个孩子如果存在就是唯一结点,他的右兄弟如果存在,也是唯一的,因此,我们设置两个指针,分别指向该结点的第一个孩子和该结点的右兄弟(I不是G的右兄弟)
    孩子兄弟表示法

    孩子双亲表示法

    用顺序存储和链式存储组合成散列链表,可以通过获取头指针获取该元素父节点。
    双亲一
    不太清楚?那就将元素的父节点单独列出来:
    双亲二


    二叉树

    二叉树是每个结点最多有两个子树的树结构

    斜树

    所有节点都只有左子树的二叉树叫做左斜树。所有节点都只有右子树的二叉树叫做右斜树。两者统称为斜树。

    斜树
    线性表结构可以理解为树的一种表达形式。

    满二叉树

    所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。

    满二叉树

    完全二叉树

    对于一个有n个节点的二叉树按层序编号,如果编号为i(1 ≤ i ≤ n)的节点与同样深度的满二叉树中编号为i的节点在二叉树中位置完全相同则该二叉树称为完全二叉树。
    完全二叉树完全二叉树

    二叉树的性质:

    • 性质1:二叉树第i层上的结点数目最多为 2i-1 ( i ≥1 )。
    • 性质2:深度为k的二叉树至多有2k-1个结点( k ≥ 1 )。
    • 性质3:包含n个结点的二叉树的深度至少为log2 (n+1)。
    • 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
    • 性质5:对于一个有n个节点的完全二叉树(深度为log2 (n+1)),节点按层序编号(从第一层到log2 (n+1),每层从左到右),对任意一个节点i有:对于第i个非根节点,其父节点是第i/2个。

    Java实现二叉树及其遍历

    这里我们采用的结构是二叉链表:
    二叉链表

    新建一个BinaryTree的类,并初始化一个测试树:

    class BinaryTree
    public class BinaryTree {
        private Node root=null;
    
        public void createTestBinaryTree(){
            /**
             *            A
             *           /  
             *          B    C
             *        /       
             *       D    E     F
             *
             */
            root=new Node(1,"A");
            Node nodeB=new Node(2,"B");
            Node nodeC=new Node(3,"C");
            Node nodeD=new Node(4,"D");
            Node nodeE=new Node(5,"E");
            Node nodeF=new Node(6,"F");
            root.leftChild=nodeB;
            root.rightChild=nodeC;
            nodeB.leftChild=nodeD;
            nodeB.rightChild=nodeE;
            nodeC.rightChild=nodeF;
            //这么写太蠢了。以后再更新二叉树的构建。。。
        }
    
        //由数组构造二叉树。
        public void createTestBinaryTree2(){
        /**
            *            A
            *           /  
            *          B    C
            *        /     /
            *       D    E F
            *
            */
    
        String[] strings=new String[]{"A","B","C","D","E","F"};
        List<Node> nodes=new ArrayList<>();
        for (int i = 0; i < strings.length; i++) {
            Node node = new Node(i,strings[i]);
            nodes.add(node);
        }
        root=nodes.get(0);
        //如果root为第一个节点,则第i个节点的左子节点是第2i个
        //这里i是从0开始的
        for (int i = 0; i < nodes.size(); i++) {
            if ((2*i+1)<nodes.size()){
                nodes.get(i).leftChild=nodes.get(2*i+1);
            }
            if ((2*i+2)<nodes.size()){
                nodes.get(i).rightChild=nodes.get(2*i+2);
            }
        }
    }
    

    根据该结构构造其节点类与get方法:

    class Node
    public class Node<T>{
        private int index;
        private T data;
        private Node leftChild;
        private Node rightChild;
    
        public Node(int index,T data){
            this.index=index;
            this.data=data;
            this.leftChild=null;
            this.rightChild=null;
        }
    
        public int getIndex() {
            return index;
        }
    
        public T getData() {
            return data;
        }
    }
    

    然后是树深度和节点数的方法:

    height方法和size方法
    public int height(){
        return getHeight(root);
    }
    
    private int getHeight(Node node){
        if (node ==null){
            return 0;
        }else {
            int i=getHeight(node.leftChild);
            int j=getHeight(node.rightChild);
            return i>j?i+1:j+1;//遍历1层就增加了1,i和j哪个大返回哪个
        }
    }
    public int size(){
        return getsize(root);
    }
    
    private int getsize(Node node){
        if (node==null){
            return 0;
        }else {
            return 1+getsize(node.leftChild)+getsize(node.rightChild);//遍历一个节点增加1,最后总数就是节点数
        }
    }
    

    接着就是四种遍历方法:先序遍历、中序遍历、后序遍历,层序遍历。先序、中序、后序是指root节点出现的方式。比如一个只有三个节点的二叉树,先序就是先遍历读取根节点,再左再右;中序就是先左,后根,然后右,后序先左后右,最后根。层序就是从上到下、从左到右依次读取。
    比较简单容易理解的方式是递归:

    先序遍历(递归)
    public void preOrder(){
        preOrder(root);
        return;
    }
    
    private void preOrder(Node node){
        if (node==null){
            return;
        }else{
            System.out.println("先序遍历:"+node.data);
        }
        if (node.leftChild!=null){
            preOrder(node.leftChild);
        }
        if (node.rightChild!=null){
            preOrder(node.rightChild);
        }
    }
    
    …………
    中序遍历(递归)
    public void midOrder(){
        midOrder(root);
        return;
    }
    
    private void midOrder(Node node){
        if (node.leftChild!=null){
            midOrder(node.leftChild);
        }
        if (node==null){
            return;
        }else{
            System.out.println("中序遍历:"+node.data);
        }
        if (node.rightChild!=null){
            midOrder(node.rightChild);
        }
    }
    
    …………
    后序遍历(递归)
    public void postOrder(){
        postOrder(root);
        return;
    }
    
    private void postOrder(Node node) {
        if (node.leftChild != null) {
            postOrder(node.leftChild);
        }
    
        if (node.rightChild != null) {
            postOrder(node.rightChild);
        }
    
        if (node==null){
            return;
        }else{
            System.out.println("后序遍历:"+node.data);
        }
    }
    

    三种遍历都能用递归的方式,但是递归运行效率较低,并且树大了之后很容易溢出,可以用栈和循环代替递归:
    (我为什么写这么啰嗦,以为博客园的折叠不能挨着,中间必须有中文字符!!!)

    先序遍历(栈)
    public void nonRecPreOrder(){
        nonRecPreOrder(root);
        return;
    }
    
    private void nonRecPreOrder(Node node){
        if (node==null){
            return;
        }
        Stack<Node> stack=new Stack<>();
        stack.push(node);
        while (!stack.isEmpty()){
            Node theNode=stack.pop();
            if (theNode.rightChild!=null){
                stack.push(theNode.rightChild);
            }
            if (theNode.leftChild!=null){
                stack.push(theNode.leftChild);
            }
            System.out.println("先序遍历(栈):"+theNode.data);
        }
    }
    
    …………
    先序遍历2(栈)
    public void nonRecPreOrder2() {
        nonRecPreOrder2(root);
        return;
    }
    
    private void nonRecPreOrder2(Node node) {
        //非递归实现
        Stack<Node> stack = new Stack<>();
        while (node != null || !stack.isEmpty()) {
            while (node != null) {
                System.out.println("先序遍历2(栈):" + node.data);
                stack.push(node);
                node = node.leftChild;
            }
            if (!stack.isEmpty()) {
                node = stack.pop();
                node = node.rightChild;
            }
        }
    }
    
    
    …………
    中序遍历(栈)
    public void nonRecMidOrder(){
        nonRecMidOrder(root);
        return;
    }
    private void nonRecMidOrder(Node node){
        if (node==null){
            return;
        }
        Stack<Node> stack=new Stack<>();
        Node p = root;//辅助节点
        stack.add(p);
        while(!stack.isEmpty()) {
            //只要你有左孩子,就将左孩子压入栈中
            if(p!=null &&  p.leftChild!=null) {
                stack.add(p.leftChild);
                p=p.leftChild;
            }else {
                p = stack.pop();//弹出栈顶节点  左孩子--->根节点
                System.out.println("中序遍历(栈)"+p.data);//访问
                if(p!=null && p.rightChild!=null) {//如果栈点元素有右孩子的话,将有节点压入栈中
                    stack.push(p.rightChild);
                    p = p.rightChild;
                }else
                    p = null;//p=stk.pop;已经访问过p了,p设置为null
            }
        }
    }
    
    
    …………
    中序遍历2(栈)
    public void nonRecMidOrder2() {
        nonRecMidOrder2(root);
        return;
    }
    
    private void nonRecMidOrder2(Node node) {
        //中序遍历费递归实现
        Stack<Node> stack = new Stack<>();
        while (node != null || !stack.isEmpty()) {
            while (node != null) {
                stack.push(node);
                node = node.leftChild;
            }
            if (!stack.isEmpty()) {
                node = stack.pop();
                System.out.println(node.data);
                node = node.rightChild;
            }
        }
    }
    
    
    …………
    后序遍历(栈)
    public void nonRecPostOrder() {
        nonRecPostOrder(root);
        return;
    }
    
    private static void nonRecPostOrder(Node biTree) {
        //后序遍历非递归实现
        int left = 1;//在辅助栈里表示左节点
        int right = 2;//在辅助栈里表示右节点
        Stack<Node> stack = new Stack<>();
        Stack<Integer> stack2 = new Stack<>();//辅助栈,用来判断子节点返回父节点时处于左节点还是右节点。
    
        while (biTree != null || !stack.empty()) {
            while (biTree != null) {//将节点压入栈1,并在栈2将节点标记为左节点
                stack.push(biTree);
                stack2.push(left);
                biTree = biTree.leftChild;
            }
    
            while (!stack.empty() && stack2.peek() == right) {//如果是从右子节点返回父节点,则任务完成,将两个栈的栈顶弹出
                stack2.pop();
                System.out.println(stack.pop().data);
            }
    
            if (!stack.empty() && stack2.peek() == left) {//如果是从左子节点返回父节点,则将标记改为右子节点
                stack2.pop();
                stack2.push(right);
                biTree = stack.peek().rightChild;
            }
        }
    }
    
    
    …………
    层次遍历(队列)
    
    public void levelOder(){
        levelOrder(root);
        return;
    }
    
    private void levelOrder(Node node){
    //层次遍历
    /*
    1.对于不为空的结点,先把该结点加入到队列中
    2.从队中拿出结点,如果该结点的左右结点不为空,就分别把左右结点加入到队列中
    3.重复以上操作直到队列为空
    */
        if(node == null)
            return;
        LinkedList<Node> list = new LinkedList<Node>();
        list.add(node);
        Node currentNode;
        while(!list.isEmpty()) {
            currentNode = list.poll();
            System.out.println("层序遍历(队列)"+currentNode.data);
            if(currentNode.leftChild != null)
                list.add(currentNode.leftChild);
            if(currentNode.rightChild != null)
                list.add(currentNode.rightChild);
        }
    }
    
    

    这里,后序遍历和层次遍历参考了这个博客,特别是后序遍历,笔者对于递归函数非递归化还有待提高,递归函数虽然简单易懂,但是数据量大了之后特别容易爆栈,所有的递归函数都可以非递归化,因此优先考虑非递归函数。
    树是一种重要的数据结构类型,通过对二叉树的研究,笔者对栈的用法有了更深的理解。

  • 相关阅读:
    创建FLASK,同步docker
    FLASK Buleprint
    restful api
    Angular JS
    线程日志
    将项目部署到linux下的docker容器中
    安装和卸载docker
    学习目录总编
    Ansible
    装饰器
  • 原文地址:https://www.cnblogs.com/lixin-link/p/10877128.html
Copyright © 2020-2023  润新知