• 数据结构(一)-----树简介、树遍历


    概念

    树是由结点或顶点和边组成的(可能是非线性的)且不存在着任何环的一种数据结构。没有结点的树称为空(null或empty)树。一棵非空的树包括一个根结点,还(很可能)有多个附加结点,所有结点构成一个多级分层结构。
    1. 有且仅有一个特定的称为根的节点。
    2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
    树的标准结构:
    在上图中,节点1是根节点(root);节点5、6、7、8是树的末端,没有“孩子”,被称为叶子节点(leaf)。图中的虚线部分,是根节点1的其中一个子树。

    示例

    例如:小灰的“家谱”

    或者企业里的职级关系

     又比如一本书的目录

    常用树

    二叉树

    概念

    二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点。
    二叉树的结构如图所示:

    二叉树节点的两个孩子节点,一个被称为左孩子(left child),一个被称为右孩子(right child)。这两个孩子节点的顺序是固定的,就像人的左手就是左手,右手就是右手,不能够颠倒或混淆。

    分类

    1、满二叉树(完美二叉树)

    概念:一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。

    图示:

    2、完全二叉树

    概念:若设二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k 层所有的结点都连续集中在最左边,这就是完全二叉树。 

    图示:

    3、平衡二叉树

    概念:它或者是一颗空树,或它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。 

    图示:

    二叉树存储

    二叉树在内存中是怎样存储的呢?
    数据结构可以划分为物理结构和逻辑结构。二叉树属于逻辑结构,它可以通过多种物理结构来表达。
    二叉树可以用哪些物理存储结构来表达呢?
    1. 链式存储结构。
    2. 数组。

    链式存储

    一个节点最多可以指向左右两个孩子节点,所以二叉树的每一个节点包含3部分。
    1. 存储数据的data变量
    2. 指向左孩子的left指针
    3. 指向右孩子的right指针

    数组存储

    使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来。为什么这样设计呢?因为这样可以更方便地在数组中定位二叉树的孩子节点和父节点。假设一个父节点的下标是parent,那么它的左孩子节点下标就是2×parent + 1;右孩子节点下标就是2×parent + 2。反过来,假设一个左孩子节点的下标是leftChild,那么它的父节点下标就是(leftChild-1)/ 2。假如节点4在数组中的下标是3,节点4是节点2的左孩子,节点2的下标可以直接通过计算得出。节点2的下标 = (3-1)/2 = 1显然,对于一个稀疏的二叉树来说,用数组表示法是非常浪费空间的。

    二叉树应用

    查找

    非常适合查找的树-----二叉查找树(Binary Search) 

    概念:二叉查找树在二叉树的基础上增加了以下几个条件:
    1. 如果左子树不为空,则左子树上所有节点的值均小于根节点的值
    2. 如果右子树不为空,则右子树上所有节点的值均大于根节点的值
    3. 左、右子树也都是二叉查找树

    图示:

    在原本二叉树的基础上增加这些条件又会带来什么样的效果呢?

    例如查找值为4的节点,步骤如下。
    1. 访问根节点6,发现4<6。

    2. 访问节点6的左孩子节点3,发现4>3。

    3. 访问节点3的右孩子节点4,发现4=4,这正是要查找的节点。

    对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样的。
    代码示例:
    package arithmetic.com.ty.binary;
    
    public class BinarySearchTree {
        private Node tree;
    
        public Node find(int data) {
            Node p = tree;
            while (p != null) {
                if (data < p.data)
                    p = p.left;
                else if (data > p.data)
                    p = p.right;
                else
                    return p;
            }
            return null;
        }
    
        public static class Node {
            private int data;
            private Node left;
            private Node right;
    
            public Node(int data) {
                this.data = data;
            }
        }
    }

    维持相对顺序

    二叉查找树要求左子树小于父节点,右子树大于父节点,正是这样保证了二叉树的有序性。因此二叉查找树还有另一个名字——二叉排序树(binary sort tree)。新插入的节点,同样要遵循二叉排序树的原则。例如插入新元素5,由于5<6,5>3,5>4,所以5最终会插入到节点4的右孩子位置。

    再如插入新元素10,由于10>6,10>8,10>9,所以10最终会插入到节点9的右孩子位置。

    但是二叉查找树也会存在一个问题,例如:

    在二叉查找树中依次插入9、8、7、6、5、4

    查询节点的时间复杂度也退化成了O(n)。

    1、插入

    插入代码示例:
        public void insert(int data) {
            if (tree == null) {
                tree = new Node(data);
                return;
            }
            Node p = tree;
            while (p != null) {
                if (data > p.data) {
                    if (p.right == null) {
                        p.right = new Node(data);
                        return;
                    }
                    p = p.right;
                } else { // data < p.data
                    if (p.left == null) {
                        p.left = new Node(data);
                        return;
                    }
                    p = p.left;
                }
            }
        }

    2、删除

    针对要删除节点的子节点个数的不同,我们需要分三种情况来处理。
    第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。
    第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。
    第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

        public void delete(int data) {
            //p指向要删除的节点,初始化指向根节点
            Node p = tree;
            //pp记录的是 p的父节点
            Node pp = null;
            while (p != null && p.data != data) {
                pp = p;
                if (data > p.data) {
                    p = p.right;
                }else {
                    p = p.left;
                }
            }
            if (p == null) {
                // 没有找到
                return;
            }
                
    
            // 要删除的节点有两个子节点
            if (p.left != null && p.right != null) { // 查找右子树中最小节点
                Node minP = p.right;
                Node minPP = p; // minPP 表示 minP 的父节点
                while (minP.left != null) {
                    minPP = minP;
                    minP = minP.left;
                }
                // 将 minP 的数据替换到 p 中
                p.data = minP.data; 
                // 下面就变成了删除 minP 了
                p = minP; 
                pp = minPP;
            }
    
            // 删除节点是叶子节点或者仅有一个子节点
            Node child; // p 的子节点
            if (p.left != null) {
                child = p.left;
            }else if (p.right != null) {
                child = p.right;
            }else {
                child = null;
            }
                
            if (pp == null) {
                // 删除的是根节点
                tree = child; 
            }else if (pp.left == p) {
                pp.left = child;
            }else {
                pp.right = child;
            }
        } 

    二叉树遍历

    遍历方式

    从节点之间位置关系的角度来看,二叉树的遍历分为4种。
    1. 前序遍历-----根结点-->左子树-->右子树
    2. 中序遍历-----左子树-->根结点-->右子树-----中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。
    3. 后序遍历-----左子树-->右子树-->根结点
    4. 层序遍历
    从更宏观的角度来看,二叉树的遍历归结为两大类。
    1. 深度优先遍历(前序遍历、中序遍历、后序遍历)
    2. 广度优先遍历(层序遍历)

    深度优先遍历

    概念:所谓深度优先,顾名思义,就是偏向于纵深,“一头扎到底”的访问方式。

    前序遍历

    二叉树的前序遍历,输出顺序是根节点、左子树、右子树。

     示例:

    1. 首先输出的是根节点1。 

    2. 由于根节点1存在左孩子,输出左孩子节点2。

    3. 由于节点2也存在左孩子,输出左孩子节点4。

    4. 节点4既没有左孩子,也没有右孩子,那么回到节点2,输出节点2的右孩子节点5。 

    5. 节点5既没有左孩子,也没有右孩子,那么回到节点1,输出节点1的右孩子节点3。

    6. 节点3没有左孩子,但是有右孩子,因此输出节点3的右孩子节点6。 

    中序遍历

    二叉树的中序遍历,输出顺序是左子树、根节点、右子树。

    1. 首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不再有左孩子的节点,并输出该节点。显然,第一个没有左孩子的节点是节点4。 

    2. 依照中序遍历的次序,接下来输出节点4的父节点2。

    3. 再输出节点2的右孩子节点5。 

    4. 以节点2为根的左子树已经输出完毕,这时再输出整个二叉树的根节点1。

    5. 由于节点3没有左孩子,所以直接输出根节点1的右孩子节点3。 

    6. 最后输出节点3的右孩子节点6。

    后序遍历

    二叉树的后序遍历,输出顺序是左子树、右子树、根节点。

    深度优先遍历代码示例:

    package arithmetic.com.ty.binary;
    
    import java.util.Arrays;
    import java.util.LinkedList;
    
    public class BinaryTreeTraversal {
    
        public static void main(String[] args) {
            LinkedList<Integer> inputList = new LinkedList<Integer>(
                    Arrays.asList(new Integer[] { 3, 2, 9, null, null, 10, null, null, 8, null, 4 }));
            TreeNode treeNode = createBinaryTree(inputList);
            System.out.println(" 前序遍历:");
            preOrderTraveral(treeNode);
            System.out.println(" 中序遍历:");
            inOrderTraveral(treeNode);
            System.out.println(" 后序遍历:");
            postOrderTraveral(treeNode);
        }
        
        /**
         * 构建二叉树
         * 
         * @param inputList 输入序列
         */
        public static TreeNode createBinaryTree(LinkedList<Integer> inputList) {
            TreeNode node = null;
            if (inputList == null || inputList.isEmpty()) {
                return null;
            }
            Integer data = inputList.removeFirst();
            if (data != null) {
                node = new TreeNode(data);
                /**
                 * 1、循环LinkedList:{3, 2, 9, null, null, 10, null, null, 8, null, 4}
                 * 2、如果List中不为null是数据,都会封装成TreeNode,并递归创建leftChild
                 * 3、当遇到index为3的null时,创建leftChild=createBinaryTree(inputList)的递归返回,此时leftChild为9
                 * 4、然后开始执行rightChild = createBinaryTree(inputList),但是index为4的数据依然为null,因此9的兄弟节点为null
                 * 5、然后开始往回执行,此时leftChild为2,开始执行rightChild = createBinaryTree(inputList),因此2的右子树为10
                 * .
                 * .
                 * .
                 */
                node.leftChild = createBinaryTree(inputList);
                node.rightChild = createBinaryTree(inputList);
            }
            return node;
        }
    
        /** 
         * 二叉树前序遍历 
         * @param node 二叉树节点 
         */
        public static void preOrderTraveral(TreeNode node) {
            if (node == null) {
                return;
            }
            System.out.println(node.data);
            /**
             * 1、leftChild也可能有leftChild以及rightChild,因此需要递归下去
             */
            preOrderTraveral(node.leftChild);
            preOrderTraveral(node.rightChild);
        }
    
        /** 
         * 二叉树中序遍历 
         * @param node 二叉树节点 
         */
        public static void inOrderTraveral(TreeNode node) {
            if (node == null) {
                return;
            }
            inOrderTraveral(node.leftChild);
            System.out.println(node.data);
            inOrderTraveral(node.rightChild);
        }
    
        /** 
         * 二叉树后序遍历 
         * @param node 二叉树节点 
         */
        public static void postOrderTraveral(TreeNode node) {
            if (node == null) {
                return;
            }
            postOrderTraveral(node.leftChild);
            postOrderTraveral(node.rightChild);
            System.out.println(node.data);
        }
    
        /** 
         * 二叉树节点 
         */
        private static class TreeNode {
            int data;
            TreeNode leftChild;
            TreeNode rightChild;
    
            TreeNode(int data) {
                this.data = data;
            }
        }
    }

    广度优先遍历

    其基本思想是尽最大程度辐射能够覆盖的节点,并对其进行访问。 

    上图就是一个二叉树的层序遍历,每个节点左侧的序号代表该节点的输出顺序。可是,二叉树同一层次的节点之间是没有直接关联的,如何实现这种层序遍历呢?这里同样需要借助一个数据结构来辅助工作,这个数据结构就是队列。
    1. 根节点1进入队列。

    2. 节点1出队,输出节点1,并得到节点1的左孩子节点2、右孩子节点3。让节点2和节点3入队。 

    3. 节点2出队,输出节点2,并得到节点2的左孩子节点4、右孩子节点5。让节点4和节点5入队。

    4. 节点3出队,输出节点3,并得到节点3的右孩子节点6。让节点6入队。

    5. 节点4出队,输出节点4,由于节点4没有孩子节点,所以没有新节点入队。

    6. 节点5出队,输出节点5,由于节点5同样没有孩子节点,所以没有新节点入队。

    7.节点6出队,输出节点6,节点6没有孩子节点,没有新节点入队。

    广度优先代码示例:

        public static void levelOrderTraversal(TreeNode root) {
            Queue<TreeNode> queue = new LinkedList<TreeNode>();
            queue.offer(root);
            while (!queue.isEmpty()) {
                TreeNode node = queue.poll();
                System.out.println(node.data);
                if (node.leftChild != null) {
                    queue.offer(node.leftChild);
                }
                if (node.rightChild != null) {
                    queue.offer(node.rightChild);
                }
            }
        }
  • 相关阅读:
    可以将class文件反编译成java文件
    软件开发者面试百问
    马云说
    反编译工具jad的使用(将*.class文件变成*.java文件,附带jad.zip包)[转]
    Rose与PowerDesigner:两款建模工具对比分析比较[转]
    Javascript中最常用的55个经典技巧
    如何将.class文件转换成.java文件——JAVA反编译工具总结[转]
    SQL Server补丁版本的检查
    SQL Server 2000 从哪里看是哪个版本
    什么是模式?什么是框架?软件为什么要分层?
  • 原文地址:https://www.cnblogs.com/alimayun/p/12778754.html
Copyright © 2020-2023  润新知