二叉树遍历算法和应用

news2024/11/25 16:29:40

二叉树是指度为 2 的树。它是一种最简单却又最重要的树,在计算机领域中有这广泛的应用。

二叉树的递归定义如下:二叉树是一棵空树,或者一棵由一个根节点和两棵互不相交的分别称为根节点的左子树和右子树所组成的非空树,左子树和右子树同样都是一棵二叉树。在二叉树中,每个节点的左子树的根节点被称为左孩子节点,右子树被称为右孩子节点。

二叉树通常使用如下数据结构来表示:

data 表示二叉树节点的数值,left_child 表示节点的左孩子节点,right_child 表示节点的右孩子节点。

struct node {
  int data;
  struct node *left_chile;
  struct node *right_child;
}

从二叉树的定义来看,有以下两点;

(1) 二叉树可以是一棵空树

(2) 二叉树的左子树和右子树不一定存在,可能同时不存在,可能同时存在,也可能左子树和右子树只存在一个

为什么要有二叉树 ?

在我们使用数据结构来存储数据的时候,最常见的两个使用场景是查找和插入。查找就是从数据结构中找到我们想要的值,插入是将一个新值插入到数据结构正确的位置。

在数据结构中,最常用的两个基础的数据结构是数组和链表,数组和链表的区别可以从如下 3 个方面来看:

① 插入元素。数组的插入效率比链表低,因为数组插入的时候,插入位置后边的元素都要向后移动;而向链表中插入元素的时候,只需要改变节点的指向就可以。前者的时间复杂度是 O(n) 的,后者的时间复杂度是 O(1) 的。

② 读取元素。读取数组中的第几个元素的时候,使用数组的下标直接读取就可以,而读取链表中的第几个元素的时候,需要对链表进行遍历。前者的时间复杂度是 O(1) 的,后者的时间复杂度是 O(n) 的。

③ cache。数组用一块连续的内存来维护,链表中每个节点的地址并不连续。从 cpu cache  LRU 算法的角度来看的话,数组中的元素离的更近,所以访问性能会更高。

从数组和链表的对比来看,没有哪种数据结构是具备所有的优点的。

二叉树有 3 个重要的变种,分别是二叉排序树,二叉平衡树,红黑树。

(1)二叉排序树

二叉排序数是一棵特殊的二叉树,在一般的二叉树中,区分左子树和右子树,但结点的值是无序的。在二叉排序树中,不仅区分左子树和右子树,而且整个树的节点是有序的。

二叉排序树又称二叉搜索树,它或者是一棵空树,或者是一棵非空二叉树,具备如下 3 个特点:

① 如果左子树非空,那么左子树上所有节点的值小于根节点的值

② 如果右子树非空,那么右子树上所有节点的值大于根节点的值,如果允许节点的值相等,那么大于等于

③ 左、右子树本身又各是一棵二叉排序树

从分析可以得出,在查找的性能上,二叉排序树的时间复杂度是 O(logn),相对于链表的 O(n),是有优化的。但是二叉排序树最差的情况,是每个节点都只包含一个子节点,这种情况下的时间复杂度就成为了 O(n),所以在二叉排序树的基础上出现了二叉平衡树。

(2)二叉平衡树

二叉平衡树首先是一颗二叉排序树,在此基础上又增加了一个条件,即每个节点的左子树和右子树的高度之差不大于 1。

有了这个限制之后,查找的时间复杂度就不会出现 O(n) 的情况。但是当向平衡树中插入元素的时候,为了保证插入之后二叉树还能满足平衡树的要求,需要进行旋转操作。对于频繁插入,删除的场景,旋转操作过于频繁,反而会增加时间的消耗。所以出现了红黑树,红黑树在查找和插入操作之间取得了平衡(这样的结论,只有专门研究红黑树算法的专业人员才能了解,在很多文章和书籍中也是这么说的,我们只需要知道这个事情即可)

(3)红黑树

红黑树的前提也是一颗二叉排序,在此基础上增加了以下 5 个条件:

① 红黑树中的节点要么是黑色,要么是红色

② 红黑树的根节点是黑色

③ 红黑树中红色节点不能连续,也就是说一个红色节点的父节点和子点都不能是红色

④ 从根节点开始,到叶子节点,每条路径上的黑色节点个数相等

