树数据结构
树是一种分层数据的抽象模型。在现实中,家谱或者公司的组织结构与树的数据结构非常相似。
树的相关术语
一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了根节点)以及零个或多个节点。
位于树顶部的节点叫作根节点。它没有父节点。树中的每个元素都叫作节点,节点分为内部节点和外部节点。至少有一个子节点的节点被称为内部节点。没有子元素的节点称为外部节点或者叶子节点。
子树由节点和它的后代构成。
节点的深度取决于它的祖先节点的数量。
二叉树
二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
二叉搜索树是二叉树的一种,但是它只允许在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大的值。
二叉树的遍历
二叉树遍历分为深度优先遍历和广度优先遍历。其中深度优先遍历又可以分为中序、先序、后序遍历。
深度优先遍历
中序遍历
中序遍历是指先访问左节点,然后访问根节点,最后访问右节点。
递归算法:
- 若二叉树为空,则算法结束;否则:
- 中序遍历根结点的左子树;
- 访问根结点;
- 中序遍历根结点的右子树;
实现:
_inOrderTraverseNode(node, cb) {
if (node !== null) {
this._inOrderTraverseNode(node.left, cb)
cb(node.key)
this._inOrderTraverseNode(node.right, cb)
}
}
非递归算法:
- 初始化一个栈,将根节点压入栈中,并标记为当前节点(node);
- 当栈为非空时,执行步骤 3,否则执行结束;
- 如果当前节点(node)有左子树且没有被 touched,则执行步骤 4,否则执行不步骤 5;
- 对当前节点(node)标记 touched,将当前节点的左子树赋值给当前节点(node=node.left) 并将当前节点(node)压入栈中,回到步骤 3;
- 清理当前节点(node)的 touched 标记,取出栈中的一个节点标记为当前节点(node),并访问,若当前节点(node)的右子树为非空,则将该结点的右子树入栈,回到步骤 3;
inOrderTraverseNoRecursion(cb, node) {
node = node || this.root
let stack = new Stack()
stack.push(node)
while (!stack.isEmpty()) {
// 左节点先入栈
if (node.left && !node.touched) {
node.touched = true
node = node.left
stack.push(node)
continue
}
node.touched && delete node.touched
node = stack.pop()
cb && cb(node.key)
node.right && stack.push(node.right)
}
}
先序遍历
递归算法:
- 若二叉树为空,则算法结束,否则:
- 访问根结点;
- 前序遍历根结点的左子树;
- 前序遍历根结点的右子树。
_preOrderTraverseNode(node, cb) {
if (node !== null) {
cb(node.key)
this._preOrderTraverseNode(node.left, cb)
this._preOrderTraverseNode(node.right, cb)
}
}
非递归算法:
- 初始化一个栈,将根节点压入栈中;
- 当栈为非空时,循环执行步骤 3 到 4,否则执行结束;
- 出队列取得一个结点,访问该结点;
- 若该结点的右子树为非空,则将该结点的右子树入栈,若该结点的左子树为非空,则将该结点的左子树入栈;
preOrderTraverseNoRecursion(cb, node) {
node = node || this.root
let stack = new Stack()
stack.push(node)
while (!stack.isEmpty()) {
node = stack.pop()
cb && cb(node.key)
// 右节点先入栈
node.right && stack.push(node.right)
node.left && stack.push(node.left)
}
}
后序遍历
递归算法:
- 若二叉树为空,则算法结束,否则:
- 后序遍历根结点的左子树;
- 后序遍历根结点的右子树;
- 访问根结点。
_postOrderTraverseNode(node, cb) {
if (node !== null) {
this._postOrderTraverseNode(node.left, cb)
this._postOrderTraverseNode(node.right, cb)
cb(node.key)
}
}
非递归算法:
- 初始化一个栈,将根节点压入栈中,并标记为当前节点(item);
- 当栈为非空时,执行步骤 3,否则执行结束;
- 如果当前节点(item)有左子树且没有被 touched,则执行 4,如果被 touched left 但没有被 touched right 则执行 5 否则执行 6;
- 对当前节点(item)标记 touched left,将当前节点的左子树赋值给当前节点(item=item.left) 并将当前节点(item)压入栈中,回到 3;
- 对当前节点(item)标记 touched right,将当前节点的右子树赋值给当前节点(item=item.right) 并将当前节点(item)压入栈中,回到 3;
- 清理当前节点(item)的 touched 标记,弹出栈中的一个节点并访问,然后再将栈顶节点标记为当前节点(item),回到 3;
postOrderTraverseNoRecursion(cb, node) {
node = node || this.root
let stack = new Stack()
stack.push(node)
while (!stack.isEmpty()) {
// 左节点先入栈
if (node.left && !node.touched) {
node.touched = 'left'
node = node.left
stack.push(node)
continue
}
// 有右节点并且未访问则入栈
if (node.right && node.touched !== 'right') {
node.touched = 'right'
node = node.right
stack.push(node)
continue
}
let item = stack.pop()
item.touched && delete item.touched
cb && cb(item.key)
// 指向栈顶的节点
node = stack.peek()
}
}
广度优先遍历
度优先遍历二叉树(层序遍历)是用队列来实现的,从二叉树的第一层(根结点)开始,自上至下逐层遍历;在同一层中,按照从左到右的顺序对结点逐一访问。
按照从根结点至叶结点、从左子树至右子树的次序访问二叉树的结点。步骤:
- 初始化一个队列,并把根结点入列队;
- 当队列为非空时,循环执行步骤 3 到 4,否则执行结束;
- 出队列取得一个结点,访问该结点;
- 若该结点的左子树为非空,则将该结点的左子树入队列,若该结点的右子树为非空,则将该结点的右子树入队列;
递归实现:
// 广度优先遍历(递归)
breadthFirstSearch(cb, node) {
node = node || this.root
let queue = new Queue()
queue.enqueue(node)
bfs(cb)
function bfs(cb) {
if (queue.isEmpty()) return
let item = queue.dequeue()
cb && cb(item.key)
item.left && queue.enqueue(item.left)
item.right && queue.enqueue(item.right)
bfs(cb)
}
}
非递归实现:
breadthFirstSearchNoRecursion(cb, node) {
node = node || this.root
let queue = new Queue()
queue.enqueue(node)
let pointer = 0
while (pointer < queue.size()) {
let item = queue.list[pointer++]
cb && cb(item.key)
item.left && queue.enqueue(item.left)
item.right && queue.enqueue(item.right)
}
}
完整实现
完整代码可查看github