Day12 二叉树:理论基础 递归遍历 迭代遍历 统一迭代

105 阅读9分钟

理论基础

满二叉树

12.01.png

这棵二叉树为满二叉树,也可以说深度为k,有2k1个节点的二叉树。这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。

完全二叉树

12.02.png

满二叉树一定是完全二叉树

完全二叉树在平时做题时会经常应用。

C++里面有一个优先级队列(priority_queue),就是一个容器,将元素放进这个容器,它会自动帮你排序,你就可以从容器中取出排好序的元素。

这个优先级队列本质是一个大顶堆,或是一个小顶堆来实现。

而堆就是一个完全二叉树,同时它还保证父节点和子节点的顺序关系。

二叉搜索树

(也叫二叉排序树)

前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树

节点便于搜索,它一定是有顺序的(左小右大)

搜索一个节点的时间复杂度O(logn)

12.03.png

要搜索元素9,直接就可以找到到达9的路径,O(logn)

平衡二叉搜索树

左子树和右子树高度的绝对值(高度差)不能超过1

12.04.png

平衡二叉搜索树在数据结构中有很广泛的应用,

C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是O(logn),查询某个元素也是O(logn)。注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。

所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!

二叉树的存储方式

链式存储

顺序存储

⚠️若下标是从0开始的,那么只适用于完全二叉树

平时做题很少用顺序存储的方式来保存二叉树,了解即可。

在LeetCode上,对节点,以及要输入的数据,都封装好了,你直接用就行。

但面试的时候,可能就会要求你自己写你传入的一些数据,假如让你传入一个二叉树,那你就该懵逼了。

内心OS:让我传入二叉树,我该怎么构造二叉树?

你让我传入一个数组,我可以构造一个数组;让我传入一个链表,我可以构造一个链表; 传入二叉树?真不会

二叉树一般都是用链式存储,那么你可以将二叉树理解为是一种链表,一个节点中有2个指针,一个指向左孩子,一个指向右孩子。(这不是双向链表!而是应该叫单向双链表)

二叉树的遍历方式

二叉树主要有两种遍历方式:

  • 深度优先遍历:先往深走,遇到叶子节点再往回走。
  • 广度优先遍历:一层一层的去遍历。

这两种遍历是图论中最基本的两种遍历方式,后面在介绍图论的时候 还会介绍到。

那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:

  • 深度优先遍历

    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历

    • 层次遍历(迭代法)

前、中、后指的是根节点访问的时机

 struct TreeNode {
     int val;
     TreeNode *left;
     TreeNode *right;
     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 };

C语言中结构体中不能定义函数,C++中才行

递归遍历

掌握其规律后,很简单,🦄必须掌握!

很多同学在写递归,没有养成一个良好的思考方式。

3道题都是递归遍历里面非常非常基础的题目,

当我们在做二叉树,特别是递归遍历的时候,要按照以下3步来思考:(递归三部曲)

1️⃣ 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

要确定递归函数的参数,没有必要一次性就确定。可以在写递归函数的过程中,我们需要什么参数,再填入什么参数就行。

大多数二叉树的题目,这些递归函数的参数并不多,基本上就是传入一个根节点;再传入一个数组,用来存放遍历的结果。

(返回值一般是void,因为我们把想要的结果直接放在参数里了)

 //传入一个根节点,用current来存
 //前序遍历的元素直接放在数组里,用vector作为容器、
 void traveral(cur, vec)
 {
     
 }

2️⃣ 确定终止条件: 写完了递归算法,运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

前序遍历二叉树,把所有元素都放到数组里。

递归遍历是DFS的一个过程(可一条路走到黑),那么一定是遇到空节点的时候,才会停止往下搜;返回,往上走。

 void traveral(cur, vec)
 {
     if (cur == NULL) {
         return;
     }
 }

3️⃣ 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

前序遍历是:中左右 我们要处理的节点就是中间节点

数组要存放遍历过的元素,

 void traveral(cur, vec)
 {
     if (cur == NULL) {
         return;
     }
     vec.push(cur->val);  //中
     traveral(cur->left, vec);  //左
     traveral(cur->right, vec);  //右
 }

void 写return是让程序跳出这个函数体

144.二叉树的前序遍历

AC代码: (核心代码模式)

 /**
  * Definition for a binary tree node.
  * struct TreeNode {
  *     int val;
  *     TreeNode *left;
  *     TreeNode *right;
  *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
  *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  * };
  */
 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->right, vec);  //右
     }
 ​
     vector<int> preorderTraversal(TreeNode* root) {
         vector<int> result;
         traversal(root, result);
         return result;
     }
 };

94.二叉树的中序遍历

AC代码: (核心代码模式)

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

145.二叉树的后序遍历

AC代码: (核心代码模式)

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

