题目

题目来源:力扣(LeetCode)

返回与给定前序遍历 preorder 相匹配的二叉搜索树(binary search tree)的根结点。

(回想一下,二叉搜索树是二叉树的一种,其每个节点都满足以下规则,对于 node.left 的任何后代,值总 < node.val,而 node.right 的任何后代,值总 > node.val。此外,前序遍历首先显示节点 node 的值,然后遍历 node.left,接着遍历 node.right。)

题目保证,对于给定的测试用例,总能找到满足要求的二叉搜索树。

示例:

输入:[8,5,1,7,10,12]
输出:[8,5,10,1,7,null,12]
image.png

思路分析

二分查找左右子树的分界线递归构建左右子树

二叉搜索树的前序遍历顺序是:根节点 -> 左子树 -> 右子树,因此对于前序序遍历的结果数组,数组的第一个元素一定是二叉树搜索树的根节点。

又根据二叉搜索树的性质,左子树的所有节点值均小于根节点值,右子树上的所有节点均大于根节点,因此,我们可以把前序遍历的结果数组除了第一个元素以外后面的部分,分成两个区间:

  • 第 1 个区间的所有元素都严格小于根节点,我们可以递归构建成根节点的左子树
  • 第 2 个区间的所有元素都严格大于根节点,我们可以递归构建成根节点的右子树

我们可以使用二分法找到这两个区间的分界线,我们通过题目中的例子具体说明。

对于数组 [8, 5, 1, 7, 10, 12],8 是二叉搜索树的根节点,5、1、7 都比 8 小,所以它们都是8的左子节点,10、12都比8大,所以它们都是 8 的右子节点
前序遍历构造二叉搜索树 - 图2
我们继续对子区间 [5, 1, 7] 进行二分拆分,5 是根节点,1 比 5 小,因此它是 5 的左子节点,7 比 5 大,因此它是 5 的右子节点。同理,子区间 [10, 12] 中 10 是根节点,比 10 大的 12 是 右子节点。
前序遍历构造二叉搜索树 - 图3

我们一直这样通过二分法拆分下去,直到不能拆分为止,最终得到如下的一棵二叉搜索树:
前序遍历构造二叉搜索树 - 图4

代码

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {number[]} preorder
  11. * @return {TreeNode}
  12. */
  13. // 前序遍历的结果数组的范围是 left (数组的第一个元素的下标) 到 right (数组的最后一个元素的下标)
  14. var buildTree = function (nums, left, right) {
  15. if (left > right) return null;
  16. // 前序遍历的结果数组的第一个元素一定是根节点
  17. // 因此从第二个元素开始到最后一个元素,查找左右子树的分界线
  18. let ind = left + 1;
  19. // 拆分成两个区间,第一个区间 [left + 1, ind] 的元素都是比根节点 nums[left] 小的
  20. // 第二个区间 [ind + 1, right] 的元素都是比根节点 nums[left] 大的
  21. while (ind <= right && nums[ind] < nums[left]) ++ind;
  22. //构造根节点
  23. let root = new TreeNode(nums[left]);
  24. // 区间 [left + 1, ind] 的所有元素都是 root 节点的左子节点
  25. root.left = buildTree(nums, left + 1, ind - 1);
  26. // 区间 [ind + 1, right] 的所有元素都是 root 节点的右子节点
  27. root.right = buildTree(nums, ind, right);
  28. return root;
  29. };
  30. var bstFromPreorder = function (preorder) {
  31. // 传入前序遍历的结果数组、数组的第一个元素的下标、数组最后一个元素的下标
  32. return buildTree(preorder, 0, preorder.length - 1);
  33. };

直接插入

二叉搜索树的前序遍历顺序是:根节点 -> 左子树 -> 右子树
二叉搜索树的性质是:

  • 左子树上所有节点的值均小于它的根节点的值;
  • 右子树上所有子节点的值均大于它的根节点的值;

