数据结构—树的详解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构—树的详解相关的知识,希望对你有一定的参考价值。
参考技术A树是非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。
使用树结构存储的集合 A,B,C,D,E,F,G,H,I,J,K,L,M 的示意图。对于数据 A 来说,和数据 B、C、D 有关系;对于数据 B 来说,和 E、F 有关系。这就是“一对多”的关系。
将具有“一对多”关系的集合中的数据元素按照图中的形式进行存储,整个存储形状在逻辑结构上看,类似于实际生活中倒着的树,所以称这种存储结构为“树型”存储结构。
使用树结构存储的每一个数据元素都被称为“结点”。例如,图 1中,数据元素 A 就是一个结点;
对于图 1中的结点 A、B、C、D 来说,A 是 B、C、D 结点的父结点(也称为“双亲结点”),而 B、C、D 都是 A 结点的子结点(也称“孩子结点”)。对于 B、C、D 来说,它们都有相同的父结点,所以它们互为兄弟结点。
每一个非空树都有且只有一个被称为根的结点。图 1中,结点A就是整棵树的根结点。
树根的判断依据为:如果一个结点没有父结点,那么这个结点就是整棵树的根结点。
如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。例如图 1中,结点 K、L、F、G、M、I、J 都是这棵树的叶子结点。
如图 1中,整棵树的根结点为结点 A,而如果单看结点 B、E、F、K、L 组成的部分来说,也是棵树,而且节点 B 为这棵树的根结点。所以称 B、E、F、K、L 这几个结点组成的树为整棵树的子树;同样,结点 E、K、L 构成的也是一棵子树,根结点为 E。
注意:单个结点也是一棵树,只不过根结点就是它本身。图 1中,结点 K、L、F 等都是树,且都是整棵树的子树。
知道了子树的概念后,树也可以这样定义:树是由根结点和若干棵子树构成的。
如果集合本身为空,那么构成的树就被称为空树。
空树中没有结点。
补充:在树结构中,对于具有同一个根结点的各个子树,相互之间不能有交集。例如,图 1中,除了根结点 A,其余元素又各自构成了三个子树,根结点分别为 B、C、D,这三个子树相互之间没有相同的结点。如果有,就破坏了树的结构,不能算做是一棵树。结点的度和层次
对于一个结点,拥有的子树数(结点有多少分支)称为结点的度(Degree)。例如,图 1中,根结点 A 下分出了 3 个子树,所以,结点 A 的度为 3。
一棵树的度是树内各结点的度的最大值。图 1表示的树中,各个结点的度的最大值为 3,所以,整棵树的度的值是 3。
从一棵树的树根开始,树根所在层为第一层,根的孩子结点所在的层为第二层,依次类推。对于图 1来说,A 结点在第一层,B、C、D 为第二层,E、F、G、H、I、J 在第三层,K、L、M 在第四层。
一棵树的深度(高度)是树中结点所在的最大的层次。图 1树的深度为 4。
如果两个结点的父结点虽不相同,但是它们的父结点处在同一层次上,那么这两个结点互为堂兄弟。例如,图 1中,结点 G 和 E、F、H、I、J 的父结点都在第二层,所以之间为堂兄弟的关系。
如果树中结点的子树从左到右看,谁在左边,谁在右边,是有规定的,这棵树称为有序树;反之称为无序树。
在有序树中,一个结点最左边的子树称为"第一个孩子",最右边的称为"最后一个孩子"。
图 1来说,如果是其本身是一棵有序树,则以结点 B 为根结点的子树为整棵树的第一个孩子,以结点 D 为根结点的子树为整棵树的最后一个孩子。
由 m(m >= 0)个互不相交的树组成的集合被称为森林。图 1中,分别以 B、C、D 为根结点的三棵子树就可以称为森林。
树可以理解为是由根结点和若干子树构成的,而这若干子树本身是一个森林,所以,树还可以理解为是由根结点和森林组成的。用一个式子表示为:
Tree =(root,F),其中,root 表示树的根结点,F 表示由 m(m >= 0)棵树组成的森林。
数据结构中为了存储和查找的方便,用各种树结构来存储文件,我们首先介绍下基本的树的种类:二叉查找树(二叉排序树)、平衡二叉树(AVL树)、红黑树、B-树、B+树、字典树(trie树)、后缀树、广义后缀树。
二叉查找树是一种动态查找表,具有这些性质:
(1)若它的左子树不为空,则左子树上的所有节点的值都小于它的根节点的值;
(2)若它的右子树不为空,则右子树上所有节点的值都大于它的根节点的值;
(3)其他的左右子树也分别为二叉查找树;
(4)二叉查找树是动态查找表,在查找的过程中可见添加和删除相应的元素,在这些操作中需要保持二叉查找树的以上性质。
含有相同节点的二叉查找树可以有不同的形态,而二叉查找树的平均查找长度与树的深度有关,所以需要找出一个查找平均长度最小的一棵,那就是平衡二叉树,具有以下性质:
(1)要么是棵空树,要么其根节点左右子树的深度之差的绝对值不超过1;
(2)其左右子树也都是平衡二叉树;
(3)二叉树节点的平衡因子定义为该节点的左子树的深度减去右子树的深度。则平衡二叉树的所有节点的平衡因子只可能是-1,0,1。
红黑树是一种自平衡二叉树,在平衡二叉树的基础上每个节点又增加了一个颜色的属性,节点的颜色只能是红色或黑色。具有以下性质:
(1)根节点只能是黑色;
(2)红黑树中所有的叶子节点后面再接上左右两个空节点,这样可以保持算法的一致性,而且所有的空节点都是黑色;
(3)其他的节点要么是红色,要么是黑色,红色节点的父节点和左右孩子节点都是黑色,及黑红相间;
(4)在任何一棵子树中,从根节点向下走到空节点的路径上所经过的黑节点的数目相同,从而保证了是一个平衡二叉树。
B-树是一种平衡多路查找树,它在文件系统中很有用。一棵m阶B-树(图为4阶B-树),具有下列性质:
(1)树中每个节点至多有m棵子树;
(2)若根节点不是叶子节点,则至少有2棵子树;
(3)除根节点之外的所有非终端节点至少有 m/2 棵子树;
(4)每个节点中的信息结构为(A0,K1,A1,K2......Kn,An),其中n表示关键字个数,Ki为关键字,Ai为指针;
(5)所有的叶子节点都出现在同一层次上,且不带任何信息,也是为了保持算法的一致性。
B+数是B-树的一种变形,它与B-树的差别在于(图为3阶B+树):
(1)有n棵子树的节点含有n个关键字;
(2)所有的叶子节点包含了全部关键字的信息,及指向这些关键字记录的指针,且叶子节点本身按关键字大小自小到大顺序链接;
(3)所有非终端节点可以看成是索引部分,节点中仅含有其子树(根节点)中最大(或最小)关键字,所有B+树更像一个索引顺序表;
(4)对B+树进行查找运算,一是从最小关键字起进行顺序查找,二是从根节点开始,进行随机查找。
字典树是一种以树形结构保存大量字符串。以便于字符串的统计和查找,经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。具有以下特点:
(1)根节点为空;
(2)除根节点外,每个节点包含一个字符;
(3)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
(4)每个字符串在建立字典树的过程中都要加上一个区分的结束符,避免某个短字符串正好是某个长字符串的前缀而淹没。
所谓后缀树,就是包含一则字符串所有后缀的压缩了的字典树。先说说后缀的定义。给定一长度为n的字符串S=S 1 S 2 ..S i ..S n ,和整数i,1 <= i <= n,子串S i S i+1 ...S n 都是字符串S的后缀。以字符串S=XMADAMYX为例,它的长度为8,所以S[1..8], S[2..8], ... , S[8..8]都算S的后缀,我们一般还把空字串也算成后缀。这样,我们一共有如下后缀。对于后缀S[i..n],我们说这项后缀起始于i。
所有这些后缀字符串组成一棵字典树:
上图,我们可以看到不少值得压缩的地方。比如蓝框标注的分支都是独苗,没有必要用单独的节点同边表示。如果我们允许任意一条边里包含多个字母,就可以把这种没有分叉的路径压缩到一条边。另外每条边已经包含了足够的后缀信息,我们就不用再给节点标注字符串信息了。我们只需要在叶节点上标注上每项后缀的起始位置。于是我们得到下图:
这样的结构丢失了某些后缀。比如后缀X在上图中消失了,因为它正好是字符串XMADAMYX的前缀。为了避免这种情况,我们也规定每项后缀不能是其它后缀的前缀。要解决这个问题其实挺简单,在待处理的子串后加一个空字串就行了。例如我们处理XMADAMYX前,先把XMADAMYX变为 XMADAMYX$,于是就得到suffix tree。
这就形成一棵后缀树了。关于如何建立一棵后缀树,已有很成熟的算法,能在o(n)时间内解决。
广义后缀树是好几个字符串的的所有后缀组成的字典树,同样每个字符串的所有后缀都具有一个相同的结束符,不同字符串的结束符不同。
传统的后缀树只能处理一个单词的所有后缀。广义后缀树存储任意多个单词的所有后缀。例如字符串“abab”和“baba”,首先将它们使用特殊结束符链接起来,如表示成"ababbaba#",然后求连接后的新字符的后缀树,遍历所得后缀树,如遇到特殊字符,如"baba#",然后求连接后的新字符的后缀树,遍历所得后缀树,如遇到特殊字符,如"","#"等则去掉以该节点为跟的子树,最后所得后缀树即为原字符串组的广义后缀树。其实质是将两个字符串的所有后缀,即:abab,bab,bab,ab,b,b,baba#,aba#,ba#,a#,组成字典树,再进行压缩处理。
广义后缀树的一个常应用就是判断两个字符串的相识度。
简单地理解,满足以下两个条件的树就是二叉树:
二叉树的性质经过前人的总结,二叉树具有以下几个性质:
性质 3 的计算方法为:对于一个二叉树来说,除了度为 0 的叶子结点和度为 2 的结点,剩下的就是度为 1 的结点(设为 n1),那么总结点 n=n 0 +n 1 +n 2 。
同时,对于每一个结点来说都是由其父结点分支表示的,假设树中分枝数为 B,那么总结点数 n=B+1。而分枝数是可以通过 n1 和 n2 表示的,即 B=n 1 +2 n 2 。所以,n 用另外一种方式表示为 n=n 1 +2 n 2 +1。
两种方式得到的 n 值组成一个方程组,就可以得出 n 0 =n 2 +1。
二叉树还可以继续分类,衍生出满二叉树和完全二叉树。
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
满二叉树除了满足普通二叉树的性质,还具有以下性质:
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
如图 3a) 所示是一棵完全二叉树,图 3b) 由于最后一层的节点没有按照从左向右分布,因此只能算作是普通的二叉树。
完全二叉树除了具有普通二叉树的性质,它自身也具有一些独特的性质,比如说,n 个结点的完全二叉树的深度为 [log 2 n ]+1。
[log 2 n ]表示取小于[log 2 n ]的最大整数。例如,[[log 2 4 ]] = 2,而 [[log 2 5 ]] 结果也是 2。
对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号(如图 3a)),对于任意一个结点 i ,完全二叉树还有以下几个结论成立:
树与二叉树数据结构详解
一、树的基本概念
1.树的知识框架
1.树的定义
树是n(n>=0)个结点的有限集。当n = 0时,称为空树。在任意一棵非空树中应满足:
- 有且仅有一个特定的称为根的结点。
- 当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,并且称为根的子树。
显然,树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中所有结点可以有零个或多个后继。
因此n个结点的树中有n-1条边。
3.树的基本术语
结合图示来说明一下树的一些基本术语和概念。
- 考虑结点K。根A到结点K的唯一路径上的任意结点,称为结点K的祖先。如结点B是结点K的祖先,而结点K是结点B的子孙。路径上最接近结点K的结点E称为K的双亲,而K为结点E的孩子。根A是树中唯一没有双亲的结点。有相同双亲的结点称为兄弟,如结点K和结点L有相同的双亲E,即K和L为兄弟。
- 树中其中某个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度。如结点B的度为2,结点D的度为3(此结点度最大),树的度为3。
- 度大于0的结点称为分支结点(又称非终端结点);度为0(没有孩子结点)的结点称为叶子结点(又称终端结点)。在分支结点中,每个结点的分支数就是该结点的度。
- 结点的深度、高度和层次。
结点的层次从树根开始定义,根结点为第1层,它的子结点为第2层,以此类推。双亲在同一层的结点互为堂兄弟,图中结点G与E,F,H,I,J互为堂兄弟。
结点的深度是从根结点开始自顶向下逐层累加的。它可以是某一层,例如结点F所在的深度为3,而L结点所在的深度为4。
结点的高度是从叶结点开始自底向上逐层累加的。高度只讲的是树的高度,树的高度为4。
树的高度(或深度)是树中结点的最大层数。图中树的高度为4。 - 有序树和无序树。**树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。**假设图为有序树,若将子结点位置互换,则变成一棵不同的树。
- 路径和路径长度。树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
注意:由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。 - 森林。森林是m (m≥0)棵互不相交的树的集合。森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林。反之,只要给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。一棵树也可以称作一个森林。
4.树的性质
注意其中两个性质:
1、
其中这两个性质是可以互相转化的,只要记住其中一个性质就行。
2、
这个性质可以推为:
1.已经父亲结点的下标为i,则其左孩子的结点为2i+1,右孩子的结点为2i+2。
2.已知孩子结点n,推父亲节点:(n-1)/2 。
面试题:
比如:假设一棵完全二叉树中总共有1000个节点,则该二叉树中多少个叶子节点,多少个非叶子节点,多少个节点只有左孩子,多少个节点只有右孩子。
答:因为该二叉树是一棵完全二叉树,所以不可能有结点是只有右孩子的,因此最后一个空为0。
再求出该二叉树一共有多少层,因为有1000个结点,又由log2的(n+1)向上取整得深度为10,但是第十层还没放满(放满的结点数为2的10次方-1)。我们还可以求出10层前的结点一共有多少个,由2的9次方-1得511,所以第十层放的是1000-511=489个结点。
因此第十层的489个结点都为叶子结点,但是不要忘了第十层是未放满的,因此推出第九层中也是有叶子结点的。将第十层的叶子结点除2,得第9层的非叶子结点为:489/2=244.5,说明有第九层非叶子结点中有一个结点是只有左节点的。第九层结点个数:2的(9-1)次方=256个。因此非叶子结点有245个。求得第九层的叶子结点为256-245=11个。所以一共加起来的叶子结点有11+489=500个。
又因为二叉树的结点数是由叶子结点和非叶子结点组成的,所以非叶子结点有1000-500==500个,叶子结点有500个,只有左孩子的只有1个,没有只有右孩子的结点。
5.树的存储结构
二叉树的链式存储是通过一个一个的节点引用起来的,常见的表示方式有二叉和三叉表示方式,具体如下:
// 孩子表示法
class Node
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
// 孩子双亲表示法
class Node
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
Node parent; // 当前节点的根节点
其实二叉树如何表示,主要是看创建的TreeNode结点类是如何设置来存储二叉树中的结点的。不过大多数情况下都是采取孩子表示法来构建二叉树。
二、二叉树的操作
1.二叉树的遍历
所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如:打印节点内容、节点内容加1)。 遍历是二叉树上最重要的操作之一,是二叉树上进行其它运算之基础。
在遍历二叉树时,如果没有进行某种约定,每个人都按照自己的方式遍历,得出的结果就比较混乱,如果按照某种规则进行约定,则每个人对于同一棵树的遍历结果肯定是相同的。如果N代表根节点,L代表根节点的左子树,R代表根节点的右子树,则根据遍历根节点的先后次序有以下遍历方式:
二叉树的遍历分为:
1.NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点—>根的左子树—>根的右子树。
2.LNR:中序遍历(Inorder Traversal)——根的左子树—>根节点—>根的右子树。
3.LRN:后序遍历(Postorder Traversal)——根的左子树—>根的右子树—>根节点。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
2.二叉树的基本操作
import java.util.*;
class TreeNode
public char val;
public TreeNode left;
public TreeNode right;
public TreeNode(char val)
this.val = val;
public class BinaryTree
public TreeNode creatTree()
TreeNode A = new TreeNode('A');
TreeNode B = new TreeNode('B');
TreeNode C = new TreeNode('C');
TreeNode D = new TreeNode('D');
TreeNode E = new TreeNode('E');
TreeNode F = new TreeNode('F');
TreeNode G = new TreeNode('G');
TreeNode H = new TreeNode('H');
A.left = B;
B.left = D;
B.right = E;
E.right = H;
A.right = C;
C.left = F;
C.right = G;
return A;
// 前序遍历
void preOrderTraversal(TreeNode root)
if (root == null)
return;
System.out.print(root.val + " ");
preOrderTraversal(root.left);
preOrderTraversal(root.right);
// 中序遍历
void inOrderTraversal(TreeNode root)
if (root == null)
return;
inOrderTraversal(root.left);
System.out.print(root.val + " ");
inOrderTraversal(root.right);
// 后序遍历
void postOrderTraversal(TreeNode root)
if (root == null)
return;
postOrderTraversal(root.left);
postOrderTraversal(root.right);
System.out.print(root.val + " ");
// 遍历思路-求结点个数
static int size = 0;
void getSize1(TreeNode root)
if (root == null)
return;
getSize1(root.left);
getSize1(root.right);
size++;
// 子问题思路-求结点个数
int getSize2(TreeNode root)
if (root == null)
return 0;
return getSize2(root.left) + getSize2(root.right) + 1;
// 遍历思路-求叶子结点个数
static int leafSize = 0;
void getLeafSize1(TreeNode root)
if (root == null)
return;
if (root.left == null && root.right == null)
leafSize++;
getLeafSize1(root.left);
getLeafSize1(root.right);
// 子问题思路-求叶子结点个数
int getLeafSize2(TreeNode root)
if (root == null)
return 0;
if (root.left == null && root.right == null)
return 1;
return getLeafSize2(root.left) + getLeafSize2(root.right);
// 子问题思路-求第 k 层结点个数
int getKLevelSize(TreeNode root, int k)
if (root == null)
return 0;
if (k == 1)
return 1;
return getKLevelSize(root.left, k - 1) + getKLevelSize(root.right, k - 1);
// 获取二叉树的高度
int getHeight(TreeNode root)
if (root == null)
return 0;
int leftNode = getHeight(root.left);
int rightNode = getHeight(root.right);
return Math.max(rightNode, leftNode) + 1;
// 查找 val 所在结点,没有找到返回 null
// 按照 根 -> 左子树 -> 右子树的顺序进行查找
// 一旦找到,立即返回,不需要继续在其他位置查找
TreeNode find(TreeNode root, char val)
if (root == null)
return null;
if (root.val == val)
return root;
TreeNode ret = find(root.left, val);
if (ret != null)
return ret;
ret = find(root.right, val);
if (ret != null)
return ret;
return null;
// 层序遍历
void levelOrderTraversal(TreeNode root)
if (root == null)
return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty())
TreeNode top = queue.poll();
System.out.print(top.val + " ");
if (top.left != null)
queue.offer(top.left);
if (top.right != null)
queue.offer(top.right);
System.out.println();
//层序遍历
public List<List<Character>> levelOrder(TreeNode root)
List<List<Character>> list = new ArrayList<>();
if (root == null)
return list;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty())
List<Character> list1 = new ArrayList<>();
int size = queue.size();
while (size != 0)
TreeNode top = queue.poll();
list1.add(top.val);
if (top.left != null)
queue.add(top.left);
if (top.right != null)
queue.add(top.right);
size--;
list.add(list1);
return list;
// 判断一棵树是不是完全二叉树
boolean isCompleteTree(TreeNode root)
if (root == null)
return true;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty())
TreeNode top = queue.poll();
if (top != null)
queue.offer(top.left);
queue.offer(top.right);
else
break;
while(!queue.isEmpty())
TreeNode cur = queue.peek();
if(cur==null)
queue.poll();
else
return false;
return true;
//求二叉树的左视图
public void leftScenery(TreeNode root)
if(root==null)
return ;
List<List<TreeNode>> list = new LinkedList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty())
List<TreeNode> list1 = new LinkedList<>();
int size = queue.size();
while(size!=0)
TreeNode top = queue.poll();
list1.add(top);
if(top.left!=null)
queue.offer(top.left);
if(top.right!=null)
queue.offer(top.right);
size--;
list.add(list1);
for (List<TreeNode> e:list)
for (TreeNode p: e)
System.out.print(p.val+" ");
break;
//二叉树的非递归先序遍历
public void preOrderTraversalNot(TreeNode root)
if(root==null)
return;
TreeNode cur = root;
Stack<TreeNode> stack = new Stack<>();
while(cur!=null||!stack.empty())
while(cur!=null)
stack.push(cur);
System.out.print(cur.val+" ");
cur=cur.left;
TreeNode top = stack.pop();
cur=top.right;
// 中序遍历
public void inOrderTraversalNot(TreeNode root)
if(root==null)
return;
TreeNode cur = root;
Stack<TreeNode> stack = new Stack<>();
while(cur!=null||!stack.empty())
while(cur!=null)
stack.push(cur);
cur=cur.left;
TreeNode top = stack.pop();
System.out.print(top.val+" ");
cur=top.right;
// 后序遍历非递归
public void postOrderTraversalNot(TreeNode root)
if(root==null)
return;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode pre = null;
while(cur!=null||!stack.empty())
while(cur!=null)
stack.push(cur);
cur=cur.left;
cur=stack.peek();
if(cur.right==null||cur.right==pre)
TreeNode top = stack.pop();
System.out.print(top.val+" ");
pre=以上是关于数据结构—树的详解的主要内容,如果未能解决你的问题,请参考以下文章