(三)数据结构-树及其应用

131 阅读16分钟

通过这短时间对二叉树及其变种进行学习和归纳,将学习成果共享。二叉树常考知识点包括排序二叉树、平衡二叉树以及树的遍历,树的遍历中包含了二叉树的递归和非递归遍历的实现,非递归遍历中除了采用堆栈的方式外还使用mirros遍历

排序二叉树&平衡二叉树

1.排序二叉树(搜索二叉树)

  • 排序二叉树的特点:

    • 排序二叉树, 又称为二叉查找树. 它有着自己显著的特点:
  • 首先一个节点左孩子的值, 一定小于它本身节点的值.

  • 一个节点右孩子的值, 一定大于它本身节点的值.

  • 左、右孩子(子树)也分别是排序二叉树.

    • 排序二叉树中不会有重复数值的节点
  • 注意:排序二叉树的中序遍历就是一个有序数组

  • 排序二叉树的插入,遍历,最大值,最小值,查找代码如下:

    注意在函数内部书写的辅助函数直接写成函数的形式不用添加在原型中,原型中的方法都是暴露在外部的方法

function Node(data) {
  this.val = data;
  this.left = null;
  this.right = null;
}
function BinarySortTree() {
  this.root = null;
}
// 二叉搜索树查找插入新节点的速度位置并插插入元素
function insertNode(node, newNode) {
  //比较当前节点和待插入节点的val的大小
  if (node.val > newNode.val) {//插入节点小于,插入在左节点
    //判断左节点是否存在
    if (!node.left) {
      node.left = newNode
    } else {
      insertNode(node.left, newNode);
    }
  } else {
    if (!node.right) {
      node.right = newNode;
    } else {
      insertNode(node.right, newNode)
    }
  }
}
// 1.对外暴露的插入方法
BinarySortTree.prototype.insert = function (data) {
  // 创建新的节点
  var newNode = new Node(data);
  if (!this.root) {
    this.root = newNode
  }
  else {
    //二叉搜索插在root树种合适的位置并插入节点newNode
    insertNode(this.root, newNode);
  }
}
// 2.排序二叉树的遍历(中序)
function inOrderTraverse(node, arr = []) {
  if (node) {
    inOrderTraverse(node.left, arr);
    arr.push(node.val);
    inOrderTraverse(node.right, arr)
  }
  return arr;
}
BinarySortTree.prototype.inOrder = function () {
  return inOrderTraverse(this.root)
}
// 3.排序二叉树的遍历(先序)
function preOrderTraverse(node, arr = []) {
  if (node) {
    arr.push(node.val);
    preOrderTraverse(node.left, arr);
    preOrderTraverse(node.right, arr);
  }
  return arr
}
BinarySortTree.prototype.preOrder = function () {
  return preOrderTraverse(this.root);
}
// 4.排序二叉树的遍历(后序)辅助函数
function postOrderTraverse(node, arr = []) {
  if (node) {
    postOrderTraverse(node.left, arr);
    postOrderTraverse(node.right, arr);
    arr.push(node.val);
  }
  return arr;
}
BinarySortTree.prototype.postOrder = function () {
  return postOrderTraverse(this.root)
}
// 5.二叉搜索树的查找(最大值,最小值,是否存在)
//最小值
function getMinNode(node) {
  if (node) {
    while (node && node.left) {
      node = node.left
    }
    return node.val
  }
  return null
}
BinarySortTree.prototype.getMin = function () {
  return getMinNode(this.root)
}
// 最大值
function getMaxNode(node) {
  if (node) {
    while (node && node.right) {
      node = node.right
    }
    return node.val
  }
  return null;
}
BinarySortTree.prototype.getMax = function () {
  return getMaxNode(this.root)
}
//查找是否存在
function searchNode(node, key) {
  if (!node) return false
  if (key < node.val) {
    return searchNode(node.left, key)
  } else if (key > node.val) {
    return searchNode(node.right, key)
  } else {
    return true
  }
}
BinarySortTree.prototype.search = function (key) {
  return searchNode(this.root, key)
}
//测试
var nodes = [4, 7, 9, 0, 3, 6];
var tree = new BinarySortTree();
nodes.forEach((item) => {
  tree.insert(item)
​
})
 console.log(tree.inOrder());// [ 0, 3, 4, 6, 7, 9 ]