我们可以根据上面的两个性质,直接将节点插入树中。例如我们在下面的二叉搜索树中插入节点 7:
前序遍历构造二叉搜索树 - 图5

第一步,节点 7 与 根节点 8 比较,7 小于 8,因此节点 7 应该插入到 8 的左子树上:

前序遍历构造二叉搜索树 - 图6

第二步,节点7 与 根节点8 的左子节点5 比较,7 大于 5,因此节点7 应该插入到节点5 的右子树上:
前序遍历构造二叉搜索树 - 图7

第三步,由于节点5的右子树是空的,因此直接将节点7作为节点5的右子树, 插入到节点5的右子节点上即可:
前序遍历构造二叉搜索树 - 图8

往树中插入一个节点的代码如下:

  1. // data 是要插入的节点值
  2. const addTreeNode = (root, data) => {
  3. // 新建一个节点
  4. const node = new TreeNode(data);
  5. let p = root;
  6. while (true) {
  7. // 如果要插入的节点值data 比节点p 的值小,则将要插入节点插入点节点p 的左子树上
  8. if (p.val > data) {
  9. // 如果节点p 的左子节点为空,直接作为节点p的左子节点
  10. if (p.left == null) {
  11. p.left = node;
  12. break;
  13. } else {
  14. p = p.left
  15. }
  16. }
  17. // 如果要插入的节点值data 比节点p 的值大,则将要插入节点插入点节点p 的右子树上
  18. else {
  19. // 如果节点p 的右子节点为空,直接作为节点p的右子节点
  20. if (p.right == null) {
  21. p.right = node;
  22. break;
  23. } else {
  24. p = p.right;
  25. }
  26. }
  27. }
  28. }

那么要构建一棵二叉搜索树,我们只需要依次数组中的元素,然后将元素一一插入到树中即可,代码如下:

  1. var bstFromPreorder = function (preorder) {
  2. const root = new TreeNode();
  3. root.val = preorder[0];
  4. // 遍历数组,将元素值添加到树上
  5. for (let i = 1; i < preorder.length; i++) {
  6. addTreeNode(root, preorder[i])
  7. }
  8. return root
  9. }

完整代码:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {number[]} preorder
  11. * @return {TreeNode}
  12. */
  13. // data 是要插入的节点值
  14. const addTreeNode = (root, data) => {
  15. // 新建一个节点
  16. const node = new TreeNode(data);
  17. let p = root;
  18. while (true) {
  19. // 如果要插入的节点值data 比节点p 的值小,则将要插入节点插入点节点p 的左子树上
  20. if (p.val > data) {
  21. // 如果节点p 的左子节点为空,直接作为节点p的左子节点
  22. if (p.left == null) {
  23. p.left = node;
  24. break;
  25. } else {
  26. p = p.left
  27. }
  28. }
  29. // 如果要插入的节点值data 比节点p 的值大,则将要插入节点插入点节点p 的右子树上
  30. else {
  31. // 如果节点p 的右子节点为空,直接作为节点p的右子节点
  32. if (p.right == null) {
  33. p.right = node;
  34. break;
  35. } else {
  36. p = p.right;
  37. }
  38. }
  39. }
  40. }
  41. var bstFromPreorder = function (preorder) {
  42. const root = new TreeNode();
  43. root.val = preorder[0];
  44. // 遍历数组,将元素值添加到树上
  45. for (let i = 1; i < preorder.length; i++) {
  46. addTreeNode(root, preorder[i])
  47. }
  48. return root
  49. }

递归构建

