详解二叉树

🌈目录

  • 一、树形结构​ 🌳
    • 1.1 概念
    • 1.2 其他概念
    • 1.3 树的表示形式
  • 二、二叉树✨
    • 2.1 概念
    • 2.2 两种特殊二叉树
    • 2.3 性质
    • 2.4 二叉树存储
  • 三、二叉树的基本操作🙌
    • 3.1 前置说明
    • 3.2 二叉树的遍历
    • 3.3 二叉树的基本操作
  • 四、二叉树的OJ✍️

一、树形结构​ 🌳

1.1 概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的

她有一个特殊的结点,叫做根结点,根节点没有前驱结点。

除根结点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、…、Tm,其中每一个集合Ti (1 <= i <=m) 又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。

树是递归定义的。

注意:1. 子树不相交;2. 除了根结点,每个结点有且只有一个父结点;3. 一颗n个结点的树有n-1条边。

1.2 其他概念

在这里插入图片描述

结点的度:一个结点含有子树的个数称为该结点的度; 如上图:A的度为6

树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6

叶子结点或终端结点度为0的结点称为叶结点; 如上图:B、C、H、I…等节点为叶结点

双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点

孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点

根结点:一棵树中,没有双亲结点的结点;如上图:A

结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推

树的高度或深度:树中结点的最大层次; 如上图:树的高度为4。深度与层次类似,这棵树最大的深度就是这颗树的高度。

树的以下概念只需了解,在看书时只要知道是什么意思即可:

非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G…等节点为分支结点

兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点

堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点

结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先

子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙

森林:由m(m>=0)棵互不相交的树组成的集合称为森林

1.3 树的表示形式

双亲表示法孩子表示法孩子双亲表示法孩子兄弟表示法等等,下面是孩子兄弟表示法

class Node {
	int value; // 树中存储的数据
	Node firstChild; // 第一个孩子引用
	Node nextBrother; // 下一个兄弟引用
}

在这里插入图片描述

二、二叉树✨

2.1 概念

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 或者是由一个根结点加上两棵别称为左子树右子树的二叉树组成。

在这里插入图片描述

特点

  1. 每个结点的子树不超过两个。度<=2
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树(顺序)。

在这里插入图片描述

2.2 两种特殊二叉树

满二叉树:每层的结点数都达到最大值(每个结点都有两个子树)每层的结点数最大为2k-1个,总结点数是2k-1。

完全二叉树:从上到下,从左到右,每棵树都紧挨着的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完全二叉树。满二叉树也是一种特殊的完全二叉树。

在这里插入图片描述

2.3 性质

  1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 2i-1次方 (i>0)个结点
  2. 若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数为2k - 1个。(每一层都放满)
  3. 具有n个结点的完全二叉树的深度k为log2(n + 1)向上取整。
  4. 对任何一颗二叉树,如果叶子结点个数为n0,度为2的结点个数为n2,则有n0 = n2 + 1。

性质4的推导:

假设二叉树有N个结点,那么N = N0 + N1 + N2①,有N-1条边。

我们知道,度为2的结点会生出2条边,度为1的结点会产生1条边,度为0的结点会产生0条边,

所以N - 1= N2 * 2 + N1②,方程①②联立,解出N0 = N2 + 1

  1. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:

    若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点

    若2i+1<n,左孩子序号:2i+1,否则无左孩子

    若2i+2<n,右孩子序号:2i+2,否则无右孩子

已知父亲下标 i,左孩子下标2*i + 1,右孩子下标2*i + 2。

已知孩子坐标 i, 则父亲下标为(i-1)/2。

习题:

1.某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )

A 不存在这样的二叉树 B 200 C 198 D 199

N0 = N2 + 1,所以选B

2.在具有 2*n 个结点的完全二叉树中,叶子结点个数为( )

A n B n+1 C n-1 D n/2

n是整数,所以有偶数个结点,又因为是完全二叉树,所以度为1的结点只有1个,所以N = N2 + 1 + 1 + N2 = 2*n

得N2 = n - 1,N0 = N2 + 1 = n,选A

3.一个具有767个节点的完全二叉树,其叶子节点个数为()

A 383 B 384 C 385 D 386

有奇数个结点,所以度为1的结点有0个,所以:

N = N2 + 1 + 0 + N2 = 767

N2 = 383,N0 = N2 + 1 = 384,选B

4.一棵完全二叉树的节点数为531个,那么这棵树的高度为( )

A 11 B 10 C 8 D 12

根据性质3:具有n个结点的完全二叉树的深度k为log2(n + 1)向上取整。

n = 531,k =[log2(532)] ≈ 10,选B。

2.4 二叉树存储

二叉树的存储结构分为:顺序存储类似于链表的链式存储

