您现在的位置是:首页 >其他 >二叉树基础知识&力扣题构造二叉树总结网站首页其他

二叉树基础知识&力扣题构造二叉树总结

RenX000 2024-08-20 12:01:02
简介二叉树基础知识&力扣题构造二叉树总结

二叉树

如何理解二叉树,This is a question!

作者在去年被布置要求学习二叉树时对二叉树的理解并不是很深刻,甚至可以说是绕道走,但是Luck of the draw only draws the unlucky,在学期初考核时,作者三道二叉树题都没做出来,连最简单的创建都忘记了,当时想着提升,却拖到了现在;

这篇文章可以说是作者二叉树算法实战经验总结

基础知识

先来回顾一下二叉树的基本知识;但还是请读者曾经是见过二叉树的

一颗很普通的二叉树,他的定义是:是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成;

其余的基本知识,还是请自行搜索一下;现在来了解几种遍历手法

image-20230602232804296

二叉树的前序遍历

 int len;
//DLR
void DFS(int*arr,struct TreeNode*root) {
     if(root == NULL) return;
     arr[len] = root->val;
     len++;
     DFS(arr,root->left);
     DFS(arr,root->right);

 }

int* preorderTraversal(struct TreeNode* root, int* returnSize){
    int*arr = (int*)malloc(sizeof(int)*100);
    len = 0;
    if(root != NULL)
        DFS(arr,root);
    *returnSize = len;
    return arr;
}

二叉树的后序遍历

int len;
//LRD
void DFS(int*arr,struct TreeNode*root) {
     if(root == NULL) return;
     DFS(arr,root->left);
     DFS(arr,root->right);
     arr[len] = root->val;
     len++;

 }

int* postorderTraversal(struct TreeNode* root, int* returnSize){
   int*arr = (int*)malloc(sizeof(int)*100);
    len = 0;
    if(root != NULL)
        DFS(arr,root);
    *returnSize = len;
    return arr;
}

二叉树的中序遍历

int len;
//LDR
void DFS(int*arr,struct TreeNode* root) {
    if(root == NULL) return;
    DFS(arr,root->left);
    arr[len] = root->val;
    len++;
    DFS(arr,root->right);
}

int* inorderTraversal(struct TreeNode* root, int* returnSize){
    len = 0;
    int*arr = (int*)malloc(sizeof(int)*100);
    if(root != NULL) {
        DFS(arr,root);
    }
    *returnSize = len;
    return arr;
}

在三种遍历方法中,我们都采用了递归的方法,而仔细观察也会发现,这三段代码的区别,从字面上仅是递归函数的位置不同,也就是遍历时的顺序不同,这个也就是我们在解决二叉树算法题时要注意的点,不同的遍历方式对应着不同的类型的解决方案

在这三种遍历代码前,都标注了三个大写字母,D、L、R 分别代表遍历根结点、遍历左子树、遍历右子树,会发现字母的顺序也就代表着优先级也就是代码呈现的顺序;

如何理解这种遍历手法,以前序遍历为例,他的三个字母顺序为DLR,也就是根节点最优先,自然就是A,之后就到B,C这两个在这里判断的条件就成了谁左谁优先,也就是B先行,B的底下还有子树,坚持D最优先原则,由于他们有一个优秀的父结点,因为所有都可以得到先行一步的遍历权利,等到遍历完了也就是在轮到C和C的子树;

总结就是:坚持大方向,父结点优先,子结点也继承了优先权

那么基础知识就讲到这里,接下来实战演练

实战演练

104.二叉树的最大深度

这是一道非常基础的题目,也就是再问一根树枝最长能有多长,也就是遍历出奇迹

第一种方法:回溯

(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”

对这种算法的解释就是:一条道走到黑,且要走完每条道

先看代码:

class Solution {
    int depth = 0;
    int res = 0;

    public int maxDepth(TreeNode root) {
        traverse(root);
        return res;
    }

    void traverse(TreeNode root) {
        if (root == null) {
            return;
        }

        depth++;
        // 遍历的过程中记录最大深度
        res = Math.max(res, depth);
        traverse(root.left);
        traverse(root.right);
        depth--;
    }
}

对代码的唯一不理解的地方应该是为何要先depth++之后又depth--,我们可以把整个函数理解为一个在四处游走的图钉,当图钉到这个点上了,depth++,现在我小图钉在这里一条道走到黑要离开去其他地方了,那么则先depth--,因为回到更上面一层去了;这跟前序遍历很像

至于res的取值,只要放在depth--之前就行了

第二种方法:动态规划

class Solution2 {
    // 定义:输入一个节点,返回以该节点为根的二叉树的最大深度
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int leftMax = maxDepth(root.left);
        int rightMax = maxDepth(root.right);
        // 根据左右子树的最大深度推出原二叉树的最大深度
        return 1 + Math.max(leftMax, rightMax); //+1的原因:算上根节点
    }
}

代码很好理解,但是作者自己想不出来;思想很简单,也是遍历然后最后得出最大值,这个顺序相当于在后序遍历

543.二叉树的直径

其实这道题跟上面一道题非常像,作者对这道题的解读就是:从根节点出发第一长的树枝和第二长的树枝长度和;那么这道题也就迎刃而解了,只要在上一题的第二种解法种稍加修改,添加一个max记录值,便可以做出该题

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int maxDiameter = 0;
    public int diameterOfBinaryTree(TreeNode root) {
        maxDepth(root);
        return maxDiameter;
    }
    
    int maxDepth(TreeNode root) {
        if(root == null) {
            return 0;
        }
        int leftMax = maxDepth(root.left);
        int rightMax = maxDepth(root.right);
        int myDiameter = leftMax + rightMax;
        maxDiameter = Math.max(myDiameter, maxDiameter);
        return 1 + (Math.max(leftMax, rightMax));
    }
}

