数据结构精讲:从原理到实战–学习笔记06

数据结构精讲:从原理到实战–学习笔记06

本笔记是记录学习 《数据结构精讲:从原理到实战》,作者是:蔡元楠,Google Brain资深工程师。

如有侵权,联系删除!

树由节点连接组成,与数组和链表不同,它是一种非线性的数据结构。

在这里插入图片描述

在上图中,A 是这棵树的根。除了根节点外的节点都有且仅有一条指向自己的边,这个边的方向代表了父节点指向了子节点。比如,A 是 B、C、D 的父节点,B 又被称为 A 的子节点,同时 B 也是 E、F、K 的父节点。

每个节点都可以连接着任意数量的子数(包括 0 个),没有子节点的节点也被称作叶子节点,在上图例子中,C、E、F、L、G 都是叶子节点。共享同样父节点的节点被称作兄弟节点,在上图例子中,B、C、D 就是兄弟节点。

一个节点的深度是从根节点到自己边的数量,比如 K 的深度是 2。 节点的高度是从最深的节点开始到自己边的数量,比如 B 的高度是 2。一棵树的高度也就是它根节点的高度。

树的递归定义是: n(n>=0)个节点的有限集合,其中每个节点都包含了一个值,并由边指向别的节点(子节点),边不能被重复,并且没有节点能指向根节点。广义的树中,节点可以有 0 个或者多个子节点。而在后面章节中将介绍的二叉树等就是广义树中的特殊类型(每个节点有两个子节点)。

广义的树可以用来表达有层次结构的数据关系,比如文件系统(File System)是一个有层次关系的集合,文件系统中每一个目录(Directory)可以包含多个目录,没有子目录的叶子节点也就是文件(File)了。

树的实现方式

树的实现方式有很多种,在这里介绍两种常见的实现方式,一种是基于链表的实现,另一种是基于数组的实现。

基于链表的实现一般是每一个节点类型维护一个子节点指针和一个指向兄弟节点的链表,我们把它称作左孩子兄弟链表法。代码是这样的:

class TreeNode {
      Data data;
      LinkedList siblings;
      TreeNode left_child;
}

在我们上面图树的例子中,如果用左孩子兄弟链表法实现节点 B 的话,来看看节点 B 在这个代码中会是什么样子呢?如下图所示,节点 B 的 siblisings 节点是一个指向 C 接着指向 D 的链表,节点 B 的 left_child 节点则指向 E。
在这里插入图片描述

另一种基于数组的实现方式是每一个节点维护一个包含它所有子节点的数组,我们把它称作孩子数组法。

class TreeNode {
      Data data;
      ArrayList children;
}

如下图所示,同样是节点 B,利用孩子数组法实现的话,需要维护一个包含 E、K、F 的数组。
在这里插入图片描述

在实现一个树的编程中,最容易犯的错误在于内存管理和节点的增、删、改。

为了更好的阐述这里的内存管理以及增删改问题,我们用 C++ 进行示范,一种简单幼稚的孩子数组法实现是这样的:

class TreeNode {
      Data data;
      std::vector<TreeNode> children;
};

可以发现,所有子节点的内存都是由父节点管理,也就是说当父节点被删除时,子节点的内存也会被自动清理。那就造成了一个很严重的问题,我们很难方便地删除任意一个非叶子节点。所以事实上用孩子数组实现的话我们往往只能维护一组子节点的指针,像是这样:

class TreeNode {
      Data data;
      std::vector<TreeNode*> children;
};

这就造成了另一个问题,子节点的内存由谁来管理呢?所以一个解决办法是除了实现这样一个 TreeNode 类,再去实现一个 NodePool 类用来管理所有的节点内存。例如下面代码,用一个简单的动态数组维护内存:

class NodePool {
  std::vector<TreeNode> nodes;
}

另一个解决办法则是模仿链表的内存管理,在左孩子兄弟链表法中比较容易实现,因为它实际上就是一个二叉的链表。

除了内存管理问题,树节点的插入、删除也是难点。如果你还记得之前学习的数组和链表基本操作,就能知道数组的随机元素增删是 O(n) 的复杂度,而链表是 O(1)。在这里树的实现方式也是类似。

在左孩子兄弟链表法中,插入一个树节点的复杂度是 O(1),相当于是在 siblings 链表中插入一个元素,或者是增加一个 left_child 节点。而在孩子数组法中插入一个节点和数组插入元素类似,需要挪动其后的所有节点,则是 O(n) 的复杂度。

删除节点相比插入节点更为复杂一些,同样的,在孩子数组法中,删除单个节点的复杂度是 O(n),因为你需要去拷贝这个节点的子树到上层节点。在左孩子兄弟链表法中,删除单个节点的复杂度为 O(1),你只需要去重新整理几根指针引用就可以了。

那么现在看来左孩子兄弟链表法各项表现都优于孩子数组法,的确如此,左孩子兄弟链表法是树的教科书范式实现方法。但在实际应用中我们还是常常见到孩子数组法,为什么呢?因为孩子数组法从 API 使用角度更为简单,使用者可以随机访问任意孩子。特别当我们想要提供一个数组的只读的镜像时(Read-Only View),孩子数组法可以作为 API 提供,但是背后可修改的树仍然是由更为复杂的私有方法实现的。

树的遍历和基本算法

前序遍历

class TreeNode {
  TreeNode* left_child;
  TreeNode* sibling;
}

void PreorderVisit(TreeNode root) {
  Visit(root);
  TreeNode* child = root->left_child;
  for (TreeNode* child = root->left_child; child != nullptr; child = child->sibling) {
    PreorderVisit(child);
  }
}

其他遍历代码:晚些时候会更新在下面。
other traverse solutions will be posted in the following segments at a later time

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值