前端算法笔记-树的基本应用

本文主要介绍了二叉树的四种遍历方法:先序、后序、中序和层序遍历的非递归实现。通过讲解和代码示例,详细阐述了每种遍历方式的逻辑和关键点,帮助读者理解数据结构中树的遍历操作。同时,提到了《剑指Offer》中的相关题目,如二叉树的镜像、对称的二叉树和序列化二叉树,进一步加深对二叉树的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

准备根据修言老师的小册子结合leetcode上剑指offer的二叉树学习这一部分。

先序、后序、中序、层序遍历的非递归实现

在我的理解里,树就是一种特殊的图。先序对应了图的深度遍历,而层序对应了图的广度遍历。

先序和后序的递归实现方式还是比较好理解,非递归实现就自己手动去实现一个递归栈。

我初学习数据结构是跟着浙大的mooc,其中对于树的遍历有一个很好理解的解释:

按照叉号读出来的顺序是前序遍历,正好是第一次经过该节点

按照星号读出来的顺序是中序遍历,正好是第二次经过该节点

按照三角形读出来的顺序是后序遍历,正好是第三次经过该节点

 

ok 画不多上~上代码

先序遍历

需要关注的是对stack数组的一个入栈顺序。

众所周知(x)先序是 root => left => right 的顺序~ 所以入栈的时候要先right 再 left 确保弹栈的时候总是left先被访问到。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const preorderTraversal = function(root) {
  // 定义结果数组
  const res = []  
  // 处理边界条件
  if(!root) {
      return res
  }
  // 初始化栈结构
  const stack = [] 
  // 首先将根结点入栈
  stack.push(root)  
  // 若栈不为空,则重复出栈、入栈操作
  while(stack.length) {
      // 将栈顶结点记为当前结点
      const cur = stack.pop() 
      // 当前结点就是当前子树的根结点,把这个结点放在结果数组的尾部
      res.push(cur.val)
      // 若当前子树根结点有右孩子,则将右孩子入栈
      if(cur.right) {
          stack.push(cur.right)
      }
      // 若当前子树根结点有左孩子,则将左孩子入栈
      if(cur.left) {
          stack.push(cur.left)
      }
  }
  // 返回结果数组
  return res
};

 

后序遍历

欸嘿,其实后序遍历和先序遍历还是很像滴!

众所周知(x)后序是  left => right => root 的顺序 

但是,我们遍历的时候总是会先访问到root。

