树
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。
树具有与大自然中的树相似的结构,自然界的树都有一条主干,而主干会分出若干条支干,一般来说,这些支干互不相交,其中每条支干又可以分出若干条支干。数据结构中的树是类似的,每棵树都会有一个根结点,通过根节点可以引出若干个集合,而这些集合又可以看作是一颗子树,每颗子树的根节点都有一个前驱,若干个后继。
树的定义由递归实现。
树的概念
结点的度:一个结点含有子树的个数称为该结点的度;例如下图1结点的度为3
树的度:一棵树中,所有结点度的最大值称为树的度;例如下图树的结点为3
叶子结点或终端结点:度为0的结点称为叶结点;例如下图的8、10、11、15、99、5、6均为叶子节点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;例如下图9是99的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;例如下图99是9的子结点
根结点:一棵树中,没有双亲结点的结点;例如下图的1
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;例如下图99结点所在层次为3,8结点所在层次为4
树的高度或深度:树中结点的最大层次;例如下图树的高度为4
树的表示方法有多种,例如双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法等
最常用的为孩子兄弟表示法:
class TreeNode {
public int val;
public TreeNode left;//左孩子的引用
public TreeNode right;//右孩子的引用
}
二叉树
二叉树是一种特殊的树形结构,它表现为每个结点最多只能有两颗子树,两棵子树有左右之分。两棵子树如果颠倒顺序,则成为另一颗二叉树。
特殊的二叉树
二叉树中也有一些特殊的二叉树,例如:左斜树和右斜树,满二叉树和完全二叉树。
左斜树和右斜树:所有结点都只有左子树的二叉树称为左斜树,同样的,所有结点都只有右子树的二叉树称为右斜树。
满二叉树:每层的结点数都达到最大值的二叉树就是满二叉树。如果一棵树的高度为h,它的结点总数为2^h - 1,那么就称它为满二叉树。
完全二叉树:对于高度为h、有n个结点的二叉树,其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
如上图所示:左图为满二叉树,右图为完全二叉树
实际上满二叉树也属于完全二叉树
二叉树的性质
对于任何一棵树,若结点个数为n,则总边数为n-1
一棵非空二叉树的第i层上最多有2^(i - 1)(i>0)个结点
深度为K的二叉树最多有2^k - 1(k>=0)个结点
对任何非空二叉树,其叶子结点个数比度为2的结点个数多1,即n0=n2+1
具有n个结点的完全二叉树的深度为log2(n+1)向上取整
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
若2i+1<n,左孩子序号:2i+1,否则无左孩子
若2i+2<n,右孩子序号:2i+2,否则无右孩子
二叉树的存储
二叉树的存储可以是顺序存储也可以是链式存储。
顺序存储由于是连续的,用来存放满二叉树或者完全二叉树非常方便,可以快速访问的同时,大大减少了内存浪费。但对于一般的二叉树,需要添加一些空结点来表示某些不存在的结点,可能造成一定的空间浪费。
链式存储的适用性更强,它通过一个个结点将二叉树串联起来。
二叉树的遍历
所谓遍历,就是指沿着某条路线,依次对二叉树中每个结点进行访问。由于访问路线的不同,遍历顺序也不同,因此二叉树有多种遍历方式。
前中后序遍历
前序遍历:访问根节点->访问根的左子树->访问根的右子树
中序遍历:访问根的左子树->访问根节点->访问根的右子树
后序遍历:访问根的左子树->访问根的右子树->访问根节点
前序遍历(也称先序遍历)的先访问根节点,接着访问左子树,左子树同样属于二叉树,也要先访问根节点,在访问左子树,当左子树中所有结点都遍历完之后,才能返回遍历右子树。右子树同样是先访问根节点->访问左子树->访问右子树。例如:
对于中序遍历和后序遍历,仅仅是访问根节点的时机不同罢了。
前序遍历可以通过递归实现:
public void preOrder(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
}
前序遍历也可以通过非递归实现,它需要借助栈完成,当结点不为空时,沿着左子树依次入栈,当结点为空时,说明左子树走完,可以走右子树,将入栈的结点依次出栈可以找到其对应的右子树:
public List<Character> preOrderTraversal2(TreeNode root) {
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
List<Character> res = new LinkedList<>();//用来记录遍历结果
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
stack.push(cur);
res.add(cur.val);
cur = cur.left;
}else {
cur = stack.pop();//pop出的元素刚好是仅仅遍历完的左子树的结点
cur = cur.right;
}
}
return res;
}
中序遍历以及后序遍历的递归实现类似,只需改变打印的位置即可。
非递归实现中序遍历与前序遍历类似,后序遍历略有不同,后序遍历要求左右子树都遍历完才能遍历根节点,在遍历完左子树时,第一次返回根结点,不能遍历根结点,需要先遍历完右树,只有第二次返回根结点时才能遍历根结点。因此,考虑每次在根结点处加一个判断,先判断右子树遍历是否完成,可以在已经访问过的结点增加一个标记,并以此判断。算法如下:
public void postOrderNor(TreeNode root) {
if (root == null) {
return;
}
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
TreeNode prev = null;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
//top == prev说明右子树已遍历完
if (top.right == null || top == prev) {
prev = stack.pop();
System.out.println(top.val + " ");
}else {
cur = top.right;
}
}
}
后序遍历的另一种递归实现:由于后序遍历与先序遍历完全相反,可以考虑把先序遍历的实现反过来进行,结果上也是可行的:
//前序遍历反过来搞 ->先左再右变为先右再左 尾插变为头插
public List<Character> postOrderTraversal2(TreeNode root) {
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
LinkedList<Character> res = new LinkedList<>();
while(cur!=null || !stack.isEmpty()){
if(cur!=null){
stack.push(cur);
res.addFirst(cur.val);
cur = cur.right;
} else {
cur = stack.pop();
cur = cur.left;
}
}
return res;
}
层序遍历
层序遍历是按照从上到下,从左到右依次遍历的顺序,由于它在没有遍历完一整棵子树又去遍历另一棵子树,因此不能靠递归实现。它又是从前到后遍历,考虑使用队列实现,依次把结点入队列,这样即使后续找不到该结点,遍历的顺序也不会改变。具体做法是先入根结点,每次出队列时,把出队列的结点的左右子树结点入队列:
public void levelOrder(TreeNode root) {
if (root == null) {
return;
}
//存放要出的结点
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode tmp = queue.poll();
System.out.print(tmp.val);
if (tmp.left != null) {
queue.offer(tmp.left);
}
if (tmp.right != null) {
queue.offer(tmp.right);
}
}
System.out.println();
}