漫画算法-树

树和二叉树

许多逻辑关系并不是简单的线性关系,常常存在一对多,多对多的情况。树和图就是典型的非线性数据结构。

比如“家谱”是一个树,企业的职级关系是一个树。一本书的目录也是一个树。

定义:树是 n (n >= 0)个节点的有限集。当 n = 0 时,称为空树。在任意一个非空树中,有如下特点:

  • 有且只有一个特定的称为根的节点。
  • 当 n > 1 时,其余节点可分为 m(m > 0)个互不相交的有限集。每一个集合本身又是一个树,并称为根的子树。

几个概念:

父节点:某个节点的上一个节点。

孩子节点:某个节点衍生出来的节点。

兄弟节点:同一个父节点衍生出来的同级节点。

叶子节点:处于树的末端,没有子节点。

二叉树(binary tree)

这种树的每个节点最多有 2 个孩子节点。

img

其中,左孩子节点和右孩子节点的顺序是固定的。

二叉树的两种特殊格式:

  • 满二叉树:所有节点都是满的,且所有叶子节点都在同一层级上。要求所有节点都是满的。
  • 完全二叉树:有 n 个节点,与相同深度的满二叉树的 1 到 n 个节点位置相同。只需保证最后一个节点之前的节点齐全。

二叉树属于逻辑结构,可以通过多种物理结构来表达:

  • 链式存储结构
  • 数组

用链表表示:

包含了存储数据的 data 变量,指向左孩子的 left 指针,指向右孩子的 right 指针。

img

用数组表示:

img

按照层级顺序把二叉树的节点放到数组相应的下标位置中。如果某个左孩子或右孩子缺失,数组相应的位置会空出来。

这样可以方便地定位孩子节点和父节点的位置。

leftChild = parent * 2 + 1;
rightChild = parent * 2 + 2;
parent = (leftChild - 1) / 2;

二叉树的应用

主要作用:查找操作和维持相对顺序。

1、查找

特殊的二叉树:二叉查找树(binary search tree)。

二叉查找树在二叉树的基础上加了几个条件:

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

二叉查找树:

img

查找步骤:

  1. 比如查找 4,发现 4 < 6,查左子树,找到 3。
  2. 发现 4 > 3,查右子树,找到 4,查找完成。

对于一个节点分布相对均匀的二叉树来说,每次都是除以 2,所以算法复杂度是 O(logn)。

2、维持相对顺序

二叉查找树要求左子树的节点小于根节点,右子树的节点大于根节点,这样就保证了二叉树的有序性。

二叉树还有另一个名字:二叉排序树(binary sort tree)。

问题:在下面的二叉查找树中插入 9、8、7、6 等。

img

插入后:

img

时间复杂度退化成了 O(n)。

解决这个问题,涉及到二叉树的自平衡:红黑树、AVL树、树堆等。

二叉树的遍历

为什么要研究遍历

在计算机中,遍历本身是一个线性操作,遍历具有同样结构的数组或链表,轻而易举。

但是二叉树是非线性数据结构,遍历时需要把非线性关联的节点转化成一个线性的序列,以不同的方式来遍历,遍历出的序列顺序也不同。

从节点之间位置关系的角度看,二叉树的遍历分为 4 种。

  1. 前序遍历
  2. 中序遍历
  3. 后序遍历
  4. 层序遍历

从更宏观的角度,二叉树的遍历归为:

  1. 深度优先遍历(前序遍历、中序遍历、后序遍历)
  2. 广度优先遍历(层序遍历)

深度优先遍历

深度优先:偏向于纵深的访问方式。

1、前序遍历

二叉树的前序遍历,输出顺序是根节点、左子树、右子树。

img

2、中序遍历

二叉树的中序遍历,输出顺序是:左子树、根节点、右子树。

img

3、后序遍历

二叉树的后序遍历,输出顺序是:左子树、右子树、根节点。

img

构建二叉树 + 二叉树遍历:

递归方式

class TreeNode {
  data;
  leftChild;
  rightChild;

  constructor(data) {
    this.data = data;
  }
}