console.log(tree.preOrder());//[ 4, 0, 3, 7, 6, 9 ]
// console.log(tree.postOrder());/[ 3, 0, 6, 9, 7, 4 ]
console.log(tree.search(6));//true
console.log(tree.search(32));//false
console.log(tree.getMax());///9
console.log(tree.getMin());//0
//注意在函数内部书写的辅助函数直接写成函数的形式不用添加在原型中,原型中的方法都是暴露在外部的方法

2.面试题:平衡二叉树结构实现和插入操作

  • 平衡二叉树的定义

    二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率 ; 平衡二叉树(Balanced Binary Tree)是二叉查找树的一个进化体 ;

    平衡二叉树要求对于每一个节点来说,它的左右子树的高度之差不能超过1,如果插入或者删除一个节点使得高度之差大于1,就要进行节点之间的 旋转,将二叉树重新维持在一个平衡状态。

    这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在 O(logN) 。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

  • 平衡二叉树的规定

    • 这种左右子树的高度相差不超过 1 的树为平衡二叉树。
    • 平衡因子:-1,0 ,1
  • 平衡二叉树的旋转

    • LL(插入在左子树的左子树上)->右旋(顺时针)

    image.png

    • RR(插入在右子树的右子树上)->左旋(逆时针)

    image.png

    • LR(插入在左子树的右子树上)->先局部左旋在整体右旋 image.png

    • RL(插入在右子树的左子树上)->先局部右旋在整体左旋

    image.png

  • 平衡二叉树插入操作

  • 平衡二叉树删除操作

// AVL树的实现
function AVLNode(data) {
  this.val = data;
  this.left = this.right = null;
  this.height;//当前节点的高度
}
//LL类型的失衡节点的调整
function BalanceTree() {
  this.root = null
}
//LL调整(右旋),参数node为失衡的节点
BalanceTree.prototype.singleRight = function (node) {
  var leftNode = node.left;//失衡节点的左孩子
  node.left = leftNode.right;
  leftNode.right = node;
  // 更新leftNode和失衡节点的高度
  node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
  leftNode.height = Math.max(getHeight(leftNode.left), getHeight(leftNode.right)) + 1;
  //返回旋转之后的头节点
  return leftNode;
}
// RR调整(左旋),参数node为失衡的节点
BalanceTree.prototype.singleLeft = function (node) {
  var rightNode = node.right;
  node.right = rightNode.left;
  rightNode.left = node
  // 更新节点高度
  node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
  rightNode.height = Math.max(getHeight(rightNode.left), getHeight(rightNode.right)) + 1;
  return rightNode;//返回头节点
}
// LR(先左旋在右旋)node表示失衡节点
BalanceTree.prototype.DoubleLeftRight = function (node) {
  node.left = this.singleLeft(node.left);
  return this.singleRight(node);
}
// RL(先右旋在左旋)node表示失衡节点
BalanceTree.prototype.DoubleRightLeft = function (node) {
  node.right = this.singleRight(node.right);
  return this.singleLeft(node);
}
// 插入操作
/**
 * data:待插入的数据
 * node:表示待插入的节点位置
 */
