【每日算法】

算法第14天| (二叉树part01)理论基础、递归遍历、迭代遍历、统一迭代


二叉树理论基础

一、 理论基础

1. 二叉树的种类

满二叉树、完全二叉树、二叉搜索树、平衡二叉树
面试会考察你对所使用容器的理解程度,会问你map的key或者set里的元素是不是有序的,答案是肯定的。因为它的底层实现是平衡二叉搜索树,它是要按元素的值进行排序的,所以map的key或者set里的元素都是有序的。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,但是unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。(所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!)

2. 二叉树的存储方式

链式存储、线性存储(很少用)
链式存储:每个节点有两个指针,一个指向左孩子,一个指向右孩子;
线性存储:找到节点i的左右孩子的下标:左孩子:2i+1; 右孩子:2i+2;
构造二叉树(二叉树就是一个链表):

3. 遍历方式

深度优先搜索:用递归实现,一条路走到黑,然后再回头换方向。前中后序遍历都是深度优先搜索。可以用递归法和迭代法实现这三种遍历。
广度优先搜索:一层一层遍历,层序遍历,层序遍历就是迭代法,是用一个队列实现对二叉树一层一层的搜索。因为队列的先进先出的规则符合一层一层遍历的需求。
递归法和迭代法:编程语言在实现递归的时候,就是用栈来实现。
面试有可能给你一道比较简单的二叉树的题目,让你用非递归的方式(也就是迭代法)来实现。
前中后序遍历指的是父节点。
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中

4. 定义方式

leetcode是核心代码模式,不需要你定义二叉树or处理输入输出。但是你自己要能手写出来一些基本数据结构的定义。(在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。)
二叉树其实就是链表,所以链式存储的二叉树的节点定义:

// 构造一个结构体
struct TreeNode {
    int val;
    // 定义指向左右孩子的指针
    TreeNode *left;
    TreeNode *right;
    // 构造函数,传下来一个值,默认左右节点为空,方便对新节点进行初始化
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

二、递归遍历

144. 二叉树的前序遍历
145. 二叉树的后序遍历
94. 二叉树的中序遍历
代码随想录链接

递归三部曲:
1.确定递归函数的参数和返回值
递归函数的参数大多数是一个根节点和一个数组,返回值一般是void,因为直接把想要的结果放进参数了。
2.确定终止条件
3.确定单层递归的逻辑

以下以前序遍历为例:

  1. 确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下:
void traversal(TreeNode* cur, vector<int>& vec)
  1. 确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
if (cur == NULL) return;
  1. 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
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 == NULL) return;
        vec.push_back(cur->val);    // 中
        // 递归遍历左、右子树
        traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};

中序遍历

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    vec.push_back(cur->val);    // 中
    traversal(cur->right, vec); // 右
}

后序遍历

void traversal(TreeNode* cur, vector<int>& vec) {
    if (cur == NULL) return;
    traversal(cur->left, vec);  // 左
    traversal(cur->right, vec); // 右
    vec.push_back(cur->val);    // 中
}

三、迭代法实现前中后序遍历

  1. 前序遍历
    前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
    在这里插入图片描述
class Solution {
public:
		// 因为要返回前序遍历之后的数组,就是把元素放进数组里,vector是C++里的一种数组,也可以用普通的数组;把二叉树传进来,只需传入根节点即可
    vector<int> preorderTraversal(TreeNode* root) {
        // 定义一个栈st 栈里存放的元素是二叉树中的节点node,node是一个结构体
        stack<TreeNode*> st;
        // 定义数组result存放遍历的结果
        vector<int> result;
        if (root == NULL) return result;
        // 根节点入栈
        st.push(root);
        // 只要栈不为空,循环处理栈
        while (!st.empty()) {
            // 把根节点弹出栈
            // 先获取元素
            TreeNode* node = st.top();   // 中
            // 弹出
            st.pop();
            result.push_back(node->val);
            if (node->right) st.push(node->right); // 先放节点的右孩子(空节点不入栈)
            if (node->left) st.push(node->left); // 再放节点的左孩子(空节点不入栈)
        }
        return result;
    }
};
  1. 后序遍历
    后序遍历是左右中,只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
    在这里插入图片描述代码如下:
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if (root == NULL) return result;
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            result.push_back(node->val);
            if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
            if (node->right) st.push(node->right); // 空节点不入栈
        }
        reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
        return result;
    }
};
  1. 中序遍历:
    左中右,不能直接在前序遍历基础上改;
    因为前序遍历中,遍历的节点和要处理的节点是一个顺序,所以才能写出以上较为简洁的代码。
    虽然后序遍历 遍历节点和处理节点也不同,但是可以通过改变左右节点的入栈顺序,以及反转reverse前序遍历得到的result来获得后序遍历结果。而在中序遍历中,则没有办法以类似的方式获得。【中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的】
    思路:
    因为遍历节点和处理节点不同,所以要定义一个指针来帮助遍历二叉树,如果是要处理的元素,就把它加入到数组里。遍历过程中,要用栈记录遍历过的顺序,因为处理元素的时候其实是按照遍历的顺序逆序输出的。
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        // 指针用于遍历二叉树,初始为根节点
        TreeNode* cur = root;
        // 进入处理二叉树的逻辑
        // while循环终止条件:当我们当前遍历的指针为空,或者 栈为空,遍历终止
        // 中间是 或 的逻辑
        while (cur != NULL || !st.empty()) {
            // 如果当前节点不为空,就把当前访问的元素加入到栈里
            if (cur != NULL) { // 指针来访问节点,访问到最底层
                st.push(cur); // 将访问的节点放进栈
                // 指针继续往左走,一路向左,直到遇到空节点
                cur = cur->left;                // 左
            } else {
                // 因为栈记录的是访问过的节点,那么从栈顶弹出的元素就是最近访问过的元素,将其弹出,即为要处理的第一个元素,处理元素就是把此元素加入到遍历的数组里 result.push_back(cur->val); 
                cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
                st.pop();
                result.push_back(cur->val);     // 中
                // 接着访问当前指针的右孩子,如果右孩子不为空,就将其加入到栈,然后继续遍历它的左孩子,如果左孩子为空,则弹出此元素(栈顶元素),然后继续看它的右孩子,如果右孩子为空,则继续走else的逻辑,继续从栈里弹出元素,加入到数组里。
                cur = cur->right;               // 右
            }
        }
        return result;
    }
};

统一迭代法统一迭代法的写法, 如果学有余力,可以掌握一下

参考链接:
作者:力扣官方题解
链接:
https://leetcode.cn/problems/binary-tree-preorder-traversal/solutions/461821/er-cha-shu-de-qian-xu-bian-li-by-leetcode-solution/
https://leetcode.cn/problems/binary-tree-postorder-traversal/solutions/431066/er-cha-shu-de-hou-xu-bian-li-by-leetcode-solution/
https://leetcode.cn/problems/binary-tree-inorder-traversal/solutions/412886/er-cha-shu-de-zhong-xu-bian-li-by-leetcode-solutio/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值