数据结构以学带练day14——二叉树基础、递归遍历(⭐)、迭代遍历、统一迭代(放弃)

77 阅读10分钟

二叉树理论基础

二叉树的分类

满二叉树

如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。下图为一颗深度为kk,有2k12^k-1个节点的满二叉树。

image.png

完全二叉树

除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置(是连续的)。若最底层为第h h 层,则该层包含 1或 2(h1)1 或 2^{(h-1)}  个节点。

image.png

图三中断了,则不是完全二叉树。

  • 优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。

二叉搜索树(二叉排序树)

二叉搜索树是有数值的,二叉搜索树是一个有序树。其搜索一个节点的时间复杂度为lognlogn

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树,即按照上述规则排序好的树。

image.png

平衡二叉搜索树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

image.png

图三中左子树高度为2,右子树高度为0,高度差超过了1,则不是平衡二叉树。

  • C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn。
  • 注意:unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。

二叉树的存储方式

二叉树可以链式存储,也可以顺序存储。 链式存储方式就用指针, 顺序存储的方式就是用数组。则顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起

链式存储如图:

image.png

顺序存储的方式如图:

image.png

如果父节点的数组下标是 ii,那么它的左孩子就是 i2+1i * 2 + 1,右孩子就是 i2+2i * 2 + 2

二叉树的遍历方式

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

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

这两种遍历是图论中最基本的两种遍历方式

深度优先遍历

  • 前序遍历(递归法,迭代法)
  • 中序遍历(递归法,迭代法)
  • 后序遍历(递归法,迭代法)

这里前中后,其实指的就是中间节点的遍历顺序,前中后序指的就是中间节点的位置就可以。

⭐例: 看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

image.png

广度优先遍历

  • 层次遍历(迭代法)

深度优先和广度优先遍历实现方式

  • 深度优先遍历实现方式:使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。
  • 广度优先遍历的实现:一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。

二叉树在代码中的定义

顺序存储使用数组实现不做说明。重点看链式存储的二叉树节点的定义方式:

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

二叉树的定义和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子。

在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。

⭐二叉树的递归遍历

⭐每次写递归,都按照这三要素来写:

  1. 确定递归函数的参数和返回值:  确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么,进而确定递归函数的返回类型。
  2. 确定终止条件:  写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑:  确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

144.二叉树的前序遍历

image.png

  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;
    }
};
c++ 函数vector传参
  • void func1(vector vet); //传送数值:调用拷贝构造函数,形参改变不会影响到实参
  • void func2(vector &vet); //引用:不调用拷贝构函数,形参改变影响到实参
  • void func3(vector *vet); //指针:不调用拷贝构函数,形参改变影响到实参

145.二叉树的后序遍历

class Solution {
public:
    void traver(TreeNode* cur,vector<int>* res){
        if(cur == nullptr) return;

        traver(cur->left,res);//左
        traver(cur->right,res);//右
        res->push_back(cur->val);//中
    }
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> re;
        traver(root,&re);
        return re;
    }
};

注意,为了区别前序遍历的vector传参方式,vector传参我用了指针,主函数里面传re应该传它的地址。

94.二叉树的中序遍历

class Solution {
public:
    void traver(TreeNode* cur,vector<int>* res){
        if(cur == nullptr) return;

        traver(cur->left,res);//左
        res->push_back(cur->val);//中
        traver(cur->right,res);//右
        
    }
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> re;
        traver(root,&re);
        return re;
    }
};

注意,为了区别前序遍历的vector传参方式,vector传参我用了指针,主函数里面传re应该传它的地址。

二叉树的迭代遍历

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。因此,用栈也是可以实现二叉树的前后中序遍历。

144.二叉树的前序遍历

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。 为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。

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

class Solution {
public:
    //前序:中左右
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;//定义一个栈
        vector<int> res;
        //如果树是空的
        if(root==nullptr){return res;}
        //存入根节点
        st.push(root);
        //如果栈不是空的
        while(st.empty()==false){
            //弹出中节点
            TreeNode* node = st.top();
            st.pop();
            //将中节点的值存入res数组
            res.push_back(node->val);
            //根据栈的特性,要按顺序弹出左右节点,必须先存右节点,再存左节点
            if(node->right) st.push(node->right);
            if(node->left)  st.push(node->left);
        }
        return res;
    }
};

145.二叉树的后序遍历

先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:

image.png

class Solution {
public:
    //后序遍历:左右中
    //中左右->中右左->reverse->左中右
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> res;
        if(root==nullptr) return res;
        st.push(root);
        while(st.empty()==false){
            TreeNode* node = st.top();
            st.pop();
            res.push_back(node->val);
            //右左,栈应当先进左
            if(node->left) st.push(node->left);
            if(node->right) st.push(node->right);
        }
        reverse(res.begin(),res.end());
        return res;
    }
};

94.二叉树的中序遍历

思路:

  • 利用指针先遍历左边,并且一边遍历一边将数值存入栈中,当左边指向空,则指针回到上一个位置i(如图中1节点处,也就是栈中top()的节点的位置),并从栈中将栈顶节点的值放入结果数组res中,同时指针遍历该位置i的右子树。
  • 若位置i的右子树也为空,则回到上上一次的位置(如图中4节点处),并从栈中将栈顶节点的值放入结果数组res中,同时指针遍历该位置的右子树。如此循环,直到st内为空或cur为空。

0b3083dcb1f6acf53f93b54f34e658d.jpg

class Solution {
public:
    //中序遍历:左中右
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        vector<int> res;
        while(cur!=nullptr || st.empty()==false){
            //持续遍历左子树
            if(cur!=nullptr){
                st.push(cur);
                cur = cur->left;
            }
            //左子树遍历完了,回退并取这个节点存入res,最后pop这个节点
            //之后要遍历回退后的节点的右边
            else{
                //回退
                cur = st.top();
                //取这个节点存入res
                res.push_back(cur->val);
                //pop
                st.pop();
                //遍历回退后的节点的右边:
                //1、若不为空,则进入上面的if条件
                //2、若为空,则又进入这个else
                cur = cur->right;
            }
        }
        return res;
    }
};

二叉树的统一迭代法(很抽象,难以理解,放弃)

使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。  这种方法也可以叫做标记法。

94.二叉树的中序遍历

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        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);  // 添加右节点(空节点不入栈)

                st.push(node);                          // 添加中节点
                st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。

                if (node->left) st.push(node->left);    // 添加左节点(空节点不入栈)
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.top();    // 重新取出栈中元素
                st.pop();
                result.push_back(node->val); // 加入到结果集
            }
        }
        return result;
    }
};

144.二叉树的前序遍历

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        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->left);    // 左
                st.push(node);                          // 中
                st.push(NULL);
            } else {
                st.pop();
                node = st.top();
                st.pop();
                result.push_back(node->val);
            }
        }
        return result;
    }
};

145.二叉树的后序遍历

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != NULL) st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != NULL) {
                st.pop();
                st.push(node);                          // 中
                st.push(NULL);

                if (node->right) st.push(node->right);  // 右
                if (node->left) st.push(node->left);    // 左

            } else {
                st.pop();
                node = st.top();
                st.pop();
                result.push_back(node->val);
            }
        }
        return result;
    }
};