二叉树的链式存储是通过一个一个的节点引用起来的,常见的表示方式有二叉和三叉表示方式,具体如下:

// 孩子表示法
class Node {
	int val; // 数据域
	Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
	Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
	int val; // 数据域
	Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
	Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
	Node parent; // 当前节点的根节点
}

三、二叉树的基本操作🙌

3.1 前置说明

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在我们对二叉树结构掌握还不够深入,为了降低学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

public class BinaryTree {
    static class TreeNode {
        public char val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(char val) {
            this.val = val;
        }
    }

    public TreeNode createTree() {
        TreeNode A = new TreeNode('A');
        TreeNode B = new TreeNode('B');
        TreeNode C = new TreeNode('C');
        TreeNode D = new TreeNode('D');
        TreeNode E = new TreeNode('E');
        TreeNode F = new TreeNode('F');
        TreeNode G = new TreeNode('G');
        TreeNode H = new TreeNode('H');

        A.left = B;
        A.right = C;
        B.left = D;
        B.right = E;
        C.left = F;
        C.right = G;
        E.right = H;

        return A;//返回根节点
    }
}

当然,这并不是创建二叉树的方式,真正创建的方式后面会讲解

3.2 二叉树的遍历

  1. 前序遍历:访问根结点—>根的左子树—>根的右子树
  2. 中序遍历:根的左子树—>根节点—>根的右子树
  3. 后序遍历:根的左子树—>根的右子树—>根节点

遍历十分重要,二叉树的很多题都用到了遍历。

在这里插入图片描述

如这张图,他的前序遍历: A B D ɸ ɸ ɸ C E ɸ ɸ F ɸ ɸ ;

中序遍历:ɸ D ɸ B ɸ A ɸ E ɸ C ɸ F ɸ

后续遍历:ɸ ɸ D ɸ B ɸ ɸ E ɸ ɸ F C A

下面这张图就分析了前序遍历,中序和后序遍历基本差不多。

在这里插入图片描述

  1. 层序遍历:设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

在这里插入图片描述

他的层序遍历就是:A B C D ɸ E F

那我们来做一下练习,写出下面这棵树的前中后层序遍历

在这里插入图片描述

前:A B D E H C F G

中:D B E H A F C G

后:D H E B F G C A

层:A B C D E F G H

选择:

1.某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为()

A: ABDHECFG B: ABCDEFGH C: HDBEAFCG D: HDEBFGCA

2.二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为()

A: E B: F C: G D: H

3.设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为()

A: adbce B: decab C: debac D: abcde

4.某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同一层从左到右)的序列为()

A: FEDCBA B: CBAFED C: DEFCBA D: ABCDEF

5.一棵非空的二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树一定满足( )

A.所有的结点均无左孩子

B.所有的结点均无右孩子

C.只有一个叶子结点

D.至多只有一个结点

1.层序的第一个肯定是根节点,A为根节点,所以在前序遍历的时候第一个肯定为A,排除C D,又因为是完全二叉树,所以根据层序输出,画出这棵树:

在这里插入图片描述

所以他的前序遍历是:A B D H E C F G,选A。

2.先序遍历知道他的根节点,为E,所以选A。那么根据中序遍历,HFI是左子树,JKG是右子树,所以画出这棵树:

在这里插入图片描述

3.根据后序遍历,得知这棵树的根节点为a,他的右子树的根节点是c,根据中序遍历,b为左子树,dce为右子树,那么d是c的左子树,e是c的右子树。所以他的前序遍历是:abcde,选D。

在这里插入图片描述

4.根据后序遍历,F为根结点,所以ABCDE均为左子树,因为中序和后序一样,所以前中后和前后中一样,也就是中后和后中一样,又因为中间结点不可少,所以证明没有后,也就是没有右子树,每个结点都没有右子树。所以这棵树:

在这里插入图片描述

按照层序输出:FEDCBA,选A。

5.前序遍历:结点 左 右

后序遍历:左 右 结点

