接下来我们将会介绍另外一种数据结构——树。二叉树是树这种数据结构的一员,后面我们还会介绍红黑树,2-3-4树等数据结构。那么为什么要使用树?它有什么优点?
前面我们介绍数组的数据结构,我们知道对于有序数组,查找很快,并介绍可以通过二分法查找,但是想要在有序数组中插入一个数据项,就必须先找到插入数据项的位置,然后将所有插入位置后面的数据项全部向后移动一位,来给新数据腾出空间,平均来讲要移动N/2次,这是很费时的。同理,删除数据也是。
然后我们介绍了另外一种数据结构——链表,链表的插入和删除很快,我们只需要改变一些引用值就行了,但是查找数据却很慢了,因为不管我们查找什么数据,都需要从链表的第一个数据项开始,遍历到找到所需数据项为止,这个查找也是平均需要比较N/2次。
那么我们就希望一种数据结构能同时具备数组查找快的优点以及链表插入和删除快的优点,于是 树 诞生了。
一、树的简介
1、树的简介
树是一种非线性的数据结构,是由n(n >=0)个结点组成的有限集合。
如果n==0,树为空树。
如果n>0,
树有一个特定的结点,根结点
根结点只有直接后继,没有直接前驱。
除根结点以外的其他结点划分为m(m>=0)个互不相交的有限集合,T0,T1,T2,...,Tm-1,每个结合是一棵树,称为根结点的子树。
树的示例如下:
2、树的度
树的结点包含一个数据和多个指向子树的分支
结点拥有的子树的数量为结点的度,度为0的结点是叶结点,度不为0的结点为分支结点,树的度定义为树的所有结点中度的最大值。
3、树的前驱和后继
结点的直接后继称为结点的孩子,结点称为孩子的双亲。
结点的孩子的孩子称为结点的孙子,结点称为子孙的祖先。
同一个双亲的孩子之间互称兄弟。
4、树中结点的层次
树中根结点为第1层,根结点的孩子为第2层,依次类推。
树中结点的最大层次称高度为树的深度或
对于任何节点n, n的深度为从根到n的唯一路径的长,因此根的深度为0;
n的高是从n到一片树叶(没有儿子的节点)的最长路径的长,因此所有的树叶的高都是0;
一颗树的高等于它根的高,一棵树的深度等他最深的树叶的深度,该深度总是等于这棵树的高。
5、树的有序性
如果树中结点的各子树从左向右是有序的,子树间不能互换位置,则称该树为有序树,否则为无序树。
6、森林
森林是由n棵互不相交的树组成的集合。
三棵树组成的森林如下:
树的效率:查找节点的时间取决于这个节点所在的层数,每一层最多有2n-1个节点,总共N层共有2n-1个节点,那么时间复杂度为O(logn),底数为2。
①树的基本概念
树(Tree)是n(n>=0)个结点的有限集。
在任意一棵非空树中:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,...,Tn,其中每个集合本身又是一棵树,并称为根的子树(SubTree)
层次 树
1 A
/ | \
2 B C D
/ \ | / | \
3 E F G H I J
/ \ |
4 K L M
★树的结点包含一个数据元素及若干指向其子树的分支。
(1)度
结点拥有的子树树称为结点的度(Degree)如:上图A的度为3,C的度为1,F的度为0。
度为0的结点称为叶子(Leaf)或终端结点,如:上图K,L,F,G,M,I,J都是树的叶子
度不为0的结点称为非终端结点或分支结点,如:上图A,B,C,D,E,H
★树的度是树内各节点的度的最大值,如:上图的树的度为 3 。
(2)结点(家谱图)
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)如:上图D是A的孩子,A是D的双亲。
同一个双亲的孩子叫兄弟(Sibling)如:上图H,I,J为互为兄弟
其双亲在同一层的结点互为堂兄弟。如上图G与E、F、H、I、J互为堂兄弟
结点的祖先是从根到该结点所经分支上的所有结点。如:上图M的祖先为A、D、H
(3)层次,深度(你家几代同堂啊?)
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层,
树中结点的最大层次成为树的深度(Depth)或高度。如:上图树的深度为4
(4)有序树与无序树(长子,次子。。)
树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则成为无序树。
有序树中最左的子树的根称为第一个孩子,最右边的称为最后一个孩子。(毕竟有序,排好了谁是老大,谁是老二,长兄有序嘛)
★森林(Forest)是m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
由此也可以森林和树相互递归的定义来描述树。
1、二叉树的定义及其主要特征
二叉树(Binary Tree)是另一种树形结构,
★二叉树的特点是:(1)每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点),
(2)二叉树的子树有左右之分,其次序不能随意颠倒。
二叉树要么为空,要么是由一个根结点加上两棵分别称为左子树和右子树的,互不相交的二叉树组成。
★二叉树的五种形态
| | A | A | A
Φ | A | B | B C | B
(1) | (2) | (3) | (4) | (5)
★二叉树的性质
(性质一)在二叉树的第 i 层上至多有 2^(i-1)个结点 (i >=1)
第一层 :1(2^0) 第二层:2(2^1) 第三层:4(2^2) 第四层:8(2^3)
(性质二)深度为 k 的二叉树至多有2^k -1个结点(k>=1)
由性质一得:第k层,2^k - 1 个结点
1+2+2^2+....+2^(k-1) = ( 1 - 2^k)/(1-2) = 2^k - 1
(性质三)★对任何一棵二叉树 T ,如果其终端结点数位n0,度为2的结点数位n2,则n0=n2+1。
式一: n = n0 + n1 + n2 (结点总数 等于 度为0 加 度为1 加 度为2)
式二: n = n0 + 2*n2 +1(n = 分支总数+1 ;分支总数 = n1+n2 (分支是由度为1,度为2的结点射出的))
式二 - 式一得: n0 = n2 + 1
树的介绍
1. 树的定义
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。
把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
(01) 每个节点有零个或多个子节点;
(02) 没有父节点的节点称为根节点;
(03) 每一个非根节点有且只有一个父节点;
(04) 除了根节点外,每个子节点可以分为多个不相交的子树。
2. 树的基本术语
若一个结点有子树,那么该结点称为子树根的"双亲",子树的根是该结点的"孩子"。有相同双亲的结点互为"兄弟"。一个结点的所有子树上的任何结点都是该结点的后裔。从根结点到某个结点的路径上的所有结点都是该结点的祖先。
结点的度:结点拥有的子树的数目。
叶子:度为零的结点。
分支结点:度不为零的结点。
树的度:树中结点的最大的度。
层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1。
树的高度:树中结点的最大层次。
无序树:如果树中结点的各子树之间的次序是不重要的,可以交换位置。
有序树:如果树中结点的各子树之间的次序是重要的, 不可以交换位置。
森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。
二叉树的介绍
1. 二叉树的定义
二叉树是每个节点最多有两个子树的树结构。它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。
2. 二叉树的性质
二叉树有以下几个性质:TODO(上标和下标)
性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1)。
性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1)。
性质3:包含n个结点的二叉树的高度至少为log2 (n+1)。
性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
2.1 性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1)
证明:下面用"数学归纳法"进行证明。
(01) 当i=1时,第i层的节点数目为2{i-1}=2{0}=1。因为第1层上只有一个根结点,所以命题成立。
(02) 假设当i>1,第i层的节点数目为2{i-1}。这个是根据(01)推断出来的!
下面根据这个假设,推断出"第(i+1)层的节点数目为2{i}"即可。
由于二叉树的每个结点至多有两个孩子,故"第(i+1)层上的结点数目" 最多是 "第i层的结点数目的2倍"。即,第(i+1)层上的结点数目最大值=2×2{i-1}=2{i}。
故假设成立,原命题得证!
2.2 性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1)
证明:在具有相同深度的二叉树中,当每一层都含有最大结点数时,其树中结点数最多。利用"性质1"可知,深度为k的二叉树的结点数至多为:
20+21+…+2k-1=2k-1
故原命题得证!
2.3 性质3:包含n个结点的二叉树的高度至少为log2 (n+1)
证明:根据"性质2"可知,高度为h的二叉树最多有2{h}–1个结点。反之,对于包含n个节点的二叉树的高度至少为log2(n+1)。
2.4 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1
证明:因为二叉树中所有结点的度数均不大于2,所以结点总数(记为n)="0度结点数(n0)" + "1度结点数(n1)" + "2度结点数(n2)"。由此,得到等式一。
(等式一) n=n0+n1+n2
另一方面,0度结点没有孩子,1度结点有一个孩子,2度结点有两个孩子,故二叉树中孩子结点总数是:n1+2n2。此外,只有根不是任何结点的孩子。故二叉树中的结点总数又可表示为等式二。
(等式二) n=n1+2n2+1
由(等式一)和(等式二)计算得到:n0=n2+1。原命题得证!
3. 满二叉树,完全二叉树和二叉查找树
3.1 满二叉树
定义:高度为h,并且由2{h} –1个结点的二叉树,被称为满二叉树。
3.2 完全二叉树
定义:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。
特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。
3.3 二叉查找树
定义:二叉查找树(Binary Search Tree),又被称为二叉搜索树。设x为二叉查找树中的一个结点,x节点包含关键字key,节点x的key值记为key[x]。如果y是x的左子树中的一个结点,则key[y] <= key[x];如果y是x的右子树的一个结点,则key[y] >= key[x]。
在二叉查找树中:
(01) 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(02) 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(03) 任意节点的左、右子树也分别为二叉查找树。
(04) 没有键值相等的节点(no duplicate nodes)。
(性质二)如果对一棵有n个结点的完全二叉树(其深度为」 log 2 n」+1 )的结点按层序编号(从第一层到最后一层,每层从左到右),对任一结点i(1<= i <=n)有
(1)如果 i =1,则结点 i 是二叉树的根,无双亲;
如果 i >1,则其双亲Parent( i ) 是结点 」i / 2」
(2)如果2*i >n,则结点 i 无左孩子(结点 i 为叶子节点) ;否则其左孩子LChild( i )是结点2*i。
(3)如果2*i+1>n,则结点 i 无右孩子;否则其右孩子RChild( i )是结点2*i+1。
【这个性质,对于建立二叉树,有着非凡的意义】
且看下面这个完全二叉树:
1
/ \
2 3 [ i / 2 ]
/ \ / \ / \
4 5 6 7 i i+1
/ \ / \ / \ / \ / \ / \
8 9 10 11 12 13 14 15 2 i 2 i+1 2 i+2 2 i+3
【性质二通俗版】从下往上看:结点1是根,没双亲;其他点的双亲是 i / 2 取下值
从上往下看:左孩子 是根节点的两倍(偶数),右孩子是根节点的两倍+1(奇数)
树的两种实现
从上述概念可以得知,树是一个递归的概念,从根节点开始,每个节点至多只有一个父节点,有多个子节点,每个子节点又是一棵树,以此递归。
树有两种实现方式:
- 数组
- 链表
数组表示:
我们可以利用每个节点至多只有一个父节点这个特点,使用 父节点表示法 来实现一个节点:
用数组实现的树表示下面的树,(其中一种 )结果就是这样的:
实现一颗树,采用数组的存储方式,将树中的节点用Node类表示,方便与操作。
首先,整棵树的数组结构如下表所示,根节点的无父节点,用“-1”表示。
index | Data | parent |
0 | A | -1 |
1 | B | 0 |
2 | C | 0 |
3 | D | 0 |
4 | E | 1 |
5 | F | 1 |
6 | G | 5 |
其次,定义一个节点Node类,用来存储每个节点的内容
其次,定义一个节点Node类,用来存储每个节点的内容
package my.tree;
public class Node<T> {
private T data;
private int parent;
public Node(){
}
public Node(T data){
this.data = data;
}
public Node(T data,int parent){
this.data = data;
this.parent = parent;
}
public void setData(T data){
this.data = data;
}
public T getData(){
return this.data;
}
public void setParent(int parent){
this.parent = parent;
}
public int getParent(){
return this.parent;
}
}
开始实现树,提供基本的插入节点、获取所有节点、获取树的深度操作(着重)这些将数组大小设置为2,主要是考虑数组能否自动扩容;
package my.tree;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MyTree<T> {
private final int DEFAULT_SIZE = 2;
private int size;
private int count;
private Object[] nodes;
public MyTree() {
this.size = this.DEFAULT_SIZE;
this.nodes = new Object[this.size];
this.count = 0;
}
public MyTree(Node<T> root) {
this();
this.count = 1;
this.nodes[0] = root;
}
public MyTree(Node<T> root, int size) {
this.size = size;
this.nodes = new Object[this.size];
this.count = 1;
this.nodes[0] = root;
}
//添加一个节点
public void add(Node<T> node) {
for (int i = 0; i < this.size; i++) {
if (this.nodes[i] == null) {
nodes[i] = node;
break;
}
}
this.count++;
}
public void check(){
if(this.count >= this.size){
this.enlarge();
}
}
//添加一个节点,并指明父节点
public void add(Node<T> node, Node<T> parent) {
this.check();
node.setParent(this.position(parent));
this.add(node);
}
//获取节点在数组的存储位置
public int position(Node<T> node) {
for (int i = 0; i < this.size; i++) {
if (nodes[i] == node) {
return i;
}
}
return -1;
}
//获取整棵树有多少节点
public int getSize(){
return this.count;
}
//获取根节点
@SuppressWarnings("unchecked")
public Node<T> getRoot(){
return (Node<T>) this.nodes[0];
}
//获取所以节点,以List形式返回
@SuppressWarnings("unchecked")
public List<Node<T>> getAllNodes(){
List<Node<T>> list = new ArrayList<Node<T>>();
for(int i=0;i<this.size;i++){
if(this.nodes[i] != null){
list.add((Node<T>)nodes[i]);
}
}
return list;
}
//获取树的深度,只有根节点时为1
@SuppressWarnings("unchecked")
public int getDepth(){
int max = 1;
if(this.nodes[0] == null){
return 0;
}
for(int i=0;i<this.count;i++){
int deep = 1;
int location = ((Node<T>)(this.nodes[i])).getParent();
while(location != -1 && this.nodes[location] != null){
location = ((Node<T>)(this.nodes[location])).getParent();
deep++;
}
if(max < deep){
max = deep;
}
}
return max;
}
public void enlarge(){
this.size = this.size + this.DEFAULT_SIZE;
Object[] newNodes = new Object[this.size];
newNodes = Arrays.copyOf(nodes, this.size);
Arrays.fill(nodes, null);
this.nodes = newNodes;
System.out.println("enlarge");
}
}
平衡二叉树
平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。 最小二叉平衡树的节点总数的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci(斐波那契)数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。
我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。
平衡二叉搜索树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。常用算法有红黑树、AVL、Treap、伸展树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log(n)),大大降低了操作的时间复杂度。
文章结构:
什么是二叉排序树(bst)
二叉排序树(Binary Sort Tree)又称二叉查找树。 它或者是一棵空树;或者是具有下列性质的二叉树: (1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值; (2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值; (3)左、右子树也分别为二叉排序树;
好了二叉排序树定义很好理解(如果还不理解,为了不浪费时间,先暂停一下,去google or baidu 下,理解了再继续),再此就不举别的例子了,下面我实现下BST的一些基本操作算法。
BST的基本操作
typedef struct _BitNode
{
int data;
struct _BitNode *lchild,*rchild;
}BitNode,*BiTree;
1.1 ,BST的搜索:
为什么先实现搜索呢?一般BST里面没有重复的元素,你增添或者删除元素,都必须要先查找一下,看有没有呀,所以BST搜索要先实现,这个搜索是很简单的,慢慢看我讲解吧,
我们先看这张图
假如你想找到数值为3的节点并给你这个树的根节点,且规定你只能看到这个根节点左右孩子(其他你们权限看到,也看不到)。那不就是很容易啦,先用3和根节点(此为6)比,显然比6小。那我就去找他的左孩子比,注意,此时4就是6的左子树的根了,那我们就和4这个新的树根比吧,显然又比4小。那我们就继续找这个新根的左孩子比,而此时3就是4的左子树的根了,那我们就和这个根比,哇塞,我们顺藤摸瓜,终于找到了哦!!,那我们就提炼一下这个过程吧,注意哦,我们每次都是和树根相比较的哦!
规则:
1.先和这棵树的根比
2.如果比这个树根小就和这个树根的左子树的根比,否则就和这个树根的右子树比。
3.重复2过程,直到根为空为止。
根据3个步骤很容易实现递归代码。
//搜索元素,参数依次为0: 根节点,1: 查找的元素,2: 找到目标元素的前一个节点指针,初始值为NULL
// 3:如果返回真把目标到元素的指针指向n,返回假,就把pre复制给n)(参数如果不明白,先不要细究,往下看吧)
什么是AVL
定义:
平衡二叉树定义(AVL):它或者是一颗空树,或者具有以下性质的二叉树:它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。
平衡因子(bf):结点的左子树的深度减去右子树的深度,那么显然-1<=bf<=1,这里我们定义:
#define EH 0,#define LH 1,#define RH -1.依次为等高,左高,右高。
typedef struct _BitNode
{
int data;
int bf;//平衡因子
struct _BitNode *lchild,*rchild;
}BitNode,*BiTree;
我们都知道,平衡二叉树是在二叉排序树(BST)上引入的(这一点很重要哦,下图为例),就是为了解决二叉排序树的不平衡性导致时间复杂度大大下降,那么AVL就保持住了(BST)的最好时间复杂度O(logn),所以每次的插入和删除都要确保二叉树的平衡,那么怎么保持平衡呢?如果还不理解看看下面的图吧。
看看AVL的魅力吧。有它就有它存在的价值,看下图便知。
图一和图二都是BST,但图二不是AVL,图一是AVL Tree,如果我们要找到10,图一比较次数为3,而图二比较次数为7次,很显然,在规模比较大的话AVL优势就很突出了。既然AVL这么强大,牛叉。那我们就把它拿下吧。