116.填充每个节点的下一个右侧指针节点

这道题的神奇之处就在于,把不是一个父结点,但是在同一层的联系在了一起,那么只要把相对位置是left&right的都链接在一起,也就是多加一条执行语句traverse(node1.right, node2.left)即可

class Solution {
    public Node connect(Node root) {
        if(root == null) {
            return null;
        }
        traverse(root.left, root.right);
        return root;
    }
    void traverse(Node node1, Node node2) {
        if(node1 == null || node2 == null) {
            return;
        }
        node1.next = node2;
        traverse(node1.left, node1.right);
        traverse(node2.left, node2.right);
        traverse(node1.right, node2.left);
    }
}

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

它使用HashMap来存储中序遍历数组中值到索引的映射关系,这有助于确定根节点并将数组划分为子树以进行递归构建。
buildTree 方法接受前序遍历和中序遍历数组作为输入,并通过将中序遍历数组中的值和索引存储在 valToIndex HashMap 中来进行初始化。然后,它调用 build 方法,传递必要的参数来构建二叉树。
build 方法是一个递归函数,用于构建二叉树。它检查子树是否为空(preStart > preEnd),如果是则返回null。否则,它从前序遍历数组中确定根节点的值(preorder[preStart]),并找到它在中序遍历数组中的索引。然后,它计算左子树的大小(index - inStart),并先构造当前根节点。接下来,它递归构建左子树和右子树,并将它们连接到根节点。最后,它返回根节点

class Solution {
    // 存储 inorder 中值到索引的映射
    HashMap<Integer, Integer> valToIndex = new HashMap<>();

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        for (int i = 0; i < inorder.length; i++) {
            valToIndex.put(inorder[i], i);
        }
        return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
    }

    /*
       定义:前序遍历数组为 preorder[preStart..preEnd],
       中序遍历数组为 inorder[inStart..inEnd],
       构造这个二叉树并返回该二叉树的根节点
    */
    TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
        if (preStart > preEnd) {
            return null;
        }

        // root 节点对应的值就是前序遍历数组的第一个元素
        int rootVal = preorder[preStart];
        // rootVal 在中序遍历数组中的索引
        int index = valToIndex.get(rootVal);
        int leftSize = index - inStart;

        // 先构造出当前根节点
        TreeNode root = new TreeNode(rootVal);
        // 递归构造左右子树
        root.left = build(preorder, preStart + 1, preStart + leftSize, inorder, inStart, index - 1);
        root.right = build(preorder, preStart + leftSize + 1, preEnd, inorder, index + 1, inEnd);
        return root;
    }
}

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

class Solution {
    // 存储 inorder 中值到索引的映射
    HashMap<Integer, Integer> valToIndex = new HashMap<>();

    public TreeNode buildTree(int[] inorder, int[] postorder) {
        for (int i = 0; i < inorder.length; i++) {
            valToIndex.put(inorder[i], i);
        }
        return build(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1);
    }

    /*
       定义:
       中序遍历数组为 inorder[inStart..inEnd],
       后序遍历数组为 postorder[postStart..postEnd],
       构造这个二叉树并返回该二叉树的根节点
    */
    TreeNode build(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd) {

        if (inStart > inEnd) {
            return null;
        }
        // root 节点对应的值就是后序遍历数组的最后一个元素
        int rootVal = postorder[postEnd];
        // rootVal 在中序遍历数组中的索引
        int index = valToIndex.get(rootVal);
        // 左子树的节点个数
        int leftSize = index - inStart;
        TreeNode root = new TreeNode(rootVal);
        // 递归构造左右子树
        root.left = build(inorder, inStart, index - 1, postorder, postStart, postStart + leftSize - 1);
        root.right = build(inorder, index + 1, inEnd, postorder, postStart + leftSize, postEnd - 1);
        return root;
    }
}

889.根据前序与后序遍历构造二叉树

class Solution {
    // 存储 postorder 中值到索引的映射
    HashMap<Integer, Integer> valToIndex = new HashMap<>();

    public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
        for (int i = 0; i < postorder.length; i++) {
            valToIndex.put(postorder[i], i);
        }
        return build(preorder, 0, preorder.length - 1, postorder, 0, postorder.length - 1);
    }

    // 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd]
    // 构建二叉树,并返回根节点。
    TreeNode build(int[] preorder, int preStart, int preEnd, int[] postorder, int postStart, int postEnd) {
        if (preStart > preEnd) {
            return null;
        }
        if (preStart == preEnd) {
            return new TreeNode(preorder[preStart]);
        }

        // root 节点对应的值就是前序遍历数组的第一个元素
        int rootVal = preorder[preStart];
        // root.left 的值是前序遍历第二个元素
        // 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点
        // 确定 preorder 和 postorder 中左右子树的元素区间
        int leftRootVal = preorder[preStart + 1];
        // leftRootVal 在后序遍历数组中的索引
        int index = valToIndex.get(leftRootVal);
        // 左子树的元素个数
        int leftSize = index - postStart + 1;

        // 先构造出当前根节点
        TreeNode root = new TreeNode(rootVal);
        // 递归构造左右子树
        // 根据左子树的根节点索引和元素个数推导左右子树的索引边界
        root.left = build(preorder, preStart + 1, preStart + leftSize, postorder, postStart, index);
        root.right = build(preorder, preStart + leftSize + 1, preEnd, postorder, index + 1, postEnd - 1);

        return root;
    }
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。