从二叉树 前序 和 后序遍历结果规则中可以看出,如果树中每个节点只有一个孩子时,遍历结果肯定是反的。(如果没有左孩子,前:结点 右;后:右 结点;如果没有右孩子,前:结点 左;后:左 结点。

img

如这个前序:1 2 3 4 5;后序:5 4 3 2 1

所以,只有一个叶子节点是满足的,选C。

根据前序遍历和后序遍历能不能创建一棵二叉树呢?

不可以!因为前序和后序确定的都是根结点,确定不了左右。

代码实现

//前序遍历
public void preOrder(TreeNode root) {
        if(root == null) {
            return;
        }
        System.out.print(root.val + " ");//先结点
        preOrder(root.left);//再左
        preOrder(root.right);//再右
}


//中序遍历
public void inOrder(TreeNode root) {
        if(root == null) {
            return;
        }
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
}


//后序遍历
public void postOrder(TreeNode root) {
        if(root == null) {
            return;
        }
        postOrder(root.left);
        postOrder(root.right);
        System.out.print(root.val + " ");
}

3.3 二叉树的基本操作

获取树中结点的个数

①遍历过程中,root只要不为空,结点数量+1

②左树结点+右数结点+1

//方法一
public int num = 0;
public int size1(TreeNode root) {
    if (root == null) {
        return 0;
    }
    num++;
    size1(root.left);
    size1(root.right);
    return num;
}

//方法二
public int size(TreeNode root) {
    if(root == null) {
        return 0;
    }
    return size(root.left) + size(root.right) + 1;
}

获取叶子节点个数

//方法一
public int getLeafNodeCount(TreeNode root) {
    if(root == null) {
        return 0;
    }
    if(root.left == null && root.right == null) {
        return 1;
    }
    return getLeafNodeCount(root.left) + getLeafNodeCount(root.right);
}

//方法二
public static int leafSize = 0;
void getLeafNodeCount1(TreeNode root) {
    if(root != null && root.left == null && root.right == null) {
        leafSize++;
        getLeafNodeCount1(root.right);
        getLeafNodeCount1(root.left);
    }
}

求第k层结点的个数

根的左树的k-1层 + 右树的k-1层

当k=1时,证明当前层为第k层,返回1

public int getKLevelNodeCount(int k, TreeNode root) {
    if(root == null) {
        return 0;
    }
    if(k == 1) {
        return 1;
    }
    return getKLevelNodeCount(k - 1, root.left) + getKLevelNodeCount(k - 1, root.right);
}

二叉树的高度

高度 = max(左数高度,右树高度) + 1

在这里插入图片描述

public int getHeight(TreeNode root) {
    if(root == null) {
        return 0;
    }
    return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}

检测值为value的元素是否存在

public TreeNode find(TreeNode root, char val) {
    if(root == null) {
        return null;
    }
    if(root.val == val) {
        return root;//root
    }
    TreeNode findLeft = find(root.left, val);//left
    if(findLeft != null) {
        return findLeft;
    }
    return find(root.right, val);//right
}

层序遍历

这个题使用了队列,先进先出,符合层序遍历特点。

遍历结点,如果root不为空,将root放入队列,再将root弹出用top记录,打印root的数值,将top的左和右树放入队列(先放左后放右);再弹出一个结点root用top记录,打印root的数值,将top的左和右树放入队列(如果不为空)(先放左后放右);如此循环,直到所有结点全部被遍历完,队列为空。

public void levelOrder(TreeNode root) {
    Queue<TreeNode> queue = new LinkedList<>();
    if(root != null) {
        queue.offer(root);//放入根结点
    }
    while(!queue.isEmpty()) {
        TreeNode top = queue.poll();//弹出一个结点
        System.out.print(top.val + " ");//打印
        if(top.left != null) {
            queue.offer(top.left);
        }
        if(top.right != null) {
            queue.offer(top.right);
        }
    }
}

判断一棵树是不是完全二叉树

类似层序遍历

在这里插入图片描述

public boolean isCompleteTree(TreeNode root) {
    if(root == null) {
        return true;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while(!queue.isEmpty()) {
        TreeNode cur = queue.poll();
        if(cur != null) {
            //这里就不用管左树右树是否为空,可以直接放null
            queue.offer(cur.left);
            queue.offer(cur.right);
        } else {
            break;//当队列弹出的元素是null时,停下
        }
    }
    while(!queue.isEmpty()) {
        //遍历剩余元素,如果都是null,证明是完全二叉树
        TreeNode cur = queue.poll();
        if(cur != null) {
            return false;//如果有不是null的,证明不是
        }
    }
    return true;
}

四、二叉树的OJ✍️


相同的树

/**
 * 判断两棵树是否相同
 * 时间复杂度为 O(min(m,n))
 */
public boolean isSameTree(TreeNode p, TreeNode q) {
    if(p == null && q != null || p != null && q == null) {
        return false;
    }
    if(p == null && q == null) {
        return true;
    }
    if(p.val != q.val) {
        return false;
    }
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}

另一颗树的子树

  1. 判断root和subRoot是不是两棵相同的树
  2. 判断是不是左树的子树
  3. 判断是不是右树的子树
/**
 * 判断是不是另一棵树的子树
 * 时间复杂度:s*t
 * 每个root结点都要和subRoot判断是否相同
 */
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
    if (root == null) {
        //判断是否为空
        return false;
    }
    if(isSameTree(root,subRoot)) {
        //判断root和subRoot是不是两棵相同的树
        return true;
    }
    return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}//判断是不是左树的子树或者是右树的子树

public boolean isSameTree(TreeNode p, TreeNode q) {
    if(p == null && q != null || p != null && q == null) {
        return false;
    }
    if(p == null && q == null) {
        return true;
    }
    if(p.val != q.val) {
        return false;
    }
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}

翻转二叉树

每一棵树的左右进行交换

public TreeNode invertTree(TreeNode root) {
    if(root == null) {
        return null;
    }
    TreeNode ret = root.left;
    root.left = root.right;
    root.right = ret;
    invertTree(root.left);
    invertTree(root.right);
    return root;
}

平衡二叉树

/**
 * 判断平衡二叉树
 * 时间复杂度为O(n^2)
 * 每个点都要求高度,求高度的复杂度是O(n),n个点,复杂度为O(n^2)
 */
public int getHeight(TreeNode root) {
    if(root == null) {
        return 0;
    }
    return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}
public boolean isBalanced1(TreeNode root) {
    if(root == null) {
        return true;
    }
    int leftH = getHeight(root.left);
    int rightH = getHeight(root.right);
    return Math.abs(leftH - rightH) < 2 && isBalanced1(root.left) && isBalanced1(root.right);
}

/**
 * 优化
 * 时间复杂度是O(n)
 * 最坏的情况下是求一遍树的高度
 */
public static int getHeight1(TreeNode root) {
    if(root == null) {
        return 0;
    }
    int leftH = getHeight1(root.left);
    int rightH = getHeight1(root.right);
    if(leftH >= 0 && rightH >= 0 && Math.abs(leftH - rightH) <= 1) {
        return Math.max(leftH, rightH) + 1;
    } else {
        return -1;
    }
}
public boolean isBalanced2(TreeNode root) {
    if(root == null) {
        return true;
    }
    return getHeight1(root) >= 0;
}

对称二叉树

  1. 判断是否都为空
  2. 判断是否一个为空一个不为空
  3. 都不为空,判断值是否一样
  4. 值一样,判断左树的左和右树的右,左树的右和右树的左
public boolean isSymmetric(TreeNode root) {
    if(root == null) return true;
    return isSymmetricChild(root.left,root.right);
}
public boolean isSymmetricChild(TreeNode leftTree,TreeNode rightTree) {
    if(leftTree == null && rightTree == null) {
        return true;
    } else if(leftTree == null && rightTree != null || leftTree != null && rightTree == null) {
        return false;
    }
    if(leftTree.val == rightTree.val) {
        return isSymmetricChild(leftTree.left, rightTree.right) && isSymmetricChild(leftTree.right,rightTree.left);
    } else {
        return false;
    }
}

二叉树的构建及遍历

因为题目给的是前序遍历的字符串,我们创建树的时候也必须得根据前序遍历的方式来创建

class TreeNode {
    public char val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(char val) {
        this.val = val;
    }
}

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String str = in.nextLine();//读取
        TreeNode root = createTree(str);//创建
        inOrder(root);//打印
    }
    /**
     * 二叉树的遍历
     * @param str
     * @return
     */
    public static int i = 0;
    public static TreeNode createTree(String str) {
        TreeNode root = null;
        char c = str.charAt(i);
        if(c != '#') {
            root = new TreeNode(c);//当此结点不为空,可以将它创建成根结点
            i++;
            root.left = createTree(str);//创建左树
            root.right = createTree(str);//创建右树
        } else {
            i++;//结点为空,不用创建
        }
        return root;
    }
    public static void inOrder(TreeNode root) {
        if(root == null) {
            return;
        }
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
    }
}