⑤ 叶子节点是黑色,在红黑树中的叶子结点不是真实存在的,叶子结点在下图中用矩形来表示

在工程应用中,本人还没见过使用平衡树的场景,只见过使用红黑树的场景。

① 在 linux 内核中,维护 struct task_struct 使用了红黑树;维护 tcp 接收侧的乱序队列,使用了红黑树;epoll 中维护监听的 fd,也是用了红黑树

② 在 c++,java 内置的 map 数据结构中,使用了红黑树

本文只记录二叉树的基本算法,不涉及二叉排序树,二叉平衡树,红黑树相关的算法。

1 二叉树遍历

二叉树的遍历有 4 中算法:前序遍历,中序遍历,后序遍历以及层序遍历。前 3 种遍历算法中的前、中、后,说的是根节点的位置:前序遍历的顺序是根节点,左子节点,右子节点;中序遍历的顺序是左子节点,根节点,右子节点;后序遍历的顺序是左子节点,右子节点,根节点。不管哪种遍历,左子节点都在右子节点之前遍历。

如果了解图的遍历算法,可以知道图的遍历算法包括两种:深度优先遍历和广度优先遍历。二叉树的遍历方法中,前 3 种类似于图中的深度优先遍历,层序遍历类似于广度优先遍历。

二叉树跟递归算法联系比较紧密,递归算法有两点组成,一个是递归退出条件,一个是递归计算逻辑,也可以称为递归体,这一点和循环是有点相似的,循环也要有循环停止条件和循环处理逻辑。每种算法的对象都是围绕着一个数据结构,都需要有一个驱动力,这个驱动力往往就是对数据结构中的元素进行遍历,对于数组来说往往就是沿着数组下标进行遍历,对于链表来说往往就是从 head 开始沿着 next 向下遍历,对于二叉树来说就是沿着 left child 和 right child 进行遍历。对数据进行遍历是算法的基础。

递归算法,不太适合从一开始沿着算法逐渐向下思考,使用递归算法的时候我们可以使用一个最简单的场景来理解。比如,对于二叉树来说,我们以只有 3 个节点(一个 root,一个 left child,一个 right child)的二叉树来思考和理解。

1.1 前序遍历

力扣:. - 力扣(LeetCode)

