二叉树的递归遍历
递归三要素:
1.确定递归函数的参数和返回值:确定哪些参数是递归过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
2.确定终止条件:写完递归算法,运行时,经常遇到栈溢出的情况,就是没写终止条件或者终止条件写得不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必定溢出。
3.确定单层递归的逻辑:确定每一层递归需要处理的信息,在这里也就会重复调用自己来实现递归的过程。
以前序遍历为例,展示递归三要素:
1.确定递归参数和返回值: 因为要打印前序遍历节点的数值,因此要用容器vector来存放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
void traversal(TreeNode* cur,vector<int>& vec)
2.确定终止条件: 在递归过程中,怎么算递归结束呢?当前遍历节点为空,那么本层递归就结束了,直接return,代码如下:
if(cur==NULL) return;
3.确定单层递归的逻辑: 单层逻辑就是前序遍历的顺序(中左右),所以在单层遍历中,先取中节点的数值,代码如下:
vec.push_back(cur->val);//中
traversal(cur->left,vec);//左
traversal(cur->right,vec);//右
所以,前序遍历的完整代码如下:
class Solution{
public:
void traversal(TreeNode* cur,vector<int>& vec){
if(cur==nullptr) return ;
vec.push_back(cur->val);
traversal(cur->left,vec);
traversal(cur->righ,vec);
}
vector<int> preorderTraversal(TreeNode* root){
vector<int> res;
traversal(root,res);
return res;
}
};
同理,中序遍历代码如下:
void traversal(TreeNode* cur,vector<int>& vec)
{
if(cur==nullptr) return;
traversal(cur->left,vec);//左
vec.push_back(cur->val);//中
traversal(cur->right,vec);//右
}
vector<int> noworderTraversal(TreeNode* root)
{
vector<int> res;
traversal(root,res);
return res;
}
};
后序遍历代码如下:
void traversal(TreeNode* cur,vector<int>& vec){
if(cur==nullptr) return;
traversal(cur->left,vec);//左
traversal(cur->right,vec);//右
vec.push_back(cur->val);//中
}
vector<int> behorderTraversal(TreeNode* root){
vector<int> res;
traversal(root,res);
return res;
}
};
例题144:给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
void traversal(TreeNode* cur, vector<int>& vec)
{
if (cur == nullptr) return;
vec.push_back(cur->val);//中
traversal(cur->left, vec);//左
traversal(cur->right, vec);//右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
traversal(root, res);
return res;
}
二叉树的迭代遍历
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值、返回值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,这就是递归为什么可以返回上一层位置的原因。
java代码:
1.注意一个文件里只能有一个public class,且为文件名,其余类不写public,调用该类下的方法需要定义对象,通过对象调用方法。
2.基础数据类型List<>,类似于c++的vector,要写明其中包含的数据类型,可以是整型Integer,list……等,加入元素调用add()。
3.栈的pop()有返回值,c++中无返回值。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();
if(root==null)
{
return res;
}
stack.push(root);
while(!stack.empty())
{
TreeNode node= stack.pop();
res.add(node.val);
if(node.right!=null)
{
stack.push(node.right);
}
if(node.left!=null)
{
stack.push(node.left);
}
}
return res;
}
}
迭代法中序遍历
迭代中有两个操作:
1.处理:将元素放入res数组;
2.遍历:遍历节点。
刚才的前序遍历不能与中序遍历通用,因为前序遍历的顺序是中左右,先访问的元素的中节点,处理的元素也是中节点,因为访问与处理的元素顺序一致,都是中间节点。
而中序遍历,顺序是左中右,但先访问的是中间节点,然后一层一层向下访问,直到到达树左边的最低层,才开始处理节点(将节点值放入res数组),这就造成了处理顺序和访问顺序不一致的问题。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();//数组基本结构
Stack<TreeNode> stack=new Stack<>();
if(root==null)
{
return res;
}
TreeNode cur=root;
while(cur!=null || !stack.empty())
{
if(cur!=null)
{
stack.push(cur);
cur=cur.left;//左
}
else{
cur=stack.pop();
res.add(cur.val);//中,处理中间节点就是把值存入res
cur=cur.right;//右
}
}
return res;
}
}
迭代法后序遍历
1.java中Collections工具类,包含一些常用的函数,如max、min、sort、reverse、replaceAll等等;
2.迭代法后序遍历:前序遍历结果是中左右、入栈顺序是中右左,因此将入栈顺序改为中左右,出栈顺序为中右左,再将res翻转为左右中就是后序
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root == null) return res;
stack.push(root);
while (!stack.empty()) {
TreeNode cur = stack.pop();
res.add(cur.val);//中
if (cur.left != null) {
stack.push(cur.left);//左
}
if (cur.right != null) {
stack.push(cur.right);//右
}
}
Collections.reverse(res);
return res;
}
}
二叉树的统一迭代法
上部分发现在迭代法中,前序和后序迭代有关联,但中序遍历风格不太统一,一会用栈一会用指针遍历,接下来介绍用迭代法写风格统一的前中后序遍历二叉树代码。
在中序遍历中,用栈无法解决访问节点与处理节点顺序不同的问题。
解决方法:把访问的节点放入栈内,把要处理的节点也放入栈内但要做标记。
如何标记?
将要处理的节点放入栈后,紧接着放入一个空指针作为标记。
统一风格的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
统一风格的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
统一风格的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new LinkedList<>();
Stack<TreeNode> st = new Stack<>();
if (root != null) st.push(root);
while (!st.empty()) {
TreeNode node = st.peek();
if (node != null) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
st.push(node); // 添加中节点
st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.peek(); // 重新取出栈中元素
st.pop();
result.add(node.val); // 加入到结果集
}
}
return result;
}
}
总结:还是递归更快
二叉树的层序遍历
与深度优先遍历不同,需要借助队列来实现,队列先进先出,符合一层一层遍历的逻辑,而栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
类似于多维数组,一行一行去遍历
例题102:给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
class Solution {
public List<List<Integer>> res=new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
checkFun02(root);
return res;
}
//队列迭代方式
public void checkFun02(TreeNode cur)
{
if(cur==null) return;
Queue<TreeNode> que=new LinkedList<TreeNode>();
que.add(cur);//offer()
while(!que.isEmpty())
{
List<Integer> curList=new ArrayList<>();
int len=que.size();
while(len>0)
{
TreeNode tNode=que.poll();
curList.add(tNode.val);
if(tNode.left!=null) que.offer(tNode.left);
if(tNode.right!=null) que.add(tNode.right);
len--;
}
res.add(curList);
}
}
}
1.队列常用函数:add/offer()添加元素(不同在于超出容量后,add会抛出异常,offer会返回false)
remove、poll()弹出元素(容量为0时,remove会抛出异常,poll会返回false)
element/peek()获取队头元素(容量为0时,element抛出异常,peek返回null)
2.队列为空函数:isEmpty();栈为empty()或isEmpty()。
二叉树的层序遍历||
例题107:给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
1.与102类似,不过一个自顶向下,这个是自底向上;
2.java中的翻转字符串reverse()函数,也是在Collections类中调用,返回类型为void;
3.与常规的层序遍历相同步骤,最后将res集合翻转就行,只翻转子集合的顺序,不翻转子集合中的元素位置。
class Solution {
List<List<Integer>> res=new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrderBottom(TreeNode root) {
if(root==null) return res;
checkFun02(root);
Collections.reverse(res);
return res;
}
public void checkFun02(TreeNode cur)
{
Queue<TreeNode> que=new LinkedList<TreeNode>();
que.add(cur);
while(!que.isEmpty())
{
int len=que.size();
List<Integer> curList=new ArrayList<Integer>();
while(len>0)
{
TreeNode tNode=que.poll();
curList.add(tNode.val);
if(tNode.left!=null) que.add(tNode.left);
if(tNode.right!=null) que.add(tNode.right);
len--;
}
res.add(curList);
}
}
}
二叉树的右视图
例题199:给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
class Solution {
List<Integer> res=new ArrayList<>();
public List<Integer> rightSideView(TreeNode root) {
if(root==null) return res;
checkFun02(root);
return res;
}
public void checkFun02(TreeNode cur)
{
Queue<TreeNode> que=new LinkedList<TreeNode>();
que.add(cur);
while(!que.isEmpty()){
List<Integer> curList=new ArrayList<>();
int len=que.size();
while(len>0)
{
TreeNode tNode=que.poll();
curList.add(tNode.val);
if(len==1)
{
res.add(tNode.val);
}
if(tNode.left!=null) que.add(tNode.left);
if(tNode.right!=null) que.add(tNode.right);
len--;
}
}
}
}
与层序遍历相同,只不过每次取每层最后一个节点值加入res
二叉树的层平均值
例题637:给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受,
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res=new ArrayList<>();
Queue<TreeNode> que=new LinkedList<TreeNode>();
que.add(root);
while(!que.isEmpty())
{
int len=que.size();
List<Integer> curList=new ArrayList<>();
double sum=0.0;
int n=len;//取每层的个数
while(len>0){
TreeNode tNode=que.poll();
sum+=tNode.val;
if(tNode.left!=null) que.add(tNode.left);
if(tNode.right!=null) que.add(tNode.right);
len--;
}
res.add(sum/n);//直接加入每层平均值到res
}
return res;
}
}
N叉树的层序遍历
例题429:给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> res=new ArrayList<List<Integer>>();
if(root==null) return res;
Queue<Node> que=new LinkedList<Node>();
que.add(root);
while(!que.isEmpty()){
int len=que.size();
List<Integer> curList=new ArrayList<>();
while(len>0)
{
Node tNode=que.poll();
curList.add(tNode.val);
if(tNode.children!=null)
{
List<Node> tList=new ArrayList<Node>();
tList=tNode.children;
for(int i=0;i<tList.size();i++){
que.add(tList.get(i));//不能像c++一样直接通过下标[i]寻址
}
}
len--;
}
res.add(curList);
}
return res;
}
}
java中遍历集合可以用for循环,但不能像c++一样直接通过[i]下标寻址,而是通过xxx.get(i)来寻址。
在每个行中找最大值
例题515:给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。
class Solution {
public List<Integer> largestValues(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null) return res;
Queue<TreeNode> que=new LinkedList<TreeNode>();
que.add(root);
while(!que.isEmpty()){
List<Integer> curList=new ArrayList<>();
int len=que.size();
int ma=que.peek().val;
while(len>0){
TreeNode curNode=que.poll();
ma= Math.max(ma,curNode.val);//调库函数
if(curNode.left!=null) que.add(curNode.left);
if(curNode.right!=null) que.add(curNode.right);
len--;
}
res.add(ma);
}
return res;
}
}
java中max,min等函数需要调用Math库;
初始化root节点时可以将其孩子节点一并初始化。
填充每个节点的下一个右侧节点指着
例题116:给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
class Solution {
public Node connect(Node root) {
if(root==null) return null;//不能省略
Queue<Node> que=new LinkedList<Node>();
que.add(root);
while(!que.isEmpty()){
int len=que.size();
while(len>0) {
Node curNode = que.poll();
if(len!=1)//只有不是最后一个节点时才指定,否则直接为空
{
curNode.next=que.peek();
}
if(curNode.left!=null) que.add(curNode.left);
if(curNode.right!=null) que.add(curNode.right);
len--;
}
}
return root;
}
}
指定下一个节点,当不是最后一个节点时直接为队列的头节点,否则自动为空。
填充每个节点的下一个右侧节点指针||
例题117:给定一个二叉树:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL 。
初始状态下,所有 next 指针都被设置为 NULL 。
class Solution {
public Node connect(Node root) {
if(root==null) return null;//不能省略
Queue<Node> que=new LinkedList<Node>();
que.add(root);
while(!que.isEmpty()){
int len=que.size();
while(len>0) {
Node curNode = que.poll();
if(len!=1)//只有不是最后一个节点时才指定,否则直接为空
{
curNode.next=que.peek();
}
if(curNode.left!=null) que.add(curNode.left);
if(curNode.right!=null) que.add(curNode.right);
len--;
}
}
return root;
}
}
与上题解法相同,都是广度优先的迭代法,如果要用递归在满二叉树和二叉树中就会有区别
二叉树的最大深度
例题104:给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
class Solution {
public int maxDepth(TreeNode root) {
int dep=0;
if(root==null) return dep;
Queue<TreeNode> que=new LinkedList<TreeNode>();
que.add(root);
while(!que.isEmpty()){
dep++;
int len=que.size();
while(len>0){
TreeNode curNode=que.poll();
if(curNode.left!=null) que.add(curNode.left);
if(curNode.right!=null) que.add(curNode.right);
len--;
}
}
return dep;
}
}
二叉树的最小深度
例题111:给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
class Solution {
public int minDepth(TreeNode root) {
int dep=0;
if(root==null) return dep;
Queue<TreeNode> que=new LinkedList<TreeNode>();
que.add(root);
while(!que.isEmpty()){
int len=que.size();
dep++;
while(len>0){
TreeNode curNode=que.poll();
if(curNode.left==null && curNode.right==null){
return dep;
}
if(curNode.left!=null) que.add(curNode.left);
if(curNode.right!=null) que.add(curNode.right);
len--;
}
}
return dep;
}
}
遇到无孩子的节点,直接返回当前深度