二叉树的分层遍历1

public List<List<Integer>> levelOrder1(TreeNode root) {
    List<List<Integer>> listAll = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    if(root != null) {
        queue.offer(root);
    } else {
        return listAll;
    }
    while(!queue.isEmpty()) {
        int size = queue.size();//记录此时队列大小,即当前层的结点个数
        List<Integer> list = new ArrayList<>();//存放当前层的数据
        while(size != 0) {//遍历当前层的节点
            TreeNode top = queue.poll();
            list.add(top.val);
            if(top.left != null) {
                queue.offer(top.left);
            }
            if(top.right != null) {
                queue.offer(top.right);
            }
            size--;//size等于零的时候,证明当前层的结点遍历完了,但不代表队列是空的,因为在pop当前结点的时候,把他的左树和右树放进队列了(这是下一层的结点)
        }
        listAll.add(list);//添加当前层的list
    }
    return listAll;
}

这棵二叉树的左视图:每层的第一个节点;右视图:每层的最后一个节点

107. 二叉树的层序遍历 II - 力扣(LeetCode)

自底向上的层序遍历

public List<List<Integer>> levelOrder1(TreeNode root) {
    List<List<Integer>> listAll = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    if(root != null) {
        queue.offer(root);
    } else {
        return listAll;
    }
    while(!queue.isEmpty()) {
        int size = queue.size();
        List<Integer> list = new ArrayList<>();
        while(size != 0) {
            TreeNode top = queue.poll();
            list.add(top.val);
            if(top.left != null) {
                queue.offer(top.left);
            }
            if(top.right != null) {
                queue.offer(top.right);
            }
            size--;
        }
        listAll.add(0, list);//除了这里不一样,其他均于1相同,在0位置插入元素。
    }
    return listAll;
}

