JavaScript 中的二叉树

367 阅读4分钟

前言

本文讲述关于二叉树的创建逻辑,各种遍历算法等等。

将会持续更新关于二叉树的知识点

构建二叉树

这里说的是,给出一个数组,通过不断的往树中添加节点,构建出二叉树。

原则:二叉树需要保证左子树小于父节点,右子树大于父节点。

先定义一个节点 Class

class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

构建树

class BinaryTree {
    constructor() {
        this.root = null;
    }

    createTree(list) {
        if (!Array.isArray(list)) return null;
        for (const value of list) {
            this.setNode(value);
        }
        return this.root;
    }

    setNode(value) {
        if (!this.root) {
            this.root = new TreeNode(value);
        } else {
            this.buildTree(this.root, value);
        }
    }

    buildTree(node, value) {
        if (node.value > value) {
            if (!node.left) {
                node.left = new TreeNode(value);
            } else {
                this.buildTree(node.left, value)
            }
        } else {
            if (!node.right) {
                node.right = new TreeNode(value);
            } else {
                this.buildTree(node.right, value)
            }
        }
    }
}

const nodes = [8, 1, 3, 6, 7, 4, 10, 14, 13];
// New 一个二叉树构造器
const treeConstructor = new BinaryTree();
// Create Tree
const tree = treeConstructor.createTree(nodes);
console.log(tree); // 输出二叉树

二叉树的相关遍历

这里使用上面构建出来的二叉树来进行遍历

前序遍历

class BinaryTree {
    constructor() {
        this.root = null;
        this.pre = [];
    }
    
    // 前序遍历
    prologueTraverse(node) {
       this.pre.push(node.value);
       if (node.left) {
           this.prologueTraverse(node.left);
        }
        if (node.right) {
            this.prologueTraverse(node.right);
        }
        return this.pre;
    }
}

// 前序遍历(先序遍历)
const pre = treeConstructor.prologueTraverse(tree);
console.log('前序遍历', pre); // 前序遍历 [8, 1, 3, 6, 4, 7, 10, 14, 13]

中序遍历

class BinaryTree {
    constructor() {
        this.root = null;
        this.mid = [];
    }
    
    // 中序遍历
    middleTraverse(node) {
        if (node.left) {
            this.middleTraverse(node.left);
        }
        this.mid.push(node.value);
        if (node.right) {
            this.middleTraverse(node.right);
        }
        return this.mid;
    }
}

// 中序遍历
const mid = treeConstructor.middleTraverse(tree);
console.log('中序遍历', mid); // 中序遍历 [1, 3, 4, 6, 7, 8, 10, 13, 14]

后序遍历

class BinaryTree {
    constructor() {
        this.root = null;
        this.aft = [];
    }
  
    afterTraverse(node) {
        if (node.left) {
            this.afterTraverse(node.left);
        }
        if (node.right) {
            this.afterTraverse(node.right);
        }
        this.aft.push(node.value);
        return this.aft;
    }
}

// 后序遍历
const aft = treeConstructor.afterTraverse(tree);
console.log('后序遍历', aft); // 后序遍历 [4, 7, 6, 3, 1, 13, 14, 10, 8]

深度优先遍历(利用栈 - 先进后出)

class BinaryTree {
    // 深度优先遍历(利用栈 - 先进后出)
    depthFirstTraversal(node) {
        const depth = [];
        const stack = [];
        stack.push(node);
        function traversal(stack) {
            if (!Array.isArray(stack) || stack.length <= 0) return;
            const _node = stack.pop();
            depth.push(_node.value);
            if (_node.right) stack.push(_node.right);
            if (_node.left) stack.push(_node.left);
            traversal(stack); // 递归
        }
        traversal(stack);
        return depth;
    }
}

// 深度优先遍历
const depth = treeConstructor.depthFirstTraversal(tree);
console.log('深度优先遍历', depth); // 深度优先遍历 [8, 1, 3, 6, 4, 7, 10, 14, 13]