​
BalanceTree.prototype.insert = function (data, node) {
  // node为空表示找到了插入的位置,创建值为data的节点
  if (!node) {
    node = new AVLNode(data);
  }
  // 二叉搜索树查找待插入的位置
  if (data < node.val) {//插入的值小于当前节点,插入到左子树中
    node.left = this.insert(data, node.left);
    //插入后调整左右子树的高度,左右子树的高度==2,表示当前节点时失衡节点
    if (getHeight(node.left) - getHeight(node.right) == 2) {
      //判断插入的节点插入的位置(插入的是左子树,调整的就是左子树)和删除的区别
      if (data < node.left.val) {//左子树上LL类型,右旋
        node = this.singleRight(node);
      } else {//右子树上,则是LR类型
        node = this.DoubleLeftRight(node);
      }
    }
  }
  // 插入到右子树上
  else if (data > node.val) {
    node.right = insert(data, node.right);
    //调整位置,
    if (getHeight(node.left) - getHeight(node.right) == 2) {
      if (data > data.right) {//右子树上,左旋
        node = this.singleLeft(node);
      } else {//左子树上RL
        node = this.DoubleRightLeft(node);
      }
    }
  }
  // 相等的情况
  node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
  return node;
}
// 删除节点
/**
 * 
 * @param {待删除的节点值} data 
 * @param {待删除的位置(通过二叉搜索树的方式进行查找)} node 
 */
BalanceTree.prototype.remove = function (data, node) {
  // 表示当前节点不存在
  if (!node) return null;
  //获取节点值,递归查找==data的节点位置
  if (data < node.val) {
    node.left = this.remove(data, node.left);
    //监测是否平衡
    if (getHeight(node.left) - getHeight(node.right) == 2) {
      var cur = node.right;//注意这里是node的右子树(删除的是左子树,需要调整的就是右子树)
      if (getHeight(cur.right) >= getHeight(cur.left)) {//RR
        node = this.singleLeft(node);
      } else {//RL
        node = this.DoubleRightLeft(node);
      }
    }
  } else if (data > node.val) {
    node.right = this.remove(data, nod.right);
    //监测平衡
    if (getHeight(node.left) - getHeight(node.right) == 2) {
      var cur = node.left;//删除的是右子树调整的就是左子树
      if (getHeight(cur.left) >= getHeight(cur.right)) {//LL
        node = this.singleRight(node);
      } else {//LR
        node = this.DoubleLeftRight(node)
      }
    }
  }
  //找到待删除的节点,并且有两个子节点
  else if (node.left && node.right) {
    //寻找替换节点
    node.val = this.findMin(node.right).val;
    //移除需要替换的节点
    node.right = remove(node.val, node.right);
  } else {//只有一个节点或者当前节点时叶子节点
    node = (node.left) ? node.left : node.right;
  }
  // 更新高度
  if (node) {
    node.height = Math.max(getHeight(node.left), getHeight(node.rigth)) + 1;
  }
  return node;//返回删除的节点
​
}
// 辅助函数
/**
 * @param {根节点} node 
 */
function findMin(node) {
  if (!node) return null
  else if (!node.left) return node
  return findMin(node.left)
}
function findMax(node) {
  if (!node) return null;
  else if (!node.right) return node
  return findMax(node.right);
}
// 计算当前节点的高度
function getHeight(node) {
  return node == null ? -1 : node.height;//直接返回node节点的属性指针height
}

二叉树的遍历(Mirros)

1. 二叉树先序遍历

二叉树先序遍历(中左右)

递归实现

// 节点定义
function Node(data){
    this.val = data;
    this.left=null;
    this.right=null;
}
// 递归实现
function preOrder(root, arr = []) {
  if (root) {
    arr.push(root.val);
    preOrder(root.left, arr);
    preOrder(root.right, arr)
  }
  return arr;
}

非递归实现

时间复杂度:O(n),空间复杂度:O(n)

  • 先根节点入栈
  • 循环条件:栈不为空
  • pop出栈->判断右子树(入栈)->左子树(入栈)->都没有进行下次pop操作
  • (入栈顺序:中右左->出栈顺序中左右先序)

image.png

// 非递归实现
function preOrder2(root) {
  if (root) {
    var stack = [];
    var res = []
    stack.push(root);//先入栈
    while (stack.length) {
      var cur = stack.pop();//出栈 
      res.push(cur.val);//打印
      if (cur.right) {
        stack.push(cur.right);
      }
      if (cur.left) {
        stack.push(cur.left)
      }
    }
    return res
  }
  return null
}

2.二叉树中序遍历

递归实现