/**
 * 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) {
        if (root == nullptr) {
            return ret;
        }
        preScan(root);
        return ret;
    }

    void preScan(TreeNode *root) {
      ret.push_back(root->val);
      if (root->left) {
          preScan(root->left);
      }

      if (root->right) {
          preScan(root->right);
      }
    }

private:
  vector<int> ret;
};

1.2 中序遍历

力扣:. - 力扣(LeetCode)

/**
 * 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> inorderTraversal(TreeNode* root) {
      if (root == nullptr) {
          return ret;
      }
      
      midScan(root);
      return ret;
    }

    void midScan(TreeNode *root) {
        if (root->left) {
            midScan(root->left);
        }

        ret.push_back(root->val);
        if (root->right) {
            midScan(root->right);
        }
    }

private:
  vector<int> ret;
};

1.3 后续遍历

力扣:. - 力扣(LeetCode)

/**
 * 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) {
      if (root == nullptr) {
          return ret;
      }
      
      postScan(root);
      return ret;
    }

    void postScan(TreeNode *root) {
        if (root->left) {
            postScan(root->left);
        }

        if (root->right) {
            postScan(root->right);
        }

        ret.push_back(root->val);
    }

private:
  vector<int> ret;
};

1.4 层序遍历

力扣:. - 力扣(LeetCode)

/**
 * 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<vector<int>> levelOrder(TreeNode* root) {
        std::queue<TreeNode *> q;
        std::vector<vector<int>> ret;
        if (root == nullptr) {
            return ret;
        }

        q.push(root);
        while (!q.empty()) {
            // 用来保存二叉树一层的数据
            // 如果只是遍历,不用保存数据的话,那么是用不着这个数据结构的
            vector<int> line;
            int q_size = q.size();
            for (int i = 0; i < q_size; i++) {
                TreeNode *node = q.front();
                q.pop();
                line.push_back(node->val);
                if (node->left) {
                    q.push(node->left);
                }

                if (node->right) {
                    q.push(node->right);
                }
            }
            ret.push_back(line);
        }
        return ret;
    }
};

2 二叉树遍历的应用

2.1 求二叉树高度

力扣:. - 力扣(LeetCode)

求二叉树的高度,也是使用递归算法。有两种算法:

(1)求出左子树和右子树的高度,然后两者的较大值加 1 就是二叉树的高度

(2)使用动态规划的算法,每一次递归计算的时候,相当于向下增加了一层,在递归函数的形参中实时维护已经遍历的二叉树的高度,二叉树遍历完之后,最大值保存的就是二叉树的高度

2.1.1 左子树和右子树较大值

int maxDepth(struct TreeNode* root) {
    if (root == NULL) {
        return 0;
    }

    int left_height = 0;
    int right_height = 0;
    if (root->left) {
        left_height = maxDepth(root->left);
    }

    if (root->right) {
        right_height = maxDepth(root->right);
    }

    if (left_height > right_height) {
        return 1 + left_height;
    } else {
        return 1 + right_height;
    }
}

2.1.2 动态规划

class Solution {
public:
    int maxDepth(TreeNode* root) {
      maxDepthHelper(root, 0);
      return max_depth_;
    }

    void maxDepthHelper(TreeNode *root, int depth) {
        if (root == nullptr) {
            return;
        }

        depth++;
        if (depth > max_depth_) {
            max_depth_ = depth;
        }

        if (root->left) {
            maxDepthHelper(root->left, depth);
        }

        if (root->right) {
            maxDepthHelper(root->right, depth);
        }
    }

private:
    int max_depth_ = 0;
};

2.2 求二叉树宽度

求二叉树的宽度和求二叉树的高度,算法是类似的,都可以通过动态规划的方式来进行。

在递归函数中用一个形参实时表示当前遍历的是哪一层的节点,每遍历一个节点都将这一层的宽度加 1。使用一个数组保存每一层的宽度,遍历结束之后,再遍历数组找到宽度最大的这一层。

class Solution {
public:
    int maxWidth(TreeNode* root) {
      maxWidthHelper(root, 0);
      for (int i = 0; i < max_depth_; i++) {
          if (width[i] > max_width_) {
              max_width_ = width[i];
          }
      }
      return max_width_;
    }

    void maxDepthHelper(TreeNode *root, int depth) {
        if (root == nullptr) {
            return;
        }
        width[depth] = width[depth] + 1;

        depth++;
        if (depth > max_depth_) {
          max_depth_ = depth;
        }
        if (root->left) {
            maxWidthHelper(root->left, depth);
        }

        if (root->right) {
            maxWidthHelper(root->right, depth);
        }
    }

private:
    int max_width_ = 0;
    int max_depth_ = 0;
    int width[100] = {0}; // 定义一个保存每一层宽度的数组,假设二叉树的高度不会超过 100
};

2.3 二叉树路径

力扣:. - 力扣(LeetCode)

/**
 * 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<string> binaryTreePaths(TreeNode* root) {
      tmp_data_path.push_back(root->val);
      binaryTreeScan(root);
      return ret;
    }

    void binaryTreeScan(TreeNode *root) {
        if (root->left == nullptr && root->right == nullptr) {
          string str_path = makePath();
          ret.push_back(str_path);
          return;
        }

        if (root->left) {
            // 使用递归算法找路径的典型代码
            // 三段式:push back --> 递归运算 --> pop back
            // 为什么递归运算结束之后,需要将元素 pop 出来
            // 因为通过递归算法,和这个节点相关的路径都已经遍历到了
            // 后边再遍历的路径,与这个节点无关了,所以需要把这个节点 pop 出来
            // 以防影响后边的路径
            // 图的遍历找路径的算法题中,主要的递归逻辑也是类似的
            tmp_data_path.push_back(root->left->val);
            binaryTreeScan(root->left);
            tmp_data_path.pop_back();
        }

        if (root->right) {
            tmp_data_path.push_back(root->right->val);
            binaryTreeScan(root->right);
            tmp_data_path.pop_back();
        }
    }

    string makePath() {
        int size = tmp_data_path.size();
        std::string str;
        for (int i = 0; i < size; i++) {
            if (i <  size - 1) {
              str += std::to_string(tmp_data_path[i]) + "->";
            } else {
              str += std::to_string(tmp_data_path[i]);
            }
        }
        return str;
    }

private:
  vector<string> ret;
  vector<int> tmp_data_path;
};

2.4 二叉树翻转

力扣:. - 力扣(LeetCode)

/**
 * 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:
    TreeNode* invertTree(TreeNode* root) {
      if (root == nullptr) {
          return root;
      }
      binaryTreeScan(root);
      return root;
    }

    void binaryTreeScan(TreeNode *root) {
      TreeNode *left = root->left;
      root->left = root->right;
      root->right = left;

      if (root->left) {
          binaryTreeScan(root->left);
      }

      if (root->right) {
          binaryTreeScan(root->right);
      }
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1591291.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】07查找

查找 1. 基本概念2. 顺序表查找2.1 顺序查找2.2 顺序查找优化-哨兵 3. 有序表查找3.1 折半查找&#xff08;二分查找&#xff09; 4. 分块查找&#xff08;索引顺序查找&#xff09;5. Hash表&#xff08;散列表&#xff09;5.1 散列函数的设计5.2 代码实现5.2.1 初始化Hash表5…

再谈C语言——理解指针(一)

内存和地址 内存 在讲内存和地址之前&#xff0c;我们想有个⽣活中的案例&#xff1a; 假设有⼀栋宿舍楼&#xff0c;把你放在楼⾥&#xff0c;楼上有100个房间&#xff0c;但是房间没有编号&#xff0c;你的⼀个朋友来找你玩&#xff0c; 如果想找到你&#xff0c;就得挨个房…

【C++11】异常知多少

> 作者&#xff1a;დ旧言~ > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;熟练掌握C11异常 > 毒鸡汤&#xff1a;有些事情&#xff0c;总是不明白&#xff0c;所以我不会坚持。早安! > 专栏选自&#xff1a;C嘎嘎进阶 &g…

Ubuntu22.04 + ROS2 Humble的环境配置

Ubuntu22.04 ROS2 Humble的环境配置 文章目录 Ubuntu22.04 ROS2 Humble的环境配置(1) Set locale(2) Setup Sources(3)安装ROS2(4)检查是否成功安装 参考官方网站ROS2-Installation ROS2的各种版本及维护计划&#xff0c;可以参考ROS2-List of Distributions (1) Set locale…

gitlab、jenkins安装及使用文档二

安装 jenkins IP地址操作系统服务版本192.168.75.137Rocky9.2jenkins 2.450-1.1 jdk 11.0.22 git 2.39.3192.168.75.138Rocky9.2gitlab-ce 16.10.0 结合上文 jenkins安装 前期准备&#xff1a; yum install -y epel-release yum -y install net-tools vim lrzsz wget…

AIGC的崛起:定义未来内容创作的新纪元

&#x1f31f;文章目录 &#x1f31f;AIGC简介&#x1f31f; AIGC的相关技术与特点&#x1f31f;AIGC有哪些应用场景&#xff1f;&#x1f31f;AIGC对其他行业影响&#x1f31f;面临的挑战与问题&#x1f31f;AIGC未来发展 &#x1f31f;AIGC十大热门网站推荐&#xff1a; 文心…

一键开启Scrum回顾会议的精彩时刻

其实回顾会议作为一个检视、反馈、改进环节&#xff0c;不仅在传统的瀑布管理模式中&#xff0c;还是在Scrum一类的敏捷管理流程中&#xff0c;都是非常重要的活动。一些团队认为它无法产生直接的价值&#xff0c;所以有意忽略了这个会议&#xff1b;一些团队在越来越多的回顾中…

【Python】面向对象(专版提升2)

面向对象 1. 概述1.1面向过程1.2 面向对象 2. 类和对象2.1 语法2.1.1 定义类2.1.2 实例化对象 2.2 实例成员2.2.1 实例变量2.2.2 实例方法2.2.3 跨类调用 3. 三大特征3.1 封装3.1.1 数据角度3.1.2 行为角度3.1.3 案例:信息管理系统3.1.3.1 需求3.1.3.2 分析3.1.3.3 设计 3.2 继…

MySQL·:执行一条查询语句期间发生了什么?

MySQL的架构分为两层&#xff0c;Server 层和存储引擎层 server层负责建立连接、分析和执行SQL&#xff0c;MySQL&#xff0c;MySQL大多数的核心功能模块都在在这里实现&#xff0c;下图上半部分都是server层做的事情&#xff0c;另外&#xff0c;所有的内置函数&#xff08;如…

Springboot+Vue项目-基于Java+MySQL的房产销售系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

JS - BOM(浏览器对象模型)

BOM 浏览器对象模型 BOM可以使我们通过JS来操作浏览器 在BOM中为我们提供了一组对象&#xff0c;用来完成对浏览器的操作 BOM对象 BOM&#xff08;Browser Object Model&#xff09;是指浏览器对象模型&#xff0c;它提供了与浏览器窗口进行交互的对象和方法。BOM包括一些核…

C++——StackQueue

目录 一Stack 1介绍 2接口 3模拟实现 4栈的oj题 二Queue 1介绍 2接口 3模拟实现 三容器适配器 1再谈栈和队列 四优先级队列 1接口 ​编辑 2仿函数 五dequeue的简单介绍 一Stack 1介绍 先来看看库中对栈的介绍&#xff1a; 1. stack是一种容器适配器&#x…

scaling laws for neural language models

关于scaling law 的正确认识 - 知乎最近scaling law 成了最大的热词。一般的理解就是&#xff0c;想干大模型&#xff0c;清洗干净数据&#xff0c;然后把数据tokens量堆上来&#xff0c;然后搭建一个海量H100的集群&#xff0c;干就完了。训练模型不需要啥技巧&#xff0c;模型…

解决 App 自动化测试的常见痛点!

App 自动化测试中有些常见痛点问题&#xff0c;如果框架不能很好的处理&#xff0c;就可能出现元素定位超时找不到的情况&#xff0c;自动化也就被打断终止了。很容易打消做自动化的热情&#xff0c;导致从入门到放弃。比如下面的两个问题&#xff1a; 一是 App 启动加载时间较…

Vue 移动端(H5)项目怎么实现页面缓存(即列表页面进入详情返回后列表页面缓存且还原页面滚动条位置)keep-alive缓存及清除keep-alive缓存

一、需求 产品要求&#xff1a;Vue移动端项目进入列表页&#xff0c;列表页需要刷新&#xff0c;而从详情页返回列表页&#xff0c;列表页则需要缓存并且还原页面滚动条位置 二、实现思路 1、使用Vue中的keep-alive组件&#xff0c;keep-alive提供了路由缓存功能 2、因为我项…

java快速构建飞书API消息推送、消息加急等功能

文章目录 飞书机器人自定义机器人自定义应用机器人 自定义应用发送消息普通文本 text富文本 post图片 image文件 file语音 audio视频 media消息卡片 interactive分享群名片 share_chat分享个人名片 share_user 批量发送消息消息加急发送应用内加急发送短信加急 发送电话加急spr…

Linux的网口名字的命名规则

在工作中&#xff0c;偶尔看到有些机器的网口名字是以ethX命令&#xff0c;有些则以enpXsX这种名字命名。网上的资料说的都不太明白,资料也无据可查&#xff0c;很难让人信服。于是决定自己查了下官方的资料和源码&#xff0c;把这些搞清楚。 官方文档&#xff1a;Predictable…

visual studio 2017开发QT框架程序

1. 配置开发环境 首先创建项目 进入到项目后&#xff0c;右键点击项目点击属性&#xff0c;配置如下&#xff1a;

使用 create-vue 脚手架工具创建一个基于 Vite 的项目,并包含加入 Vue Router 等可选项

如果你打算启动一个新项目&#xff0c;你可能会发现使用 create-vue 这个脚手架工具更容易&#xff0c;它能创建一个基于 Vite 的项目&#xff0c;并包含加入 Vue Router 的选项&#xff0c;指令如下&#xff1a; // npm npm create vuelatest// yarn yarn create vue// pnpm …

C语言中抽象的编译和链接原理

今天04.12&#xff0c;身体小有不适&#xff0c;但是睡不着觉&#xff0c;秉着不能浪费时间的原则&#xff0c;现在就简单写一下有关我们C语言中编译和链接的大体过程吧&#xff0c;因为编译和链接是比较抽象的&#xff0c;而且内容是比较底层&#xff0c;我们这里就简单了解它…