二叉树的最近公共祖先

有三种情况,一是p或者q是root,那么root就是最近公共祖先;二是p和q分别在根节点的左和右子树;三是p和q都在根节点的左子树或者右子树上。

在这里插入图片描述

//法一
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if(root == null) {
        return null;
    }
    if(p == root || q == root) {
        return root;
    }
    TreeNode leftRet = lowestCommonAncestor(root.left, p, q);//在左树找pq
    TreeNode rightRet = lowestCommonAncestor(root.right, p, q);//在右树找pq
    //要么左有右无:pq公共祖先是左子树的根节点
    //要么左无右有:pq公共祖先是右子树的根节点
    //要么左右都有:pq公共祖先是当前的结点
    if(leftRet != null && rightRet != null) {
        return root;
    } else if(leftRet != null) {
        return leftRet;
    } else {
        return rightRet;
    }
}

还有一种方法,借助栈来实现,具体思路是:把从root到p和root到q的所有结点分别放入两个栈,将栈大的那个弹出元素,直到两个栈一样大小,然后再一起弹出元素,直到弹出的元素相等,这个元素就是他们的最近公共祖先。

为什么用栈而不是用队列来实现呢?

因为这个题目要找的是最近的公共祖先,我们知道,栈是后进先出,栈中存放的元素是距离p q越远的越先放入栈中,距离p q越近的越后放入栈中,所以最先弹出的元素是最靠近p q的,找到的也就是最近公共祖先。

而队列是从队头开始出队,弹出的一定是距离p q最远的。

在这里插入图片描述

//法二
public TreeNode lowestCommonAncestor2(TreeNode root, TreeNode p, TreeNode q) {
    if(root == null) {
        return null;
    }
    Stack<TreeNode> stack1 = new Stack<>();
    Stack<TreeNode> stack2 = new Stack<>();
    getPath(root, p, stack1);
    getPath(root, q, stack2);
    int size1 = stack1.size();
    int size2 = stack2.size();

    //弹出多余元素
    if(size1 > size2) {
        int size = size1 - size2;
        while (size != 0) {
            stack1.pop();
            size--;
        }
    } else {
        int size = size2 - size1;
        while (size != 0) {
            stack2.pop();
            size--;
        }
    }
    while (!stack1.empty() && !stack2.empty()) {
        TreeNode val1 = stack1.pop();
        TreeNode val2 = stack2.pop();
        if(val1 == val2) {
            return val1;
        }
    }
    return null;
}
/**
 * 再root这棵树上,找到node这个节点的位置
 */
private boolean getPath(TreeNode root, TreeNode node, Stack<TreeNode> stack) {
    if(root == null) {
        return false;
    }
    stack.push(root);
    if(root == node) {
        return true;
    }
    //如果当前的root不是node,那就找左右树
    boolean ret = getPath(root.left, node, stack);//找左树
    if(ret == true) {//找到了,直接返回
        return true;
    }
    boolean ret2 = getPath(root.right, node, stack);//找右树
    if (ret2 ==true) {
        return true;
    }
    stack.pop();//如果在一棵树的左边和右边都没有找到想要的node,那么证明这棵树的根节点不是我要找node的路径上的节点,那就可以把这个节点弹出,然后返回false表示没找到
    return false;
}

105. 从前序与中序遍历序列构造二叉树

前序遍历到的每个结点都是他自己这棵树的根结点,所以先创建根结点,再在中序遍历中找到该结点的位置,该结点的左面就是它的左子树,右边就是右子树。

下面这张图就是整个流程:

在这里插入图片描述

