【LeetCode分享】二叉树及其遍历方式

1. 二叉树的构建

本文的二叉树默认指的是二叉搜索树BST,二叉搜索树有如下特点

  1. 一个节点如果有左孩子,那么左孩子的值小于该节点
  2. 一个节点如果有右孩子,那么右孩子的值大于该节点

在此基础上我们进行二叉树的构建,首先定义节点类TreeNode

export class TreeNode {
    constructor(val, left, right) {
        this.val = val == null ? 0 : val
        this.left = left == null ? null : left
        this.right = right == null ? null : right
    }
}

有了这个节点类,我们就可以进行二叉树的构建了,定义一个类为BinaryTree,初始化根节点为null

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

我们可以定义一个方法insert(val)来向数插入节点,首先需要判断是否根节点存在,如果不存在则插入节点为根节点,存在的话则通过和根节点比较,选择插入的位置。在比较时,执行下列步骤:

  1. 选定根节点root,判断插入节点val和根节点值的大小
  2. 如果插入节点的值小于根节点,则往左查找根节点的左孩子。
    1. 如果左孩子不存在,则找到插入位置
    2. 如果左孩子存在,则将根节点设定为左孩子,回到1
  3. 如果插入节点的值大于根节点,则往右查找根节点的右孩子
    1. 如果右孩子不存在,则找到插入位置
    2. 如果右孩子存在,则将根节点设定为右孩子,回到1
function insert(val) {
  if (!this.root) {
    this.root = new TreeNode(val)
  } else {
    this.insertNode(this.root, val)
  }
}

function insertNode(root, val) {
  if (val < root.val) {
    if (root.left == null) {
      root.left = new TreeNode(val)
    } else {
      this.insertNode(root.left, val)
    }
  } else if (val > root.val) {
    if (root.right == null) {
      root.right = new TreeNode(val)
    } else {
      this.insertNode(root.right, val)
    }
  }
}

定义一个接收需要插入节点的值的数组,创建二叉树

function createBinaryTree(node_list) {
  if (!node_list || !node_list.length) {
    return null
  }
  for (let val of node_list) {
    if (val) {
      this.insert(val)
    }
  }
}

2. 递归方式遍历二叉树

二叉树的前中后序遍历是经典的三种遍历方式,我们可以用递归的方式,也可以用迭代的方式进行,下面先介绍递归的方式。

二叉树前序遍历是指我们对给定节点root,先获取该节点的值,再遍历其左子树,当完成左子树遍历后,遍历右子树,那么递归的方式可以理解为

print 根节点
遍历左子树
遍历右子树

具体代码如下

/**
 * 递归前序遍历
 * @param {TreeNode} root
 */
function preOrder(root) {
  let res = []
  const pre = (root) => {
    if (!root) {
      return
    }
    res.push(root.val)
    pre(root.left)  // 递归遍历左子树
    pre(root.right) // 递归遍历右子树
  }
  pre(root)
  return res
}

而中序遍历的顺序则是将print 根节点放在遍历完左子树后执行

遍历左子树
print 根节点
遍历右子树

后序遍历顺序则是将print 根节点放在遍历完右子树后执行

遍历左子树
遍历右子树
print 根节点

具体代码如下

/**
 * 递归中序遍历
 * @param {TreeNode} root
 */
function inOrder(root) {
  let res = []
  const midOrder = (root) => {
    if (!root) {
      return
    }
    midOrder(root.left)
    res.push(root.val)
    midOrder(root.right)
  }
  midOrder(root)
  return res
}

/**
 * 递归后续遍历
 * @param {TreeNode} root
 */
function postOrder(root) {
  let res = []
  const posOrder = (root) => {
    if (!root) {
      return
    }
    posOrder(root.left)
    posOrder(root.right)
    res.push(root.val)
  }
  posOrder(root)
  return res
}

3. 迭代方式遍历二叉树

