算法随笔-数据结构(树的基础概念和遍历)

257 阅读7分钟

算法随笔-数据结构(树的基础概念和遍历)

本文主要介绍数据结构中树的基本定义、类型、二叉树介绍、二叉树的JS实现、二叉树的4种遍历方式:前序遍历、中序遍历、后序遍历、层次遍历。供自己以后查漏补缺,也欢迎同道朋友交流学习。

引言

数据结构中是相对复杂的,所以我分两篇文章来介绍。

树形结构在我们生活中其实非常普遍:家庭的家谱图,公司的管理结构,学校的年级班级体系都是树的应用,都是从最顶层分出几个分支去管理子级、子级下面又有分支去管理孙级,一级一级的多层结构。

对于我们前端来说,树更不陌生,天天说的DOM (Virtual DOM) 树都听出老茧了。其他的诸如UI组件文件夹结构数据可视化语法解析都是树的表现。

基本定义

树(Tree)是一种非常重要的非线性数据结构,用于表示层次关系的数据集合。

  • :一个有限的非空节点集合,其中一个特定的节点称为根(root),其余节点分成 m (m ≥ 0) 个互不相交的集合 T1、T2、...、Tm,每个集合本身又是一个树。
  • 节点:树中的基本单元,可以包含数据或其他信息。
  • :连接树中节点的连线,表示节点之间的关系。

树的类型