广度优先遍历(利用队列 - 先进先出)

class BinaryTree {
    // 广度优先遍历(利用队列 - 先进先出)
    breadthFirstTraversal(node) {
        const breadth = [];
        const queue = [];
        queue.push(node)
        function traversal(queue) {
            if (!Array.isArray(queue) || queue.length <= 0) return;
            const _node = queue.shift();
            breadth.push(_node.value);
            if (_node.left) queue.push(_node.left);
            if (_node.right) queue.push(_node.right);
            traversal(queue); // 递归
        }
        traversal(queue);
        return breadth;
    }
}

// 广度优先遍历
const breadth = treeConstructor.breadthFirstTraversal(tree);
console.log('广度优先遍历', breadth); // 广度优先遍历 [8, 1, 10, 3, 14, 6, 13, 4, 7]

根据前序、中序、后续构建二叉树

二叉树只能通过【前序 + 中序】或者【后序 + 中序】 构建二叉树。

无法通过【前序 + 后序】构建二叉树,因为,前序 和 后序,只能说面父子节点关系,而无法说明左右节点关系,比如:

前序:ab

后续:ba

他可能为

   a              a
  /                \
 b                  b

上诉可看出无法知道 b 是 a 的左节点还是右节点

前序遍历 + 中序遍历 = 构建出树

前序遍历 [8, 1, 3, 6, 4, 7, 10, 14, 13]

中序遍历 [1, 3, 4, 6, 7, 8, 10, 13, 14]

首先这里第一想到要用递归容易一些!

二叉树前序遍历的顺序为:

  • 先遍历根节点;

  • 随后递归地遍历左子树;

  • 最后递归地遍历右子树。

二叉树中序遍历的顺序为:

  • 先递归地遍历左子树;

  • 随后遍历根节点;

  • 最后递归地遍历右子树。

在「递归」地遍历某个子树的过程中,我们也是将这颗子树看成一颗全新的树,按照上述的顺序进行遍历。挖掘「前序遍历」和「中序遍历」的性质,我们就可以得出本题的做法。

思路:

对于任意一颗树而言,前序遍历的形式总是

[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]

即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是

[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]

这里就是一种分而治之的思想,把整个序列就看成是一个根节点、一个左子树、一个右子数。
因为前序遍历结果第一个一定是根节点,那么就秉着一个想法,拿到第一个节点 New 一个 TreeNode,然后赋值根节点的 Left 节点和 Right 节点,
当然,这里的 Left 节点当然就是 [左子树的前序遍历结果] 作为完整子树重新递归获取了。

到这里可知道每次递归的时候,需要知道当前作为完整子树的左右节点,在[前序遍历结果]的位置,这样才能够从[前序遍历结果]中拿到左右节点的子数组。

下面来计算一下各个节点的索引:

/**
 *  y - (preLeft + 1) = pIndex - 1 - midLeft
 *  y = pIndex - midLeft + preLeft
 * 
 * 重点:分而治之思想
 * 
 * 前序遍历:
 *
 *   [][                  左节点                  ][                  右节点                  ]
 *   ↑               ↑                                         ↑ ↑                                         ↑
 *   preLeft         preLeft + 1     pIndex - midLeft + preLeft pIndex - midLeft + preLeft + 1            preRight
 *
 * 中序遍历:
 *
 *   [                  左节点                  ][][                  右节点                  ]
 *   ↑                                         ↑ ↑              ↑                                          ↑
 *   midLeft                          pIndex - 1 pIndex         pIndex + 1                                midRight
 */
  1. 先定义 preLeft、preRight、midLeft、midRight 这些都是一开始可知道的数值
  2. 之后再定义 pIndex,这个值可以通过获取[前序遍历结果的]第一个元素在[中序遍历结果]中找到位置(这里注明:节点值没有重复)
  3. 进而计算可得到:preLeft + 1、pIndex - 1、pIndex + 1
  4. 接下来剩下[前序遍历结果]的左节点右边和右节点左边的值未得到,首先这里假设左节点右边为y,有因为前序和中序的左节点个数肯定相等,可得到等式:

    y - (preLeft + 1) = pIndex - 1 - midLeft

    y = pIndex - midLeft + preLeft

  5. 这样[前序遍历结果]的左节点右边位置就为:pIndex - midLeft + preLeft
  6. 这样[前序遍历结果]的右节点左边位置就为 pIndex - midLeft + preLeft + 1