递归方式代码比较简单,下面我们来看一下迭代方式遍历二叉树。由于递归的过程是一个入栈出栈的过程,那么我们可以知道迭代方式的话,需要显式维护一个栈,模拟递归的过程。

3.1 前序遍历

通过递归的过程我们可以知道,前序遍历是一种深度优先遍历,从根节点开始不断遍历左孩子,直到左孩子为空的时候停止,开始执行回归的过程,在回归的过程中,查找右孩子是否存在,存在的话又开始把右孩子作为根节点开始进行递归。

用栈来描述就是不断将左孩子入栈,如果左孩子为空则停止,此时开始出栈的过程,在出栈的时候判断是否右孩子存在,存在的话则将右孩子作为根节点入栈,又开始查找该右节点的左孩子。即回到了这段话的开头。

/**
 * 用栈的方式进行前序遍历
 * @param {TreeNode} root
 */
function preOrderByStack(root) {
  if (!root) return []
  let res = []
  let stack = []
  while (root || stack.length) {
    // 根节点及左孩子入栈
    while (root) {
      res.push(root.val)
      stack.push(root)
      root = root.left
    }
    root = stack.pop()
    // 查找右孩子是否存在,存在则遍历右孩子及右孩子的所有左孩子
    root = root.right
  }
  return res
}

3.2 中序遍历

中序遍历的过程和前序遍历非常相似,从递归过程我们可以看出,中序遍历只是和前序遍历在输出根节点的时机上不同,此处不做赘述

/**
 * 用栈的方式进行中序遍历
 * @param {TreeNode} root
 */
function inOrderByStack(root) {
  if (!root) return []
  let res = []
  let stack = []
  while (root || stack.length) {
    // 将根节点及其左孩子入栈
    while (root) {
      stack.push(root)
      root = root.left
    }
    root = stack.pop()
    res.push(root.val)
    // 查找右孩子是否存在,存在则遍历右孩子及右孩子的所有左孩子
    root = root.right
  }
  return res
}

3.3 后序遍历

后序遍历稍稍复杂,我们举一个例子来说明一下执行的过程

               4
           2       6
        1    3   5    7
        
如上的一颗二叉树中,我们后续遍历的顺序是 1, 3, 2, 5, 7, 6, 4
即遍历是从下到上,从左到右的一个顺序
那么我们可以以根节点为分界线,把二叉树分为几个部分,即 中 - 左 - 右
为了保证输出顺序是 左 - 右 - 中,我们可以使得出栈顺序为 中 - 左 - 右
再将出栈的节点插入到数组的头部,则输出顺序就得到了保证

为了得到这个出栈的顺序,我们每当遇到中间节点入栈后,就可以将其出栈并插入到数组头部
然后按顺序入栈其左右节点,然后分别出栈添加到数组头部即可

代码如下

/**
 * 使用单栈的方式进行后序遍历
 * @param {TreeNOde} root
 */
function postOrderByOneStack(root) {
  let res = [] // 保存结果。
  let stack = [] // 使用栈进行遍历和输出结果。
  root && stack.push(root) // 链表有值时才开始遍历。

  // 不断遍历直到清空栈
  while (stack.length) {
    // 每次从栈中弹出一个节点,由于入栈顺序是从上到下,从左到右。
    // 出栈顺序就会变成从上到下,从右到左。
    const node = stack.pop()

    // 由于最终输出的结果是从下到上,从左到右。
    // 每次将弹出的节点插入到数组前端,即保证了最终输出的结果顺序。
    res.unshift(node.val)

    // 将子节点按照从左到右的顺序入栈,保证了输出顺序为从右到左。
    node.left && stack.push(node.left)
    node.right && stack.push(node.right)
  }
  return res
}

其实还有一种是采用双栈的方法,思路和单栈相同,也是为了保证输出顺序为左 - 右 - 中,需要第二个输出栈的出战顺序是中 - 右 - 左,而第一个栈则可以同单栈的思路一样,先出栈到第二个栈,在分别入栈并出栈到第二个栈,既可以保证第二个栈的输出顺序

