前言
树结构是一种常见的数据结构,在计算机科学和算法设计中广泛应用。
树的基本特点
在计算机科学中,树是一种非线性的数据结构,由节点(Node)和边(Edge)组成。它具有以下定义:
- 根节点(Root):树中的顶级节点,没有父节点,是树的起始点。
- 节点(Node):树中的元素,包含存储的数据以及指向其他节点的连接。
- 边(Edge):节点之间的连接线,表示节点之间的关系。
- 父节点(Parent):某个节点连接到它的子节点的节点。
- 子节点(Child):被父节点连接的节点。
- 叶节点(Leaf):没有子节点的节点,也称为终端节点。
- 子树(Subtree):树中某个节点及其所有后代节点所构成的子结构,也是一棵树。
树的特点是有层级关系、分支结构和无环路。每个节点可以有零个或多个子节点,但只能有一个父节点(除了根节点)。同一个节点可以在树中出现多次,即可拥有多个父节点。
二叉树
二叉树是树结构的一种特殊形式,它可以为空,如果不为空,则必须包含一个根节点,以及左子树和右子树,而且左右子树都是二叉树。值得注意的是,二叉树并不要求每个节点的度都为2,即一个节点可以有零个、一个或者两个子节点。
递归遍历
包括前序遍历、中序遍历和后序遍历。
观察上面图片,我们分别用先序遍历,中序遍历,后序遍历求出遍历结果。
首先我们观察可得出,这是一个二叉树。
代码实现:
kotlin
// 二叉树
function TreeNode(val) {
this.val = val;
this.left = null
this.right = null
}
代码中定义了一个表示二叉树节点的构造函数 TreeNode
。每个节点包含一个值 val
,以及左子节点 left
和右子节点 right
。如果某个节点没有左子节点或右子节点,对应的属性值为 null
。
接下来,使用给定的节点结构创建了一棵二叉树,根节点为 A
,其左子节点为 B
,右子节点为 C
。B
的左子节点为 D
,右子节点为 E
,C
的右子节点为 F
。
css
let root = {
val: 'A',
left: {
val: 'B',
left: {
val: 'D',
},
right: {
val: 'E'
}
},
right: {
val: 'C',
left: null,
right: {
val: 'F'
}
}
}
这样就构建了一个简单的二叉树。
前序遍历
前序遍历先访问根节点,然后按照左子树、右子树的顺序递归遍历;
kotlin
function TreeNode(val) {
this.val = val;
this.left = null
this.right = null
}
let root = {
val: 'A',
left: {
val: 'B',
left: {
val: 'D',
},
right: {
val: 'E'
}
},
right: {
val: 'C',
left: null,
right: {
val: 'F'
}
}
}
function preOrder(root) {
if (!root) return []
let res = []
res.push(root.val)
let resL = preOrder(root.left)
let resR = preOrder(root.right)
return res.concat(resL, resR)
}
console.log(preOrder(root));
定义了一个名为 preOrder
的函数,用于实现二叉树的前序遍历算法。
前序遍历的顺序是先访问根节点,然后递归地遍历左子树和右子树。
在 preOrder
函数中,首先判断根节点是否存在,若不存在则返回一个空数组。接着,创建一个用于存储遍历结果的数组 res
,将根节点的值添加到 res
中。
然后,分别递归调用 preOrder
函数遍历左子树和右子树,将两个子树的遍历结果按顺序连接到 res
中。最后,返回遍历结果数组 res
。
在最后一行代码中,调用 preOrder
函数并将根节点 root
作为参数传入,然后将结果打印输出。
中序遍历
先按照左子树的顺序递归遍历,然后访问根节点,最后按照右子树的顺序递归遍历.
核心代码改动如下:
scss
function preOrder(root) {
if (!root) return []
let res = []
let resL = preOrder(root.left)
res.push(root.val)
let resR = preOrder(root.right)
return resL.concat(res, resR)
}
在修改后的代码中,首先判断根节点是否存在,若不存在则返回一个空数组。
接着,创建一个空数组 res
用于存储遍历结果。
然后,递归调用 preOrder
函数遍历左子树,并将结果存储在 resL
中。
接下来,将根节点的值 root.val
添加到 res
中。
最后,递归调用 preOrder
函数遍历右子树,并将结果存储在 resR
中。最后,返回左子树结果 resL
、根节点值 res
和右子树结果 resR
拼接而成的数组。
后序遍历
先按照左子树、右子树的顺序递归遍历,最后访问根节点。
核心代码改动如下:
scss
if (!root) return []
let res = []
let resL = preOrder(root.left)
let resR = preOrder(root.right)
res.push(root.val)
return resL.concat(resR, res)
首先判断根节点是否存在,若不存在则返回一个空数组。
接着,创建一个空数组 res
用于存储遍历结果。然后,递归调用 preOrder
函数遍历左子树,并将结果存储在 resL
中。
接下来,递归调用 preOrder
函数遍历右子树,并将结果存储在 resR
中。
然后,将根节点的值 root.val
添加到 res
中。最后,返回左子树结果 resL
、右子树结果 resR
和根节点值 res
拼接而成的数组。
迭代遍历
最常见的是层次遍历。层次遍历按照从上到下、从左到右的顺序逐层访问树中的节点。具体实现时,可以借助队列数据结构来记录待访问的节点。
scss
var preOrderTraversal = function(root) {
if (!root) return
// 合理安排入栈和出栈的顺序
const res = []
const stack = []
stack.push(root)
while(stack.length > 0) {
const cur = stack.pop()
res.push(cur.val)
if (cur.right) {
stack.push(cur.right)
}
if (cur.left) {
stack.push(cur.left)
}
}
return res
}
首先,判断根节点是否存在,如果不存在则直接返回。然后,创建一个空数组 res
用于存储遍历结果,以及一个栈 stack
。将根节点入栈。
进入循环,当栈不为空时,执行以下操作:
- 弹出栈顶元素,将其值添加到结果数组
res
中。 - 检查当前节点的右子节点是否存在,如果存在,则将右子节点入栈。
- 检查当前节点的左子节点是否存在,如果存在,则将左子节点入栈。
循环结束后,返回遍历结果数组 res
。
这段代码通过使用一个栈来模拟递归的过程,实现了二叉树的前序遍历。首先将根节点入栈,然后在每次循环中弹出栈顶节点并将其值加入结果数组,然后按照根、左、右的顺序将子节点入栈。这样可以保证在遍历时先处理根节点,然后处理左子树,最后处理右子树,符合前序遍历的顺序。
总之,树结构是一种十分重要的数据结构,在计算机科学和算法设计中扮演着重要的角色。掌握树的结构和遍历方式能够帮助我们更好地理解和处理各种复杂的问题。