【数据结构】——二叉树详解
目录
- 一、二叉树的定义
- 二、二叉树的形态
- 三、二叉树的性质
- 四、二叉树的存储
- 五、二叉树的创建与遍历(递归)
- 六、二叉树的非递归遍历
- 七、二叉树的层序遍历(递归与非递归)
- 八、四种遍历方式的时间和空间复杂度
- 九、根据遍历序列确定二叉树
- 十、二叉树遍历算法的应用
一、二叉树的定义
二叉树(Binary Tree) 是由n个结点构成的有限集(n≥0),n=0时为空树,n>0时为非空树。对于非空树TTT:
- 有且仅有一个根结点;
- 除根结点外的其余结点又可分为两个不相交的子集TLT_LTL和TRT_RTR,分别称为TTT的左子树和右子树,且TLT_LTL和TRT_RTR本身又都是二叉树。
很明显该定义属于递归定义,所以有关二叉树的操作使用递归往往更容易理解和实现。
从定义也可以看出二叉树与一般树的区别主要是两点,一是每个结点的度最多为2;二是结点的子树有左右之分,不能随意调换,调换后又是一棵新的二叉树。
二、二叉树的形态
五种基本形态
从上面二叉树的递归定义可以看出,二叉树或为空,或为一个根结点加上两棵左右子树,因为两棵左右子树也是二叉树也可以为空,所以二叉树有5种基本形态:
三种特殊形态
三、二叉树的性质
任意二叉树第 iii 层最大结点数为2i−1。(i≥1)2^{i-1}。(i≥1)2i−1。(i≥1)
归纳法证明。深度为 kkk 的二叉树最大结点总数为2k−1。(k≥1)2^k-1。(k≥1)2k−1。(k≥1)
证明:∑i=1k2i−1=2k−1\sum_{i=1}^k 2^{i-1} =2^k-1∑i=1k2i−1=2k−1对于任意二叉树,用n0,n1,n2n_0,n_1,n_2n0,n1,n2分别表示叶子结点,度为1的结点,度为2的结点的个数,则有关系式n0=n2+1n_0=n_2+1n0=n2+1。
证明:总结点个数n=n0+n1+n2n=n_0+n_1+n_2n=n0+n1+n2;总结点中除根结点外,其余各结点都有一个分支进入,设mmm为分支总数,则有n=m+1n=m+1n=m+1,又因为这些分支都是由度为1或2的结点射出的,所以有m=n1+2n2m=n_1+2n_2m=n1+2n2,于是有n=n1+2n2+1n=n_1+2n_2+1n=n1+2n2+1;最后将关于nnn的两个关系式化简得证。nnn个结点完全二叉树深度为⌊log2n⌋+1\lfloor log_2 n \rfloor+1⌊log2n⌋+1。
证明:设深度kkk,则有2k−1≤n<2k⇒k−1≤log2n<k⇒k=⌊log2n⌋+12^{k-1}≤n<2^k⇒k-1≤log_ 2n<k⇒k=\lfloor log_2 n \rfloor+12k−1≤n<2k⇒k−1≤log2n<k⇒k=⌊log2n⌋+1性质5其实描述的是完全二叉树中父子结点间的逻辑对应关系。 假如对一棵完全二叉树的所有顶点按层序遍历的顺序从1开始编号,对于编号后的结点iii:
(1)i=1i=1i=1时表示iii是根结点;
(2)i>1i>1i>1时:①iii的根结点为i2\frac i22i。②若2i>n2i>n2i>n,结点iii无左孩子,且为叶子结点。③若2i+1>n2i+1>n2i+1>n,结点iii无右孩子,可能为叶子结点。
当然如果完全二叉树的顶点从0开始编号,那么上述关系就要相应修改一下。
四、二叉树的存储
存的目的是为了取,而取的关键在于如何通过父结点拿到它的左右子结点,不同存储方式围绕的核心也就是这。
顺序存储
使用一组地址连续的存储单元存储,例如数组。为了在存储结构中能得到父子结点之间的映射关系,二叉树中的结点必须按层次遍历的顺序存放。具体是:
- 对于完全二叉树,只需要自根结点起从上往下、从左往右依次存储。
- 对于非完全二叉树,首先将它变换为完全二叉树,空缺位置用某个特殊字符代替(比如#),然后仍按完全二叉树的存储方式存储。
假设将一棵二叉树按此方式存储到数组后,左子结点下标=2倍的父结点下标+1,右子节点下标=2倍的父结点下标+2。若数组某个位置处值为#,代表此处对应的结点为空。
可以看出顺序存储非常适合存储接近完全二叉树类型的二叉树,对于一般二叉树有很大的空间浪费,所以对于一般二叉树,一般用下面这种链式存储。
链式存储
对每个结点,除数据域外再多增加左右两个指针域,分别指向该结点的左孩子和右孩子结点,再用一个头指针指向根结点。对应的存储结构:
五、二叉树的创建与遍历(递归)
二叉树由三个基本单元组成:根结点,左子树,右子树,因此存在6种遍历顺序,若规定先左后右,则只有以下3种:
1.先序遍历
若二叉树为空,则空操作;否则:
(1)访问根结点
(2)先序遍历左子树
(3)先序遍历右子树
2.中序遍历
若二叉树为空,则空操作;否则:
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树
3.后序遍历
若二叉树为空,则空操作;否则:
(1)后序遍历左子树
(2)后序遍历右子树
(3)访问根结点
从上可以看出先中后其实是相对根结点来说。
对于下面这棵二叉树,其遍历顺序:
先序:ABDEHCFIG
中序:DBHEAFICG
后序:DHEBIFGCA
下面是将以下二叉树的顺序存储[A,B,C,D,E,F,G,#,#,H,#,#,I]转换为链式存储的代码,结点不存在用字符#表示,并分别遍历。
java代码
class TreeNode {char data;TreeNode lchild; // 左子结点指针TreeNode rchild; // 右子结点指针public TreeNode(char data) {this.data = data;}
}public class BinaryTree {// 将二叉树的顺序存储变为链式存储public static TreeNode buildTree(char[] arr, int index) {TreeNode root = null;if (index < arr.length) {if (arr[index] == '#') {return null;}root = new TreeNode(arr[index]);root.lchild = buildTree(arr, 2 * index + 1);root.rchild = buildTree(arr, 2 * index + 2);}return root;}// 先序遍历public static void preOrderTraverse(TreeNode T) {if (T != null) {System.out.print(T.data);preOrderTraverse(T.lchild);preOrderTraverse(T.rchild);}}// 中序遍历public static void inOrderTraverse(TreeNode T) {if (T != null) {inOrderTraverse(T.lchild);System.out.print(T.data);inOrderTraverse(T.rchild);}}// 后序遍历public static void postOrderTraverse(TreeNode T) {if (T != null) {postOrderTraverse(T.lchild);postOrderTraverse(T.rchild);System.out.print(T.data);}}public static void main(String[] args) {char[] arr = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', '#', '#', 'H', '#', '#', 'I' };TreeNode T = buildTree(arr, 0);System.out.print("先序遍历-->");preOrderTraverse(T);System.out.println();System.out.print("中序遍历-->");inOrderTraverse(T);System.out.println();System.out.print("后序遍历-->");postOrderTraverse(T);System.out.println();}
}
仔细观察先序、中序、后序的结点访问顺序可以发现:
六、二叉树的非递归遍历
以上二叉树的创建及遍历都是通过递归实现,由三(三)、利用栈将递归转换为非递归可以将二叉树的递归遍历转换为非递归。
非递归的先序遍历
public static void preOrderWithoutRecursion(TreeNode T) {Stack<TreeNode> s = new Stack<>();while (T != null || !s.empty()) {if (T != null) {System.out.print(T.data);s.push(T);T = T.lchild;} else {T = s.pop();T = T.rchild;}}
}
非递归的中序遍历
public static void inOrderWithoutRecursion(TreeNode T) {Stack<TreeNode> s = new Stack<>();while (T != null || !s.empty()) {if (T != null) {s.push(T);T = T.lchild;} else {T = s.pop();System.out.print(T.data);T = T.rchild;}}
}
非递归的后序遍历
/** 后序遍历要比先序中序遍历复杂一些,原因是需要判断上次访问的结点是位于左子树还是位于右子树。* 若位于左子树,需要跳过根结点并进入右子树,回头再访问根结点。* 若位于右子树,则直接访问根结点。*/
public static void postOrderWithoutRecursion(TreeNode T) {Stack<TreeNode> s = new Stack<>();// 保存上次访问的结点TreeNode lastVisit = null; // 首先一路压栈进入到左子树最左下端while (T != null) {s.push(T);T = T.lchild;}while (!s.empty()) {T = s.pop();// 一个根结点被访问的前提是:无右子树或右子树已被访问过if (T.rchild == null || T.rchild == lastVisit) {System.out.print(T.data);// 修改最近访问的结点lastVisit = T;} else {// 否则根结点再次入栈并直接进入到右子树s.push(T);T = T.rchild;// 并一路压栈进入到右子树的最左下端while (T != null) {s.push(T);T = T.lchild;}}}
}
总结这种改写其实就是代码跟着思维走,模拟递归过程,用的仍然是递归的思想。
这里推荐一篇博客,作者将二叉树的非递归遍历讲解的透彻易懂,博客地址
七、二叉树的层序遍历(递归与非递归)
层次遍历是指从二叉树的根结点开始,按从上到下,从左到右的顺序遍历,也就是按从左往右的顺序先遍历第一层即根结点,再遍历第二层,…,直至最后一层。
还是上面的那棵二叉树,层序遍历顺序为:ABCDEFGHI。
递归
/** 递归相比非递归优点就是思路清晰,代码易读,缺点是系统开销大。* 但是层序遍历的递归实现反而使算法更复杂。* 下面代码只提供层序遍历的一种递归思路,并不能算是真正意义上的递归。*/
public static void levelOrderWithRecursion(TreeNode T) {if (T == null) {return;}// 获取树的深度int depth = depth(T);// 对每层循环遍历for (int i = 1; i <= depth; i++) {levelOrder(T, i);}
}private static void levelOrder(TreeNode T, int level) {// if语句中去掉条件level<1不影响遍历结果,但对指定层遍历完成后仍然会继续递归调用,直到最下面结点的左右子结点为空结束递归if (T == null || level < 1) {return;}if (level == 1) {System.out.print(T.data);}levelOrder(T.lchild, level - 1);levelOrder(T.rchild, level - 1);
}private static int depth(TreeNode T) {if (T == null) {return 0;}int l = depth(T.lchild);int r = depth(T.rchild);if (l > r) {return l + 1;} else {return r + 1;}
}
非递归
/** 非递归遍历需要借助队列完成。* 因为对同一层的结点A和结点B,如果A在B的左边先被遍历,那么下一层中A的孩子也一定在B的孩子左边先被遍历。*/
public static void levelOrderTraverse(TreeNode T) {if (T == null)return;Queue<TreeNode> q = new LinkedList<>();TreeNode node = null;// 首先根结点入队q.add(T);while (!q.isEmpty()) {// 队头出队node = q.remove();System.out.print(node.data);// 左右子结点入队if (node.lchild != null) {q.add(node.lchild);}if (node.rchild != null) {q.add(node.rchild);}}
}
八、四种遍历方式的时间和空间复杂度
四种遍历方式中,无论是递归还是非递归,二叉树的每个结点都只被访问一次,所以对于nnn个结点的二叉树,时间复杂度均为O(n)Ο(n)O(n)。
除层序遍历外的其它三种遍历方式,所需辅助空间为遍历过程中栈的最大容量,也就是二叉树的深度,所以空间复杂度与二叉树的形状有关。对于nnn个结点的二叉树,最好情况下是完全二叉树,空间复杂度O(log2n)Ο(log_2 n)O(log2n);最坏情况下对应斜二叉树,空间复杂度O(n)Ο(n)O(n)。层序遍历所需辅助空间为遍历过程中队列的最大长度,也就是二叉树的最大宽度kkk,所以空间复杂度O(k)Ο(k)O(k)。
九、根据遍历序列确定二叉树
从上面二叉树的遍历可以看出:如果二叉树中各结点值不同,那么先序、中序、以及后序遍历所得到的序列也是唯一。反过来,如果知道任意两种遍历序列,能否唯一确定这棵二叉树?
首先明确:只有先序中序,后序中序这两种组合可以唯一确定,先序后序不能。
如果知道的是先序中序,那么根据先序遍历先访问根结点的特点在中序序列中找到这个结点,该结点将中序序列分成两部分,左边是这个根结点的左子树中序序列,右边是这个根结点的右子树中序序列。根据这个序列,在先序序列中找到对应的左子序列和右子序列,根据先序遍历先访问根的特点又可以确定两个子序列的根结点,并根据这两个根结点可以将上次划分的两个中序子序列继续划分,如此下去,直到取尽先序序列中的结点时,便可得到对应的二叉树。
后序中序同理,区别是先序序列中第一个结点是根结点,而后序序列中最后一个结点是根结点。
先序和后序不能确定的原因在于不能确定根结点的左右子树。 比如先序序列AB,后序序列BA,根结点是A可以确定,但B是A的左子结点还是右子结点就确定不了了。
具体代码实现参考牛客网上《剑指Offer》中的一道题。查看
题目描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解题代码:
/**下面代码来源原题下面的解答,思路很强!*用的是递归思想,每次将左右两棵子树当成新的子树进行处理,中序的左右子树开始结束索引很好找。*前序的左右子树的开始结束索引通过根结点与中序中左右子树的大小来计算。*然后递归求解,直到startPre>endPre||startIn>endIn说明子树整理完到。方法每次返回左子树和右子树的根结点。*事实上startPre>endPre||startIn>endIn两条件任选其一即可。为什么?仔细想想!*/
public class ReBuildBinaryTree { private static class TreeNode {int data;TreeNode lchild;TreeNode rchild;public TreeNode(int data) {super();this.data = data;}}public static TreeNode reBuildBinaryTree(int[] pre, int[] in) {TreeNode root = reBuildBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1);return root;}private static TreeNode reBuildBinaryTree(int[] pre, int startPre, int endPre, int[] in, int startIn, int endIn) {// 两条件也可任选其一if (startPre > endPre || startIn > endIn)return null;TreeNode root = new TreeNode(pre[startPre]);for (int i = startIn; i <= endIn; i++) {if (in[i] == pre[startPre]) {// startPre+i-startIn=startPre+(i-startIn),表示的意义是根结点索引+根结点的左子树结点总数,对应前序的左子树的结束索引。root.lchild = reBuildBinaryTree(pre, startPre + 1, startPre + i - startIn, in, startIn, i - 1);// i-startIn+startPre+1即在上面的基础上加1,前序左子树的结束索引+1=前序右子树的开始索引。root.rchild = reBuildBinaryTree(pre, i - startIn + startPre + 1, endPre, in, i + 1, endIn);break;}}return root;}
}
十、二叉树遍历算法的应用
在上面二叉树递归遍历的基础上,将具体的访问结点操作换成其它的某些操作就可以实现另外的一些功能。
1.按先序遍历的顺序建立二叉树
比如图(a)中的这棵二叉树,左子树或右子树不存在用字符#表示,对应的先序序列[ABC##DE#G##F###],那么根据这个先序序列,也可以还原这棵二叉树。
要注意的是下面代码看似没有递归出口,其实不是,当输入的先序序列已经可以唯一确定二叉树的时候程序也会随之停止调用并结束。(写这块的时候花了一个晚上,原因是方法里的参数开始传的是先序数组和数组下标,然后递归的时候数组下标+1,忽视了逐层往里递归的时候虽然数组下标也逐层+1正确但最内部递归结束返回次内部往另一个方向递归的时候传入的数组下标还是该层数组下标没有+1时的值)
public static TreeNode build(Scanner sc) {char ch = (sc.nextLine().charAt(0));if (ch == '#') {return null;}TreeNode root = null;root = new TreeNode(ch);root.lchild = build(sc);root.rchild = build(sc);return root;
}
2.按先序遍历的顺序复制二叉树
public static TreeNode copy(TreeNode T) {if (T == null) {return null;}TreeNode newT = new TreeNode(T.data);newT.lchild = copy(T.lchild);newT.rchild = copy(T.rchild);return newT;
}
3.按后序遍历的顺序计算二叉树深度
二叉树深度为左右子树深度中的最大值+1,代码在二叉树递归的层序遍历中含有。
4.按先序遍历的顺序统计二叉树中结点个数
(1)统计所有结点个数
①当树为空时,结点个数为0;
②否则为根结点+根的左子树中结点个数+根的右子树中结点的个数。
public static int count(TreeNode T) {if (T == null)return 0;return count(T.lchild) + count(T.rchild) + 1;
}
(2)统计所有度为0也就是叶子结点的个数
①当树为空时,叶子结点个数为0;
②当某个结点的左右子树均为空时,说明该结点为叶子结点,返回1。
③否则表明该结点有左子树,或者有右子树,或者既有左子树又有右子树时,说明该结点不是叶子结点,因此叶结点个数等于左子树中叶子结点个数+右子树中叶子结点的个数。
public static int count(TreeNode T) {if (T == null)return 0;if (T.lchild == null && T.rchild == null)return 1;return count(T.lchild) + count(T.rchild);
}
(3)统计所有度为1的结点个数
①当树为空时,度为1的结点个数为0;
②当某个结点只有左子树或者只有右子树时,说明该结点是度为1的结点,返回1;
③否则表明该结点左右子树都存在,因此度为1的结点个数=左子树中度为1的结点个数+右子树中度为1的结点个数。
public static int count(TreeNode T) {if (T == null)return 0;// 条件中可以不用括号,短路与“&&”的优先级高于短路或“||”。if ((T.lchild == null && T.rchild != null) || (T.lchild != null && T.rchild == null))return 1;return count(T.lchild) + count(T.rchild);
}
(4)统计所有度为2也就是满结点的个数
①当树为空时,度为2的结点个数为0;
②当某个结点左子树和右子树都存在时,说明该结点是度为2的结点,返回1;
③否则表明该结点存在且不是满结点,因此满结点个数=该结点左子树中的满结点个数+该结点右子树中的满结点个数(必有一子树不存在,但会返回0不影响结果)。
public static int count(TreeNode T) {if (T == null)return 0;if (T.lchild != null && T.rchild != null)return count(T.lchild) + count(T.rchild) + 1;return count(T.lchild) + count(T.rchild);
}
【数据结构】——二叉树详解相关推荐
- 数据结构-二叉树-详解
目录 一.树的概念及结构 1.1树的概念 1.2 树的相关概念 1.3树的表示 1.4树在实际中的运用(表示文件系统的目录树结构) 二. 二叉树的概念及结构 2.1概念 2.2特殊二叉树 2.3二叉 ...
- 【Java】数据结构---二叉树 详解
快速导航: 1 树形结构 1.1 树形结构 概念 1.2 需要记忆概念 1.3 树的表现形式 2 二叉树 2.1 概念 2.2 两种特殊的二叉树 2.3 二叉树的性质 2.4 相关例题讲解 2.4 二 ...
- 数据结构--二叉树--详解
本章目录 1. 树概念及结构 1.1树概念 1.2树的表示 2. 二叉树概念及结构 2.1概念 2.2数据结构中的二叉树 2.3特殊的二叉树 2.4二叉树的存储结构 2.4.1顺序存储 2.4.2链式 ...
- java数据结构-链表详解
文章目录 1.数据结构-链表详解 1.1单链表 1.1.1单链表节点的尾部添加 1.1.2单链表节点的自动排序添加 1.1.3单链表节点的修改 1.1.4单链表节点的删除 1.2单链表面试题 1.2. ...
- 线索二叉树详解(C语言版)
文章目录 一.定义 二.结构 三.常用操作 结语 附录 一.定义 前面学习了二叉树,在操作过程中发现了几个问题: 问题一:二叉树如何才能实现从一个指定结点开始遍历呢? 问题二:在二叉树 ...
- 【线索二叉树详解】数据结构06(java实现)
线索二叉树 1. 线索二叉树简介 定义: 在二叉树的结点上加上线索的二叉树称为线索二叉树. 二叉树的线索化: 对二叉树以某种遍历方式(如先序.中序.后序或层次等)进行遍历,使其变为线索二叉树的过程称为 ...
- 《数据结构C语言版》——二叉树详解(图文并茂)
哈喽!这里是一只派大鑫,不是派大星.本着基础不牢,地动山摇的学习态度,从基础的C语言语法讲到算法再到更高级的语法及框架的学习.更好地让同样热爱编程(或是应付期末考试 狗头.jpg)的大家能够在学习阶段 ...
- 【二叉树详解】二叉树的创建、遍历、查找以及删除等-数据结构05
二叉树 1. 二叉树简介 定义: 每一个结点的子节点数量不超过 2 二叉树的结点分为:左节点.右节点 满二叉树: 每个结点都有两个子结点的二叉树(除了叶子结点外) 完全二叉树: 除去最后一层,是一个满 ...
- 【数据结构和算法】二叉树详解,动图+实例
最新文章
- 航天智慧物流创意组-技术培训
- 用R语言 画条形图(基于ggplot2包)
- python详细安装教程3.7.4-python 3.7.4下载与安装的问题
- 如何改android device monitor文件的权限
- javaScript实现E-mail 验证
- 分布式Zookeeper安装搭建详解
- COM组件设计与应用(三)(转载)
- linux string
- Web安全之权限攻击
- python中的str方法和repr方法_详解Python中__str__和__repr__方法的区别
- CMSIS-SVD Schema File Ver. 1.0
- Spring MVC异常处理详解 ExceptionHandler good
- hdu1402(大数a*bfft模板)
- 论文翻译 SLAM综述
- JavaMD5加密MD5Utils工具类
- 如何按行政区划下载谷歌地图并裁剪
- 分布式数据库BLP安全模型介绍
- JAVA理财管理系统(JAVA 毕业设计)
- 10.5 欧拉通路与哈密顿通路
- xp系统怎么关闭wmi服务器,XP系统电脑中解决wmi服务被禁用的开启方法
热门文章
- 签约火爆|一米定智“新潮宅·智生活”品牌新品发布会全程直击
- 通达信超音速巡航指标公式
- JAVA小程序简单学籍系统参考代码,登陆小程序,Jtree //Jtree,JDBC,Jframe
- AutoJS4.1.0实战教程 ---火火视频极速版签到、清理缓存和自动评论
- 视频监控系统中的流媒体服务器,视频监控系统中的流媒体服务器、直写和全切换三种取流架构方案...
- U821升级到U810.1注意事项
- -sql语法-2-部门表
- 华为彭红华:700MHz!两大创新不可少
- js用FileSystemObject 对象实现文件控制
- php在windows编译_在Windows上编译PHP