迭代遍历

编程语言实现递归的逻辑,也是用栈这种数据结构来实现递归的。

因此,我们用迭代法来模拟递归的时候也是用

前序遍历用栈,层序遍历用队列,

这是因为模拟递归必须用栈的特性,队列实现不了。

144.二叉树的前序遍历(迭代法)

12.05.gif

代码思路:

vector在C++里面就是一种数组

 vector<int> function(root) {  //要把二叉树传进来,就把它的根节点传进来就可以了
     //定义一个栈,里面放的元素是node (即二叉树的节点)
     stack<node> st;  //这个节点通常是一个结构体,里面有value (存放元素),有2个指针(指向左孩子、右孩子)
     vector<int> vec;  //数组存放遍历的结果
     st.push(root);  //根节点入栈
     //开始循环处理这个栈
     while (!st.empty()) {  //只要栈不为空,就一直执行下面的这段逻辑
         node = st.top();  //获取5这个元素
         st.pop();  //栈做对应的弹出
         
         if (node != null) {  //node不为空的话
             vec.push_back(node->val);  //把元素放进数组里      中
         }
         else {  //为空
             continue;  //进入下一次循环
         }
         st.push_back(node->right);  //右
         st.push_back(node->left);  //左
     }
     return vec;
 }

AC代码: (核心代码模式)

 /**
  * Definition for a binary tree node.
  * struct TreeNode {
  *     int val;
  *     TreeNode *left;
  *     TreeNode *right;
  *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
  *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  * };
  */
 class Solution {
 public:
     vector<int> preorderTraversal(TreeNode* root) {
         stack<TreeNode*> st;
         vector<int> result;  //数组存放遍历的结果
         if (root == nullptr) 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;
     }
 };

94.二叉树的中序遍历(迭代法)

为了解释清楚,我说明一下 刚刚在迭代的过程中(处理二叉树的时候),其实我们有两个操作:

  • 访问:遍历节点(在二叉树中,根据根节点,一个节点一个节点去访问)
  • 处理:将节点元素放进result数组中 (要输出的顺序)

给我们一个二叉树,我们先访问的一定是根节点(5)

但先处理的可不是5,(中序遍历按照左中右

造成了访问的顺序和我们要处理的顺序是不一致的!

12.06.gif

因此,需要一个指针,帮我们遍历二叉树,处理的时候就将元素加入到数组里。

就是用指针来遍历节点,用栈来记录我们遍历过的节点,然后再从栈里弹出元素。

代码思路:

LeetCode上要求返回一个数组,这个数组遍历我们记录的顺序

 vector<int> traversal(root) {  //传入根节点
     vector<int> result;  //定义一个数组
     stack<node> st;  //定义一个栈,栈里的元素是node (类型是结构体)
     node* cur = root;  //定义一个指针,用来遍历二叉树
     
     //进入处理二叉树的逻辑里
     while (cur!= NULL || st.empty()) {  //终止条件:当遍历的指针为空,并且栈也为空的时候
         if(cur != NULL) {  //如果当前的指针不为空
             st.push(cur);  //就把当前访问的元素加入到栈里  (栈里记录指针访问过的元素)
             cur = cur->left;  //指针继续往左走
         }
         else {
             cur = st.top();
             st.pop();  //将 1 弹出
             result.push_back(cur->val);  //1 弹出之后,就把当前的节点的元素放进result
             cur = cur->right;  //指针往右走(遍历当前指针的右孩子)
         }
     }
     return result;
 }

思路会复杂一些,好好理解。

网友:记住一个重点就是遍历节点和处理节点不一致,我们要想办法变成一致

AC代码: (核心代码模式)

 class Solution {
 public:
     vector<int> inorderTraversal(TreeNode* root) {
         vector<int> result;
         stack<TreeNode*> st;
         TreeNode* cur = root;
         while (cur != NULL || !st.empty()) {
             if (cur != NULL) { // 指针来访问节点,访问到最底层
                 st.push(cur); // 将访问的节点放进栈
                 cur = cur->left;                // 左
             } else {
                 cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
                 st.pop();
                 result.push_back(cur->val);     // 中
                 cur = cur->right;               // 右
             }
         }
         return result;
     }
 };

145.二叉树的后序遍历(迭代法)

AC代码: (核心代码模式)

 /**
  * Definition for a binary tree node.
  * struct TreeNode {
  *     int val;
  *     TreeNode *left;
  *     TreeNode *right;
  *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
  *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  * };
  */
 class Solution {
 public:
     vector<int> postorderTraversal(TreeNode* root) {
         stack<TreeNode*> st;
         vector<int> result;  //数组存放遍历的结果
         if (root == nullptr) 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;
     }
 };

统一迭代

迭代法中序遍历

迭代法前序遍历

迭代法后序遍历