目录
一、前言
1.关于递归
2.树的基本概念
1)为什么要有树结构?
2)数据结构常用的树结构
3)树的基本概念
二、二叉树
1.两种特殊的二叉树
1)满二叉树
2)完全二叉树
3)完全二叉树的节点编号
2.二叉树的存储方式
3.二叉树的遍历
1)遍历:
2)四大遍历方式
4.二叉树代码实现
1)节点类的定义
2)前中后序遍历
3)统计节点个数
⭐99^100and100^99大小
4)元素是否存在
5)二叉树高度
三、leetcode题型(二叉树遍历)
1.Num102:二叉树的层序遍历(借助队列)
2.三大遍历的非递归实现(借助栈)
1)Num144:前序遍历
2)Num94:中序遍历
3)Num145:后序遍历
1)递归的应用场景
a.大问题可以拆分成小问题
b.拆分后的小问题和大问题除了数据规模,解决思路全一样
c.存在递归终止条件,拆分拆到底的条件
2)如何写递归程序:不要纠结内部运行,学会使用递归的语义解决问题
二叉树就是树形结构(天然的查找语义),树形结构可以更加高效的进行查找和搜索。如电脑中的文件系统就是一个树形结构。
如果文件是线性结构会怎么样:线性结构是所有文件逻辑上连续,成一条直线排列,换句话说电脑中假设有100w个文件,都在一个位置存储,检索特点的某个文件,就要遍历这个集合n次,O(n),如果是树,根据文件夹查找特定文件,时间复杂度实际上就是文件树的深度:O(logN)。
a.BST:二分搜索树【二叉树的元素查找】
b.平衡二分搜索树:AVL(严格平衡),红黑树(非严格平衡)
c.堆
d.并查集
e.字符串:线段树;字典树(Trie)
树是一种非线性的数据结构,由n(n>=0)个有限节点组成一个具有层次关系的集合,把他叫做树是因为它看起来像一颗倒挂的树,也就是说它是根朝上,叶朝下,子树是不能相交的,除了根节点外,每个节点有且仅有一个父节点,一棵N个节点的树有N-1条边,树是递归定义的。
a.树中的每个单元称为节点
b.节点的度:该节点含有的子树的个数就称为节点都度
c.树的度:该树中最大的节点的度就是该树的度
d.叶子节点:度为0的节点,也称为终端节点
e.根节点:树中没有父亲节点的节点:A就是根节点
f.关于节点的层次:从根开始计算,根节点是第一层
g.树的高度:节点的最大层次
每个节点最多只有两个子树,二叉树中节点的度不超过2,两个子树有左右之分。
a.概念
一个二叉树如果每一层的节点树都能达到最大值(所有非叶子节点的度都为2),则这个二叉树是满二叉树。
b.特点:对一棵二叉树来讲
1)高度为k,则该二叉树最多有2^k-1个节点【节点最多的情况就是满二叉树】
2)层次为k,第k层最多有2^(k-1)个节点
3)边长和节点个数关系:边长=节点个数-1。设度为0的节点个数为n0,度为1的节点个数n1,度为2的节点个数n2,则:n0=n2+1(叶子节点的个数=度为2的节点个数+1),设总的个数为n,n=n0+n1+n2,边长=n-1。
a.概念
完全二叉树是效率很高的数据结构,它实际上就是满二叉树“缺了个右下角”,在完全二叉树中,度为1的节点若存在,只可能有一个度为1的节点,且只有左子树没有右子树,不存在只有右子树没有左子树的节点(节点要靠左排列!)。
b.问题1:
一个完全二叉树第六层有8个叶子节点,则该完全二叉树至多有多少个节点(求该完全二叉树全部的节点个数)
分析:已知第六层8个叶子节点,让完全二叉树前六层拉满,存在第七层。前6层而言,最多的节点个数:2^6-1=63;第6层总共有2^(6-1)=32,有8个叶子节点,非叶子节点=32-8=24个,这24个节点的子树就是第七层的节点,这24节点都有左右子树,第7层节点个数:24*2=48,总的节点个数=前六层拉满的节点+第7层的节点=63+48=111。
c.问题2:
500,500,1,0
分析:总结点1000,1024之内(2^10-1=1023,2^9-1=511),二叉树高度为10,前九层节点个数拉满:511,第十层节点个数:1000-511=489(全是叶子节点),第九层非叶子节点=第10层节点个数/2=488/2+1(度为2的节点+度为1的节点)=245,第九层总节点个数:2^(9-1)=256,第九层叶子节点=256-245=11。综上,总叶子节点=11+489=500,非叶子节点=1000-500=500,度为2的节点=499,度为1的节点=1。
若根节点从1开始编号,设父节点的编号为k,则左子树2k,右子树2k+1;若根节点从0开始编号,左子树2k+1,右子树2k+2。
同链表一样,有顺序存储和链式存储(引用)。顺序存储是将二叉树采用数组的方式存储(只能存储完全二叉树,在堆章节介绍),普通二叉树采用引用方式存储。
class Node
{ E val; Node left; Node right;//左右孩子表示法,一般都可以使用该方法来存储二叉树的节点
}
class Node
{//之后平衡树会用到 E val; Node left; Node right;
Node parent;//父节点地址
}
按照一定的顺序“访问(根据不同的场景,访问的需求是不同的,如打印节点值或是计算节点个数)”这个集合的所有元素,不重复,不遗漏。
对于二叉树这种非线性结构而言,遍历比线性结构就复杂得多,有四大遍历方式(对于二叉树来讲,遍历操作是其他操作的基础):前中后序遍历、层序遍历。
注意:在写前三种遍历方式时可以借用栈结构,保证做到不重不漏不出错,此时的“访问”就是输出结点的值
a.前序遍历:【preOrder】
先访问根节点,递归访问左子树,递归访问右子树,“根左右”,第一次访问根节点就可以输出节点值。
b.中序遍历:【inOrder】
先递归访问左子树,然后访问根节点,最后递归访问右子树,“左根右”。
c.后序遍历:【postOrder】
先递归访问左子树,递归访问右子树,再访问根节点,"左右根"。
拓展:后序的转置输出恰好是前序遍历的镜像:根右左
d.层序遍历:【levelOrder】
按照二叉树的层次一层层访问节点,先左再有。
A先入队列,输出打印A,A左右不为空,左右孩子BC入队,此时A就可以出队了,此时队首B,处理B,B先做一个输出,B有左孩子D无右孩子,D入队同时B出队,此时队首C,处理C先让C输出,处理左右子树是EF,入队,这时C处理完了,出队,此时队首D.......
注意:1.队列为空,层序遍历就处理完毕 2.队列中保存的都是下一层要处理的元素
eg:
前序:ABDEGHCF
中序:DBGHEACF
后序:DHGEBFCA
层序:ABCDEFGH
public class MyBinTree {
private static class TreeNode{//创建内部类
char val;
//左子树根节点
TreeNode left;
//右子树根节点
TreeNode right;
public TreeNode(char val){
this.val=val;
}
}
}
同链表一样,传递一棵二叉树,传入该树的根节点即可,通过根节点的不同遍历方式取得所有节点值。
/**
* 基础二叉树实现
* 使用左右孩子表示法
*/
public class MyBinTree {
private static class TreeNode{//创建内部类
char val;
//左子树根节点
TreeNode left;
//右子树根节点
TreeNode right;
public TreeNode(char val){
this.val=val;
}
}
/**
* 创建一个二叉树,返回根节点
* @return
*/
public static TreeNode build(){
TreeNode nodeA=new TreeNode('A');
TreeNode nodeB=new TreeNode('B');
TreeNode nodeC=new TreeNode('C');
TreeNode nodeD=new TreeNode('D');
TreeNode nodeE=new TreeNode('E');
TreeNode nodeF=new TreeNode('F');
TreeNode nodeG=new TreeNode('G');
TreeNode nodeH=new TreeNode('H');
nodeA.left=nodeB;
nodeA.right=nodeC;
nodeB.left=nodeD;
nodeB.right=nodeE;
nodeE.right=nodeH;
nodeC.left=nodeF;
nodeC.right=nodeG;
return nodeA;
}
/**
* 先序遍历:根左右
* 传入一个二叉树根节点,就可以按照以先序遍历的方式输出节点值
* @param root
*/
public static void preOrder(TreeNode root){
//边界条件-判空
if(root==null){
return;
}
//先打印根节点的值
System.out.print(root.val+" ");
//按照先序遍历的方式递归访问左树
preOrder(root.left);
//按照先序遍历的方式递归访问右树
preOrder(root.right);
}
/**
* 中序遍历:左根右
* 传入一棵二叉树的根节点,就能按照中序遍历的方式来输出结果集
* @param root
*/
public static void inOrder(TreeNode root){
if(root==null){
return;
}
//先递归访问左子树
inOrder(root.left);
System.out.print(root.val+" ");
inOrder(root.right);
}
public static void postOrder(TreeNode root){
if(root==null){
return;
}
//先递归访问左子树
postOrder(root.left);
//再递归访问右子树
postOrder(root.right);
//打印根节点
System.out.print(root.val+" ");
}
public static void main(String[] args) {
TreeNode root=build();
System.out.println("前序遍历的结果为:");
preOrder(root);
System.out.println();
System.out.println("中序遍历结果为:");
inOrder(root);
System.out.println();
System.out.println("后序遍历结果为:");
postOrder(root);
}
}
a.总共节点个数
/**
* 传入一棵二叉树的根节点,就能统计出当前二叉树中一共有多少个节点,返回节点数
* 此时的访问就不再是输出节点值,而是统计节点个数
* @param root
* @return 当前二叉树的节点个数
*/
public static int getNodes(TreeNode root){
if(root==null){
return 0;
}
//当前根节点不为空,说明当前根节点要统计一次
//然后加上左右子树的节点数就是整棵树的节点数
return 1+getNodes(root.left)+getNodes(root.right);
}
public static void main(String[] args) {
TreeNode root=build();
System.out.println("当前二叉树一共有"+getNodes(root)+"个节点数");
}
b.叶子节点个数
/**
* 求一棵二叉树叶子节点的个数
* 传入一棵二叉树根节点,就能统计出当前二叉树叶子节点个数
* @param root
* @return
*/
public static int getLeafNodes(TreeNode root){
if(root==null){
return 0;
}
if(root.left==null&&root.right==null){
//说明二叉树只有根节点,这个根节点就是叶子节点
return 1;
}
//当前数不为空且存在子树,则返回子树的叶子节点数
return getLeafNodes(root.left)+getLeafNodes(root.right);
}
public static void main(String[] args) {
TreeNode root=build();
System.out.println("当前二叉树一共有"+getLeafNodes(root)+"个叶子节点");
}
递归函数实际上具体的处理过程都在终止条件,递归过程只是将多个结果拼起来而已。
c.第k层节点个数⭐
以root为节点的第k层节点个数=以root.left为根节点的第k-1层节点个数+以root.right为根节点的第k-1层节点个数。
/**
* 求出以root为根节点的二叉树第k层节点个数
* @param root
* @param k
* @return
*/
public static int getKLevelNodes(TreeNode root,int k){
if(root==null||k<=0){
return 0;
}
if(k==1){
return 1;
}
//二叉树不为空且k>=2
//以root为根节点的第k层=以root.left为根节点的k-1层+以root.right为根节点的k-1层
return getKLevelNodes(root.left,k-1)+getKLevelNodes(root.right,k-1);
}
99^100大一些,在二进制、八进制、十进制、十六进制等这些进制中,最高效的实际上是三进制,和3越近数越大。2^4,3^3,4^2;2^5,3^4,4^3。
在二叉树中判断给定元素是否存在:遍历,这里访问是判断节点值是否与给定值相等
/**
* 判断当前二叉树中是否包含指定元素val
* 若存在返回true,否则返回false
* @param val
* @return
*/
public static boolean contains(TreeNode root,char val){
if(root==null){
return false;
}
if(root.val==val){
return true;
}
//二叉树不为空且根节点值不是val,在子树中继续寻找
return contains(root.left,val)||contains(root.right,val);
}
/**
* 传入一个以root为根节点的二叉树,求出该树的高度
* @param root
* @return
*/
public static int height(TreeNode root){
if(root==null){
return 0;
}
return 1+Math.max(height(root.left),height(root.right));
// return 1+(height(root.left)>height(root.right)?height(root.left):height(root.right));//法2
// int leftHeight=height(root.left);//法1
// int rightHeight=height(root.right);
// int max=Math.max(leftHeight,rightHeight);
// return 1+max;
}
借助双端队列实现遍历过程。
Dequequeue=new LinkedList<>();
分析:
1.队列为空,层序遍历就处理完毕
2.队列中保存的都是下一层要处理的元素
queue.size():
计算出当前层的元素个数,按这个元素个数循环,将队列中当前层元素从队列中取出并放入temp集合同时将下一层元素加入队列。
queue.add()和queue.offer()
都是用来向队列中添加元素,容量已满情况下add是抛出异常,offer是返回false。
/**
* 二叉树的层序遍历
* 用二维数组保存:一层用一个小集合存储,最后大集合存储所有层
*/
public class Num102_LevelOrder {
public List> levelOrder(TreeNode root) {
List> ret=new ArrayList<>();
if(root==null){
return ret;
}
//借助队列实现遍历过程-双端队列
Deque queue=new LinkedList<>();
queue.offer(root);//根节点入队
while(!queue.isEmpty()){
//使用一个temp数组保存当前层的元素
List temp=new ArrayList<>();
//取出当前层所有元素添加进temp中
int size= queue.size();//当前层元素的个数(不能写到循环里)
for (int i = 0; i < size; i++) {
TreeNode cur=queue.poll();//依次从队列中取队首元素
temp.add(cur.val);//队首元素的值添加到当前层集合中
if(cur.left!=null){
queue.offer(cur.left);//cur左树不为空就将左树入队
}
if(cur.right!=null){
queue.add(cur.right);
}
}
ret.add(temp);
}
return ret;
}
}
Dequestack=new ArrayDeque<>();
第一次走到根节点时就可以访问。入栈时先入右子树C再入左子树B,这样才能先出左子树,B才能在C上面【因为栈是一个LIFO的结构,先遍历的节点后入】
/**
* 借助栈实现前序遍历
*/
public class Num144_PreOrderNonRecursion {
public List preorderTraversal(TreeNode root) {
List ret=new ArrayList<>();
if(root==null){
return ret;
}
//双端队列
Deque stack=new ArrayDeque<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.pop();
//先访问根节点
ret.add(cur.val);
//先压入右孩子
if(cur.right!=null){
stack.push(cur.right);
}
//再压入左孩子
if(cur.left!=null){
stack.push(cur.left);
}
}
return ret;
}
}
第二次访问根节点才能输出
public class Num94_InOrderNonRecursion {
public List inorderTraversal(TreeNode root) {
List ret=new ArrayList<>();
if(root==null){
return ret;
}
//当前走到的节点
TreeNode cur=root;
Deque stack=new ArrayDeque<>();
while(cur!=null||!stack.isEmpty()){
//不管三七二十一,先一路向左走到根
while(cur!=null){
stack.push(cur);
cur=cur.left;
}
//cur为空,说明走到了null
//此时栈顶就存放了左树为空的节点
cur=stack.pop();
ret.add(cur.val);
//继续访问右子树
cur=cur.right;
}
return ret;
}
}
引用一个prev引用保存上一个被完全处理过的节点,当cur第二次访问出队时,看右子树是否为空或被我们访问过(利用prev是否==cur.right),不为空且没有处理过的话根节点再压回栈中,继续处理右子树,为空或者访问过就需要输出并更新prev。
public class Num145_PostOrderNonRecursion {
public List postorderTraversal(TreeNode root) {
List ret=new ArrayList<>();
if(root==null){
return ret;
}
TreeNode cur=root;
//保存上一个完全处理过的节点(左右根都处理过的节点)
TreeNode prev=null;
Deque stack=new ArrayDeque<>();
while(cur!=null||!stack.isEmpty()){
//先一路向左走到根
while(cur!=null){
stack.push(cur);
cur=cur.left;
}
//此时cur走到null,栈顶存放了左树为null的节点,栈顶元素出队给cur,第二次访问
cur=stack.pop();
//判断右树是否为空或者被我们访问过
if(cur.right==null||prev==cur.right){
ret.add(cur.val);
//当前节点cur就是最后处理的根节点,cur已经处理完毕,更新prev引用,变为cur,进行下一轮循环
prev=cur;
cur=null;
}else{
//此时右树不为空且没有处理过
//根节点再压回栈中,继续处理右子树
stack.push(cur);
cur=cur.right;
}
}
return ret;
}
}
下一篇:数据结构之二叉树2—二分搜索树_林纾y的博客-CSDN博客
相关:二叉树的搜索与回溯问题(leetcode)_林纾y的博客-CSDN博客
二叉树1—二叉树的遍历_林纾y的博客-CSDN博客_从上到下打印二叉树
二叉树2—对称性递归问题_林纾y的博客-CSDN博客