/**
 * 用栈的方式进行后序遍历
 * @param {TreeNode} root
 */
postOrderByStack(root) {
  if (!root) return []
  let res = []
  let stack1 = []
  let stack2 = []
  stack1.push(root)
  while (stack1.length) {
    root = stack1.pop()
    stack2.push(root)
    // 注意下面两个if的顺序不要反了
    // 因为stack2中的入栈顺序是 中-右-左,所以stack1中关于左右的
    // 入栈顺序应该是 左-右
    if (root.left) {
      stack1.push(root.left)
    }
    if (root.right) {
      stack1.push(root.right)
    }
  }
  while (stack2.length) {
    res.push(stack2.pop().val)
  }
  return res
}

3.4 层序遍历

层序遍历实际上就是一层一层从上往下遍历,它的过程如下

               4
           2       6
        1    3   5    7
      
加入4
退出4并进行遍历,加入2,6
退出2,6并进行遍历,加入1,3,5,7
退出1,3,5,7并进行遍历

我们注意到上述过程是一个先进先出的过程,所以这个地方可以采用队列来进行

/**
 * 层序遍历,采用队列的方式进行广度优先搜索
 * @param {TreeNode} root
 */
function levelSearch(root) {
  if (!root) return []
  let res = []
  let queue = []
  queue.push(root)
  while (queue.length) {
    let root = queue.shift()
    res.push(root.val)
    if (root.left) {
      queue.push(root.left)
    }
    if (root.right) {
      queue.push(root.right)
    }
  }
  return res
}

顺便说个题外话,层序遍历我们可以用队列来做,能不能用双栈来做呢,即用双栈模拟队列的先进先出来实现?

最后贴一个完整代码

