【数据结构与算法分析】0基础带你学数据结构与算法分析07--二叉树

news2024/10/7 6:40:29

在学习上一章后,我们对树加以限制,如果树的度为 2,那么就称这颗树为 二叉树 (binary tree)。

二叉树的性质

在一棵二叉树上,有一些重要的性质:

  1. 第 i 层 (i∈N) 上最多有 2^(i−1) 个结点
  2. 层次为 k(k∈N) 的树最多有 2^k−1 个结点
  3. 如果叶结点的数量为 n0​ , degree=2 的结点的数量为 n2​ ,则 n0=n2+1

如果将二叉树的每一层填满,那么这颗二叉树被称之为 满二叉树 (full binary tree);如果这颗二叉树除最后一层外都是满的,且最后一层要么是满的,要么是右边缺少连续的若干结点,那么称这颗二叉树为 完全二叉树 (complete binary tree)。

 由于 full binary tree 与 complete binary tree 是特殊的二叉树,因此它们也有一些确定性的性质。我们假设总结点数为 k ,树的高度 (即树的层数) 为 h ,其中某一层为第 i 层,则有以下性质:

 

 二叉树的实现

 为实现二叉树,我们可以为其采用双向链表的结构,但不再是指向结点的 prev 和 next,而是指向该结点的 left child 和 right child。

struct BinaryTreeBaseNode {
  BinaryTreeBaseNode* left,* right;
};
template <class Element>
struct BinaryTreeNode : BinaryTreeBaseNode {
  Element data;
};

在这里给出求解二叉树 root 中,node 的高度和深度

// 求解结点 node 的高度
int node_height(BinaryTreeBaseNode* node) {
  if (node == nullptr) {
    return -1;
  }
  return max(binary_tree_height(node->left),
             binary_tree_height(node->right)) + 1;
}
// 求解结点 node 在树 root 中的深度
int node_depth(BinaryTreeBaseNode* root, const BinaryTreeBaseNode* node) {
  if (root == nullptr) {
    return -1;
  }
  if (root == node) {
    return 0;
  }
  int left_depth = node_depth(root->left, node);
  int right_depth = node_depth(root->right, node);
  return left_depth == -1 ? (right_depth += right_depth != -1) : (left_depth + 1);
}

如果想要从结点向上求解某些数据时,并不容易做到,因为 child 没有指向 parent 的指针,需要遍历树找到 node 的 parent 才能操作。

// 求解结点 node 的 parent,如果不存在返回 nullptr
BinaryTreeBaseNode* get_parent(BinaryTreeBaseNode* root, const BinaryTreeBaseNode* node) {
  if (root == nullptr || root == node) {
    return nullptr;
  }
  if (root->left == node || root->right == node) {
    return root;
  }
  auto left = get_parent(root->left, node);
  return left == nullptr ? get_parent(root->right, node) : left;
}

为了方便实现我们自然而然的会在链域中添加指向 parent 的指针。这样在求解 sibling、 uncle 时十分方便,并且求解结点的深度时不再需要将其等价为 root 到 node 的路径长。需要注意的是,root 是没有 parent 的。

二叉树的遍历

还记得之前提到的 postorder traversal 与 preorder traversal 吗,它们对二叉树同样适用。不过先别急,既然现在 child 的数量确定了,能不能将对结点的处理放在两个结点的处理之间完成呢?当然没问题!这种处理方式就是 中序遍历 (inorder traversal),当然这也是 DFS 的一种。

如果将当前结点标记为 N,左子结点标记为 L,右子结点标记为 R,那么前序遍历就可以表示为 NLR,中序遍历可以表示为 LNR,后序遍历可以表示为 LRN。

二叉树的前序遍历

recursion

void preorder(BinaryTreeBaseNode* root) {
  if (root == nullptr) {
    return;
  }
  process(root);
  preorder(root->left);
  preorder(root->right);
}

loop

void preorder(BinaryTreeBaseNode* root) {
  stack s;
  while (!s.empty() || root != nullptr) {
    while (root != nullptr) {
      process(root);
      s.push(root);
      root = root->left;
    }
    root = s.top();
    s.pop();
    root = root->rightl;
  }
}

二叉树的中序遍历

recursion

void inorder(BinaryTreeBaseNode* root) {
  if (root == nullptr) {
    return;
  }
  preorder(root->left);
  process(root);
  preorder(root->right);
}

loop

void preorder(BinaryTreeBaseNode* root) {
  stack s;
  while (!s.empty() || root != nullptr) {
    while (root != nullptr) {
      s.push(root);
      root = root->left;
    }
    root = s.top();
    s.pop();
    process(root);
    root = root->rightl;
  }
}

二叉树的后序遍历

recursion

void postorder(BinaryTreeBaseNode* root) {
  if (root == nullptr) {
    return;
  }
  preorder(root->left);
  preorder(root->right);
  process(root);
}

