一、什么是树
树是n(n>=0)个结点的有限集合,它:
(1)或者是一棵空树(n=0),空树不包括任何结点;
(2)或者是一棵非空树(n>0),此时有且仅有一个特定的根结点;当n>1时,其余结点可分为m个互不相交的有限集T1,…,Tm,其中每一个本身又是一棵树,称为根的子树。
注意:有且仅有一个根结点!
(一)基本概念
1.结点的层次:从根开始定义,根节点的层次为0,其子树的根节点层次为1,依次递加。
2.树的深度:树中结点的最大层次树为树的深度。
3.结点的度:结点拥有的子树的数目(即子结点的个数)称为结点的度。度为0的结点为叶子结点。
性质1:树中的结点数=树的边数+1=所有结点度数之和+1
性质2:树中的任意两点之间都存在唯一的路径。
性质3:树中所有结点最大度数为m的有序树称为m叉树
二、二叉树
(一)基本性质
每个结点的度数不超过2的有序树称为二叉树
性质1:二叉树的第i层最多有2^i个结点
性质2:高度为h的二叉树最多有2^(h+1) -1个结点
性质3:如果二叉树的终端结点数为n0,度为2的结点数为n2,则n0=n2+1
满二叉树:每层的结点数都达到最大(然而这个问题是有歧义的,有些人认为,所有节点要么是叶子结点,要么有两个子节点,则这个树是满二叉树)
完全二叉树:相较于满二叉树而言,在最下层从最右侧起有部分结点不满
性质4:有n个结点的完全二叉树的高度为[log n]
性质5:含有n>=1个结点的二叉树,高度最大为n-1,最小为[log n]
性质6:若根节点编号为1,从左到右、从上到下的顺序依次编号,则结点i的父结点为[i/2],左子节点为2i,右子结点为2i+1。
(二)存储结构
方法一:顺序存储
将元素逐一存放到数组中,根据性质6来在O(1)的时间内直接找到父结点、左右结点。
特点:适合满二叉树或完全二叉树,对于普通二叉树而言需要用虚结点补成完全二叉树后存储,易造成空间浪费
方法二:链式存储
通过链表来存储树中结点,结点的定义往往有两种:
(1)Data+lChild+rChild
(2)Data+lChild+rChild+parent
(三)常见的遍历方法
前序遍历:
(1)若二叉树为空,则进行空操作
(2)访问根结点
(3)访问左子结点
(4)访问右子结点
后序遍历:
(1)若二叉树为空,则进行空操作
(2)访问左子结点
(3)访问右子结点
(4)访问根结点
中序遍历:
(1)若二叉树为空,则进行空操作
(2)访问左子结点
(3)访问根结点
(4)访问右子结点
三种遍历的常用实现方法:
(1)递归
(2)栈
(1)递归实现代码
前序遍历
void helpFun(TreeNode root, List<Integer> list){
if(root ==null ){//如果root为空结点
return ;
}else{
list.add(root.val);
helpFun(root.left,list);
helpFun(root.right,list);
}
}
public List<Integer> preorderTraversal(TreeNode root) {
//前序遍历代码,将结果存储在list中
List<Integer> list = new ArrayList<>();
if(root == null){
return list;//root为空结点
}else{
helpFun(root, list);
return list;
}
}
(2)栈
前序遍历:
public List<Integer> preorderTraversal(TreeNode root){
//前序遍历:根-左-右(先打印再访问左右子树)
List<Integer> res = new LinkedList<>();
TreeNode p = root;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty() || p != null){
if(p != null){
stack.push(p);
res.add(p.val);
p = p.left;
}else{
p=stack.pop();
p=p.right;
}
}
return res;
}
中序遍历:
public List<Integer> inorderTraversal(TreeNode root) {
//中序遍历:左-根-右
List<Integer> list = new LinkedList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode p = root;
while(p != null || !stack.empty()){//当p为空且栈为空(没有需要检查的点了),就结束
if(p != null){//非空先一直向左遍历
stack.push(p);//将经过的结点都入栈,为的是访问右侧的结点
p = p.left;
}else{
TreeNode t = stack.pop();
list.add(t.val);
p=t;
// stack.push(p);
p=p.right;
}
}
return list;
}
主要思想:
(1)无脑入栈
(2)遇到null就弹出上一个
(3)弹出时访问右结点
前序遍历于中序遍历的基本思想基本相同,但是由于后序遍历在出栈后,无法保存根节点的,方法有所不同。
但是换个思路,左-右-根反过来就是根-右-左,所以只需要在最后将得到的list反过来就行。
public List<Integer> postorderTraversal(TreeNode root) {
//左、右、根 1 2 3
List<Integer> list = new LinkedList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode p = root;
while(!stack.isEmpty() || p != null){
if(p != null){
list.add(p.val);
stack.push(p);
p = p.right;
}else{
p = stack.pop();
p = p.left;
}
}
Collections.reverse(list);
return list;
}
(四)层次遍历(用的比较少)
依次从上到下、从左到右访问结点
方法:通过队列来存储(先到先出)
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();
if(root == null){
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
List<Integer> help = new LinkedList<>();
for(int i = 0; i < size; i++){
TreeNode tn = queue.poll();
help.add(tn.val);
// System.out.println(tn.val);
if(tn.left != null){
queue.offer(tn.left);
}
if(tn.right != null){
queue.offer(tn.right);
}
}
// System.out.println(queue.size());
res.add(help);
}
return res;
}