1. 遍历二叉树

有如下树形结构的数据:

  1. let tree = {
  2. value: 0,
  3. children: [{
  4. value: 11,
  5. children: [{
  6. value: 21,
  7. children: [{
  8. value: 31,
  9. children: []
  10. }, {
  11. value: 32,
  12. children: []
  13. }, {
  14. value: 33,
  15. children: []
  16. }]
  17. }, {
  18. value: 22,
  19. children: []
  20. }]
  21. }, {
  22. value: 12,
  23. children: [{
  24. value: 23,
  25. children: []
  26. }, {
  27. value: 24,
  28. children: []
  29. }]
  30. }, {
  31. value: 13,
  32. children: []
  33. }]
  34. }

二叉树 - 图1

1.1 递归遍历

  1. function recursiveTraverse (node, action) {
  2. if (!node || !node.children) { return }
  3. action(node.value)
  4. node.children.forEach(function(item, index) {
  5. recursiveTraverse(item, action)
  6. })
  7. }
  8. recursiveTraverse(tree, console.log)

1.2 栈实现 — 非递归深度优先(DFS)遍历

  1. function nonRecursiveDepthFirstTraversal (node, action) {
  2. if (!node || !node.children) { return }
  3. // 借助一个栈
  4. let _stack = []
  5. _stack.push(node)
  6. while (_stack.length) {
  7. // 推出栈顶元素
  8. let _curNode = _stack.pop()
  9. action(_curNode.value)
  10. // 将子节点依次放入到栈顶
  11. if (_curNode.children.length) {
  12. // 将推出栈顶元素的子节点依次放入到栈顶,每次先遍历后进栈的元素
  13. _stack = _curNode.children.concat(_stack)
  14. }
  15. }
  16. }
  17. nonRecursiveDepthFirstTraversal(tree, console.log)

栈是一种遵从后进先出、先进后出(LIFO)原则的有序集合。新添加的或待删除的元素都保存在栈的末尾,称作栈顶。另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都靠近栈底。

1.3 队列实现 — 非递归广度优先(BFS)遍历

  1. function nonRecursiveWidthFirstTraversal (node, action) {
  2. if (!node || !node.children) { return }
  3. // 借助一个队列
  4. let _queue = []
  5. _queue.push(node)
  6. while (_queue.length) {
  7. // 推出队头元素
  8. let _curNode = _queue.shift()
  9. action(_curNode.value)
  10. // 将子节点依次推入队列中
  11. if (_curNode.children.length) {
  12. _queue = _queue.concat(_curNode.children)
  13. }
  14. }
  15. }
  16. nonRecursiveWidthFirstTraversal(tree, console.log)

队列遵循的是先进先出、后进后出(FIFO)原则的一组有序的项。队列从尾部添加新元素,并从顶部移除元素,最新添加的元素必须排列在队列的末尾。

2. 二叉搜索树

2.1 定义

二叉搜索树又称为排序二叉树,为二叉树的一种类型,其特点为:

  1. 节点分为左右子树;
  2. 在不为空的情况下,左子树子节点的值都小于父节点的值;
  3. 在不为空的情况下,右子树子节点的值都大于父节点的值;
  4. 每个节点的左右子树都按照上述规则排序。

如图:

image.png

2.2 数据结构

  • 节点用对象来描述,节点特性用对象属性来描述
  1. class Node {
  2. constructor(key) {
  3. this.key = key // 节点值
  4. this.left = null // 左指针
  5. this.right = null // 右指针
  6. }
  7. }
  • 二叉树结构用对象来描述。
  1. class BinaryTree {
  2. constructor() {
  3. this.root = null; // 根节点
  4. }
  5. insert(key) {
  6. const newNode = new Node(key)
  7. if (this.root === null) {
  8. this.root = newNode
  9. return
  10. }
  11. this.insertNode(this.root, newNode)
  12. }
  13. insertNode(root, newNode) {
  14. if (!newNode) return
  15. let tail = root
  16. // 找到可以插入的节点位置
  17. while (tail) {
  18. if (newNode.key > tail.key) {
  19. if (tail.right) {
  20. tail = tail.right
  21. } else {
  22. tail.right = newNode
  23. return tail
  24. }
  25. } else {
  26. if (tail.left) {
  27. tail = tail.left
  28. } else {
  29. tail.left = newNode
  30. return tail
  31. }
  32. }
  33. }
  34. }
  35. }
  • 具体用法:
  1. // 实例化二叉树
  2. const binaryTree = new BinaryTree()
  3. // key值
  4. const keys = [19, 8, 15, 24, 45, 12, 5]
  5. // 生成排序二叉树
  6. keys.forEach(key => binaryTree.insert(key))
  • 结果:

