前端学算法-二叉树(一)

基本概念

二叉树是一种重要的树形数据结构,在计算机科学中广泛应用。它由节点(也称为顶点)和边组成,每个节点最多有两个子节点,通常称为左子节点和右子节点。

满二叉树

如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。比如这种

满二叉树,也可以说深度为k,有2^k-1个节点的二叉树

完全二叉树

完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。

换句话说,完全二叉树可以看作是满二叉树从右向左删除若干节点得到的。

存储

二叉树存储可以链式存储,也可以顺序存储,链式存储用的是指针,而顺序存储用的就是数组,链式存储是通过指针将分布在各个地址的节点串联在一起,而顺序存储就是存储的元素在内存中是连续分布的。链式存储比较简单了,这里看下顺序存储,也就是数组存储。

看下这个图

有一个规律就是:父节点数组下标是i,那么左孩子就是i^2+1,右孩子就是i^2+2

上面说的只是索引,也就是在数组中的位置,这个索引和二叉树的值没有任何关系,只是在将数组转成二叉树时,会根据索引来确定节点关系,比如跟节点索引是0,那么它的左孩子索引必然是i^2+1,也就是1,右孩子就是i^2+2,也就是2,当然这里的1和2,就是索引,取得就是数组中对应索引的值。

接下来写一个将数组转为树的方法。

ini 复制代码
class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}
 
function arrayToCompleteBinaryTree(arr, index = 0) {
    if (index >= arr.length) {
        return null;
    }
 
    const root = new TreeNode(arr[index]);
    root.left = arrayToCompleteBinaryTree(arr, 2 * index + 1);
    root.right = arrayToCompleteBinaryTree(arr, 2 * index + 2);
 
    return root;
}
遍历

二叉树主要有两种遍历方式:

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。主要有前序遍历、中序遍历、后序遍历
  2. 广度优先遍历:一层一层的去遍历。一般有层序遍历
前序遍历

首先了解下:这里的前中后序都是中间根节点的顺序,先遍历根节点就是前序遍历,

前序遍历遍历的顺序就是根节点、左孩子、右孩子

scss 复制代码
function inOrderTraversal(root) {
    if (root === null) {
        return;
    }
    inOrderTraversal(root.left);
    console.log(root.value);
    inOrderTraversal(root.right);
}
中序遍历

中序遍历就是根节点在中间,左孩子、根节点、右孩子

scss 复制代码
function inOrderTraversal(root) {
    if (root === null) {
        return;
    }
    inOrderTraversal(root.left);
    console.log(root.value);
    inOrderTraversal(root.right);
}
后序遍历

后续遍历就是根节点在最后,左孩子、右孩子、根节点

scss 复制代码
function postOrderTraversal(root) {
    if (root === null) {
        return;
    }
    inOrderTraversal(root.left);
    inOrderTraversal(root.right);
    console.log(root.value);
    
}
练习
144.二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

就按照前面描述的,前序就是节点、左孩子、右孩子

scss 复制代码
var preorderTraversal = function(root) {
    let result = [];
    
    function traverse(node) {
        if (node === null) return; // 如果节点为 null,直接返回
        result.push(node.val); // 访问当前节点
        traverse(node.left); // 递归访问左子树
        traverse(node.right); // 递归访问右子树
    }
 
    traverse(root); // 从根节点开始遍历
    return result; // 返回遍历结果
};
145.二叉树的后序遍历

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历

也比较简单,就按照后续遍历顺序,左孩子、右孩子、中节点

scss 复制代码
var postorderTraversal = function(root) {
    let result = []
​
    function traverse(node){
        if(node == null) return
        traverse(node.left)
        traverse(node.right)
        result.push(node.val)
    }
    traverse(root)
    return result
};
94.二叉树的中序遍历

有了前面两个,这个也比较简单,一遍过

遍历顺序就是左孩子、中、右孩子

