二叉树专题——前端算法和数据结构

200 阅读10分钟
// 二叉树构造函数
function TreeNode(val) {
   this.val = val;
   this.left = this.right = null;
}

二叉树前中后序遍历--递归实现

“先”、“中”、“后”其实就是指根结点的遍历时机

const root = {
  val: "A",
  left: {
    val: "B",
    left: {
      val: "D"
    },
    right: {
      val: "E"
    }
  },
  right: {
    val: "C",
    right: {
      val: "F"
    }
  }
};

前序:

// 所有遍历函数的入参都是树的根结点对象
function preorder(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
     
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
    // 递归遍历左子树 
    preorder(root.left)  
    // 递归遍历右子树  
    preorder(root.right)
}

中序:

// 所有遍历函数的入参都是树的根结点对象
function inorder(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
     
    // 递归遍历左子树 
    inorder(root.left)  
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
    // 递归遍历右子树  
    inorder(root.right)
}

后序:

function postorder(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
     
    // 递归遍历左子树 
    postorder(root.left)  
    // 递归遍历右子树  
    postorder(root.right)
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
}

翻转二叉树:

const invertTree = function(root) {
	// 每棵树的左右节点进行互换,意味着重复----> 递归解决
	// 递归边界
	if(!root) {
		return root
	}
	let left = invertTree(root.left)
	let right = invertTree(root.right)
	// 交换当前遍历到的两个左右孩子结点
	root.left = right
	root.right = left
	return root
}

二叉树前中后序遍历--迭代实现

前序

前序遍历----> 根节点->左节点->右节点,则:

出栈 ”根节点->左节点->右节点“

入栈 ”根节点->右节点->左节点“ 根节点入栈,取出根节点,若有左右子树就 右子树入栈,左子树入栈

/**
 * @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
};

后序

后序遍历---> 左节点->右节点->根节点

与前序遍历比 根节点的位置不同,因此 往前插节点值->unshift()

根据res的unshift,左孩子要先入栈

/**
 * @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
};

中序

中序遍历--->左节点->根节点->右节点

一路向左,向左时候记录节点,之后用于回溯

/**
 * @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
};

二叉搜索树

二叉搜索树也不例外,它的递归定义如下:

  1. 是一棵空树
  2. 是一棵由根结点、左子树、右子树组成的树,同时左子树和右子树都是二叉搜索树,且**「左子树」上所有结点的数据域都「小于等于」根结点的数据域,「右子树」上所有结点的数据域都「大于等于」**根结点的数据域

查找数据域为某一特定值的结点

  1. 递归遍历二叉树,若当前遍历到的结点为空,就意味着没找到目标结点,直接返回。
  2. 若当前遍历到的结点对应的数据域值刚好等于n,则查找成功,返回。
  3. 若当前遍历到的结点对应的数据域值大于目标值n,则应该在左子树里进一步查找,设置下一步的遍历范围为 root.left 后,继续递归。
  4. 若当前遍历到的结点对应的数据域值小于目标值n,则应该在右子树里进一步查找,设置下一步的遍历范围为 root.right 后,继续递归。
function search(root, n) {
    // 若 root 为空,查找失败,直接返回
    if(!root) {
        return 
    }
    // 找到目标结点,输出结点对象
    if(root.val === n) {
        console.log('目标结点是:', root)
    } else if(root.val > n) {
        // 当前结点数据域大于n,向左查找
        search(root.left, n)
    } else {
        // 当前结点数据域小于n,向右查找
        search(root.right, n)
    }
}

插入新结点

function insertIntoBST(root, n) {
    // 若 root 为空,说明当前是一个可以插入的空位
    if(!root) { 
        // 用一个值为n的结点占据这个空位
        root = new TreeNode(n)
        return root
    }
    
    if(root.val > n) {
        // 当前结点数据域大于n,向左查找
        root.left = insertIntoBST(root.left, n)
    } else {
        // 当前结点数据域小于n,向右查找
        root.right = insertIntoBST(root.right, n)
    }

    // 返回插入后二叉搜索树的根结点
    return root
}

删除指定节点

想要删除某个结点,首先要找到这个结点。在定位结点后,我们需要考虑以下情况:

  1. 结点不存在,定位到了空结点。直接返回即可。
  2. 需要删除的目标结点没有左孩子也没有右孩子——它是一个叶子结点,删掉它不会对其它结点造成任何影响,直接删除即可。
  3. 需要删除的目标结点存在左子树,那么就去左子树里寻找小于目标结点值的最大结点,用这个结点覆盖掉目标结点
  4. 需要删除的目标结点存在右子树,那么就去右子树里寻找大于目标结点值的最小结点,用这个结点覆盖掉目标结点
  5. 需要删除的目标结点既有左子树、又有右子树,这时就有两种做法了:要么取左子树中值最大的结点,要么取右子树中取值最小的结点。两个结点中任取一个覆盖掉目标结点,都可以维持二叉搜索树的数据有序性
function deleteNode(root, n) {
    // 如果没找到目标结点,则直接返回
    if(!root) {
        return root
    }
    // 定位到目标结点,开始分情况处理删除动作
    if(root.val === n) {
        // 若是叶子结点,则不需要想太多,直接删除
        if(!root.left && !root.right) {
            root = null
        } else if(root.left) {
            // 寻找左子树里值最大的结点
            const maxLeft = findMax(root.left)
            // 用这个 maxLeft 覆盖掉需要删除的当前结点  
            root.val = maxLeft.val
            // 覆盖动作会消耗掉原有的 maxLeft 结点
            root.left = deleteNode(root.left, maxLeft.val)
        } else {
            // 寻找右子树里值最小的结点
            const minRight = findMin(root.right)
            // 用这个 minRight 覆盖掉需要删除的当前结点  
            root.val = minRight.val
            // 覆盖动作会消耗掉原有的 minRight 结点
            root.right = deleteNode(root.right, minRight.val)
        }
    } else if(root.val > n) {
        // 若当前结点的值比 n 大,则在左子树中继续寻找目标结点
        root.left = deleteNode(root.left, n)
    } else  {
        // 若当前结点的值比 n 小,则在右子树中继续寻找目标结点
        root.right = deleteNode(root.right, n)
    }
    return root
}

二叉树验证

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
const isValidBST = function(root) {
  // 定义递归函数
  function dfs(root, minValue, maxValue) {
      // 若是空树,则合法
      if(!root) {
          return true
      }
      // 若右孩子不大于根结点值,或者左孩子不小于根结点值,则不合法
      if(root.val <= minValue || root.val >= maxValue) return false
      // 左右子树必须都符合二叉搜索树的数据域大小关系
      return dfs(root.left, minValue,root.val) && dfs(root.right, root.val, maxValue)
  }
  // 初始化最小值和最大值为极小或极大
  return dfs(root, -Infinity, Infinity)
};

将有序数组转换为二叉搜索树

二叉搜索树的特性:题目中指明了目标树是一棵二叉搜索树,二叉搜索树的中序遍历序列是有序的,题中所给的数组也是有序的,「因此我们可以认为题目中给出的数组就是目标二叉树的中序遍历序列」。中序遍历序列的顺序规则是 左 -> 根 -> 右,因此数组中间位置的元素一定对应着目标二叉树的根结点。以根结点为抓手,把这个数组“拎”起来,得到的二叉树一定是符合二叉搜索树的排序规则的。

/**
 * @param {number[]} nums
 * @return {TreeNode}
 */