编码:

class BinaryTree {
    // 前序遍历 + 中序遍历 = 构建出树
    createTreeByPreAndMid(pre = [], mid = []) {
        const preLength = pre.length;
        const midLength = mid.length;

        // 针对中序遍历结果遍历出 hash 值,方便快速找到根节点索引
        const hashMap = new Map();
        for (let i = 0; i < midLength; i ++) {
            hashMap.set(mid[i], i);
        }
        return this.buildTreeByPreAndMid(pre, 0, preLength - 1, hashMap, 0, midLength - 1);
    }

    buildTreeByPreAndMid(pre, preLeft, preRight, hashMap, midLeft, midRight) {
        if (preLeft > preRight || midLeft > midRight) return null;
        
        const rootValue = pre[preLeft];
        const root = new TreeNode(rootValue);
        const pIndex = hashMap.get(rootValue);

        // 分而治之的思想,将左右子树看做一个完整的前序遍历结果,重新放入
        root.left = this.buildTreeByPreAndMid(pre, preLeft + 1, pIndex - midLeft + preLeft, hashMap, midLeft, pIndex - 1);
        root.right = this.buildTreeByPreAndMid(pre, pIndex - midLeft + preLeft + 1, preRight, hashMap, pIndex + 1, midRight);

        return root;
    }
}

后序遍历 + 中序遍历 = 构建出树

中序遍历 [1, 3, 4, 6, 7, 8, 10, 13, 14]

后序遍历 [4, 7, 6, 3, 1, 13, 14, 10, 8]

这里和上面的类似的,只要稍作调整即可:

思路:

**
 *  y - aftLeft = pIndex - 1 - midLeft
 *  y = pIndex - 1 - midLeft + aftLeft
 * 
 * 重点:分而治之思想
 * 
 * 前序遍历:
 *   [                  左节点                  ][                  右节点                  ][]
 *   ↑                                         ↑ ↑                                         ↑               ↑
 *   aftLeft      pIndex - 1 - midLeft + aftLeft pIndex - midLeft + aftLeft    aftRight - 1               aftRight
 * 中序遍历:
 *   [                  左节点                  ][][                  右节点                  ]
 *   ↑                                         ↑ ↑               ↑                                         ↑
 *   midLeft                          pIndex - 1 pIndex         pIndex + 1                                midRight
 */

编码:

class BinaryTree {
    // 后序遍历 + 中序遍历 = 构建出树
    createTreeByAftAndMid(aft = [], mid = []) {
        const aftLength = aft.length;
        const midLength = mid.length;

        const hashMap = new Map();
        for (let i = 0; i < midLength; i ++) {
            hashMap.set(mid[i], i);
        }
        return this.buildTreeByAftAndMid(aft, 0, aftLength - 1, hashMap, 0, midLength - 1);
    }

    buildTreeByAftAndMid(aft, aftLeft, aftRight, hashMap, midLeft, midRight) {
        if (aftLeft > aftRight || midLeft > midRight) return null;
        
        const rootValue = aft[aftRight];
        const root = new TreeNode(rootValue);
        const pIndex = hashMap.get(rootValue);

        // 分而治之的思想,将左右子树看做一个完整的后续遍历结果,重新放入
        root.left = this.buildTreeByAftAndMid(aft, aftLeft, pIndex - 1 - midLeft + aftLeft, hashMap, midLeft, pIndex - 1);
        root.right = this.buildTreeByAftAndMid(aft, pIndex - midLeft + aftLeft, aftRight - 1, hashMap, pIndex + 1, midRight);

        return root;
    }
}

源码

Github:链接