// 节点定义
function Node(data) {
  this.val = data;
  this.left = null;
  this.right = null;
}
// 递归
function inOrder(root, arr = []) {
  if (root) {
    inOrder(root.left, arr);
    arr.push(root.val);
    inOrder(root.right, arr);
  }
  return arr
}

非递归实现

  • 非递归的中序遍历借助栈,遇到树的左边界全部进栈
  • 反之,出栈,并判断右子树是否存在,存在则进栈

image.png

function inOrder(root) {
  if (!root) return null;
  let stack = [];
  let head = root;
  let res = [];//打印输出的数据
  while (stack.length || head) {
    if (head) {//无需先入栈,左边的先入栈
      stack.push(head);
      head = head.left;
    } else {
      //head为空则出栈并将右边的入栈
      head = stack.pop();
      res.push(head.val);
      if (head.right) {
        //注意这里只是指向右节点,进栈都是由if(head)中操作
        head = head.right;
      }
    }
  }
  return res
}

注意:栈的作用就是可以保存上一个节点的信息并返回上一个节点

3 后序遍历

递归

// 节点定义
function Node(data) {
  this.val = data;
  this.left = null;
  this.right = null;
}
// 递归
function postOrder(root, arr = []) {
  if (root) {
    postOrder(root.left, arr);
    postOrder(root.right, arr);
    arr.push(root.val);
  }
  return arr
}

非递归

这里非递归的实现主要有两种方式

  • 方式1:借助两个栈

    • 先按照中右左顺序出栈,出栈之后的元素在进栈,做一个逆序操作,打印出的顺序左右中,
    • 这里先序遍历的出栈顺序中左右进行对比

image.png

// 非递归实现:方案1:通过先序遍历转化出栈顺序中右左,入栈逆序转化出栈顺序左右中
function postOrder1(root) {
  if (!root) return null
  var stack1 = []
  var stack2 = []
  stack1.push(root)//先入栈
  while (stack1.length) {
    var cur = stack1.pop();//出栈
    stack2.push(cur);//这里通过stack2逆序暂存,最后输出stack2中的顺序就是后序遍历的顺序
    //注意这里是左子树先入栈
    if (cur.left) {
      stack1.push(cur.left)
    }
    if (cur.right) {
      stack1.push(cur.right);
    }
  }
  //stack2中的出栈顺序就是后序遍历
  var res = []
  while (stack2.length) {
    res.push(stack2.pop())
  }
  return res;
} 
  • 方式2:只用一个栈实现后续遍历,借助两个指针

    • h:表示上一个遍历过的节点(在遍历的过程中初始指向root,)
    • cur:表示当前指针
// 方案2:通过一个栈实现,借助两个指针h:记录上一个遍历过的节点,cur表示当前节点
function postOrder2(root) {
  if (!root) return null;
  var h = root;
  var cur = null;
  var stack = [];
  stack.push(h);
  var res = []
  while (stack.length) {
    //获取栈顶元素的值,没有出栈
    cur = stack[stack.length - 1]
    //表示还未开始打印直接进站
    if (cur.left && cur.left != h && cur.right != h) {
      stack.push(cur.left);
    } else if (cur.right && cur.right != h) {
      stack.push(cur.right);
    } else {
      res.push(stack.pop().val);
      //此时将h指向遍历过的节点
      h = cur;
    }
  }
  return res
}

小结

  • 先序和后续都需要先进栈,(循环条件一致,栈不为空)

    • 先序:右子树先进栈,左子树在进栈
    • 后序:左子树先进栈,右子树在进栈
  • 中序遍历:通过遍历左边界的方式,无需先进栈(循环条件:栈不为空||根节点存在)

4 二叉树的层次遍历

4.1 二叉树的层次遍历

从上往下打印出二叉树的每个节点,同层节点从左至右打印

function printTopToBottom(root) {
  if (!root) return null
  //队列存储
  var queue = [];
  queue.push(root);
  var res  =[]
  while (root || queue.length) {
    var cur = queue.shift();//从头取出
    if(cur.left){
      queue.push(cur.left)
    }
    if(cur.right){
      queue.push(cur.right)
    }
    res.push(cur.val);
  }
  return res
}