scss 复制代码
var inorderTraversal = function(root) {
    let result = []
    function traversal(node){
        if(node === null) return
        traversal(node.left)
        result.push(node.val)
        traversal(node.right)
    }
    traversal(root)
    return result
};
​
set HTTP_PROXY=http://127.0.0.1:7890
set HTTPS_PROXY=http://127.0.0.1:7890
层序遍历

这个层序遍历就是遍历二叉树广度优先的一种方式,

ini 复制代码
function levelOrder(root) {
    if (!root) return [];
    
    const result = [];
    const queue = [root];
    
    while (queue.length > 0) {
        const levelSize = queue.length;
        const currentLevel = [];
        
        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            currentLevel.push(node.val);
            
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }
        
        result.push(currentLevel);
    }
    
    return result;
}  

这里就是先定义一个队列,然后开始迭代这个队列,迭代条件就是队列的长度,因为后续会向这个队列中塞入[root],[root.left,root.right], 然后挨个从队列中取出节点,将结果保存在currentLevel中。

开始练习下

练习
102.二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

这个和上面那个方法一样,一遍过

ini 复制代码
var levelOrder = function(root) {
    if(root?.val == null) return []
    let result = []
    let queen = [root]
​
    while(queen.length >0){
        let level = queen.length
        let currentVal = []
​
        for(let i=0; i< level; i++){
            let node = queen.shift()
            currentVal.push(node.val)
            node.left && queen.push(node.left)
            node.right && queen.push(node.right)
        }
        result.push(currentVal)
    }
    return result
};
107.二叉树的层次遍历II

给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

第一想法就是就正常的层序遍历,然后将结果数组reverse翻转下应该可以的

ini 复制代码
var levelOrderBottom = function(root) {
    if(root?.val == null) return []
    let result = []
    let queue = [root]
​
    while(queue.length > 0){
        let level = queue.length
        let currentVal = []
        for(let i=0;i<level;i++){
            let node = queue.shift()
            currentVal.push(node.val)
            node.left && queue.push(node.left)
            node.right && queue.push(node.right)
        }
        result.push(currentVal)
    }
    return result.reverse()
};

也是一遍过

看了题解,发现一种更好的方法,就是深度优先,深度优先遍历都是从底层往上遍历的,很适合这个题目

scss 复制代码
var levelOrderBottom = function(root) {
    const result = [];
    
    function dfs(node, depth) {
        if (!node) return; // 递归终止条件
        
        // 如果当前深度未初始化,先创建一个空数组
        if (!result[depth]) {
            result[depth] = [];
        }
        result[depth].push(node.val); // 将节点值存入对应深度
        
        // 递归处理左右子树
        dfs(node.left, depth + 1);
        dfs(node.right, depth + 1);
    }
    
    dfs(root, 0); // 从根节点开始,深度为 0
    return result.reverse(); // 反转结果数组,实现自底向上
};

看下来也不算严格意义上的深度优先啊,就是记录一个depth,来记录遍历的层级,还是从最上面一层开始遍历的

199.二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

没写出来,看了题解,评论中有一句话说的好,层序遍历的最后一个节点就是你想要的东西

ini 复制代码
var rightSideView = function(root) {
    if (root?.val == null) return [];
    let result = [];
    let queue = [root];
 
    while (queue.length > 0) {
        let levelSize = queue.length;
        let rightMostValue = null; // 存储当前层的最右节点值
 
        for (let i = 0; i < levelSize; i++) {
            let node = queue.shift();
            rightMostValue = node.val; // 更新为当前层的最后一个节点值
 
            // 先加入左子节点,再加入右子节点,确保右子节点在队列末尾
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }
 
        result.push(rightMostValue); // 只加入当前层的最右节点值
    }
 
    return result;
};

用递归的思路,可以这样做:

scss 复制代码
var rightSideView = function(root) {
    if (!root) return [];
    let result = [];
 
    function dfs(node, level) {
        if (!node) return;
 
        // 如果当前层还没有记录节点值,则记录当前节点值
        if (result[level] === undefined) {
            result[level] = node.val;
        }
 
        // 先递归右子树,再递归左子树,确保右子树的节点值覆盖左子树
        dfs(node.right, level + 1);
        dfs(node.left, level + 1);
    }
 
    dfs(root, 0);
    return result;
};
637.二叉树的层平均值

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。

常规的层序遍历,然后取平均值

先用队列写一个

ini 复制代码
var averageOfLevels = function(root) {
    if (!root) return [];
    let result = [];
    let queue = [root]; // 初始化队列,加入根节点
 
    while (queue.length > 0) {
        let levelSize = queue.length;
        let levelSum = 0;
 
        for (let i = 0; i < levelSize; i++) {
            let node = queue.shift();
            levelSum += node.val; // 累加当前层的节点值
 
            // 将子节点加入队列
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }
 
        // 计算当前层的平均值并加入结果
        result.push(levelSum / levelSize);
    }
 
    return result;
};

用递归的方法再写一个

ini 复制代码
var averageOfLevels = function(root) {
    const levelSums = [];  // 存储每层的总和
    const levelCounts = []; // 存储每层的节点数
    
    // 深度优先搜索递归函数
    const dfs = (node, level) => {
        if (!node) return;
        
        // 如果当前层还没有初始化,则初始化
        if (level >= levelSums.length) {
            levelSums.push(0);
            levelCounts.push(0);
        }
        
        // 更新当前层的总和和节点计数
        levelSums[level] += node.val;
        levelCounts[level] += 1;
        
        // 递归处理左右子树
        dfs(node.left, level + 1);
        dfs(node.right, level + 1);
    };
    
    // 从根节点开始遍历
    dfs(root, 0);
    
    // 计算每层的平均值
    const averages = [];
    for (let i = 0; i < levelSums.length; i++) {
        averages.push(levelSums[i] / levelCounts[i]);
    }
    
    return averages;
};
429.N叉树的层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

有点懵逼啊这个,N叉树如何去取下面层级的值呢,看了力扣的结构定义明白了,通过children来将树节点关联起来,用队列的方法来试试:

和常规的层序遍历不太一样,常规的层序遍历直接将left``right

scss 复制代码
var levelOrder = function(root) {
    if(!root) return []
    let result = []
    let queue = [root]
​
    while(queue.length){
        let level = queue.length
        let current = []
        for(let i=0;i<level;i++){
            let node = queue.shift()
            current.push(node.val)
            // queue.push(node.children)
            if (node.children) {
                for (let child of node.children) {
                    queue.push(child);
                }
            }
        }
        result.push(current)
    }
    return result
};  

用递归的思路写一下

scss 复制代码
var levelOrder = function(root) {
    if(!root) return []
    let result = []
    function dfs(node,level){
        if(node == null) return
        if(!result[level]){
            result[level] = []
        }
        result[level].push(node.val)
        if(node.children){
            node.children.map(item =>{
                dfs(item,level + 1)
            })
        }
    }
    dfs(root,0)
    return result
};  

有了上面的迭代案例,递归就好些很多了,一遍过

515.在每个树行中找最大值

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

和429题目差不多,在向result塞值的时候比较下当前值和数组中值,大了就push

队列思路写一下:

ini 复制代码
var largestValues = function(root) {
    if (root == null) return [];
    let result = []
    let queue = [root]
​
    while(queue.length){
        let level = queue.length
        let currentMax = -Infinity
​
        for(let i=0;i < level;i++){
            let node = queue.shift()
            node.val > currentMax && (currentMax = node.val)
            // currentMax = Math.max(currentMax, node.val)
            node.left && queue.push(node.left)
            node.right && queue.push(node.right)
        }
        result.push(currentMax)
    }
    return result
};

和之前的思路大差不差,currentMax保存每层遍历的最大值,然后push到result中

尝试下递归的写法

