树结构(javascript)-1:二叉树的深度和广度优先遍历实现

什么是二叉树?

二叉树是树的一种特殊形式,这种树的每个节点最多有2个孩子节点(也可能只有1个或者没有)。二叉树节点的两个孩子节点,一个被称为左孩子,一个被称为右孩子。这两个孩子节点的顺序是不能颠倒或混淆的。

二叉树有两种特殊形式,一个叫满二叉树,另一个叫完全二叉树

  • 满二叉树:指树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上
  • 完成全叉树:完成二叉树和满二叉树有点像,只不过满二叉树要求所有节点(除叶子节点)都是满的,而完全二叉树只需保证最后一个节点之前的节点都齐全即可

我们知道,所有的数据结构(逻辑结构,包括栈、队列、树、图等)都可以由两种物理结构来实现:数组链式存储结构

如果是链式存储,其实很容易实现一棵树:

  • 有一个data用于存储当前节点值
  • 有一个左指针,指向它的左孩子节点
  • 有一个右指针,指向它的右孩子节点
// 定义一个二叉树节点,节点包括自身值,一个左指针和一个右指针
function TreeNode(val) {
  this.data = val;
  this.left = null; // 指向它的左孩子节点
  this.right = null;  // 指向它的右孩子节点
}

二叉树包含许多特殊形式,每一种形式自己的作用,但是其最主要的应用还在于进行查找操作维持相对顺序这两方面,比如下面这种二叉查找树:

  • 二叉查找树
    1. 如果左子树不为空,则左子树上的所有节点的值都小于根节点的值
    2. 如果右子树不为空,则右子树上的所有节点的值都大于根节点的值
    3. 左、右子树也都是二叉查找树

如其名,其主要作用就是用于查找操作。

二叉树的遍历

二叉树遍历一般归为两大类:深度优先遍历(前、中、后序遍历)和广度优先遍历(层序遍历)

深度优先遍历

深度优先遍历中的前、中、后序的说法,其实是相对于根节点的遍历顺序而言的。先左后右的相对顺序是不会变的,前中后的差别在于根节点的位置在哪,如前序,根在前,后左再右;如中序,左在前,根在中,右在后;如后序,根在最后,前面自然是左右。这样,是不是就很清楚容易记得三者的区别。

  • 前序遍历,输出顺序为:根节点 => 左子树 => 右子树
  • 中序遍历,输出顺序为:左子树 => 根节点 => 右子树
  • 后序遍历,输出顺序为:左子树 => 右子树 => 根节点

注意,这里的左子树和右子树是一个集合,而非单个孩子节点,而是除了根节点和另一边子树外的所有节点的集合。而左子树和右子树中的遍历,输出顺序也是如此的。

按照前序遍历的思想生成一个二叉树

给定一个序列,先生成根节点,再遍历生成左子树的所有节点,最后再生成右子树的所有节点

// javascript实现二叉树

// 创建一个二叉树
function createTree(nodeList) {
  // 检测输入是否为一个数组
  if(Array.isArray(nodeList)) {
    const len = nodeList.length
    if(!len) return;
    else {
      // 获取最前头的节点值
      const data = nodeList.shift()
      // 构建二叉树
      let node = null;
      // 如果值不为null,则递归为此节点生成左子树和右子树
      if(data) {
        node = new TreeNode(data)
        node.left = createTree(nodeList)
        node.right = createTree(nodeList)
      }
      return node
    }
  }else{
    throw Error("请输入一个节点序列")
  }
}

const tree = createTree([1,2,null,null,3,4,5,null,7])
console.log(tree);
// TreeNode {
//   data: 1,
//   left: TreeNode { data: 2, left: null, right: null },
//   right:
//    TreeNode {
//      data: 3,
//      left: TreeNode { data: 4, left: [TreeNode], right: undefined },
//      right: undefined } }

递归实现深度优先遍历(前中序)

// 二叉树前序遍历
function preOrderTraveral(nodeTree) {
  if(!nodeTree) return;
  else {
    console.log(nodeTree.data);
    preOrderTraveral(nodeTree.left)
    preOrderTraveral(nodeTree.right)
  }
}

console.log("前序遍历:");
console.log(preOrderTraveral(tree)); // 1, 2, 3, 4, 5, 7

// 二叉树中序遍历
function middleOrderTreveral(nodeTree) {
  if(!nodeTree) return;
  else {
    middleOrderTreveral(nodeTree.left)
    console.log(nodeTree.data);
    middleOrderTreveral(nodeTree.right)
  }
}

console.log("中序遍历:");
console.log(middleOrderTreveral(tree)); // 2, 1, 5, 7, 4, 3

// 二叉树后序遍历
function postOrderTreveral(nodeTree) {
  if(!nodeTree) return;
  else {
    postOrderTreveral(nodeTree.left)
    postOrderTreveral(nodeTree.right)
    console.log(nodeTree.data);
  }
}
console.log("后序遍历:");
console.log(postOrderTreveral(tree)); // 2, 7, 5, 4, 3, 1

使用栈实现深度优先遍历

绝大多数可以用递归解决的问题,其实都可以用另一种数据结构来解决,即栈。因为递归和栈都有回溯的特性。

使用递归的方式,真的是有手就行,所以面试官往往也不会考查我们递归的实现方式,而是会更倾向于考查我们栈的实现方式。

/*
  栈实现深度优先遍历
*/
// 栈方式实现前序遍历
function stackPreTravel(nodeTree) {
  if(!nodeTree) return;
  let stack = []
  let node = nodeTree
  // 如果节点不为空,且栈不为空,则继续循环
  while(node || stack.length) {
    // 如果节点不为空,则继续向左遍历
    while(node) {
      console.log(node.data);
      // 将存在的左节点入栈
      stack.push(node)
      node = node.left
    }
    // 当左节点为空时,但栈不为空,遍历右节点子树
    if(stack.length) {
      node = stack.pop()
      node = node.right;
    }
  }
 
}
// console.log(stackPreTravel(tree));

// 栈方式实现中序遍历
function stackMiddleTravel(nodeTree) {
  if(!nodeTree) return;
  let stack = []
  let node = nodeTree
  while(node || stack.length) {
    while(node) {
      stack.push(node)
      node = node.left
    }
    
    if(stack.length) {
      node = stack.pop()
      console.log(node.data);
      node = node.right
    }
  }
  
}
// console.log(stackMiddleTravel(tree));

广度优先遍历

如其名字的含义,广度优先遍历,是将树按层级来一层一层遍历的,每一层遍历的顺序是从左往右。因为广度优先是要按顺序排序的,所以与队列的特性是相似的,即先进先出。所以一般使用队列来实现广度优先遍历。

/*
  广度优先遍历:使用队列实现
*/
function levelOrderTravel(nodeTree) {
  // 如果树为空,结束
  if(!nodeTree) return;
  // 初始化一个队列
  let queue = []
  // 将根节点入队
  queue.push(nodeTree)
  let node = null
  // 只要队列不为空,继续循环
  while(queue.length) {
    // 按顺序取出队列中最早入队的节点
    node = queue.shift()
    console.log(node.data);
    // 如果出队节点存在左孩子,就将其左孩子入队
    if(node.left) {
      queue.push(node.left)
    }
    // 如果出队节点存在右孩子,就将其右孩子入队
    if(node.right) {
      queue.push(node.right)
    }
  }
}
console.log("广度优先遍历-使用队列实现:");
console.log(levelOrderTravel(tree));
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容