1.在数据结构中,树的定义如下:
树(tree)是n(n>0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点:
① 有且仅有一个特定的称为根的节点。
② 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
2.什么是二叉树?
二叉树是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。
注意,这里是最多有2个,也可能只有一个,或者没有孩子节点。
二叉树节点的两个孩子节点,一个被称为左孩子,一个被称为右孩子。这两个孩子节点的顺序是固定的,就像人的左手和右手,不能颠倒或混淆。
此外,二叉树还有两种特殊形式,一个叫满二叉树,另一个叫完全二叉树。
2.1 什么是满二叉树呢?
一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。简单点说,满二叉树的每一个分支都是满的。
2.2 什么是完全二叉树呢?
对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。
3.二叉树可以用哪些物理存储结构来表达呢?
3.1 链式存储结构
链式存储时二叉树最直观的存储方式,之前学过链表,它是一对一的存储方式,每一个链表节点拥有data变量和一个指向下一节点的next指针。
而二叉树稍微复杂一些,一个节点最多可以指向左右两个孩子节点,所以二叉树的每一个节点包含3部分:
-
-
- 存储数据的data变量
- 指向左孩子的left指针
- 指向右孩子的right指针
-
3.2 数组
使用数组存储时,会按照层级顺序把二叉树节点放到数组中对应的位置,如果某一个节点的左孩子或右孩子空缺,则数组相应的位置也空出来。
为什么这样设计呢?因为这样可以更方便的在数组中定位二叉树的孩子节点和父节点。
假设一个父节点的下标是parent,那么它的左孩子节点下标就是2*parent + 1;右孩子节点下标就是2*parent + 2。
反过来,假设一个左孩子节点的下标是leftChild,那么它的父节点下标就是(leftChild - 1)/ 2。
假设节点4在数组中的下标为3,节点4是节点2的左孩子,节点2的下标可以直接通过计算得出。
节点2下标 = (3-1)/2 = 1;
显然,对于一个稀疏的二叉树来说,用数组表示法是非常浪费空间的。
4.二叉树的应用
二叉树包含许多特殊的形式,每一种形式都有自己的作用,但是其主要的应用还在于进行查找操作和维持相对顺序这两个方面。
4.1 查找
二叉树的树形机构使它很适合扮演索引的角色。
这里可以介绍一下二叉查找树,它在二叉树的基础上增加了以下几个条件;
-
-
- 如果左子树不为空,则左子树上所有节点的值均小于根节点的值
- 如果右子树不为空,则右子树上所有节点的值均大于根节点的值
- 左、右子树也都是二叉查找树
-
对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样的。
这种依靠比较大小来逐步查找的方式,和二分查找算法非常相似。
4.2 维持相对顺序
这一点仍然要说二叉查找树。二叉查找树要求左子树小于父节点,右子树大于父节点,正是这样保证了二叉树的有序性。因此二叉查找树还有另外一个名字——二叉排序树
5.二叉树的遍历
从节点之间位置关系的角度来看,二叉树的遍历分为4种:
① 前序遍历
② 中序遍历
③ 后序遍历
④ 层序遍历
从更宏观的角度来看,二叉树的遍历归结为两大类:
① 深度优先遍历(前序遍历,中序遍历,后序遍历)
② 广度优先遍历(层序遍历)
5.1 深度优先遍历
深度优先和广度优先这两个概念不止局限于二叉树,它们更是一种抽象的算法思想,决定了访问某些复杂数据结构的顺序。在访问树,图或其他一些复杂数据结构时,这两个概念常常被使用到。
所谓深度优先,顾名思义,就是偏向于纵深,“一头扎到底”的访问方式。
5.1.1 前序遍历
二叉树的前序遍历,输出顺序是根节点,左子树,右子树。
5.1.2 中序遍历
二叉树的中序遍历,输出顺序是左子树,根节点,右子树。
2.1.3 后序遍历
二叉树的后序遍历,输出顺序是左子树,右子树,根节点。
二叉树的这三种遍历方式,用递归的思路可以非常简单的实现出来,可以看一下代码实现:
1 package com.algorithm.test; 2 3 import java.util.Arrays; 4 import java.util.LinkedList; 5 6 /** 7 * @Author Jack丶WeTa 8 * @Date 2020/7/29 16:08 9 * @Description 利用线性的链表转化为非线性的二叉树,然后利用递归方式完成前序、中序、后序遍历。 10 */ 11 public class BinaryTreeTest { 12 13 /** 14 * 构建二叉树 15 * @param inputList 输入序列 16 * @return 17 */ 18 public static TreeNode createBinaryTree(LinkedList<Integer> inputList){ 19 20 TreeNode node = null; 21 if(null == inputList || inputList.isEmpty()){ 22 return null; 23 } 24 Integer data = inputList.removeFirst(); 25 //这里的判空很关键。如果元素为空,则不再进一步递归 26 if(null != data){ 27 node = new TreeNode(data); 28 node.leftChild = createBinaryTree(inputList); 29 node.rightChild = createBinaryTree(inputList); 30 } 31 return node; 32 } 33 34 /** 35 * 二叉树前序遍历 36 * @param node 二叉树节点 37 */ 38 public static void perOrderTraveral(TreeNode node){ 39 if (null == node){ 40 return; 41 } 42 System.out.println(node.data); 43 perOrderTraveral(node.leftChild); 44 perOrderTraveral(node.rightChild); 45 } 46 47 /** 48 * 二叉树中序遍历 49 * @param node 二叉树节点 50 */ 51 public static void inOrderTraveral(TreeNode node){ 52 if(null == node){ 53 return; 54 } 55 inOrderTraveral(node.leftChild); 56 System.out.println(node.data); 57 inOrderTraveral(node.rightChild); 58 } 59 60 /** 61 * 二叉树后序遍历 62 * @param node 二叉树节点 63 */ 64 public static void postOrderTraveral(TreeNode node){ 65 if(null == node){ 66 return; 67 } 68 postOrderTraveral(node.leftChild); 69 postOrderTraveral(node.rightChild); 70 System.out.println(node.data); 71 } 72 /** 73 * 二叉树节点 74 */ 75 private static class TreeNode { 76 int data; 77 TreeNode leftChild; 78 TreeNode rightChild; 79 80 TreeNode(int data) { 81 this.data = data; 82 } 83 } 84 85 public static void main(String[] args) { 86 LinkedList<Integer> inputList = new LinkedList<>(Arrays.asList(new Integer[]{3,2,9,null,null,10,null,null,8,null,4})); 87 TreeNode treeNode = createBinaryTree(inputList); 88 System.out.println("前序遍历:"); 89 perOrderTraveral(treeNode); 90 System.out.println("中序遍历:"); 91 inOrderTraveral(treeNode); 92 System.out.println("后续遍历:"); 93 postOrderTraveral(treeNode); 94 } 95 }
代码种值得注意的是二叉树的构建。二叉树的构建方法有很多,这里把一个线性的链表转换成非线性的二叉树,链表节点的顺序恰恰是二叉树前序遍历的顺序,链表种的空值,代表二叉树节点的左孩子或右孩子为空的情况。
绝大多数可以用递归解决的问题,其实都可以用另一种数据结构来解决,这种数据结构就是栈。因为递归和栈都有回溯的特性,利用栈来简单的实现二叉树的前序排列吧。代码如下;
1 /** 2 * 利用栈完成二叉树的前序遍历 3 * @param node 4 */ 5 public static void perOrderTraveralWithStack(TreeNode node){ 6 Stack<TreeNode> stack = new Stack<>(); 7 TreeNode treeNode = node; 8 while (null != treeNode || !stack.isEmpty()){ 9 //迭代访问节点的左孩子,并入栈 10 while (null != treeNode){ 11 System.out.println(treeNode.data); 12 stack.push(treeNode); 13 treeNode = treeNode.leftChild; 14 } 15 //如果节点没有左孩子,则弹出栈顶节点,访问节点右孩子 16 if (!stack.isEmpty()){ 17 treeNode = stack.pop(); 18 treeNode = treeNode.rightChild; 19 } 20 } 21 }
5.2 广度优先遍历
如果说深度优先遍历是在一个方向上“一头扎到底”,那么广度优先遍历则恰恰相反,先在各个方向上走出1步,然后再各个方向上走出第2步、第3步......一直到各个方向全部走完。
层序遍历,顾名思义,就是二叉树按照从根节点到叶子节点的层次关系,一层一层横向遍历各个节点,在实现层序遍历时,我们需要借助一个数据结构来辅助工作,这个数据结构就是队列。实现代码如下:
1 /** 2 * 利用队列实现二叉树的层序遍历 3 * @param node 4 */ 5 public static void levelOrderTraveral(TreeNode node){ 6 Queue<TreeNode> queue = new LinkedList<>(); 7 queue.offer(node); 8 while (!queue.isEmpty()){ 9 TreeNode treeNode = queue.poll(); 10 System.out.println(treeNode.data); 11 if (null != treeNode.leftChild) 12 queue.offer(treeNode.leftChild); 13 if (null != treeNode.rightChild) 14 queue.offer(treeNode.rightChild); 15 } 16 }