4.2 把二叉树打印成多行

题目

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

思路

  • 在单行打印的基础上设置变量当前行节点数和子节点数,并保存每行的暂存节点数
/* function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
} */
function Print(pRoot)
{
 if (!root) return []
    let queue = [], res = [];
​
    let curArr = [];//保存当前行的元素
    let curCount = 1;//当前行的元素个数
    let childCount = 0;
    
    queue.push(root);
    while (queue.length>0) {
        let current = queue.shift();
        curArr.push(current.val);
        curCount--;
​
        if (current.left) {
        queue.push(current.left);
        childCount++;
        }
        if (current.right) {
        queue.push(current.right);
        childCount++;
        }
        
        //当前行的个数为0表示已经遍历完当前行,将当前行该为下一行
        if (curCount == 0) {
        res.push(curArr);
        curCount = childCount;
        childCount = 0;
        curArr = []
        }
    }
    return res
}

4.3 按之字形顺序打印二叉树

题目

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

分析

  • 采用两个栈分别保存奇数行和偶数行
  • 奇数从左到右
  • 偶数从右到左
  • 边出栈边保存打印顺序
/* function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
} */
​
function print(root) {
  if (!root) return [];
  let res = [], temp = [];
  let oddStack = [], evenStack = [];
  oddStack.push(root);
  while (oddStack.length || evenStack.length) {
    while (oddStack.length) {
      let cur = oddStack.pop();
      temp.push(cur.val);
      if (cur.left) {
        evenStack.push(cur.left);
      }
      if (cur.right) {
        evenStack.push(cur.right);
      }
    }
    if (temp.length) {
      res.push(temp);
      temp = [];
    }
    while (evenStack.length) {
      let cur = evenStack.pop();
      temp.push(cur.val);
      if (cur.right) {
        oddStack.push(cur.right)
      }
      if (cur.left) {
        oddStack.push(cur.left)
      }
    }
    if (temp.length) {
      res.push(temp);
      temp = [];
    }
  }
  return res
}

5 遍历二叉树的神级方法-Morris遍历

前面讲的二叉树的遍历都有一定问题就是必须借助栈O(n)才能实现,

Mirros遍历可以实现空间复杂度O(1)

morris遍历是一种节省空间复杂度的方法,将叶子节点上的空指针利用起来,指向父节点,再次遍历到这个节点的时候在修改回来,这样最后二叉树的结构是不会发生变化的

题目

给定一棵二叉树的头结点head,完成二叉树的先序,中序和后序遍历,如果二叉树的节点数为N,要求时间复杂度O(n),额外空间复杂度O(1)

分析morris遍历的思想

  • 判断当前节点是否有左孩子

    • 无->当前节点向右移动

      • 判断当前节点的左孩子的最右指针是否指向当前节点;

        • 没有->让当前节点的左孩子的最右指针指向当前节点,当前节点向左移动(表示第一次访问当前节点)
        • 已经指向->当前节点的左孩子的最右指针指向空(恢复树的原先结构)当前节点向右移动(第二次访问当前节点)

image.png

小结

  • 注意当前节点向右移动在两种情况下:(1)没有左孩子(2)有左孩子但是左孩子的最右节点已经指向当前节点
  • 当前节点向左移动只有一种情况.

5.1 morris遍历先序和中序

先序和中序遍历的思想是一样的就是打印保存的时间不同

  • 先序遍历:在第一次访问自己的时候打印
  • 中序遍历:在第二次访问自己的时候遍历(叶子节点只会访问自己一次在第一次访问的时候就打印)
