Day14 二叉树 基础

150 阅读6分钟

1. 理论基础

最好熟悉自己使用语言的常见容器底层如何实现,最基本的map set,这样性能分析才方便

1.1 二叉树种类

一般做题过程中常见:满二叉树完全二叉树

  • 满二叉树:一棵树只有度为0的结点和度为2的结点,且度为0的结点在同一层

    • 结点个数,2^k - 1,k为深度
  • 完全二叉树:除最底层结点可能没填满外,其余每层结点数都达到最大,且最下面一层的节点都集中在该层最左边的若干位置

    • 若最底层为第h层,则该层包含1~2^(h-1)个节点
    • 之前的优先队列其实就是一个堆,堆就是一颗完全二叉树,同时保证父子节点的顺序关系

1.2 带数值的二叉树

  • 二叉搜索树

    • 是一个有序数,左子树不为空,则左子树上的所有节点值均小于他的根节点的值
    • 若右子树不为空,则右子树上的所有节点值均大于他的根节点的值
    • 他的左右子树也分别为二叉搜索树
  • 平衡二叉搜索树(AVL,Adelson-Velsky and Landis)

    • 他是一颗空树或它的左右两颗子树的高度差的绝对值不超过1,且左右子树都是一颗平衡二叉树

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

1.3 二叉树的存储方式

  • 一般链式存储(用指针),更易理解,也可顺序存储(用数组,奇偶索引查找)

2. 遍历方式

深度优先遍历,先往深走,遇到叶子结点再往回走,以下的前中后指的是中间结点的顺序

  • 前序遍历(递归法,迭代法)
  • 中序遍历(递归法,迭代法)
  • 后序遍历(递归法,迭代法)
  • 经常使用递归来写,前面提到栈来实现递归,也就是说栈可以来实现非递归方式

广度优先遍历,一层一层去遍历

  • 层次遍历(迭代法)
  • 一般采用队列来实现,由于其具有先进先出特点
// 链式存储二叉树结点
struct TreeNode {
	int val;
	TreeNode *left;
	TreeNode *right;
	TreeNode(int x) : val(x), left(NULL), right(NULL) {}  
};

3. 递归遍历

三步走写好递归

    1. 确定函数参数和返回值
    1. 确定终止条件
  • 3.确定单层递归的逻辑

144. 二叉树的前序遍历

145. 二叉树的后序遍历

94. 二叉树的中序遍历

// 前序遍历为例
// 1.要保存遍历的结果必然传入vector保存,当然结点需要传入,没有返回值,直接在vector中保存
void traversal(TreeNode *cur, vector<int>& vec)
// 2.确定终止条件,当前结点空结点,当然返回空
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 == 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;
    }
};

// 中序
class Solution {
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == NULL) return;
				traversal(cur->left, vec);  // 左
        vec.push_back(cur->val);    // 中
        traversal(cur->right, vec); // 右
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};

// 后序
class Solution {
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == NULL) return;
				traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
				vec.push_back(cur->val);    // 中
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};

4. 迭代遍历

递归的实现就是:每一次递归调用都会把函数的局部变量、参数变量值和返回地址等压入调用栈中,然后递归返回时,从栈顶弹出上一次递归的各项参数,这就是递归为什么能返回上一层的原因

  • 前序和后序可以一种风格,主要在于其访问顺序和修改顺序存在一致性或者强关联性,如结果逆序特点等
  • 中序比较特殊,另一种风格,需要借助指针,一路往左
  • 上述两种差异原因在于,访问结点(遍历结点,始终第一个是顶)和处理结点(把元素放进结果里)和处理结点不一致
/**
 * 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) {
        vector<int> result;
        stack<TreeNode*> st;
        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;
    }
};

// 中序遍历——迭代
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        if (root == nullptr) return result;
        while (cur != nullptr || !st.empty()) {
            if (cur != nullptr) {
                st.push(cur);
                cur = cur->left; // 左,借助指针一直走到最左侧叶子结点以下的NULL
            } else {
                cur = st.top();
                st.pop();
                result.push_back(cur->val); // 中,弹出的即为上次压进去的NULL结点的父节点
                cur = cur->right; // 右,NULL父节点可能有右子结点
            }
        }
        return result;
    }
};
// 后序遍历——迭代 
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        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;
    }
};

5. 统一迭代

该写法能够统一前中后的写法,但是较难理解

  • 难点在于访问顺序都是从上往下,往下的过程中遇到的都是“根”结点,所以需要特殊处理
  • 自己理解的是访问时压栈出栈,是为了能保留根节点的值,能找到路径,与传统遇到根结点就根据根节点的左右逻辑关系对其进行处理(保存操作)不同,统一法在于最后统一按照递归逻辑处理
// 中序
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != nullptr) 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;
    }
};

// 前序
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != nullptr) st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != nullptr) {
                st.pop();
                if (node->right) st.push(node->right); // 与中序相比仅仅改变下面的中序位置,改成有左中
                if (node->left) st.push(node->left);

                st.push(node);
                st.push(nullptr);
            } else {
                st.pop();
                node = st.top();
                st.pop();
                result.push_back(node->val);
            }
        }
        return result;
    }
};
// 后序
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        if (root != nullptr) st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != nullptr) {
                st.pop();

								st.push(node);
                st.push(nullptr);

                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;
    }
};