在这里插入图片描述

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return buildTreeChild(preorder, inorder, 0, inorder.length - 1);
    }
    public int preIndex;//遍历整个前序遍历,必须是全局变量,如果定义成局部变量,那么每次递归她都会被重置成0,达不到遍历的效果
    private TreeNode buildTreeChild(int[] preorder, int[] inorder, int inbegin, int inend) {
        //inbegin:中序遍历的开始
        //inend:中序遍历的结束
        //递归出口
        if(inbegin > inend) {
            //根据图二,我们发现不管是在寻找左子树还是右子树,只要子树为空,inbegin > inend 是一定的,所以我们可以在这设定一个递归结束的条件,为空返回null
            return null;
        }
        TreeNode root = new TreeNode(preorder[preIndex]);//根节点
        int inIndex = findIndex(inorder, inbegin, inend, preorder[preIndex]);//找到中序遍历中,这个根结点的位置,在inbegin和inend间的区间内
        preIndex++;
        //preIndex++之后,先遍历到的结点是此时结点的左子树的根结点,所以先创建左树,再创建右树
        root.left = buildTreeChild(preorder, inorder, inbegin, inIndex - 1);//此结点的左子树等于inbegin到inIndex - 1之间的所有
        root.right = buildTreeChild(preorder, inorder, inIndex + 1, inend);//此结点的右子树等于inIndex + 1到inend之间的所有
        return root;//返回这个结点
    }
    private int findIndex(int[] inorder, int inbegin, int inend, int key){
        //中序遍历,在inbegin到inend中找结点位置
        for(int i = inbegin; i <= inend; i++) {
            if(inorder[i] == key) {
                return i;
            }
        }
        return -1;
    }
}

106. 从中序与后序遍历序列构造二叉树

后序遍历 :左 右 根

在后序遍历中从最后往前遍历,先拿到根,再在中序遍历中找根所在的位置

class Solution2 {
    public int postIndex;
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        postIndex = postorder.length - 1;
        return buildTreeChild(postorder, inorder, 0, inorder.length - 1);
    }

    private TreeNode buildTreeChild(int[] postorder, int[] inorder, int inbegin, int inend) {
        //递归出口
        if(inbegin > inend) {
            return null;
        }
        TreeNode root = new TreeNode(postorder[postIndex]);//根节点
        int rootIndex = findIndex(inorder, inbegin, inend, postorder[postIndex]);
        postIndex--;
        //因为postIndex--之后,指向的那个结点是此时结点右子树,所以先创建右树,再创建左树
        root.right = buildTreeChild(postorder, inorder, rootIndex + 1, inend);
        root.left = buildTreeChild(postorder, inorder, inbegin, rootIndex - 1);
        return root;
    }
    private int findIndex(int[] inorder, int inbegin, int inend, int key){
        for(int i = inbegin; i <= inend; i++) {
            if(inorder[i] == key) {
                return i;
            }
        }
        return -1;
    }
}

606. 根据二叉树创建字符串

当当前节点不为空,就先拼接它的值,然后开始遍历左树,

如果左树不为空,那就先拼接一个(,然后继续遍历左树的这整棵树,遍历完了就再加上个).

如果左树为空,那就再看一下右树,如果右树不为空,就拼接上(),在表左树为空,如果右树也为空,则可以省略()

遍历完左树就开始遍历右树,如果右树不为空,那就和左树不为空的步骤一样。

如果右树为空,则可以直接省略括号。

在这里插入图片描述

public String tree2str(TreeNode root) {
    if(root == null) {
        return null;
    }
    StringBuilder stringBuilder = new StringBuilder();
    tree2strChild(root, stringBuilder);
    return stringBuilder.toString();

}
private void tree2strChild(TreeNode t, StringBuilder stringBuilder){
    if(t == null) {
        return;
    }
    stringBuilder.append(t.val);
    if(t.left != null) {
        stringBuilder.append("(");
        tree2strChild(t.left, stringBuilder);
        stringBuilder.append(")");
    } else {
        if(t.right == null) {
            return;
        } else {
            stringBuilder.append("()");
        }
    }
    if(t.right != null) {
        stringBuilder.append("(");
        tree2strChild(t.right, stringBuilder);
        stringBuilder.append(")");
    }
}

二叉树前序非递归遍历实现

借助栈,模拟递归过程。

定义一个cur来遍历(前序)结点,只要结点不为空,那么就将他放入栈中,并打印,如果此时cur为空,则弹出栈顶结点,遍历栈顶结点的右边,继续遍历,直到栈为空。

