(一)二叉树的递归遍历
利用系统执行函数调用时的压栈弹栈来实现深度遍历二叉树,二叉树的递归版本较为简单。实现先序、中序、后序只需要在执行子函数的前、中、后打印树的信息即可。代码如下:
先序
function iterator (tree) {
if (!tree) {
return
}
console.log(tree.val)
iterator(tree.left)
iterator(tree.right)
}
复制代码
中序
function iterator (tree) {
if (!tree) {
return
}
iterator(tree.left)
console.log(tree.val)
iterator(tree.right)
}
复制代码
后序
function iterator (tree) {
if (!tree) {
return
}
iterator(tree.left)
iterator(tree.right)
console.log(tree.val)
}
复制代码
总结:时间复杂度:O(n),空间复杂度:O(h),h是树的高度
(二)二叉树的迭代遍历
先序
二叉树的先序遍历( 头 => 左 => 右),用迭代实现。思路:
- 使用一个栈,存入tree节点
- 不断的从栈中弹出一个节点,打印节点(遇到头节点即打印,再操作子节点,即为先序遍历,顺序为: 头 => 左 => 右)
- 依次向栈中添加弹出节点的右节点、左节点(因为是栈,后进先出,所以要先添加右子节点,后添加左子节点。下次循环会先打印左子节点, 后打印右节点)
// 先序遍历 (头 左 右)
function iterator (tree) {
let stack = [tree]
while (stack.length) {
tree = stack.pop()
console.log(tree.val)
tree.right && (stack.push(tree.right))
tree.left && (stack.push(tree.left))
}
}
复制代码
中序
二叉树的后序遍历(左 => 头 => 右),用迭代实现。思路:
- 依次向栈中添加tree的所有左子节点,直到遍历到叶子节点
- 弹出节点并打印
- 指针右移,继续向栈中添加弹出节点的所有左子节点
// 中序遍历 (左 头 右)
function iterator (tree) {
let stack = [tree]
while (stack.length) {
if (tree.left) {
stack.push(tree.left)
tree = tree.left
continue
}
tree = stack.pop()
console.log(tree.val)
if (tree.right) {
tree = tree.right
stack.push(tree)
}
}
}
复制代码
后序
二叉树的后序遍历(左 => 右 => 头),用迭代实现,需要额外一个辅助栈。思路:
- 使用一个栈,存入tree节点
- 不断的从栈中弹出一个节点,添加到辅助栈中(辅助栈中先添加头节点)
- 依次向栈中添加弹出节点的左节点、右节点(栈后进先出,再下两次循环中,辅助栈中会依次添加右节点,左节点)
- 遍历辅助栈(辅助栈中,添加的顺序是 头=> 右 => 左,栈后进先出,即遍历的顺序是左 => 右 => 头)
function iterator (tree) {
let stack = [tree]
let helpStack = []
while (stack.length) {
tree = stack.pop()
helpStack.push(tree)
if (tree.left) {
stack.push(tree.left)
}
if (tree.right) {
stack.push(tree.right)
}
}
while (helpStack.length) {
tree = helpStack.pop()
console.log(tree.val)
}
}
复制代码
总结:先序,中序同递归都是深度遍历,时间复杂度:O(n),空间复杂度:O(h),h是树的高度,后序因为有辅助栈,空间复杂度是O(n)
(三)广度优先遍历二叉树
层序遍历
二叉树的层序遍历,用迭代实现,思路:
- 使用一个队列,存入tree节点
- 不断的从队列中弹出一个节点,并打印
- 依次向队列中添加弹出节点的左节点、右节点
// 层序遍历
function iterator(tree) {
let queue = [tree]
while (queue.length) {
tree = queue.shift()
console.log(tree.val)
tree.left && queue.push(tree.left)
tree.right && queue.push(tree.right)
}
}
复制代码
(四)Morris遍历二叉树
morris 遍历
morris遍历的特点,用树的叶子节点(左右指针都是null)的空闲节点,遍历的过程中,改写叶子的空闲节点来控制遍历的走向,可以做到空间复杂度为O(1),时间复杂度为O(n)。
- 如果tree无左节点,tree向右移动(tree=tree.right)
- 如果tree有左节点,找到tree左子树上最右的节点,记为mostright
- 如果mostright的right指针指向空,让其指向cur,cur向左移动(tree=tree.left)
- 如果mostright的right指针指向cur,让其指向空,cur向右移动(tree=tree.right)
解释一下为什么递归和非递归遍历需要用到栈。递归写法中虽然没有显式用到栈,但是本质上还是用栈实现的。
因为二叉树中只有父节点指向子节点的指针,遍历的过程中,如果想从子节点回到父节点怎么办,这里使用栈这种数据结构,先把父节点入栈,访问子节点,访问完成后,父节点出栈,就相当于从子节点回到了父节点。
Morris遍历也需要遍历整棵二叉树,那么Morris遍历中是如何做到从子节点回到父节点的呢。从Morris遍历的规则可以知道,Morris遍历是通过mostRight的右孩子的指向来从子节点回到父节点的。
function iterator(tree) {
let mostRight = null
while (tree) {
if (tree.left) {
mostRight = tree.left;
while (mostRight.right && mostRight.right !== tree) {
mostRight = mostRight.right;
}
if (!mostRight.right) {
// 修改叶节点的right指针指向自己
mostRight.right = tree
tree = tree.left
continue
}
// 被修改节点的right指针调整为原来的null
mostRight.right = null
}
tree = tree.right
}
}
复制代码
morris遍历过程中,对有左子节点的节点会遍历两次,对于没有左节点的节点只会遍历一次。根据这个特性可以改写先序,中序和后序。
morris先序
先序:对于没有左节点的节点,只会遍历到一次,则遍历到该节点即打印。对于对有左子节点的节点会遍历两次,取第一次遍历时机打印(该节点的左树最右节点为空)。
function iterator(tree) {
let mostRight = null
while (tree) {
if (tree.left) {
mostRight = tree.left;
while (mostRight.right && mostRight.right !== tree) {
mostRight = mostRight.right;
}
if (!mostRight.right) {
console.log(tree.val)
mostRight.right = tree
tree = tree.left
continue
}
mostRight.right = null
} else {
console.log(tree.val)
}
tree = tree.right
}
}
复制代码
morris中序
中序:对于没有左节点的节点,只会遍历到一次,则遍历到该节点即打印。对于对有左子节点的节点会遍历两次,取第二次遍历时机打印(该节点的左树最右节点指向自己)。
// morris 中序遍历
function iterator(tree) {
let mostRight = null
while (tree) {
if (tree.left) {
mostRight = tree.left;
while (mostRight.right && mostRight.right !== tree) {
mostRight = mostRight.right;
}
if (!mostRight.right) {
mostRight.right = tree
tree = tree.left
continue
} else {
console.log(tree.val)
mostRight.right = null
}
} else {
console.log(tree.val)
}
tree = tree.right
}
}
复制代码
morris后序
后序:
- 对于没有左子树的节点,这些节点只会遍历一次,不用关心这些节点。
- 对于含有左子树的节点,这些节点会被遍历两次,在第二次遍历的时候逆序打印左子树的右边界。
- 最后在函数退出之前再单独逆序打印整棵树的右边界。
为了做到不适用额外空间的做法做到逆序打印树的右边界(额外空间的做法:用栈收集右边界,再打印栈), 可以联想到类似链表的翻转,对节点的right指针翻转之后,即可以做到逆序打印,再把指向调整回来即可。如图:
// morris 后序遍历
function iterator(tree) {
// 翻转树(以right指针翻转)
function reverseTree(tree) {
let pre = null
let next = null
let cur = tree
while (cur) {
next = cur.right
cur.right = pre
pre = cur
cur = next
}
return pre
}
function operateTree (tree) {
let cur
// 先翻转右边界
tree = reverseTree(tree)
cur = tree
// 打印翻转后的节点
while (cur) {
console.log(cur.val)
cur = cur.right
}
// 翻转tree,调整为原来的顺序
reverseTree(tree)
}
let cur = tree
let mostRight = null
while (tree) {
if (tree.left) {
mostRight = tree.left;
while (mostRight.right && mostRight.right !== tree) {
mostRight = mostRight.right;
}
if (!mostRight.right) {
mostRight.right = tree
tree = tree.left
continue
} else {
mostRight.right = null
operateTree(tree.left)
}
}
tree = tree.right
}
operateTree(cur)
}
复制代码