读书笔记 --- 数据结构与算法分析C语言描述 --- 12.21 --- Chapter4 树 page65 - 69

4.0 引言

  • 为解决链表的线性访问时间过慢这个问题,介绍一种大部分操作时间平均为O(log N)的数据结构 — 树
  • 第二种修改,对于长的指令序列每种操作的运行时间基本上是O(log N) — 二叉查找树(Binary Search Tree)

树在cs中是非常有用的抽象概念,本章我们将学习:

  1. 树在操作系统中的文件系统的应用
  2. 树如何能够用来计算算术表达式的值(常考)
  3. 树支持O(log N)进行的各种搜索操作以及如何细化得到最坏情况O(log N)

4.1 预备知识

从课本上抄一个最基本的树的定义:n(n >= 0)个结点的有限集

  1. 这个集合可以为空集
  2. 若此集合不为空集,那么由称作root的根结点以及0个/多个非空的子树(subtree)T1, T2, T3 … Tk组成。其中每一棵树的根都被来自根的一条有向边所连接。(熟悉的同学一听就会会条件反射似的:嗯 这很递归!)
    在这里插入图片描述不难发现,一棵树是N个结点和N - 1条边的集合。那个-1就是根root造成的。

一个结点上方一层与他有连线的结点就是他的父亲,下方一层与他有连线的结点就是他的儿子。计算机世界与现实世界一样,你可以子孙满堂,却不能四处认爹。 同样,我们可以以这种方式去定义爷爷与孙子,太爷爷与曾孙子…
从节点n1到nk的路径用大白话讲拿手指着n1,从n1开始像走迷宫一样一直走到nk,中间经过的结点就是路径。每个结点都有自己到自己的一条路径,从根到每个结点恰好存在一条路径。
于是深度和高度这个定义就由路径这个定义展开,ni的深度(depth)是根到该结点的唯一路径的长。而ni的高度(height)就是ni到一片树叶最长路径的长,一棵树的高就等于根的高。
若存在一条路径从n1到n2,那么n1是n2的一位祖先(ancestor),n2是n1的一个后裔(descendant)。之前说过了,结点到自己本身也是有一条路径的,那么如果n1 != n2,则称n1是n2的一位真祖先(proper ancestor),n2是n1的一位真后裔(proper descendant)。 你可以开玩笑说"我是我自己的爸爸",这没有问题,但这也只能是个玩笑。

其实上述定义属于非常简单的定义,因为我们通常学习时看到的树结构是一目了然的,类似这种高度深度的概念由路径定义这回事在之前我是毫无察觉的,我忽略了这些东西,而这些基础往往在某些场景发挥神奇的作用,我深有体会。

4.1.1 树的实现

我们实现树的数据结构除了需要结点之外还需要一些指针,可是由于每个结点的儿子数不确定,直接建立起到各个儿子的结点是困难的,且这样会产生很多浪费的空间。因此我实现一棵树的一个准则就是"唯一"。而使用下面的这种方法:

struct TreeNode {
	elemType elem;
	TreeNode *firstChild;
	TreeNode *nextSibling;
}

而firstChild和nextSibling都一定是确定的,唯一的。这就是我们熟知的孩子兄弟表示法。

再提一嘴,有人会问,这个结构他不香嘛?

struct TreeNode {
	elemType elem;
	TreeNode *right;
	TreeNode *left;
}

emmm,这是二叉树的结构,不是树,二叉树之所以可以使用这样的定义方式原因也很简单:一个结点最多只有两个孩子,左右孩子均唯一确定
下面这棵具体的树:
在这里插入图片描述

即等价于下面的结构:
在这里插入图片描述

4.1.2 树的遍历及应用

在这里插入图片描述
以UNIX系统的目录结构为例:/usr为根,其三个儿子分别为mark,alex,bill,mark又有三个儿子book,course,junk…最直观的反应到正常的电脑文件操作系统即是:点开usr文件夹,里面有三个文件夹mark,alex,bill,点开mark,里面有book文件夹,course文件夹,junk.c…因此在不同目录下两个文件可以享有相同的名字,因为路径不同!(又是路径)。

下面有一个任务:列出目录中所有文件的名字

// depth参数是一个内部簿记变量
static void listDir(DirectoryOrFile D, int depth) {
	// 假设D是一个合法入口
	if ( D is a legitimate entry ) {
		// 打印D的文件名
		printName(D, depth);
		// 内部再进行D是一个判断,假设D是一个计算机磁盘中的目录
		// 非常符合递归特征,我们写这段代码时只需要考虑本级递归需要做什么
		// 我们需要判断D里面还有没有东西,有东西则通过给他打印出来
		if ( D is a directory ) {
			for each child, C of D
				// 对下一depth的文件进行操作
				listDir(C, depth + 1);
		}
	}
}

void printAllName(DirectoryOrFile D) {
	listDir(D, 0);
}

另外,这是一个先序遍历,我愣了…
先序遍历对结点的处理工作是在诸多儿子被处理之前进行的。的确,我们先打印姓名,再对接下来的D是否为目录进行判断并对儿子进行递归处理。我对先序遍历理解的不透彻造成了我的懵圈。

第二个任务:计算该树所有文件占用的磁盘总数

static int sizeDirectory(DirectoryOrFile D) {
	int totalSize = 0;

	if ( D is a legitimate entry ) {
		totalSize = fileSize(D);
		if ( D is a directory ) {
			for each child, C of D
				totalSize += sizeDirectory(C);
		}
	}
	
	return totalSize;
}

D如果不是一个目录,sizeDirectory只返回D所占用块数,否则先处理子结点占用块数再将占用块数大小加入。因此是一个处理子结点优先,操作放在后面的过程,因此这是一个后序遍历。这个任务与LeetCode 112.路经总和这道题非常的像,有兴趣可以去挑战一下。思路上唯一的一点偏差就是这里从0开始加,在这道题中可以用目标sum减至0,其余思路几乎一致。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值