// 先序遍历
function preOrder(root) {
  if (!root) return null
  var cur1 = root;
  var cur2 = null;
  var res = []
  while (cur1) {
    cur2 = cur1.left
    // 有左子树
    if (cur2) {
      //遍历左子树上的最右指针
      while (cur2.right && cur2.right != cur1) {
        cur2 = cur2.right
      }
      //最右指针为空(第一次访问)
      if (cur2.right == null) {
        cur2.right = cur1;
        // 先序遍历打印位置
        res.push(cur1.val)
        cur1 = cur1.left;
        continue;//这里会跳出本次循环,执行下次循环
      } else {
        cur2.right = null;
      }
    } else {
      //没有左子树向右移动(没有左子树的节点只遇到自己一次,这时候直接打印)
      // 先序遍历打印位置
      res.push(cur1.val)
    }
    //公共的执行向右移动的代码
    cur1 = cur1.right;//注意这里的向右移动 在没有左子树和将右子树设置为空的情况下都执行
  }
  return res
}
// 中序遍历
function inOrder(root) {
  if (!root) return null
  var cur1 = root;
  var cur2 = null;
  var res = []
  while (cur1) {
    cur2 = cur1.left
    // 有左子树
    if (cur2) {
      //遍历左子树上的最右指针
      while (cur2.right && cur2.right != cur1) {
        cur2 = cur2.right
      }
      //最右指针为空,第一次遇到,
      if (cur2.right == null) {
        cur2.right = cur1;
        cur1 = cur1.left;//向左移动
        continue;//跳出本次循环执行下次循环
      } else {
        cur2.right = null;//将第二次访问将右子树设置为空,同时向右移动(执行下面的代码)
      }
    }
    //没有左子树向右移动
    // 中序遍历打印位置(打印第二次访问的顺序就是中序)
    res.push(cur1.val)
    cur1 = cur1.right;//这块代码都会执行
  }
  return res;
}

5.2 morris后续遍历

  • 后序遍历在栈中是第三次遇到自己的时候打印,在morris中是没有第三次访问自己的情况

实现:

  • 后序遍历:依次逆序打印第二次回到自己的节点的左子树的右边界,最后在逆序打印整棵树的右边界

第一个两次回到自己的节点2->逆序的左子树的右边界为 4

第二个两次回到自己的节点1->5,2

第三个两次回到自己的节点3->6

逆序整棵树的右边界-> 7 3 1

-------------------------->拼接结果就是后序遍历的结果:4 5 2 6 7 3 1

  • 逆序操作的实现:

image.png

  • 时间复杂度:O(N)相当于将一棵树划分为右边界组成,不管走几遍总个数都是N
  • 空间复杂度:O(1)
//边界打印函数(打印的是左子树的右边界以及整棵树的右边界)
function printEdge(head) {
  var tail = reverseEdge(head);//将节点开始的右边界逆序,并返回逆序后的首个节点(也就是逆序前的尾结点)
  var cur = tail;
  var res = []
  while (cur) {
    //打印逆序后的节点
    res.push(cur.val);
    cur = cur.right;
  }
  //打印完成之后还原逆序之前的结构
  reverseEdge(tail);
  return res
}
// 链表逆序操作
function reverseEdge(head) {
  var pre = null;
  var next = null;
  while (head) {
    next = head.right;
    head.right = pre;
    pre = head;
    head = next;
  }
  return pre;
}
//morris后序遍历
function postOrder(root) {
  if (!root) return null;
  var cur1 = root;
  var cur2 = null;
  var res = [];
  while (cur1) {
    cur2 = cur1.left;
    if (cur2) {
      while (cur2 && cur2.right != cur1) {
        cur2 = cur2.right;
      }
      //表示该节点是第一次访问
      if (cur2.right == null) {
        cur2.right = cur1;
        cur1 = cur1.left;
        continue
      } else {
        //第二次访问
        cur2.right = null;
        res.push(...printEdge(cur1.left));
      }
    }
    //不存在则转右子树,第二次访问转右子树
    cur1 = cur1.right;
  }
  //遍历结束还要添加整棵树的右边界
  res.push(...printEdge(root))
  //返回后序遍历的结果
  return res
}

6 找到二叉树中的最大搜索子树(收集节点信息)

题目

给定一棵二叉树的头结点head,已知其中所有节点的值都不一样,找到含有节点最多的搜索二叉子树,并返回这棵树的头结点

