递归
时间:N,访问每个节点
空间:N,最坏情况下树为链表
前序/中序/后序
void order(TreeNode root)
{
if(root == null)
{
return;
}
//前序遍历代码位置
preorder(root.left);
//中序遍历代码位置
preorder(root.right);
//后序遍历代码位置
}
栈
栈的思路就是将递归所使用的虚拟机栈模拟出来
时间:N
空间:N
前序遍历
- 递归思路:先根节点,再左子树,再右子树递归遍历左右子树,即从根开始一直访问左下的左子节点(每个节点运行前序遍历代码),到达左下角后返回,遇上有右子树的进入右子树
- 迭代思路:先访问根节点,再遍历左子树、右子树,因为栈的先进后出原则,遍历左子树之前先将右子树压入栈中
void preorder(TreeNode root)
{
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || !stack.isEmpty())
{
while(root != null)
{
//前序遍历代码位置
stack.push(root.right);
//访问左子树
root = root.left;
}
//访问右子树
root = stack.pop();
}
}
中序遍历
- 递归思路:先左子树,再根节点,再右子树,递归遍历左右子树,即先到达左下角,从左下角开始不断向上(这个过程才运行中序遍历代码),节点有右子树便转入右子树
- 迭代思路:在节点弹出时访问,所以先压入栈
void inorder(TreeNode root)
{
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || !stack.isEmpty())
{
while(root != null)
{
stack.push(root.left);
root = root.left;
}
root = stack.pop();
//中序遍历代码位置
root = root.right;
}
}
后序遍历
第一种思路:前序遍历是中左右,后序遍历是左右中,将前序遍历改为中右左后翻转
void preorder(TreeNode root)
{
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || !stack.isEmpty())
{
while(root != null)
{
//后序遍历代码位置
stack.push(root.left);
root = root.right;
}
root = stack.pop();
}
reverse();
}
第二种思路:使用pre指针来记录已经遍历的树的根节点
void preorder(TreeNode root)
{
Deque<TreeNode> stack = new LinkedList<>();
TreeNode pre = null;
while (root != null || !stack.isEmpty())
{
while (root != null)
{
stack.push(root);
root = root.left;
}
root = stack.pop();
//如果右子树不存在或已经遍历过
if (root.right == null || root.right == pre)
{
//后序遍历代码
pre = root;
root = null;
}
else
{
stack.push(root);
root = root.right;
}
}
}
Morris
Morris遍历使用二叉树节点中大量指向null的指针,由Joseph Morris 于1979年发明。
时间复杂度:O(n)
空间复杂度:1
Morris的整体思路就是将 以某个根结点开始,找到它左子树的最右侧节点之后与这个根结点进行连接,我们可以从 图2 看到,如果这么连接之后,cur 这个指针是可以完整的从一个节点顺着下一个节点遍历,将整棵树遍历完毕,直到 7 这个节点右侧没有指向。
1.新建临时节点,令该节点为 root;
2.如果当前节点的左子节点为空,将当前节点加入答案,并遍历当前节点的右子节点;
3.如果当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点:
- 如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点。然后将当前节点加入答案,并将前驱节点的右子节点更新为当前节点。当前节点更新为当前节点的左子节点。
- 如果前驱节点的右子节点为当前节点,将它的右子节点重新设为空。当前节点更新为当前节点的右子节点。
重复步骤 2 和步骤 3,直到遍历结束。
void order(TreeNode root)
{
TreeNode pre = null;
while(root != null)
{
if(root.left != null){
//寻找左子树最右侧节点
pre = root.left;
//右子树不为空且没有连接过,否则又回到了根节点
while(pre.right != null && pre.right != root)
{
pre = pre.right;
}
//让pre右指针指向root,继续遍历左子树
if(pre.right == null)
{
pre.right = root;
root = root.left;
}
//左子树的最右节点指向了root,说明又回到了root,左子树访问完毕,断开连接
else
{
pre.right = null;
root = root.right;
}
}
//没有左孩子则直接访问右孩子
else
{
root = root.right;
}
}
}
前序遍历
根据前序遍历的顺序,
1.在根节点创建连线的时候打印该根节点
2.打印无法创建连线的节点
void preorder(TreeNode root)
{
TreeNode pre = null;
while(root != null)
{
if(root.left != null)
{
//寻找左子树最右侧节点
pre = root.left;
//右子树不为空且没有连接过,否则又回到了根节点
while(pre.right != null && pre.right != root)
{
pre = pre.right;
}
//让pre右指针指向root,继续遍历左子树
if(pre.right == null)
{
//前序遍历代码位置
pre.right = root;
root = root.left;
continue;
}
//左子树的最右节点指向了root,说明又回到了root,左子树访问完毕,断开连接
else
{
pre.right = null;
}
}
//右子树为空的情况下打印自身
else
{
//前序遍历代码位置
}
//左子树走不通则一直进入右子树
root = root.right;
}
}
中序遍历
从最左侧开始顺着右节点打印
void preorder(TreeNode root)
{
TreeNode pre = null;
while(root != null)
{
if(root.left != null)
{
//寻找左子树最右侧节点
pre = root.left;
//右子树不为空且没有连接过,否则又回到了根节点
while(pre.right != null && pre.right != root)
{
pre = pre.right;
}
//让pre右指针指向root,继续遍历左子树
if(pre.right == null)
{
pre.right = root;
root = root.left;
continue;
}
//左子树的最右节点指向了root,说明又回到了root,左子树访问完毕,断开连接
else
{
pre.right = null;
}
}
//中序遍历代码位置
//左子树走不通则一直进入右子树
root = root.right;
}
}