function createBinaryTree(list) {
  let node = null;
  if (list === null || !(list && list.length)) {
    return null;
  }
  let data = list.shift();
  if (data !== null) {
    node = new TreeNode(data);
    node.leftChild = createBinaryTree(list);
    node.rightChild = createBinaryTree(list);
  }
  return node;
}

const list = [3, 2, 9, null, null, 10, null, null, 8, null, 4];
const treeNode = createBinaryTree(list);

构建二叉树结果:

img

// 前序遍历
function preorderTraversal(node) {
  if (node === null) {
    return;
  }
  console.log(node.data); // 输出根节点
  preorderTraversal(node.leftChild); // 遍历左节点
  preorderTraversal(node.rightChild); // 遍历右节点
}

// 中序遍历
function inorderTraversal(node) {
  if (node === null) {
    return;
  }
  preorderTraversal(node.leftChild); // 遍历左节点
  console.log(node.data); // 输出根节点
  preorderTraversal(node.rightChild); // 遍历右节点
}

// 后序遍历
function postorderTraversal(node) {
  if (node === null) {
    return;
  }
  preorderTraversal(node.leftChild); // 遍历左节点
  preorderTraversal(node.rightChild); // 遍历右节点
  console.log(node.data); // 输出根节点
}

console.log('前序遍历');
preorderTraversal(treeNode);
// 3
// 2
// 9
// 10
// 8
// 4
console.log('中序遍历');
inorderTraversal(treeNode);
// 2
// 9
// 10
// 3
// 8
// 4
console.log('后序遍历');
postorderTraversal(treeNode);
// 2
// 9
// 10
// 8
// 4
// 3

栈方式

// 前序遍历
function preorderTraversalWithStack(node) {
  const arr = new Array();
  let treeNode = node;
  while (treeNode !== null || (arr && arr.length)) {
    while (treeNode !== null) {
      console.log(treeNode.data);
      arr.push(treeNode); // 入栈
      treeNode = treeNode.leftChild;
    }
    // 当前节点没有左孩子节点,将该节点弹出,访问该节点的右节点
    if (arr && arr.length) {
      treeNode = arr.pop(); // 弹出该节点,父节点设置为该弹出节点
      treeNode = treeNode.rightChild; // 访问弹出节点的右孩子节点
    }
  }
}

preorderTraversalWithStack(treeNode);
// 中序遍历
function inorderTraversalWithStack(node) {
  const arr = new Array();
  let treeNode = node;
  while (treeNode !== null || (arr && arr.length)) {
    while (treeNode !== null) {
      arr.push(treeNode);
      treeNode = treeNode.leftChild;
    }
    if (arr && arr.length) {
      treeNode = arr.pop();
      console.log(treeNode.data);
      treeNode = treeNode.rightChild;
    }
  }
}
inorderTraversalWithStack(treeNode);
// 后序遍历
function postorderTraversalWithStack(node) {
  const arr1 = new Array();
  const arr2 = new Array();
  let treeNode = node;
  while (treeNode !== null || (arr1 && arr1.length)) {
    while (treeNode !== null) {
      arr1.push(treeNode);
      arr2.push(treeNode);
      treeNode = treeNode.rightChild;
    }

    if (arr1 && arr1.length) {
      treeNode = arr1.pop();
      treeNode = treeNode.leftChild;
    }
  }
  while (arr2 && arr2.length) {
    treeNode = arr2.pop();
    console.log(treeNode.data);
  }
}
postorderTraversalWithStack(treeNode);

广度优先遍历

img

利用队列来实现遍历。

  1. 根节点 1 入队

img

  1. 根节点 1 出队,输出节点 1,找到子节点 2、3,子节点 2、3 入队。

img

  1. 节点 2 出队,输出 2,找到子节点 4、5,4、5入队

img

  1. 节点 3 出队,输出 3,找到子节点 6,节点 6 入队。

img

  1. 节点 4 出队,输出 4,没有子节点入队

img

  1. 节点 5 出队,输出 5,没有子节点入队

img

  1. 节点 6 出队,输出 6,没有子节点入队

img

Last Updated: 8/27/2019, 11:35:42 AM