代码随想录算法训练营Day15 二叉树| 层序遍历 10 226.翻转二叉树 101.对称二叉树 2

代码随想录算法训练营

代码随想录算法训练营Day15 二叉树| 层序遍历 10 226.翻转二叉树 101.对称二叉树 2

层序遍历10

题目链接:层序遍历10
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

总体思路

层序遍历二叉树即图的广度优先遍历,层序遍历二叉树需要借助辅助数据结构队列来实现。队列的先进先出符合层序遍历的实现逻辑。
AltText|600
这样就实现了层序从左到右遍历二叉树。
代码如下:这份代码也可以作为二叉树层序遍历的模板,打十个就靠它了

代码实现

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
		queue<TreeNode*> que;
		if(root!= NULL) que.push(root);
		vector<vector<int>> result;
		while(!que.empty()){
			int size=que.size();
			vector<int> vec;
			//大小一定要使用固定大小的size,不能使用que.size()因为que.size()是不断变化的。
			for(int i=0;i<size;i++){
				TreeNode* node=que.front();
				que.pop();
				vec.push_back(node->val);
				if(node->left) que.push(node->left);
				if(node->right) que.push(node->right);
			}
			result.push_back(vec);
		}
		return result;
	}
}

递归法:

class Solution {
public:
    void order(TreeNode* cur, vector<vector<int>>& result, int depth)
    {
        if (cur == nullptr) return;
        if (result.size() == depth) result.push_back(vector<int>());
        result[depth].push_back(cur->val);
        order(cur->left, result, depth + 1);
        order(cur->right, result, depth + 1);
    }
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        int depth = 0;
        order(root, result, depth);
        return result;
    }
}; 

226.翻转二叉树

题目链接:226.翻转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

总体思路

表面上是对整个树进行翻转,实际上是对其左右孩子进行交换即可。
AltText|500
本题的关键就在于前中后序选择哪种遍历。
只要把每个节点的左右孩子进行翻转,即可达到整体翻转的效果
本题前序和后序遍历都可以,唯独中序遍历不方便,因为会把某些节点的左右孩子翻转两次。
层序遍历一样可以完成。

递归法:

递归三部曲:

  1. 确定递归函数的参数和返回值
    参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归逻辑时发下需要其他的参数时,随时补充。
    返回值的话也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就函数的返回类型为TreeNode*
TreeNode* invertTree(TreeNode* root)
  1. 确定终止条件
    当前节点为空时,就返回。
if(root=NULL) return root;
  1. 确定单层递归的逻辑
    因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。
swap(root->left,root->right);
invertTree(root->left);
invertTree(root->right)

代码实现

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if (root == NULL) return root;
        swap(root->left, root->right);  // 中
        invertTree(root->left);         // 左
        invertTree(root->right);        // 右
        return root;
    }
};

迭代法

深度优先遍历 代码实现

前序遍历:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
	    if(root==NULL) return root;
	    stack<TreeNode*> st;
	    st.push(root);
	    while(!st.empty()){
		    TreeNode* node=st.top();
		    st.pop();
		    swap(node->left,node->right);
		    if(node->right) st.push(node->right);
		    if(node->left) st.push(node->left);
	    }
	    return root;
    }
}    

C++代码如下迭代法(前序遍历)

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
	    stack<TreeNode*> st;
	    if(root!=NULL) st.push(root);
	    while(!st.empty()){
		    TreeNode* node=st.top();
		    if(node!=NULL){
			    st.pop();
			    if(node->right) st.push(node->right);
			    if(node->left) st.push(node->lift);
			    st.push(node);
			    st.push(NULL);
		    }else{
			    st.pop();
			    node=st.top();
			    st.top();
			    swap(node->lift,node->right);
		    }
	    }
	    return root;
    }
}

广度优先遍历

也就是层序遍历,层序遍历也是可以翻转的。

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        while (!que.empty()) {
            int size = que.size();
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                swap(node->left, node->right); // 节点处理
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return root;
    }
};

101.对称二叉树

题目链接:101.对称二叉树
给定一个二叉树,检查它是否是镜像对称的。

总体思路

首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
那么如何比较呢?
AltText|500
遍历顺序:本题只能是后序遍历,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
但都可以理解算是后序遍历,尽管已经不是严格在一个树上进行后序遍历了。
后序便利也可以理解为一种回溯。

递归法

递归三部曲

  1. 确定递归函数的参数和返回值
    因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
    返回值自然是bool类型。
    代码如下:
bool compare(TreeNode* left, TreeNode* right)
  1. 确定终止条件
    要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
    节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点
  • 左节点为空,右节点不为空,不对称,return false
  • 左不为空,右为空,不对称 return false
  • 左右都为空,对称,返回true
    此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
  • 左右都不为空,比较节点数值,不相同就return false
    此时左右节点不为空,且数值也不相同的情况我们也处理了。
    代码如下:
if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
else if (left->val != right->val) return false; // 注意这里我没有使用else

注意上面最后一种情况,我没有使用else,而是else if, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。
3. 确定单层递归的逻辑
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。

  • 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
  • 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
  • 如果左右都对称就返回true ,有一侧不对称就返回false 。
    代码如下:
bool outside = compare(left->left, right->right);   // 左子树:左、 右子树:右
bool inside = compare(left->right, right->left);    // 左子树:右、 右子树:左
bool isSame = outside && inside;                    // 左子树:中、 右子树:中(逻辑处理)
return isSame;

