算法专题(二叉树)

151 阅读14分钟

迭代实现遍历

先序遍历

题目描述:给定一个二叉树,返回它的前序(先序)遍历序列。

示例:

输入: [1,null,2,3]

1   
 \   
  2   
 /  
3 
输出: [1,2,3]

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

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

后序遍历

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

二叉树的剪枝操作

剑指 Offer II 047. 二叉树剪枝

中序遍历

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

二叉树的下一个节点

题目

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
思路
中序遍历的顺序 左 - 根 - 右
所以寻找下一个节点的优先级应该反过来 优先级 右 - 根 - 左

  • 右节点不为空 - 取右节点的最左侧节点
  • 右节点为空 - 如果节点是父亲节的左节点 取父节点
  • 右节点为空 - 如果节点是父亲节的右节点 父节点已经被遍历过,再往上层寻找...
  • 左节点一定在当前节点之前被遍历过

以下图的二叉树来分析: 



中序遍历: CBDAEF\

  • B - 右节点不为空,下一个节点为右节点D
  • C - 右节点为空,C是父节点的左节点,取父节点B
  • D - 右节点为空,D是父节点的右节点,再往上蹭分析,B是其父节点的左节点,取B的父节点A
  • F - 右节点为空,F是父节点的右节点,没有符合条件的节点,F为遍历的最后一个节点,返回null
    /*function TreeLinkNode(x){
        this.val = x;
        this.left = null;
        this.right = null;
        this.next = null;//指向父节点的指针
    }*/
    function GetNext(pNode) {
      if (!pNode) {
        return null;
      }
      if (pNode.right) {
        pNode = pNode.right;
        while (pNode.left) {
          pNode = pNode.left;
        }
        return pNode;
      } else {
        while (pNode) {
          if (!pNode.next) {
            return null;
          } else if (pNode == pNode.next.left) {
            return pNode.next;
          }
          pNode = pNode.next;
        }
        return pNode;
      }
    }

二叉树转排序数组 剑指 Offer II 056. 二叉搜索树中两个节点之和

剑指 Offer II 052. 展平二叉搜索树

层序遍历

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

  3
 / \
9  20
  /  \
 15   7

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
const levelOrder = function(root) {
    // 初始化结果数组
    const res = []  
    // 处理边界条件
    if(!root) {
        return res
    }  
    // 初始化队列
    const queue = []   
    // 队列第一个元素是根结点
    queue.push(root)  
    // 当队列不为空时,反复执行以下逻辑
    while(queue.length) {
        // level 用来存储当前层的结点,每一层存储一个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
};

翻转二叉树

题目描述:翻转一棵二叉树。

示例:
输入:\

     4
   /   \
  2     7
 / \   / \
1   3 6   9

输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
const invertTree = function(root) {
    // 定义递归边界
    if(!root) {
        return root;
    }
    // 递归交换右孩子的子结点
    let right = invertTree(root.right);
    // 递归交换左孩子的子结点
    let left = invertTree(root.left);
    // 交换当前遍历到的两个左右孩子结点
    root.left = right;
    root.right = left;
    return root;
};

二叉搜索树

题目描述:给定一个二叉树,判断其是否是一个有效的二叉搜索树。

  • 假设一个二叉搜索树具有如下特征:
  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。 示例 1:
输入:

    2
   / \
  1   3
输出: true

示例 2:

输入:

    5
   / \
  1   4
     / \
    3   6
输出: false

解释: 输入为: [5,1,4,null,null,3,6]。 根节点的值为 5 ,但是其右子节点值为 4\

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

基操

查找

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
}

删除

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
}

// 寻找左子树最大值
function findMax(root) {
    while(root.right) {
        root = root.right
    }
    return root 
}

// 寻找右子树的最小值
function findMin(root) {
    while(root.left) {
        root = root.left
    }
    return root
}

构造

题目描述:将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:

  • 给定有序数组: [-10,-3,0,5,9],
  • 一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
      0
     / \
   -3   9
   /   /
 -10  5
