二叉树基础概念&递归/迭代遍历理解

基础概念:

先从最基础的说起,本身是一种层次结构的抽象数据类型,用于表示元素之间的层级关系。树结构由节点(Node)和边(Edge)组成

  • 根节点(Root Node):树的顶端节点,只有一个根节点。根节点没有父节点。
  • 子节点(Child Node):根节点以下的节点。每个节点都可以有多个子节点。
  • 父节点(Parent Node):与子节点相连的节点。如果一个节点B是节点A的子节点,那么A就是B的父节点。
  • 叶子节点(Leaf Node):没有子节点的节点,称为叶子节点。
  • 内部节点(Internal Node):除根节点和叶子节点之外的节点称为内部节点。

特点

  • 深度(Depth):从根节点到某个节点的路径长度(这里计量的是线段数,比如线性的三个点,从第一个点到第三个点,深度为2),根节点的深度为0。

  • 高度(Height):从某个节点到叶子节点的最长路径称为该节点的高度,叶子节点的高度为0。
  • 度(Degree):节点拥有的子节点的个数,树的度是所有节点中最大的度。

稍微过一下就好,之后用起来就会很熟练了


二叉树

在学习前面的内容(栈,队列)后,我在看到二叉树之后自然而然的产生了一项疑惑——二叉树也是一种容器适配器吗?

实际上这是错误的认知,容器适配器依赖于底层的顺序容器(如vectordequelist),而二叉树底层是用链式结构来解决问题。

简单的底层实现如下:一个元素+两个分别指向左右节点的指针。

struct TreeNode{
	int val;

	TreeNode* left;
	TreeNode* right;

	TreeNode(int x, TreeNode* leftChild, TreeNode* rightChild) : val(x), left(leftChild), right(rightChild) {

	};

};

多种遍历方式

学习参考:代码随想录 (programmercarl.com)

面向习题:

144. 二叉树的前序遍历 - 力扣(LeetCode)

94. 二叉树的中序遍历 - 力扣(LeetCode)

145. 二叉树的后序遍历 - 力扣(LeetCode)

可以使用不同遍历方式来完成上述三道习题


递归遍历:

递归通常用于将问题分解成更小的子问题,直到问题足够小可以直接解决。每个递归函数都在处理一部分问题,最终通过合并子问题的解,得到整个问题的解。

关于递归,我们在写代码前要抓住三点来思考:什么是一个规模足够小的单次循环,我要在这个单次循环里处理哪些内容(应该传入哪些元素),我要在什么时候中止这个循环。

递归调用的方向:在每次递归中,将当前状态传入下一次递归调用,并确保递归的规模逐步缩小,最终达到基准条件。

递归体的处理对象:当前递归调用中处理哪些内容?应该传入哪些参数来递归调用下一步?通常这些参数包括当前节点、或问题的当前状态——在二叉树中,我要处理的参数即为——当前指针,原来接收当前指针指向数据的动态数组(问题需要的即为该数组);

递归终止条件:什么时候停止递归(基准条件)?这是递归停止的判断标准——当当前指针指向NULL时,此为结束条件,结束循环。

void travelsal(TreeNode* cur, vector<int>& vec) {

    if (cur == NULL) {

        return;

    }

    vec.push_back(cur->val);//前序遍历,之后的中序/后序都是调整下面三行的位置

    travelsal(cur->left, vec);
    travelsal(cur->right, vec);


}

完整代码:

class Solution {
public:


    void travelsal(TreeNode* cur, vector<int>& vec) {

        if (cur == NULL) {

            return;

        }

        vec.push_back(cur->val);//前序遍历,之后的中序/后序都是调整下面三行的位置

        travelsal(cur->left, vec);
        travelsal(cur->right, vec);


    }

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        travelsal(root, result);
        return result;
    }


};

迭代遍历:

递归本质上依赖系统调用栈来管理函数调用,而迭代则需要我们手动管理这种栈行为

迭代的三大要素:

  • 手动管理的栈(或队列):用于保存当前的计算状态或节点位置。
  • 循环控制:用 whilefor 循环来实现不断推进的逻辑,直到处理完所有节点。
  • 当前状态更新:每次循环迭代都要明确下一步的操作,例如是移动到下一个节点,还是从栈中取出一个节点继续处理。


值得注意的是——迭代遍历的前后序的写法与中序的写法略有不同,此时先给出前序遍历的方式

1)创建所需容器

stack<TreeNode*> stc;
//显式调用栈,用于保存与弹出
vector<int> vec;
//用于接收弹出的数据

2)最小循环

一开始找树,找到树后确定位置——开始循环

vector<int> preOrderTraversal(treeNode* cur){

  if(cur == NULL){
    return result;
   }
//此处一开始传入的是树的根节点,就跟链条的头结点一样,要是一开始就没有,那自然是返回空的内容

  stc.push(cur);//代表找到了树的位置

  while(!stc.empty()){
    
    treeNode* node = stc.top();
   
    stc.pop();

    result.push_back(node->val)

    //上面这一段是弹出与接收,后面的则是左右节点的压入


    if(node->right){
     
     stc.push(node->right);
     }

     if(node->left){
     
     stc.push(node->left);
     }


         
   }
   //循环结束,此时可返回结果
   return result;
}
  

完整实现如下

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> stc;
        vector<int> result;
        if (root == NULL) return result;
        stc.push(root);
        while (!stc.empty()) {
            TreeNode* node = stc.top();                       // 中
            stc.pop();
            result.push_back(node->val);
            if (node->right) stc.push(node->right);           // 右(空节点不入栈)
            if (node->left) stc.push(node->left);             // 左(空节点不入栈)
        }
        return result;
    }
};


中序遍历:

我们要先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

学习参考:代码随想录 (programmercarl.com)

关键点是在while循环中加一个逻辑判断:

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

二者区别

这里大家可能跟我一样有疑惑——递归和迭代看起来不都是执行一个最小单元的内容吗,他们之间的区别到底在哪呢?

  • 递归迭代的确在不断重复处理一个最小的基本单元,它们都通过这种重复来解决问题。你可以将递归看作是系统调用栈自动管理的“循环”,而迭代是通过显式的循环结构来达到类似的效果。

  • 主要区别在于状态管理和控制方式

  • 递归使用系统调用栈来管理状态和控制递归调用。——状态变化依赖函数调用的自我嵌套。每次函数调用都会创建一个新的“上下文”(局部变量、函数参数等),而这些状态通过递归的深度来进行管理。每个函数调用都会自动保存上一次调用的状态。

  • 迭代则通过手动管理状态,使用显式循环控制整个流程。——程序员需要显式管理状态,通过变量、栈、队列或数组来存储当前的状态。比如在二叉树的迭代遍历中,程序员需要手动使用栈来保存当前节点及其子节点。

我们可以可以把递归看成是一种“隐式的循环”,而迭代是一种“显式的循环”。掌握这两者的关键在于理解如何管理当前状态,以及如何通过“不断缩小问题规模”来逐步解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值