如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。

class Solution {
public:
    bool compare(TreeNode* left, TreeNode* right) {
        // 首先排除空节点的情况
        if (left == NULL && right != NULL) return false;
        else if (left != NULL && right == NULL) return false;
        else if (left == NULL && right == NULL) return true;
        // 排除了空节点,再排除数值不相同的情况
        else if (left->val != right->val) return false;

        // 此时就是:左右节点都不为空,且数值相同的情况
        // 此时才做递归,做下一层的判断
        bool outside = compare(left->left, right->right);   // 左子树:左、 右子树:右
        bool inside = compare(left->right, right->left);    // 左子树:右、 右子树:左
        bool isSame = outside && inside;                    // 左子树:中、 右子树:中 (逻辑处理)
        return isSame;

    }
    bool isSymmetric(TreeNode* root) {
        if (root == NULL) return true;
        return compare(root->left, root->right);
    }
};

代码整理如下

class Solution {
public:
    bool compare(TreeNode* left, TreeNode* right) {
        if (left == NULL && right != NULL) return false;
        else if (left != NULL && right == NULL) return false;
        else if (left == NULL && right == NULL) return true;
        else if (left->val != right->val) return false;
        else return compare(left->left, right->right) && compare(left->right, right->left);

    }
    bool isSymmetric(TreeNode* root) {
        if (root == NULL) return true;
        return compare(root->left, root->right);
    }
};

这个代码就很简洁了,但隐藏了很多逻辑,条理不清晰,而且递归三部曲,在这里完全体现不出来。

迭代法

这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。
这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(注意这不是层序遍历
通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等。
代码实现

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if (root == NULL) return true;
        queue<TreeNode*> que;
        que.push(root->left);   // 将左子树头结点加入队列
        que.push(root->right);  // 将右子树头结点加入队列
        
        while (!que.empty()) {  // 接下来就要判断这两个树是否相互翻转
            TreeNode* leftNode = que.front(); que.pop();
            TreeNode* rightNode = que.front(); que.pop();
            if (!leftNode && !rightNode) {  // 左节点为空、右节点为空,此时说明是对称的
                continue;
            }

            // 左右一个节点不为空,或者都不为空但数值不相同,返回false
            if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) {
                return false;
            }
            que.push(leftNode->left);   // 加入左节点左孩子
            que.push(rightNode->right); // 加入右节点右孩子
            que.push(leftNode->right);  // 加入左节点右孩子
            que.push(rightNode->left);  // 加入右节点左孩子
        }
        return true;
    }
};

补充:C++vector容器

基本介绍

vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。
vector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)
vector 容器以类模板 vector<T>( T 表示存储元素的类型)的形式定义在 <vector> 头文件中,并位于 std 命名空间中。因此,在创建该容器之前,代码中需包含如下内容:

#include <vector>
using namespace std;

创建vector容器

  1. 如下代码展示了如何创建存储 double 类型元素的一个 vector 容器:
std::vector<double> values

如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。
注意,这是一个空的 vector 容器,因为容器中没有元素,所以没有为其分配空间。当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存。
在创建好空容器的基础上,还可以像下面这样通过调用 reserve() 成员函数来增加容器的容量:

values.reserve(20);

这样就设置了容器的内存分配,即至少可以容纳 20 个元素。注意,如果 vector 的容量在执行此语句之前,已经大于或等于 20 个元素,那么这条语句什么也不做;另外,调用 reserve() 不会影响已存储的元素,也不会生成任何元素,即 values 容器内此时仍然没有任何元素。

还需注意的是,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector<T> 容器的元素可能已经被复制或移到了新的内存地址。所以后续再使用这些迭代器时,最好重新生成一下。

  1. 除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数,比如:
std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};

这样就创建了一个含有8个素数的vector容器。
3) 在创建 vector 容器时,也可以指定元素个数:

1.  std::vector<double> values(20);

注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。

std::vector<double> values(20,1.0)

表示定义容器内20个元素的初始值,都是1.0
值得一提的是,圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:

1.  int num=20;
2.  double value =1.0;
3.  std::vector<double> values(num, value);

4)通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:

1.  std::vector<char>value1(5, 'c');
2.  std::vector<char>value2(value1);

也可以用指针指定范围

1.  int array[]={1,2,3};
2.  std::vector<int>values(array, array+2);//values 将保存{1,2}
3.  std::vector<int>value1{1,2,3,4,5};
4.  std::vector<int>value2(std::begin(value1),std::begin(value1)+3);//value2保存{1,2,3}

表 1 vector 容器的成员函数

函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回实际元素个数。
max_size() 返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
resize() 改变实际元素的个数。
capacity() 返回当前容量。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
reserve() 增加容器的容量。
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。
operator[\ ] 重载了\ [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。
at() 使用经过边界检查的索引访问元素。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
data() 返回指向容器中第一个元素的指针。
assign() 用新元素替换原有内容。
push_back() 在序列的尾部添加一个元素。
pop_back() 移出序列尾部的元素。
insert() 在指定的位置插入一个或多个元素。
erase() 移出一个元素或一段元素。
clear() 移出所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
emplace() 在指定的位置直接生成一个元素。
emplace_back() 在序列尾部生成一个元素。
posted @ 2023-02-15 15:21  百里长川  阅读(21)  评论(0编辑  收藏  举报