image.png

2.3 遍历

traversal: [trəˈvərs(ə)l]遍历

二叉树遍历分为:

  • 前序遍历:根-左-右
  • 中序遍历:左-根-右
  • 后序遍历:左-右-根

以上面一个例子来说:

  • 前序遍历的结果:[19, 8, 5, 15, 12, 24, 45]
  • 中序遍历的结果:[5, 8, 12, 15, 19, 24, 45]
  • 后序遍历的结果:[5, 12, 15, 8, 45, 24, 19]

image.png

2.3.1 前序遍历

思路:

  1. 取跟节点为目标节点,开始遍历;
  2. 访问目标节点;
  3. 左孩子入栈,直至左孩子为空的节点;
  4. 节点出栈,以右孩子为目标节点,再依次执行234
  1. function preorderTraversal(root) {
  2. let res = []
  3. if (!root) return res
  4. let stack = []
  5. let tail = root
  6. while(tail || stack.length) {
  7. if (tail) {
  8. res.push(tail.key)
  9. stack.push(tail)
  10. tail = tail.left
  11. } else {
  12. let node = stack.pop()
  13. tail = node.right
  14. }
  15. }
  16. return res
  17. }

2.3.3 中序遍历

中序遍历又叫升序遍历,从小到大排列

思路:

  1. 取跟节点为目标节点,开始遍历;
  2. 左孩子入栈,直至左孩子为空的节点;
  3. 节点出栈,访问该节点;
  4. 以右孩子为目标节点,再依次执行234
  1. function inorderTraversal(root) {
  2. let res = []
  3. if (!root) return res
  4. let stack = []
  5. let tail = root
  6. while (tail || stack.length) {
  7. if (tail) {
  8. stack.push(tail)
  9. tail = tail.left
  10. } else {
  11. let node = stack.pop()
  12. res.push(node.key)
  13. tail = node.right
  14. }
  15. }
  16. return res
  17. }

2.3.4 后序遍历

思路:

  1. 取跟节点为目标节点,开始遍历;
  2. 右孩子入栈,直至右孩子为空的节点;
  3. 节点出栈,访问该节点;
  4. 以左孩子为目标节点,再依次执行234
  1. function postorderTraversal(root) {
  2. let res = []
  3. if (!root) return res
  4. let stack = []
  5. let tail = root
  6. while (tail || stack.length) {
  7. if (tail) {
  8. res.unshift(tail.key)
  9. stack.push(tail)
  10. tail = tail.right
  11. } else {
  12. let node = stack.pop()
  13. tail = node.left
  14. }
  15. }
  16. return res
  17. }

2.4 查找

2.4.1 查找最大值

根据排序二叉树的特点,右子树的值都大于父节点的值。只需要进入节点的右子树查找,当某个节点的右子树为空时,该节点就是最大值节点。

  1. function searchMaxNode(root) {
  2. if (!root) return null
  3. let tail = root
  4. while (tail.right) {
  5. tail = tail.right
  6. }
  7. return tail.key
  8. }

2.4.2 查找最小值

根据排序二叉树的特点,左子树的值都小于父节点的值。只需要进入节点的左子树查找,当某个节点的左子树为空时,该节点就是最小值节点。

  1. function searchMinNode(root) {
  2. if (!root) return null
  3. let tail = root
  4. while (tail.left) {
  5. tail = tail.left
  6. }
  7. return tail.key
  8. }

2.4.3 查找给定值

根据排序二叉树的特点,比较给定值与节点值,小于时进入节点左子树。大于时进入节点右子树。等于时返回真。层层对比,最后如果子树为空,则表示没有找到。

  1. function searchNode(root, key) {
  2. if (!root || !key) return false
  3. let tail = root
  4. while (tail) {
  5. if (key > tail.key) {
  6. tail = tail.right
  7. } else if (key < tail.key) {
  8. tail = tail.left
  9. } else if (key === tail.key) {
  10. return true
  11. }
  12. }
  13. return false
  14. }