loop

在后序遍历中,在左子结点处理完成后,只有结点没有右子结点或右子结点处理完之后,才能对结点进行处理。因此需要判别当前结点的 右子结点为空 或 刚刚处理过的结点 是该结点的右子结点。判断右子结点为空十分简单,但是问题是如何记录刚刚访问过的结点?

利用一个变量指向正在处理的结点,当指向下一个待处理的结点时,其值就是该结点的上一个处理的结点,即处理前驱。

void postorder(BinaryTreeBaseNode* root) {
  stack s;
  BinaryTreeBaseNode* prev = nullptr;
  while (!s.empty() || root != nullptr) {
    while (root != nullptr) {
      s.push(root);
      root = root->left;
    }
    root = s.top();
    s.pop();
    if (root->right == nullptr || root->right == prev) {
      prev = root;
      process(root);
      root = nullptr;
    } else {
      s.push(root);
      root = root->right;
    }
  }
}

一个异构的前序遍历

如果你在对一个单词串进行翻转时,有一个简单可行的方法:先将单词串整体翻转,之后再逐词翻转。这样你就得到了一个对单词串的翻转!

 

这种异构的翻转也可以用在二叉树的 DFS 遍历上,前序遍历时遍历的结点顺序为 NLR (Node->Left->Right),而后续遍历的结点顺序为 LRN ,对后续遍历的顺序进行翻转就变为了 NRL 。如果以 NRL 的顺序进行遍历,最后将结果翻转也可以得到一个后序遍历的序列,这本质上是一种前序遍历的异构。

二叉树的层序遍历

DFS 天生与 stack 结合在一起,而 BFS 与 queue 结合在一起。因此对于以上三种 DFS 遍历,使用 recursion 是一种简单、高效的理解与编码,而层序遍历则更适合于 loop。

void levelorder(BinaryTreeBaseNode* root) {
  queue q;
  q.push(root);
  while (!q.empty()) {
    for (int i = 0, cur_level_size = q.size(); i < cur_level_size; ++i) {
      root = q.front();
      q.pop();
      process(root);
      if (root->left != nullptr) {
        q.push(root->left);
      }
      if (root->right != nullptr) {
        q.push(root->right);
      }
    }
  }
}

Morris 遍历

在以上介绍的三种 DFS 遍历中,无论是 recursion 还是 loop 实现,都需要 O(N) 的时间复杂度与 O(N) 的空间复杂度。而 1979 年由 J.H.Morris 在他的 论文 中提出了一种遍历方式,可以利用 O(N) 的时间复杂度与 O(1) 的空间复杂度完成遍历。其核心思想是利用二叉树中的空闲指针,以实现空间复杂度的降低。

以 postorder 为例说明其算法的具体思路:

  1. 如果当前结点的左子树为空,则遍历右子树
  2. 如果当前结点的左子树不为空,在当前结点的左子树中找到当前结点在中序遍历中的前驱结点
    1. 如果前驱的右子结点为空,则将前驱结点的右子结点设置为当前结点,当前结点更新为其左子结点
    2. 如果前驱的右子结点为当前结点,则将其重新置空。倒序处理从当前结点的左子结点到该前驱结点路径上的所有结点。完成后将当前结点更新为当前结点的右子结点
  3. 重复步骤 1、2 直到遍历结束

void __reverse_process(BinaryTreeBaseNode* node) {
  if (node == nullptr) {
    return;
  }
  __reverse_process(node->right);
  process(node);
}
void postorderTraversal(BinaryTreeBaseNode* root) {
  BinaryTreeBaseNode* cur = root,* prev = nullptr;
  while (cur != nullptr) {
    prev = cur->left;
    if (prev != nullptr) {
      while (prev->right != nullptr && prev->right != cur) {
        prev = prev->right;
      }
      if (prev->right == nullptr) {
        prev->right = cur;
        cur = cur->left;
        continue;
      }
      prev->right = nullptr;
      __reverse_process(cur->left);
    }
    cur = cur->right;
  }
  __reverse_precess(root);
}

 迭代器

既然可以遍历一棵树,那么依然希望可以在这棵树上暂停下来,对结点进行一些操作,再继续进行迭代。当我们选择的遍历方法不一样时,其迭代时的前驱与后继就不相同。

如果现在给定一个迭代器,应该如何找到迭代器的前驱与后继迭代器。这里给出求解中序遍历前驱的算法步骤与代码,求解中序遍历后继的算法与前驱的算法类似,因此只给出代码。

  • 求解前驱
    • 如果结点的左子树存在,则前驱是结点左子树上最大的结点
    • 如果结点的左子树不存在,则需要寻找结点的 parent
      • 若结点是 parent 的右子树上的结点,则 parent 是其前驱
      • 若结点是 parent 的左子树上的结点,继续向上寻找,直到 parent 为 nullptr 或是其 parent 的右子树上的结点
