算法随笔-数据结构(树的基础概念和遍历)
本文主要介绍数据结构中树的基本定义、类型、二叉树介绍、二叉树的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']]