对于树的类型,我们对二叉树会比较了解,学习的也更多。

  • 无序树(Unordered Tree:树中的子节点之间没有顺序关系。
  • 有序树(Ordered Tree:树中的子节点之间存在固定的顺序
  • 二叉树(Binary Tree:每个节点最多有两个子节点,通常分为左子节点右子节点
  • 多叉树(n-ary Tree:每个节点可以有任意数量的子节点。
  • 平衡树(Balanced Tree:左右子树的高度差不大于给定值的树,如AVL树红黑树等。
  • 满二叉树(Full Binary Tree:除了叶子节点外,每一个节点都有两个子节点
  • 完全二叉树(Complete Binary Tree:除了最后一层外,每一层的节点都是满的最后一层的节点都靠左排列

二叉树

二叉树(Binary Tree)是一种特殊的树形数据结构,每个节点最多只有两个子节点,分别称为左子节点(left child)和右子节点(right child)。

二叉树因其简单而强大的特性,在计算机科学中有广泛的应用,包括但不限于数据存储搜索排序等方面。

二叉树的定义

  • 二叉树:每个节点最多有两个子节点的树形结构。
  • 左子树(Left Subtree):每个节点的左分支指向的子树。
  • 右子树(Right Subtree):每个节点的右分支指向的子树。
  • 根节点(Root Node):树的顶部节点,没有父节点。
  • 叶子节点(Leaf Node):没有子节点的节点。

二叉树的图例

二叉树的图例

二叉树的JS实现

// 定义二叉树节点类
class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null; // 左子节点
    this.right = null; // 右子节点
  }
}

// 定义二叉树类
class BinaryTree {
  constructor() {
    this.root = null; // 根节点
  }

  // 插入节点
  insert(value) {
    let newNode = new TreeNode(value);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  // 辅助函数:递归地插入节点
  insertNode(node, newNode) {
    if (newNode.value < node.value) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }
}

二叉树的遍历

假设我们有个满二叉树如下:

const tree = {
  val: "1",
  left: {
    val: "2",
    left: { val: "4", left: null, right: null },
    right: { val: "5", left: null, right: null },
  },
  right: {
    val: "3",
    left: { val: "6", left: null, right: null },
    right: { val: "7", left: null, right: null },
  },
};

// 对象的具体形式如下:
//       1
//     /   \
//    2     3
//   / \   / \
//  4   5 6   7

二叉树有4种遍历方式:

  • 前序遍历(Preorder Traversal:访问顺序为“根 -> 左子树 -> 右子树”。二叉树遍历顺序为:1,2,4,5,3,6,7
  • 中序遍历(Inorder Traversal:访问顺序为“左子树 -> 根 -> 右子树”,特别适用于二叉搜索树。二叉树遍历顺序为:4,2,5,1,6,3,7
  • 后序遍历(Postorder Traversal:访问顺序为“左子树 -> 右子树 -> 根”,常用于删除树释放内存。二叉树遍历顺序为:4,5,2,6,7,3,1
  • 层次遍历(Level Order Traversal:按照从上到下、从左到右的顺序访问节点。二叉树遍历顺序为:1,2,3,4,5,6,7

前序遍历JS实现

递归形式

递归是一种比较简单的方法,但占用内存和时间比较大。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const preorderTraversal = (root) => {
  let arr = [];
  var fun = (node) => {
    if (node) {
      // 先根节点
      arr.push(node.val);
      // 遍历左子树
      fun(node.left);
      // 遍历右子树
      fun(node.right);
    }
  };

  fun(root);
  return arr;
};

preorderTraversal(tree); // ['1', '2', '4', '5', '3', '6', '7']

栈的形式

使用栈的形式,极大的提升了时间和内存占用效率。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const preorderTraversal = (root) => {
  if (!root) return [];
  let arr = [];
  // 根节点入栈
  let stack = [root];
  while (stack.length) {
    // 出栈
    let node = stack.pop();
    arr.push(node.val);

    // 先添加right再添加left,出栈是先left在right
    node.right && stack.push(node.right);
    node.left && stack.push(node.left);

    // 第1遍遍历,arr入数组1,stack入栈[3,2]
    // 第2遍遍历,arr入数组1,2,stack入栈[3,5,4]
    // 第3遍遍历,arr入数组1,2,4,stack入栈[3,5]
    // 第4遍遍历,arr入数组1,2,4,5,stack入栈[3]
    // 第5遍遍历,arr入数组1,2,4,5,3,stack入栈[7,6]
    // ...
  }
  return arr;
};

preorderTraversal(tree); // ['1', '2', '4', '5', '3', '6', '7']

中序遍历JS实现

递归形式

递归是一种比较简单的方法,但占用内存和时间比较大。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = (root) => {
  const arr = [];

  const fun = (node) => {
    if (!node) return;
    fun(node.left);
    arr.push(node.val);
    fun(node.right);
  };

  fun(root);

  return arr;
};

inorderTraversal(tree); // ['4', '2', '5', '1', '6', '3', '7']

栈的形式

使用栈的形式,极大的提升了时间和内存占用效率。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = (root) => {
  if (!root) return [];
  let arr = [];
  let stack = [];
  let node = root;
  // 只要有栈和节点,一直循环
  while (stack.length || node) {
    // 循环当前节点
    while (node) {
      // 把当前节点入栈
      stack.push(node);
      // 当前节点变更为left
      node = node.left;
    }
    // 第一遍遍历入栈为[1,2,4]
    // 第2遍遍历,因为right是空的,不执行
    // 第3遍遍历入栈为[1,5]
    // 第4遍遍历,因为right是空的,不执行
    // 第5遍遍历入栈[3,6]
    // ...

    // 出栈
    const n = stack.pop();
    // 入栈值
    arr.push(n.val);
    // 修改当前node为right
    node = n.right;
    // 第一遍遍历stack入栈为[1,2], arr入数组4, node节点为null
    // 第2遍遍历stack入栈为[1], arr入数组4,2,node节点为2的右节点5
    // 第3遍遍历入栈为[1],arr入数组4,2,5,node节点为null
    // 第4遍遍历入栈为[],arr入数组4,2,5,1,node节点为1的右节点3
    // ...
  }
  return arr;
};

inorderTraversal(tree); // ['4', '2', '5', '1', '6', '3', '7']

后序遍历JS实现

递归形式

递归是一种比较简单的方法,但占用内存和时间比较大。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = (root) => {
  const arr = [];
  const fun = (node) => {
    if (node) {
      fun(node.left);
      fun(node.right);
      arr.push(node.val);
    }
  };
  fun(root);
  return arr;
};

postorderTraversal(tree); // ['4', '5', '2', '6', '7', '3', '1']

栈的形式

使用栈的形式,极大的提升了时间和内存占用效率。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = (root) => {
  if (!root) return [];
  let arr = [];
  let stack = [root];
  while (stack.length) {
    const node = stack.pop();
    arr.unshift(node.val);
    // 先添加left再添加right,出栈是先right在left,
    // unshift 是在前面添加 会先添加right 再添加 left
    // 最后数组的形式就是 left right root
    node.left && stack.push(node.left);
    node.right && stack.push(node.right);

    // 第1遍遍历,arr入数组1,stack入栈[2,3]
    // 第2遍遍历,arr入数组3,1,stack入栈[2,6,7]
    // 第3遍遍历,arr入数组7,3,1,stack入栈[2,6]
    // 第4遍遍历,arr入数组6,7,3,1,stack入栈[2]
    // 第5遍遍历,arr入数组2,6,7,3,1,stack入栈[4,5]
    // ...
  }
  return arr;
};

postorderTraversal(tree); // ['4', '5', '2', '6', '7', '3', '1']

层次遍历JS实现

递归形式

递归是一种比较简单的方法,但占用内存和时间比较大。

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
  const arr = [];
  const fun = (node, level) => {
    if (node === null) return;

    // 如果当前层次的结果数组不存在,则创建一个新的数组
    if (level >= arr.length) {
      arr.push([]);
    }

    // 将当前节点的值添加到当前层次的结果数组中
    arr[level].push(node.val);

    // 递归地处理左子树和右子树
    fun(node.left, level + 1);
    fun(node.right, level + 1);
  };
  fun(root, 0);
  return arr;
};

levelOrder(tree); // [['1'], ['2', '3'], ['4', '5', '6', '7']]

栈的形式

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
  if (!root) return [];
  let arr = [];
  let stack = [root];
  while (stack.length) {
    let tempArr = [];
    let tempStack = [];
    stack.forEach(item => {
      tempArr.push(item.val);
      item.left && tempStack.push(item.left);
      item.right && tempStack.push(item.right);
    })
    stack = tempStack;
    arr.push(tempArr)
  }

  return arr;
};

levelOrder(tree); // [['1'], ['2', '3'], ['4', '5', '6', '7']]