// 寻找结点 node 的前驱
BinaryTreeBaseNode* get_previous(BinaryTreeBaseNode* node) {
  if (node->left != nullptr) {
    node = node->left;
    while (node->right != nullptr) {
      node = node->right;
    }
  } else {
    auto parent = get_parent(node);
    while (parent != nullptr && parent->left == node) {
      node = parent;
      parent = get_parent(parent);
    }
    node = parent;
  }
  return node;
}
// 寻找结点 node 的后继
BinaryTreeBaseNode* get_next(BinaryTreeBaseNode* node) {
  if (node->right != nullptr) {
    node = node->right;
    while (node->left != nullptr) {
      node = node->left;
    }
  } else {
    auto parent = get_parent(node);
    while (parent != nullptr && parent->right == node) {
      node = parent;
      parent = get_parent(parent);
    }
    node = parent;
  }
  return node;
}

 示例:表达式树

下图展示了一棵 表达式树 (expression tree),leaf node 是操作数 (operand),而 internal node 为运算符 (operator)。由于所有操作都是二元的,因此这颗树为二叉树。每个 operator 的 operand 分别是其两个子树的运算结果。

 

这个树对应的表达式为 a+b∗c+(d∗e+f)∗g ,如果我们对这颗树进行 postorder traversal 将得到序列 abc∗+de∗f+g∗+ ,这是一个后缀表达式;如果对其进行 preorder traversal,则会得到前缀表达式 ++a∗bc∗+∗defg ;最后试一下 inorder traversal,其结果应该是中缀表达式,不过其序列并没有带括号。

从 postorder traversal 的结果,可以很轻松的构建其这棵树。留给读者进行实现,这里将不再说明。

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

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

相关文章

进程地址空间

目录 程序地址空间 感知虚拟地址空间的存在 进程地址空间 分页 & 虚拟地址空间 Linux2.6内核进程调度队列 程序地址空间 我们在学习C语言的时候了解过程序地址空间的分布&#xff1a; 需要注意的是&#xff1a;程序地址空间不是内存。我们在linux操作系统中通过代码来…

刷题笔记之二(字符串中找出连续最长的数字串+数组中出现次数超过一半的数字+另类加法+计算糖果+进制转换)

目录 1. 多层继承问题 2. 继承中子类的构造要引用super 3. 比较地址 4. 字符串中找出连续最长的数字串(编程题) 5. 数组中出现次数超过一半的数字&#xff08;编程题&#xff09; 6. 另类加法&#xff08;编程题&#xff09; 7. Interface 接口中定义方法 8. 实现或继…

C语言学习(二)之字符串和格式化输入/输出

文章目录一、字符串二、 输入2.1 scanf()作用2.2 两种用法三、输出3.1 printf()3.1.1 printf 四种用法3.1.2 常用输出控制符3.1.3 为什么需要输出控制符一、字符串 字符串是一个或多个字符的序列。如&#xff1a;“Hello World” 双引号不是字符串的一部分。仅告知编译器它括…

【学习笔记】《深入浅出Pandas》第16章:可视化

文章目录16.1 plot方法16.1.1 plot概述16.1.2 plot基础方法16.1.3 图形类型16.1.4 x轴和y轴16.1.5 图形标题16.1.6 字体大小16.1.7 线条样式16.1.8 背景辅助线16.1.9 图例16.1.10 图形大小16.1.11 色系16.1.12 绘图引擎16.1.14 图形叠加16.1.15 颜色的表示16.1.16 解决图形中的…

量子笔记:量子计算 toy python implementation from scratch

目录 0. 概要 1. 量子比特表示&#xff1a;用二维张量表示 2. 张量积的实现 2.1 用scipy.linalg.kron()实现张量积 2.2 用张量积计算双量子系统的基 3. 多量子系统基向量表示和生成 3.1 Helper function: bin_ext 3.2 多量子系统的基的生成 3.3 numpy.matrix numpy.m…

基于多尺度注意力网络单图像超分(MAN)

引言 Transformer的自注意力机制可以进行远距离建模&#xff0c;在视觉的各个领域表现出强大的能力。然而在VAN中使用大核分解同样可以得到很好的效果。这也反映了卷积核的发展趋势&#xff0c;从一开始的大卷积核到vgg中采用堆叠的小卷积核代替大卷积核。 上图展现了MAN网络在…

使用T0,方式2,在P1.0输出周期为400µs,占空比为4:1的矩形脉冲,要求在P1.0引脚接有虚拟示波器,观察P1.0引脚输出的矩形脉冲波形