import { TreeNode } from './util.mjs'

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

    /**
     * 构建二叉树
     * @param {number[]} node_list
     */
    createBinaryTree(node_list) {
        if (!node_list || !node_list.length) {
            return null
        }
        for (let val of node_list) {
            if (val) {
                this.insert(val)
            }
        }
    }

    /**
     *
     * @param {number} val
     */
    insert(val) {
        if (!this.root) {
            this.root = new TreeNode(val)
        } else {
            this.insertNode(this.root, val)
        }
    }

    /**
     *
     * @param {TreeNode} root
     * @param {number} val
     */
    insertNode(root, val) {
        if (val < root.val) {
            if (root.left == null) {
                root.left = new TreeNode(val)
            } else {
                this.insertNode(root.left, val)
            }
        } else if (val > root.val) {
            if (root.right == null) {
                root.right = new TreeNode(val)
            } else {
                this.insertNode(root.right, val)
            }
        }
    }

    /**
     * 递归前序遍历
     * @param {TreeNode} root
     */
    preOrder(root) {
        let res = []
        const pre = (root) => {
            if (!root) {
                return
            }
            res.push(root.val)
            pre(root.left)
            pre(root.right)
        }
        pre(root)
        return res
    }

    /**
     * 递归中序遍历
     * @param {TreeNode} root
     */
    inOrder(root) {
        let res = []
        const midOrder = (root) => {
            if (!root) {
                return
            }
            midOrder(root.left)
            res.push(root.val)
            midOrder(root.right)
        }
        midOrder(root)
        return res
    }

    /**
     * 递归后续遍历
     * @param {TreeNode} root
     */
    postOrder(root) {
        let res = []
        const posOrder = (root) => {
            if (!root) {
                return
            }
            posOrder(root.left)
            posOrder(root.right)
            res.push(root.val)
        }
        posOrder(root)
        return res
    }

    /**
     * 用栈的方式进行前序遍历
     * @param {TreeNode} root
     */
    preOrderByStack(root) {
        if (!root) return []
        let res = []
        let stack = []
        while (root || stack.length) {
            // 根节点及左孩子入栈
            while (root) {
                res.push(root.val)
                stack.push(root)
                root = root.left
            }
            root = stack.pop()
            // 查找右孩子是否存在,存在则遍历右孩子
            root = root.right
        }
        return res
    }

    /**
     * 用栈的方式进行中序遍历
     * @param {TreeNode} root
     */
    inOrderByStack(root) {
        if (!root) return []
        let res = []
        let stack = []
        while (root || stack.length) {
            // 将根节点及其左孩子入栈
            while (root) {
                stack.push(root)
                root = root.left
            }
            root = stack.pop()
            res.push(root.val)
            // 此时需要遍历右孩子
            root = root.right
        }
        return res
    }

    /**
     * 用栈的方式进行后序遍历
     * @param {TreeNode} root
     */
    postOrderByStack(root) {
        if (!root) return []
        let res = []
        let stack1 = []
        let stack2 = []
        stack1.push(root)
        while (stack1.length) {
            root = stack1.pop()
            stack2.push(root)
            // 注意下面两个if的顺序不要反了
            // 因为stack2中的入栈顺序是 中-右-左,所以stack1中关于左右的
            // 入栈顺序应该是 左-右
            if (root.left) {
                stack1.push(root.left)
            }
            if (root.right) {
                stack1.push(root.right)
            }
        }
        while (stack2.length) {
            res.push(stack2.pop().val)
        }
        return res
    }

    /**
     * 使用单栈的方式进行后序遍历
     * @param {TreeNOde} root
     */
    postOrderByOneStack(root) {
        let res = [] // 保存结果。
        let stack = [] // 使用栈进行遍历和输出结果。
        root && stack.push(root) // 链表有值时才开始遍历。

        // 不断遍历直到清空栈
        while (stack.length) {
            // 每次从栈中弹出一个节点,由于入栈顺序是从上到下,从左到右。
            // 出栈顺序就会变成从上到下,从右到左。
            const node = stack.pop()

            // 由于最终输出的结果是从下到上,从左到右。
            // 每次将弹出的节点插入到数组前端,即保证了最终输出的结果顺序。
            res.unshift(node.val)

            // 将子节点按照从左到右的顺序入栈,保证了输出顺序为从右到左。
            node.left && stack.push(node.left)
            node.right && stack.push(node.right)
        }
        return res
    }

    /**
     * 层序遍历,采用队列的方式进行广度优先搜索
     * @param {TreeNode} root
     */
    levelSearch(root) {
        if (!root) return []
        let res = []
        let queue = []
        queue.push(root)
        while (queue.length) {
            let root = queue.shift()
            res.push(root.val)
            if (root.left) {
                queue.push(root.left)
            }
            if (root.right) {
                queue.push(root.right)
            }
        }
        return res
    }
}

// test
const tree = new BinaryTree()
tree.createBinaryTree([4, 2, 6, 1, 3, 5, 7])
/**
 *              4
 *          2       6
 *       1    3   5    7
 */
// console.log(tree.preOrder(tree.root)) // 4, 2, 1, 3, 6, 5, 7
// console.log(tree.inOrder(tree.root)) // 1, 2, 3, 4, 5, 6, 7
// console.log(tree.postOrder(tree.root)) // 1, 3, 2, 5, 7, 6, 4

console.log(tree.preOrderByStack(tree.root)) // 4, 2, 1, 3, 6, 5, 7
console.log(tree.inOrderByStack(tree.root)) // 1, 2, 3, 4, 5, 6, 7
console.log(tree.postOrderByStack(tree.root)) // 1, 3, 2, 5, 7, 6, 4
console.log(tree.postOrderByOneStack(tree.root)) // 1, 3, 2, 5, 7, 6, 4
console.log(tree.levelSearch(tree.root)) // 4, 2, 6, 1, 3, 5, 7
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容