/**
 * @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
};

平衡二叉树

判定

题目描述:给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为: 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
示例 1:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回 true 。\

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

       1
      / \
     2   2
    / \
   3   3
  / \
 4   4

返回 false 。

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 ,我们就称这棵二叉搜索树是平衡的。
如果有多种构造方法,请你返回任意一种。
示例:
输入:root = [1,null,2,null,3,null,4,null,null]
输出:[2,1,3,null,null,null,4] 解释:这不是唯一的正确答案,[3,1,4,null,2,null,null] 也是一个可行的构造方案。 提示:
树节点的数目在 1 到 10^4 之间。 树节点的值互不相同,且在 1 到 10^5 之间。

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

二叉搜索树与双向链表

题目
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
思路
二叉搜索树的中序遍历即排序后的序列

  • 1.递归左子树,找到左子树的最后一个节点,根节点左侧连接到左子树的最后一个节点
  • 2.当前节点变为已经转换完成的链表的最后一个节点
  • 3.递归右子树,找到当前树的最后一个节点
  • 4.回溯到上一层,进行链接... 代码
    function Convert(pRootOfTree) {
      if (!pRootOfTree) {
        return null;
      }
      ConvertCore(pRootOfTree);
      while (pRootOfTree.left) {
        pRootOfTree = pRootOfTree.left;
      }
      return pRootOfTree;
    }

    function ConvertCore(node, last) {
      if (node.left) {
        last = ConvertCore(node.left, last)
      }
      node.left = last;
      if (last) {
        last.right = node;
      }
      last = node;
      if (node.right) {
        last = ConvertCore(node.right, last);
      }
      return last;
    }

完全二叉树(堆)

完全二叉树是指同时满足下面两个条件的二叉树:

  • 从第一层到倒数第二层,每一层都是满的,也就是说每一层的结点数都达到了当前层所能达到的最大值
  • 最后一层的结点是从左到右连续排列的,不存在跳跃排列的情况(也就是说这一层的所有结点都集中排列在最左边)。

大顶堆

小顶堆

取出堆顶元素

取出元素本身并不难,难的是如何在删除元素的同时,保持住队的“大顶”结构特性。为了做到这点,我们需要执行以下操作:

// 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
function downHeap(low, high) {
    // 初始化 i 为当前结点,j 为当前结点的左孩子
    let i=low,j=i*2+1 
    // 当 j 不超过上界时,重复向下对比+交换的操作
    while(j <= high) {
        // 如果右孩子比左孩子更大,则用右孩子和根结点比较
        if(j+1 <= high && heap[j+1] > heap[j]) {
            j = j+1
        }
        
        // 若当前结点比孩子结点小,则交换两者的位置,把较大的结点“拱上去”
        if(heap[i] < heap[j]) {
            // 交换位置
            const temp = heap[j]  
            heap[j] = heap[i]  
            heap[i] = temp
            
            // i 更新为被交换的孩子结点的索引
            i=j  
            // j 更新为孩子结点的左孩子的索引
            j=j*2+1
        } else {
            break
        }
    }
}

往堆里追加一个元素

// 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
function upHeap(low, high) {
    // 初始化 i(当前结点索引)为上界
    let i = high  
    // 初始化 j 为 i 的父结点
    let j = Math.floor((i-1)/2)  
    // 当 j 不逾越下界时,重复向上对比+交换的过程
    while(j>=low)  {
        // 若当前结点比父结点大
        if(heap[j]<heap[i]) {
            // 交换当前结点与父结点,保持父结点是较大的一个
            const temp = heap[j] 
            heap[j] = heap[i]  
            heap[i] = temp
            
            // i更新为被交换父结点的位置
            i=j   
            // j更新为父结点的父结点
            j=Math.floor((i-1)/2)  
        } else {
            break
        }
    }
}

构造

题目描述:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

  • 输入: [3,2,1,5,6,4] 和 k = 2
  • 输出: 5

示例 2:

  • 输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
  • 输出: 4

说明:你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
const findKthLargest = function(nums, k) {
   // 初始化一个堆数组
   const heap = []  
   // n表示堆数组里当前最后一个元素的索引
   let n = 0
   // 缓存 nums 的长度
   const len = nums.length  
   // 初始化大小为 k 的堆
   function createHeap() {
       for(let i=0;i<k;i++) {
           // 逐个往堆里插入数组中的数字
           insert(nums[i])
       }
   }
   
   // 尝试用 [k, n-1] 区间的元素更新堆
   function updateHeap() {
       for(let i=k;i<len;i++) {
           // 只有比堆顶元素大的才有资格进堆
           if(nums[i]>heap[0]) {
               // 用较大数字替换堆顶数字
               heap[0] = nums[i]  
               // 重复向下对比+交换的逻辑
               downHeap(0, k)
           }
       }
   }
   
   // 向下对比函数
   function downHeap(low, high) {
       // 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
       let i=low,j=i*2+1 
       // 当 j 不超过上界时,重复向下对比+交换的操作
       while(j<=high) {
           // // 如果右孩子比左孩子更小,则用右孩子和根结点比较
           if(j+1<=high && heap[j+1]<heap[j]) {
               j = j+1
           }
           
           // 若当前结点比孩子结点大,则交换两者的位置,把较小的结点“拱上去”
           if(heap[i] > heap[j]) {
               // 交换位置
               const temp = heap[j]  
               heap[j] = heap[i]  
               heap[i] = temp
               
               // i 更新为被交换的孩子结点的索引
               i=j  
               // j 更新为孩子结点的左孩子的索引
               j=j*2+1
           } else {
               break
           }
       }
   }
   
   // 入参是堆元素在数组里的索引范围,low表示下界,high表示上界
   function upHeap(low, high) {
       // 初始化 i(当前结点索引)为上界
       let i = high  
       // 初始化 j 为 i 的父结点
       let j = Math.floor((i-1)/2)  
       // 当 j 不逾越下界时,重复向上对比+交换的过程
       while(j>=low)  {
           // 若当前结点比父结点小
           if(heap[j]>heap[i]) {
               // 交换当前结点与父结点,保持父结点是较小的一个
               const temp = heap[j] 
               heap[j] = heap[i]  
               heap[i] = temp
               
               // i更新为被交换父结点的位置
               i=j   
               // j更新为父结点的父结点
               j=Math.floor((i-1)/2)  
           } else {
               break
           }
       }
   }

   // 插入操作=将元素添加到堆尾部+向上调整元素的位置
   function insert(x) {
       heap[n] = x  
       upHeap(0, n)
       n++
   }
   
   // 调用createHeap初始化元素个数为k的队
   createHeap()
   // 调用updateHeap更新堆的内容,确保最后堆里保留的是最大的k个元素
   updateHeap()
   // 最后堆顶留下的就是最大的k个元素中最小的那个,也就是第k大的元素
   return heap[0]
};