const sortedArrayToBST = function(nums) {
    // 处理边界条件
    if(!nums.length) {
        return null
    }
    
    // root 结点是递归“提”起数组的结果
    const root = buildBST(0, nums.length-1)

    // 定义二叉树构建函数,入参是子序列的索引范围
    function buildBST(low, high) {
        // 当 low > high 时,意味着当前范围的数字已经被递归处理完全了
        if(low > high) {
            return null
        }
        // 二分一下,取出当前子序列的中间元素
        const mid = Math.floor(low + (high - low)/2)  
        // 将中间元素的值作为当前子树的根结点值
        const cur = new TreeNode(nums[mid]) 
        // 递归构建左子树,范围二分为[low,mid)
        cur.left = buildBST(low,mid-1)
        // 递归构建左子树,范围二分为为(mid,high]
        cur.right = buildBST(mid+1, high)
        // 返回当前结点
        return cur
    }
    // 返回根结点
    return root
};

平衡二叉树

平衡二叉树(又称 AVL Tree)指的**「是任意结点」「左右子树高度差绝对值都不大于1」的二叉「搜索树」**。

判断平衡二叉树

const isBalanced = function(root) {
  // 立一个flag,只要有一个高度差绝对值大于1,这个flag就会被置为false
  let flag = true
  // 定义递归逻辑
  function dfs(root) {
      // 如果是空树,高度记为0;如果flag已经false了,那么就没必要往下走了,直接return
      if(!root || !flag) {
          return 0 
      }
      // 计算左子树的高度
      const left = dfs(root.left)  
      // 计算右子树的高度
      const right = dfs(root.right)  
      // 如果左右子树的高度差绝对值大于1,flag就破功了
      if(Math.abs(left-right) > 1) {
          flag = false
          // 后面再发生什么已经不重要了,返回一个不影响回溯计算的值
          return 0
      }
      // 返回当前子树的高度
      return Math.max(left, right) + 1
  }
  
  // 递归入口
  dfs(root) 
  // 返回flag的值
  return flag
};

将二叉搜索树变平衡

「二叉搜索树的中序遍历序列是有序的」

思路:

  1. 中序遍历求出有序数组
  2. 逐个将二分出来的数组子序列“提”起来变成二叉搜索树
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
const balanceBST = function(root) {
    // 初始化中序遍历序列数组
    const nums = []
    // 定义中序遍历二叉树,得到有序数组
    function inorder(root) {
        if(!root) {
            return 
        }
        inorder(root.left)  
        nums.push(root.val)  
        inorder(root.right)
    }
    
    // 这坨代码的逻辑和上一节最后一题的代码一模一样
    function buildAVL(low, high) {
        // 若 low > high,则越界,说明当前索引范围对应的子树已经构建完毕
        if(low>high) {
            return null
        }
        // 取数组的中间值作为根结点值
        const mid = Math.floor(low + (high -low)/2)
        // 创造当前树的根结点
        const cur = new TreeNode(nums[mid])  
        // 构建左子树
        cur.left = buildAVL(low, mid-1) 
        // 构建右子树
        cur.right = buildAVL(mid+1, high)  
        // 返回当前树的根结点 
        return cur
    }
    // 调用中序遍历方法,求出 nums
    inorder(root)
    // 基于 nums,构造平衡二叉树
    return buildAVL(0, nums.length-1)
};

参考

主要是学习修言大佬前端算法与数据结构面试:底层逻辑解读与大厂真题训练以及刷LeetCode留下的雪泥鸿爪,在此自我总结一下把知识串起来,侵删~