2.5 删除

2.5.1 删除指定节点

删除节点分为以下几张情况:

  • 被删除节点为叶子节点时;

思路:将该叶子节点的父节点指向的子节点的引用值设为空。

  • 被删除节点仅有一个子树;

思路:将该节点的父节点指向该节点的引用改成指向该节点的子节点。

  • 被删除节点有两个子树。

思路:从待删除节点的右子树找节点值最小的节点A,替换待删除节点,并删除节点A

  1. function removeNode(root, key) {
  2. if (!root) return false
  3. let tail = root
  4. // 存储父节点
  5. let parent = null
  6. while (tail) {
  7. if (key > tail.key) {
  8. parent = tail
  9. tail = tail.right
  10. } else if (key < tail.key) {
  11. parent = tail
  12. tail = tail.left
  13. } else if (key === tail.key) {
  14. if (!tail.left && !tail.right) {
  15. // 被删除节点为叶子节点
  16. if (parent.left && parent.left.key === tail.key) {
  17. parent.left = null
  18. } else {
  19. parent.right = null
  20. }
  21. } else if (!(tail.left && tail.right)) {
  22. // 被删除节点仅有一个子树
  23. if (tail.left) {
  24. if (parent.left && parent.left.key === tail.key) {
  25. parent.left = tail.left
  26. } else {
  27. parent.right = tail.left
  28. }
  29. } else {
  30. if (parent.left && parent.left.key === tail.key) {
  31. parent.left = tail.right
  32. } else {
  33. parent.right = tail.right
  34. }
  35. }
  36. } else {
  37. // 被删除节点有两个子树
  38. let minKey = searchMinNode(tail.right)
  39. tail.key = minKey
  40. removeNode(tail.right, minKey)
  41. }
  42. break
  43. }
  44. }
  45. }

3. 验证二叉搜索树

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

  • 节点的左子树只包含小于当前节点的数;
  • 节点的右子树只包含大于当前节点的数;
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例1

  1. 输入:
  2. 2
  3. / \
  4. 1 3
  5. 输出: true

示例2

  1. 输入:
  2. 5
  3. / \
  4. 1 4
  5. / \
  6. 3 6
  7. 输出: false
  8. 解释: 输入为: [5, 1, 4, null, null, 3, 6]。根节点的值为 5 ,但是其右子节点值为 4

解题:

  • 解法:二叉搜索树中序遍历
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

中序遍历又叫升序遍历,所以仅需判断当前出栈的节点与上一个节点的大小,小于则不符合。

  1. function validBST(root) {
  2. if (!root) return false
  3. let stack = []
  4. let tail = root
  5. let lastNodeKey = null
  6. while (tail || stack.length) {
  7. if (tail) {
  8. stack.push(tail)
  9. tail = tail.left
  10. } else {
  11. let node = stack.pop()
  12. if (lastNodeKey != null && lastNodeKey >= node.key) {
  13. return false
  14. }
  15. lastNodeKey = node.key
  16. tail = node.right
  17. }
  18. }
  19. }

4. 验证平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1

示例1

  1. 给定二叉树 [3,9,20,null,null,15,7]
  2. 3
  3. / \
  4. 9 20
  5. / \
  6. 15 7
  7. 返回 true

示例2

  1. 给定二叉树 [1,2,2,3,3,null,null,4,4]
  2. 1
  3. / \
  4. 2 2
  5. / \
  6. 3 3
  7. / \
  8. 4 4
  9. 返回 false

解题思路:

  • 后续遍历二叉树
  • 在遍历二叉树每个节点前都会遍历其左右子树
  • 比较左右子树的深度,若差值大于1则返回一个标记,-1表示当前子树不平衡
  • 左右子树有一个不是平衡的,或左右子树差值大于1,则整课树不平衡
  • 若左右子树平衡,返回当前树的深度(左右子树的深度最大值+1
  1. function IsBalanced_Solution(pRoot) {
  2. return balanced(pRoot) != -1
  3. }
  4. function balanced(node) {
  5. if(!node) return 0
  6. // 计算左子树的深度
  7. const left = balanced(node.left)
  8. // 计算右子树的深度
  9. const right = balanced(node.right)
  10. if (left === -1 || right === -1 || Math.abs(left - right) > 1) {
  11. return -1
  12. }
  13. return Math.max(left, right) + 1
  14. }