原理与上面的直接插入方式相同,只是上面的方法中插入节点的时候使用的是 while 循环,代码量较多,我们可以改成递归的方式:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {number[]} preorder
  11. * @return {TreeNode}
  12. */
  13. const addTreeNode = (root, val) => {
  14. if (root == null)
  15. return new TreeNode(val);
  16. // 如果要插入的节点值data 比节点p 的值小,则将要插入节点插入点节点p 的左子树上
  17. else if (root.val > val)
  18. root.left = addTreeNode(root.left, val);
  19. // 如果要插入的节点值data 比节点p 的值大,则将要插入节点插入点节点p 的右子树上
  20. else
  21. root.right = addTreeNode(root.right, val);
  22. return root;
  23. }
  24. var bstFromPreorder = function (preorder) {
  25. const root = new TreeNode();
  26. root.val = preorder[0];
  27. // 遍历数组,将元素值添加到树上
  28. for (let i = 1; i < preorder.length; i++) {
  29. addTreeNode(root, preorder[i])
  30. }
  31. return root
  32. }

借助单调栈构建

我们使用一个栈来维护二叉搜索树中的节点,栈中存放的是已经构建好的二叉搜索树的节点 (但不是全部,有些已经可能出栈了),其中栈中元素从栈底掉栈顶是单调递减的。

算法

  1. 首先将先序遍历的结果数组的第一个元素作为二叉搜索树的根节点,即 root = new TreeNode(preorder[0]),并将其放入栈中。
  2. 使用for循环遍历先序遍历的结果数组中剩下的元素,将栈顶的元素作为父节点,当前遍历的元素作为子节点:
    1. 如果当前遍历的元素的值 小于 栈顶元素的值,我们直接让当前元素作为栈顶元素节点的左子节点,然后将当前元素入栈
    2. 如果当前遍历的元素的值 大于 栈顶元素的值,我们就让栈顶元素出栈,直到当前元素的值 小于 栈顶元素的值或栈为空为止。此时最后一个被弹出栈的元素节点就是当前遍历元素的父节点,而当前元素是它父节点的右子节点。
  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {number[]} preorder
  11. * @return {TreeNode}
  12. */
  13. let bstFromPreorder = (preorder) => {
  14. //生成根节点并将其推入栈中
  15. let root = new TreeNode(preorder[0]);
  16. // 栈中存放的是已经构建好的二叉搜索树的结点(但不是全部,有些可能已经出栈了)
  17. // 其中栈中元素从栈底到栈顶是递减的
  18. let stack = [root];
  19. //遍历preorder插入节点
  20. for (let i = 1; i < preorder.length; i++) {
  21. // 创建节点
  22. let currentNode = new TreeNode(preorder[i]);
  23. // 栈顶元素
  24. let node = stack[stack.length - 1];
  25. // 如果当前遍历的元素的值 大于 栈顶元素的值,
  26. // 我们就让栈顶元素出栈,直到当前元素的值 小于 栈顶元素的值或栈为空为止。
  27. // 此时最后一个被弹出栈的元素节点就是当前遍历元素的父节点,而当前元素是它父节点的右子节点
  28. if (currentNode.val > node.val) {
  29. //把栈中小于当前节点的值全部弹出
  30. while (stack.length && stack[stack.length - 1].val < currentNode.val) {
  31. node = stack.pop();
  32. }
  33. // 此时的node指向栈最后一个弹出的节点
  34. node.right = currentNode;
  35. }
  36. // 如果当前遍历的元素的值 小于 栈顶元素的值,我们直接让当前元素作为栈顶元素节点的左子节点,
  37. // 然后将当前元素入栈
  38. else {
  39. // 此时的node指向栈中的最后一个节点
  40. node.left = currentNode;
  41. }
  42. stack.push(currentNode);
  43. // 当前节点当作下一个父节点
  44. };
  45. return root;
  46. };

参考: https://leetcode-cn.com/problems/construct-binary-search-tree-from-preorder-traversal/solution/javadai-ma-de-5chong-jie-ti-si-lu-by-sdwwld/ https://leetcode-cn.com/problems/construct-binary-search-tree-from-preorder-traversal/solution/jian-kong-er-cha-shu-by-leetcode/