所以有个很好的办法,之前我们都是在res数组的尾部新增一个val。后序的时候就可以改成在res数组的头部新增一个val。(等于把res从一个队列变成了一个栈

相对应的,原本考虑到出栈,总是让right压在栈底,left放在栈顶。

在后序遍历时这个思路就要反一下了,我们应该先压入left ,再压入 right~

这也right会被率先弹出,又压入res数组中~ res 的顺序就变成了:  left => right => root

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const postorderTraversal = function(root) {
  // 定义结果数组
  const res = []  
  // 处理边界条件
  if(!root) {
      return res
  }
  // 初始化栈结构
  const stack = [] 
  // 首先将根结点入栈
  stack.push(root)  
  // 若栈不为空,则重复出栈、入栈操作
  while(stack.length) {
      // 将栈顶结点记为当前结点
      const cur = stack.pop() 
      // 当前结点就是当前子树的根结点,把这个结点放在结果数组的头部
      res.unshift(cur.val)
      // 若当前子树根结点有左孩子,则将左孩子入栈
      if(cur.left) {
        stack.push(cur.left)
      }  
      // 若当前子树根结点有右孩子,则将右孩子入栈
      if(cur.right) {
        stack.push(cur.right)
      }
  }
  // 返回结果数组
  return res
};

中序遍历

中序遍历相比前两个比较相似的遍历方式呢,就显得有些格格不入(x)

众所周知(x)中序是  left => root  => right 的顺序 

代码思路:
while(栈非空 || 指针非空):
          all的左子树入栈; //包括根节点
          出栈访问;
          指针指向右结点

这也的好处是,在出栈访问的时候,实际访问的是root点。然后,right才会入栈,确保了遍历的顺序~

 

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const inorderTraversal = function(root) {
  // 定义结果数组
  const res = []  
  // 初始化栈结构
  const stack = []   
  // 用一个 cur 结点充当游标
  let cur = root  
  // 当 cur 不为空、或者 stack 不为空时,重复以下逻辑
  while(cur || stack.length) {
      // 这个 while 的作用是把寻找最左叶子结点的过程中,途径的所有结点都记录下来 
      while(cur) {
          // 将途径的结点入栈
          stack.push(cur)  
          // 继续搜索当前结点的左孩子
          cur = cur.left  
      }
      // 取出栈顶元素
      cur = stack.pop()  
      // 将栈顶元素入栈
      res.push(cur.val)  
      // 尝试读取 cur 结点的右孩子
      cur = cur.right
  }
  // 返回结果数组
  return res
};

层序遍历

修言老师在这里引了一题,关于层序遍历。我觉得层序遍历还是比较简单滴,直接看代码就很好理解了~

题目描述:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

 

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
const levelOrder = function(root) {
    // 初始化结果数组
    const res = []  
    // 处理边界条件
    if(!root) {
        return res
    }  
    // 初始化队列
    const queue = []   
    // 队列第一个元素是根结点
    queue.push(root)  
    // 当队列不为空时,反复执行以下逻辑
    while(queue.length) {
        // level 用来存储当前层的结点
        const level = []  
        // 缓存刚进入循环时的队列长度,这一步很关键,因为队列长度后面会发生改变
        const len = queue.length  
        // 循环遍历当前层级的结点
        for(let i=0;i<len;i++) {
            // 取出队列的头部元素
            const top = queue.shift()  
            // 将头部元素的值推入 level 数组
            level.push(top.val)
            // 如果当前结点有左孩子,则推入下一层级
            if(top.left) {
                queue.push(top.left)
            }
            // 如果当前结点有右孩子,则推入下一层级
            if(top.right) {
                queue.push(top.right)
            }
        }
        // 将 level 推入结果数组
        res.push(level)
    }
    // 返回结果数组
    return res
};

 

剑指offer

最近和周同学在按tag刷剑指offer。进度是每天一道题。最近发现lc手机版贼好用,坐地铁和中午午休的时间都可以用手机刷题~

剑指 Offer 27. 二叉树的镜像

这题在没看修言老师的小册子前就做完了。

思路也很简单:递归的方式,遍历树中的每一个结点,并将每一个结点的左右孩子进行交换。


/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var mirrorTree = function(root) {
     //定义递归边界
      if(root) {
        //交换左右子树
        let tmp = root.left
        root.left = root.right
        root.right = tmp
        //递归
        mirrorTree(root.left)
        mirrorTree(root.right)

        return root
      }else {
        return root
      }
};

剑指 Offer 28. 对称的二叉树

也是递归的一道题。

因为是判断对称,从根节点开始看,左边是不是等于右边。

如果等于的话,就判断:

1.左节点的左节点 是否等于 右节点的右节点

2.左节点的右节点 是否等于 右节点的左节点

var isSymmetric = function(root) {
    return check(root, root)
};

const check = (leftPtr, rightPtr) => {
    // 如果只有根节点,返回true
    if (!leftPtr && !rightPtr) return true
    // 如果左右节点只存在一个,则返回false
    if (!leftPtr || !rightPtr) return false
    
    return leftPtr.val === rightPtr.val && check(leftPtr.left, rightPtr.right) && check(leftPtr.right, rightPtr.left)
}

剑指 Offer 37. 序列化二叉树

这题我用了层序遍历

//序列化
var serialize = function(root) {
    let res=[]
    let queue=[root]
    while(queue.length){
        let node=queue.shift()
        if(node){
            res.push(node.val)
            queue.push(node.left)
            queue.push(node.right)
        }else{
            res.push('X')
        }
    }
    return res.join(',')
};

//反序列化
var deserialize = function(data) {
      if (data == 'X') return null;
      
      // 序列化字符串split成数组
      const list = data.split(',');  
      // 获取首项,构建根节点
      const root = new TreeNode(list[0]); 
      // 根节点推入队列
      const queue = [root];
      // 初始指向list第二项          
      let cursor = 1;                
      // 指针越界,即扫完了序列化字符串
      while (cursor < list.length) {  
         // 考察出列的节点
        const node = queue.shift(); 
         // 它的左儿子的值
        const leftVal = list[cursor]; 
        // 它的右儿子的值    
        const rightVal = list[cursor + 1]; 

     // 判断是否是真实节点
    if (leftVal != 'X') {              
      const leftNode = new TreeNode(leftVal); 
      node.left = leftNode;                  
      queue.push(leftNode);                  
    }

    if(rightVal != 'X'){
        const rightNode = new TreeNode(rightVal);
        node.right=rightNode;
        queue.push(rightNode);
    }
   // 一次考察一对儿子,指针加2
    cursor += 2; 
      }
   return root; 
};

 

 

 

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值