scss 复制代码
var largestValues = function(root) {
    if (root == null) return [];
    let result = []
    function dfs(node,level){
        if(node == null) return
        if (result[level] === undefined) {
            result[level] = -Infinity;
        }
        // if(!result[level]){
        //     result[level] = -99999999
        // }
        result[level] = Math.max(result[level],node.val)
        node.left && dfs(node.left,level +1)
        node.right && dfs(node.right,level +1)
​
    }
​
    dfs(root,0)
    return result
};
104.二叉树的最大深度(opens new window)

这个放在层序遍历这里,也是立马就有思路了,前面用递归写层序遍历时,会向递归函数写一个level,最后返回这个level就是最终的结果了

scss 复制代码
var maxDepth = function(root) {
    if(root == null) return 0
    let result = []
    let saveLevel = 0
    function dfs(node,level){
        if(node == null) return
        if(result[level] == undefined){
            result[level] = []
        }
        saveLevel = Math.max(level,saveLevel)
        result[level].push(node.val)
        node.left && dfs(node.left,level +1)
        node.right && dfs(node.right,level +1)
        
    }
    dfs(root,0)
    return saveLevel + 1
};

一遍过,总感觉这个题有更简单的做法,试着用队列的方法做一下

队列的方法也是一遍过,毕竟练习了这么多

ini 复制代码
var maxDepth = function(root) {
    if(root == null) return 0
    let result = []
    let queue = [root]
    while(queue.length){
        let level = queue.length
        let current = []
        for(let i=0;i<level;i++){
            let node = queue.shift()
            current.push(node.val)
            node.left && queue.push(node.left)
            node.right && queue.push(node.right)
        }
        result.push(current)
    }
    return result.length
};

看了题解有个很简单的写法:

ini 复制代码
var maxDepth = function(root) {
    if (root === null) return 0;
    return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
};

确实牛逼这种写法

111.二叉树的最小深度

有了最大深度的练习,应该很好写,发现我想多了,写不出来,问了ai才写出来,大概思路就是层序遍历,遇到第一个叶子节点就直接返回。

ini 复制代码
var minDepth = function(root) {
    if (root == null) return 0;
    
    let queue = [[root, 1]];
    
    while (queue.length > 0) {
        let [node, depth] = queue.shift();
        
        // 检查当前节点是否为叶子节点
        if (node.left === null && node.right === null) {
            return depth;
        }
        
        // 将子节点加入队列
        if (node.left !== null) {
            queue.push([node.left, depth + 1]);
        }
        if (node.right !== null) {
            queue.push([node.right, depth + 1]);
        }
    }
    
    return 0; // 如果没有叶子节点(理论上不会发生)
};
相关推荐
Sun_light4 分钟前
深入理解JavaScript中的「this」:从概念到实战
前端·javascript
小桥风满袖6 分钟前
Three.js-硬要自学系列33之专项学习基础材质
前端·css·three.js
Raven100868 分钟前
L1G2-OpenCompass 评测书生大模型实践
算法
聪明的水跃鱼10 分钟前
Nextjs15 构建API端点
前端·next.js
NAGNIP11 分钟前
RAG信息检索-如何让模型找到‘对的知识’
算法
小明爱吃瓜27 分钟前
AI IDE(Copilot/Cursor/Trae)图生代码能力测评
前端·ai编程·trae
水冗水孚30 分钟前
🚀四种方案解决浏览器地址栏预览txt文本乱码问题🚀Content-Type: text/plain;没有charset=utf-8
javascript·nginx·node.js
不爱说话郭德纲32 分钟前
🔥Vue组件的data是一个对象还是函数?为什么?
前端·vue.js·面试
绅士玖35 分钟前
JavaScript 中的 arguments、柯里化和展开运算符详解
前端·javascript·ecmascript 6
每天都想着怎么摸鱼的前端菜鸟37 分钟前
【uniapp】uniapp热更新WGT资源,简单的多环境WGT打包脚本
javascript·uni-app