大家学过一段时间的单片机了&#xff0c;今天我们来说说单片机里的定时器&#xff0c;又叫计数器。首先&#xff0c;我们通过案例来了解一下什么是定时器。 【例】使用T0&#xff0c;方式2&#xff0c;在P1.0输出周期为400s&#xff0c;占空比为4&#xff1a;1的矩形脉冲&…

如何编写优秀的测试用例,建议收藏和转发

1、测试点与测试用例 测试点不等于测试用例&#xff0c;这是我们首先需要认识到的。 问题1&#xff1a;这些测试点在内容上有重复&#xff0c;存在冗余。 问题2&#xff1a;一些测试点的测试输入不明确&#xff0c;不知道测试时要测试哪些。 问题3&#xff1a;总是在搭相似…

串口通信协议【I2C、SPI、UART、RS232、RS485、CAN】

&#xff08;1&#xff09;I2C 集成电路互连总线接口(Inter IC)&#xff1a;同步串行半双工传输总线&#xff0c;连接嵌入式处理器及其外围器件。 支持器件&#xff1a;LCD驱动器、Flash存储器 特点&#xff1a; ①有两根传输线&#xff08;时钟线SCL、双向数据线SDA&#…

python基础19-36题

题目&#xff1a; 代码十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十三十一三十二三十三三十四三十五三十六十九 birthday int(input(“请输入生日日期&#xff1a;”)) Set1 [1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31] Set2 [2,3,6,7,10,11,…

【CV】第 7 章:目标检测基础

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

几何等变图神经网络综述

许多科学问题都要求以几何图形&#xff08;geometric graphs&#xff09;的形式处理数据。与一般图数据不同&#xff0c;几何图显示平移、旋转和反射的对称性。研究人员利用这种对称性的归纳偏差&#xff08;inductive bias&#xff09;&#xff0c;开发了几何等变图神经网络&a…

SpringMVC | 快速上手SpringMVC

&#x1f451; 博主简介&#xff1a;    &#x1f947; Java领域新星创作者    &#x1f947; 阿里云开发者社区专家博主、星级博主、技术博主 &#x1f91d; 交流社区&#xff1a;BoBooY&#xff08;优质编程学习笔记社区&#xff09; 前言&#xff1a;在上一节中我们了解…

多分类评估指标计算

文章目录混淆矩阵回顾Precision、Recall、F1回顾多分类混淆矩阵宏平均&#xff08;Macro-average&#xff09;微平均&#xff08;Micro-average&#xff09;加权平均&#xff08;Weighted-average&#xff09;总结代码混淆矩阵回顾 若一个实例是正类&#xff0c;并且被预测为正…

Linux(Nginx)

目录 一、Nginx简介 二、Nginx使用 Nginx安装 tomcat负载均衡 Nginx配置 三、Nginx部署项目 项目打包前 将前端项目打包&#xff08;测试本地项目打包后没问题&#xff09; ip/host主机映射 完成Nginx动静分离的default.conf的相关配置 将前台项目打包(配合Nginx动静…

real-word super resulution: real-sr, real-vsr, realbasicvsr 三篇超分和视频超分论文

real-world image and video super-resolution 文章目录real-world image and video super-resolution1. Toward Real-World Single Image Super-Resolution:A New Benchmark and A New Model&#xff08;2019&#xff09;1.1 real-world数据集制作1.2 LP-KPN网络结构1.3 拉普拉…

近八成中国程序员起薪过万人民币,你过了么?

打工者联盟为了抵抗996、拖欠工资、黑心老板、恶心公司&#xff0c;让我们组成打工者联盟。客观评价自己任职过的公司情况&#xff0c;为其他求职者竖起一座引路的明灯。https://book.employleague.cn/一项调查显示&#xff0c;近八成中国程序员本科毕业生起薪过万&#xff08;…

Oracle数据库中的数据完整性

目录 1.数据完整性约束作用 2.数据完整性约束的分类 3.完整性约束的状态 4.域完整性的实现 &#xff08;1&#xff09;check约束 ①可视化方式创建check约束 ②命令方式创建约束 ③修改表创建的约束 ④删除约束 &#xff08;2&#xff09;实体完整性约束实现 ①prim…

思科dhcp服务器动态获取ip地址

项目要求: 某公司共有网管中心、行政部、技术部、三个部门&#xff0c;分别处在一栋大楼中的两个楼层&#xff0c;为了保证公司内部主机始终能够连接Internet&#xff0c;采用双向冗余设计&#xff0c;分别使用路由器R1与路由器R2连接中国电信和中国联通。 1.首先为了避免不必要…

【算法详解】数据结构:7种哈希散列算法,你知道几个?

一、前言 哈希表的历史 哈希散列的想法在不同的地方独立出现。1953 年 1 月&#xff0c;汉斯彼得卢恩 ( Hans Peter Luhn ) 编写了一份IBM内部备忘录&#xff0c;其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一时间&#xff0c;IBM Researc…