解题思路

  • 后序遍历:必须到达自己三次(先要判断自己的左子树,在判断自己的右子树,最后在整个自己的节点信息返回给上一个节点)
  • 所以这里采用递归的方式进行信息的整合

阶梯规则

以node为头结点的树中,最大搜索二叉子树可能来自以下两种情况

  • 第一种:来自node左子树上的最大搜索二叉子树是以node.left为头;来自node右子树上的最大搜索子树是以node.right为头;node左子树上的最大值小于node.value,node右子树的最小值大于node.value;则以node为节点的整个树都是搜索二叉树;
  • 第二种:不满足第一种情况,则以node节点为头结点的的树整体不能连成搜索二叉子树,此时node为头的最大搜索子树是来自node.left和node.right的最大搜索子树之间,节点数较多的那个;
  • 整个过程:二叉树的后续遍历

1595155134176

复杂度

  • 时间复杂度:O(n)
  • 空间复杂度:O(h),h为树的高度

代码

function Node(data) {
  this.val = data;
  this.left = null;
  this.right = null;
}
// 后序遍历,要回到自己三次,并在递归回到自己三次的时候判断当前节点是否是最大搜索子树
function BiggestBST(root) {
  //定义数组存储信息,record[0]->size二叉搜索树的节点个数,record[1]->二叉搜索子树的最小值,record[2]->二叉搜索子树的最大值
  var record = new Array(3);
  return postOrder(root, record);
}
function postOrder(head, record) {
  if (!head) {
    record[0] = 0;
    record[1] = Number.MAX_VALUE;//给最小值赋值系统的最大值
    record[2] = Number.MIN_VALUE;//给最大值赋值系统的最小值
    return null;
  }
  //定义变量来保存值
  var value = head.val;
  var left = head.left;
  var right = head.right;
  var lBST = postOrder(left, record);
  var lSize = record[0];
  var lMax = record[1];
  var lMin = record[2];
  var rBST = postOrder(right, record);
  var rSize = record[0];
  var rMax = record[1];
  var rMin = record[2];
  record[1] = Math.min(lMin, value);
  record[2] = Math.max(rMax, value);
  //此时当前节点就是最大的搜索二叉子树
  if (left == lBST && right == rBST && lMax < value && rMin > value) {
    record[0] = lSize + rSize + 1;
    return head;
  }
  //如果不满足上述条件,此时的最大节点就是左右子树中size最大的值
  record[0] = Math.max(lSize, rSize);
  return lSize > rSize ? lBST : rBST;
}

求**最大键值和**也是一样的

var maxSumBST = function (root) {
  let maxSum = 0;
  /* 返回一个对象,其中属性
  isBST 返回以 root 为根的二叉树是不是 BST 如果是的话则为true 否则为false
  min 返回以 root 为根的这棵 BST 的最小值
  max 返回以 root 为根的这棵 BST 的最大值
  sum 返回以 root 为根的这棵 BST 所有节点之和 
  */
  let dfs = (root) => {
    // base case
    if (root == null) {
      return {
        isBST: true,
        min: Infinity,
        max: -Infinity,
        sum: 0,
      };
    }
    // 递归计算左右子树
    let left = dfs(root.left);
    let right = dfs(root.right);
    if (
      left.isBST &&
      right.isBST &&
      root.val > left.max &&
      root.val < right.min
    ) {
      // 计算以 root 为根的这棵 BST 所有节点之和
      let sum = left.sum + right.sum + root.val;
      maxSum = Math.max(sum, maxSum);
      return {
        // 以root为根的二叉树是BST
        isBST: true,
        // 计算以 root 为根的这棵 BST 的最小值
        min: Math.min(left.min, root.val),
        // 计算以 root 为根的这棵 BST 的最大值
        max: Math.max(right.max, root.val),
​
        sum,
      };
    } else {
      // 以 root 为根的二叉树不是 BST
      return {
        isBST: false,
      };
    }
  };
  dfs(root);
  return maxSum;
};