public void preOrder2(TreeNode root) {
    if(root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(cur != null || !stack.empty()){
        //这里进入循环的条件不能只有!stack.empty(),否则一开始就进不去循环
        while(cur != null) {
            //不是空就入栈
            stack.push(cur);
            System.out.print(cur.val + " ");
            cur = cur.left;
        }
        //cur == null
        TreeNode top = stack.pop();//弹出栈顶元素
        cur = top.right;//遍历右边
        //又开始循环遍历,直到栈的元素为空
    }
}

二叉树中序非递归遍历实现

和前序遍历的思路是一样的,就是打印的时机变了

左 根 右

public void inOrder2(TreeNode root) {
    if(root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while(cur != null || !stack.empty()){
        while(cur != null) {
            stack.push(cur);
            //这里不能打印只有左树遍历到尽头才能打印
            cur = cur.left;
        }
        //cur == null
        //证明 左树 没有节点可以遍历了
        TreeNode top = stack.pop();
        //开始弹出打印 根
        System.out.print(top.val + " ");
        //继续遍历右树
        cur = top.right;
    }
}

二叉树后序非递归遍历实现

public void postOrder2(TreeNode root) {
    if(root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    TreeNode prev = null;
    while(cur != null || !stack.empty()){
        while(cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        TreeNode top = stack.peek();
        //再去遍历右树,遍历完右树,再去打印根
        if(top.right == null || top.right == prev) {
            System.out.print(top.val + " ");
            prev = top;//纪录被打印过的点,防止死循环
            stack.pop();
        } else {
            cur = top.right;
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/20650.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

康耐视智能相机insight主从触发以及康耐视insight视觉系统之间数据特有交互方式

1、一个相机设置为主系统,指定自己的名称,下方可以指定发给从系统的数据,触发方式不要设置 成网络 2、另外一个相机触发方式设置成网络,然后下方指定主系统的名称 3、主系统每触发一次,就会带动从系统触发,同时指定的数据会发送到从系 统,从系统使用函数getstring获取…

爱奇艺2020校招Java方向笔试题(第一场)

1.计算下列程序的时间复杂度&#xff08;&#xff09; for (i1;i<n;i)for(j1;j<m;j){a1,a2,a3,a4};A.O(n) B.O(mn) C.O(m) D.O(1) 答案:B 2.求递归方程T(n)4T(n/2)n 的解 ( ) A.O(n) B.O(n^2) C.O(n^3) D.O(logn) 答案:B 用Mater公式计算 3.下列关于动态规划算法说法错…

【rust】| 06——语言特性 | 所有权

系列文章目录 【rust】| 00——开发环境搭建 【rust】| 01——编译并运行第一个rust程序 【rust】| 02——语法基础 | 变量(不可变?)和常量 【rust】| 03——语法基础 | 数据类型 【rust】| 04——语法基础 | 函数 【rust】| 05——语法基础 | 流程控制 【rust】| 06——语言特…

国货大佬“卡脖子”后王者风范不减?小米卷出光学拍摄“天花板”?| 手机行业社媒心智品牌榜出炉

Social Power 核心解读 1、智能手机“乍暖还寒”&#xff0c;龙头品牌仍稳占消费者心智 比拼屏幕、赶超系统、迭代形态、拓展概念&#xff1f;眼花缭乱过后&#xff0c;产品精益求精&#xff0c;建立稳固的消费者认知&#xff0c;才是“保鲜”关键。在最新发布的数说故事5月…

MyBatis Plus 拦截器实现数据权限控制

一、介绍 上篇文章介绍的MyBatis Plus 插件实际上就是用拦截器实现的&#xff0c;MyBatis Plus拦截器对MyBatis的拦截器进行了包装处理&#xff0c;操作起来更加方便 二、自定义拦截器 2.1、InnerInterceptor MyBatis Plus提供的InnerInterceptor接口提供了如下方法&#x…

基于AI技术的API开发工具,自动化和智能化快速高效开发API

一、开源项目简介 ApiCat 是一款基于 AI 技术的 API 开发工具&#xff0c;它旨在通过自动化和智能化的方式&#xff0c;帮助开发人员更快速、更高效地开发 API。ApiCat 支持 OpenAPI 和 Swagger 的数据文件导入和导出&#xff0c;并可以对用户输入的 API 需求进行分析和识别&a…

真题详解(补码转换)-软件设计(七十四)

真题详解(索引查询)-软件设计&#xff08;七十三)https://blog.csdn.net/ke1ying/article/details/130659024 Composite模式&#xff1a; 以树形结构来表示”整体-部分”的关系&#xff0c;使得单个和团体的使用都具有一致性。 对一个基本有序的数组进行排序&#xff0c;适合…

精彩回顾 | Fortinet Accelerate 2023·中国区巡展厦门站

Fortinet Accelerate 2023中国区 5月16日&#xff0c;Fortinet Accelerate 2023中国区巡展来到魅力“鹭岛”——厦门&#xff0c;技术、产品和业务专家&#xff0c;携手亚马逊云科技、唯一网络等云、网、安合作伙伴&#xff0c;与交通、物流、金融等各行业典型代表客户&#x…

HR真的会嫌弃跳槽频繁的测试人员吗?

我们都知道&#xff1a;“跳槽≠涨薪”&#xff0c;但是对于测试人来说&#xff0c;跳槽绝哔能和升职加薪画上等号啊。 所以&#xff0c;有很多测试人在一家公司常常待不到1年&#xff0c;就另觅新东家&#xff0c;来借此达成升职加薪的目的。 有人提出质疑&#xff1a;为什么测…

嚯——ChatGPT是很强,但也会胡说八道。。。

现在的ChatGPT确实强&#xff0c;但是也会一本正经的胡说八道&#xff0c;例如它回答“nineteen”中有12个字母、或是旗鱼是哺乳动物…… 尽管ChatGPT可以生成流畅甚至优雅的散文&#xff0c;轻松通过困扰了AI领域超过70年的图灵测试基准&#xff0c;但它也可能看起来非常愚蠢…

探讨接口测试颗粒度

偶然间在论坛上看到一个帖子&#xff0c;帖子内容如下&#xff1a; 假设现在有一个新增商品的接口&#xff0c;返回的参数中有新增商品的 id&#xff08;每次返回的 id 都不一样&#xff09;、success&#xff08;判断是否成功&#xff0c;0 失败 1 成功&#xff09; 1. 接口…

Elasticsearch painless脚本教程(包含Java API和SpringDataElasticsearch调用脚本)

Elasticsearch painless脚本 1.什么是painless2.painless的特性3.使用kibana进行准备操作3.1 使用kibana创建索引和映射3.2 使用kibana添加测试数据 4.使用painless执行查询操作关于脚本查询须知4.1 字段查询脚本4.1 排序查询脚本 5.如何使用painless执行更新操作关于脚本查询须…

【机器学习】第二节:线性回归和线性分类

作者&#x1f575;️‍♂️&#xff1a;让机器理解语言か 专栏&#x1f387;&#xff1a;机器学习sklearn 描述&#x1f3a8;&#xff1a;本专栏主要分享博主学习机器学习的笔记和一些心得体会。 寄语&#x1f493;&#xff1a;&#x1f43e;没有白走的路&#xff0c;每一步都算…

Text-to-SQL提示工程【Prompt Engineering】

我们刚刚启动了一个开源项目pg-text-query&#xff0c;目标是为文本到 SQL 制作生产就绪的大型语言模型 (LLM) 提示。 我们的目标是 利用 LLM、我们自己对 PostgreSQL 数据库的深入了解以及严格的测试来开发一流的文本到 SQL 的翻译。 推荐&#xff1a;用 NSDT设计器 快速搭建…

【MySQL】C语言连接数据库

在使用数据库的时候&#xff0c;我们是操作客户端方的&#xff0c;向服务器请求数据。MySQL的端口号-3306 连接mysql需要头文件&#xff1a;#include<mysql/mysql.h> 基础连接步骤&#xff1a; 1.创建连接句柄 MYSQL mysql_con;//连接句柄 2.初始化连接句柄mysql_i…

「OceanBase 4.1 体验」|大厂开始接入的国产分布式数据库,不来了解了解?

OceanBase 4.1 体验 前言OCP Express在线升级功能租户级物理备库TP&#xff08;事务处理&#xff09;和AP&#xff08;分析处理&#xff09;优化TP 性能优化AP 性能优化 结尾 前言 上次我们讲了本人自己亲自上手OceanBase 4.1的初体验&#xff0c;国产的分布式数据库也太太太太…

有哪些好用的AI工具?

现在有很多好用的AI工具&#xff0c;以下是一些常用的&#xff1a; 1. TensorFlow&#xff1a;谷歌开发的深度学习框架&#xff0c;支持多种编程语言&#xff0c;包括Python、C、Java等。 2. PyTorch&#xff1a;Facebook开发的深度学习框架&#xff0c;易于使用&#xff0c;…

Redis源码分析之网络模型

Redis网络模型 阅读源码的初衷Redis源码阅读 阅读源码的初衷 很多网上解释这个Redis为啥这么块&#xff1f;都会说Redis这么快的原因会有一个Redis才用了单线程&使用了多路io复用来检查io事件&#xff0c;单线程可以避免多线程对资源的竞争。如果我们使用了多线程那么就需…

ChatGPT其实并不想让开发人员做这5件事情

前言 ChatGPT已经火爆了快半年了吧&#xff0c;紧接着国内也开始推出了各种仿制品&#xff0c;我甚至一度怀疑&#xff0c;如果人家没有推出ChatGPT&#xff0c;这些仿制品会不会出现。而很多人也嗨皮得不行&#xff0c;利用各种方法开始科学上网&#xff0c;用ChatGPT做各种觉…

[读书笔记] Variational AutoEncoders

小全读书笔记 《Variational AutoEncoders》 1. Generative Model &#xff08;生成式模型&#xff09;简述2. 简单生成模型 AutoEncoders2.1 结构2.2 不足 3. Variational AutoEncoders4. Variational AutoEncoders的数学支持 此读书笔记来自于Joseph Rocca的Understanding Va…