赞
踩
目录
树是一种抽象的数据类型ADT,特点:
树有有序树和无序树,无序树就是任意节点的子节点之间没有顺序关系,也叫自由树,它没有太大的实际意义,因此我们不做讨论,下面主要介绍有序树;有序树
二叉树
霍夫曼树(最优二叉树)
B树(B-tree)
B+树
B*树
红黑树
1.顺序存储
将数据结构存储在固定的数组中,然在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树。二叉树通常以链式存储。
二叉树一般用链式存储.
2.链式存储
1.概念
每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”
2.性质
使用等比数列可以进行推导
3.二叉树的遍历
广度优先遍历:就是按照从左往右从上往下一次进行遍历
深度优先遍历:包含先序遍历(根左右),中序遍历(左根右),后序遍历(左右根)
代码实现:
- class Node:
- def __init__(self, elem, lchild=None, rchild=None):
- self.elem = elem
- self.lchild = lchild
- self.rchild = rchild
-
-
- class Tree:
- def __init__(self, root=None):
- self.root = root
-
- def add(self, elem):
- node = Node(elem) # 创建节点
- if self.root == None: # 如果根节点为None,就说明当前的树是空,那么我们将传入的节点直接当做根节点就可以了
- self.root=node
- else: # 这时候根节点不是空的
- queue=[] # 创建一个队列用来存储存在的节点
- queue.append(self.root) # 我们将根节点加入到队列中
- while queue: # 如果队列中没有了数据,说明可以退出了
- cur=queue.pop(0) # 将队列中的第一个节点弹出来,并把该节点作为当前节点
- if cur.lchild==None: # 如果当前节点的左孩子是None,我们就把node作为该节点的左孩子,并退出
- cur.lchild=node
- return True
- elif cur.rchild==None: # 如果当前节点的右孩子是None,我们就把node作为该节点的右孩子,并退出
- cur.rchild=node
- return True
- else:
- # 该节点的左孩子和右孩子都不是空的,那么我们可以将它的左孩子和右孩子都加入队列中,重新进行判断
- queue.append(cur.lchild)
- queue.append(cur.rchild)
-
- # 广度优先遍历
- def breadth_traversal(self):
- if self.root==None:
- return None
- else:
- queue=[]
- queue.append(self.root)
- while queue:
- cur=queue.pop(0)
- print(cur.elem,'\t',end='')
- if cur.lchild is not None:
- queue.append(cur.lchild)
- if cur.rchild is not None:
- queue.append(cur.rchild)
-
- # 先序遍历:根 左 右
- # 递归的三个注意点:每次递归携带的参数;递归的退出条件;递归都是逆序的
- def preorder(self,node):
- if node==None:
- return None
- else:
- print(node.elem,'\t',end='')
- self.preorder(node.lchild)
- self.preorder(node.rchild)
-
- # 中序遍历
- def midorder(self,node):
- if node==None:
- return None
- else:
- self.midorder(node.lchild)
- print(node.elem, '\t', end='')
- self.midorder(node.rchild)
-
- # 后序遍历
- def postorder(self,node):
- if node==None:
- return None
- else:
- self.postorder(node.lchild)
- self.postorder(node.rchild)
- print(node.elem, '\t', end='')
-
- if __name__ == '__main__':
- tree=Tree()
- tree.add(1)
- tree.add(2)
- tree.add(7)
- tree.add(4)
- tree.add(5)
- tree.breadth_traversal()
- print()
- tree.preorder(tree.root)
- print()
- tree.midorder(tree.root)
- print()
- tree.postorder(tree.root)

注意:得到当前节点,在队列中就会删除当前节点,下一次取第一个节点的时候就是新的节点,这个新节点没有改变
逆推二叉树
已知先序和中序或者已知后序和中序我们就可以得到二叉树了,技巧:拿先序和中序举例,先序第一个二根节点,根据这个值找到中序中对应的值,通过中序将先序中的值分隔成两份,以此类推,从被分割的两份中找到第一个值,拿着该值去中序中寻找,根据中序中找的位置,我们又可以根据被分割的数量中将先序得到的结果进行分割,....
概念:
设二叉树的深度二k,除第k层外,其他各层节点树都达到了最大值,k层所有的节点都连续集中在最左边
特点:
图解:
特点:
图解
特点:
平衡二叉树的左子树和右子树的深度之差的绝对值不超过1.
1.概念:
二叉查找树的左孩子小于当前结点的值,右孩子大于当前节点的值.图解:
2.创建二叉查找树:
3.删除二叉查找树节点
4.二叉查找树增加节点
与查找操作相似,由于二叉搜索树的特殊性,待插入的节点也需要从根节点开始进行比较,小于根节点则与根节点左子树比较,反之则与右子树比较,直到左子树为空或右子树为空,则插入到相应为空的位置,在比较的过程中要注意保存父节点的信息 及 待插入的位置是父节点的左子树还是右子树,才能插入到正确的位置。
5.特点
1.基本概念
2.构造霍夫曼树
霍夫曼编码:编码规则是从根节点出发,向左标记为0,向右标记为1。
构造方式:构造方法详解,在这里感谢这位小哥的解答.此篇博文中出现同权不同构问题,这个问题中尽管他们的构造方式不同,但是他们的权值是相同的,这种情况是不会影响结果的,具体请查看相关资料和书籍.
C++实现构造
- #include <iostream>
- #include <stdlib.h>
- using namespace std;
- const int MaxValue = 10000;//初始设定的权值最大值
- const int MaxBit = 4;//初始设定的最大编码位数
- const int MaxN = 10;//初始设定的最大结点个数
- struct HaffNode//哈夫曼树的结点结构
- {
- int weight;//权值
- int flag;//标记
- int parent;//双亲结点下标
- int leftChild;//左孩子下标
- int rightChild;//右孩子下标
- };
- struct Code//存放哈夫曼编码的数据元素结构
- {
- int bit[MaxBit];//数组
- int start;//编码的起始下标
- int weight;//字符的权值
- };
- void Haffman(int weight[], int n, HaffNode haffTree[])
- //建立叶结点个数为n权值为weight的哈夫曼树haffTree
- {
- int j, m1, m2, x1, x2;
- //哈夫曼树haffTree初始化。n个叶结点的哈夫曼树共有2n-1个结点
- for (int i = 0; i<2 * n - 1; i++)
- {
- if (i<n)
- haffTree[i].weight = weight[i];
- else
- haffTree[i].weight = 0;
- //注意这里没打else那{},故无论是n个叶子节点还是n-1个非叶子节点都会进行下面4步的初始化
- haffTree[i].parent = 0;
- haffTree[i].flag = 0;
- haffTree[i].leftChild = -1;
- haffTree[i].rightChild = -1;
- }
- //构造哈夫曼树haffTree的n-1个非叶结点
- for (int i = 0; i<n - 1; i++)
- {
- m1 = m2 = MaxValue;//Maxvalue=10000;(就是一个相当大的数)
- x1 = x2 = 0;//x1、x2是用来保存最小的两个值在数组对应的下标
-
- for (j = 0; j<n + i; j++)//循环找出所有权重中,最小的二个值--morgan
- {
- if (haffTree[j].weight<m1&&haffTree[j].flag == 0)
- {
- m2 = m1;
- x2 = x1;
- m1 = haffTree[j].weight;
- x1 = j;
- }
- else if(haffTree[j].weight<m2&&haffTree[j].flag == 0)
- {
- m2 = haffTree[j].weight;
- x2 = j;
- }
- }
- //将找出的两棵权值最小的子树合并为一棵子树
- haffTree[x1].parent = n + i;
- haffTree[x2].parent = n + i;
- haffTree[x1].flag = 1;
- haffTree[x2].flag = 1;
- haffTree[n + i].weight = haffTree[x1].weight + haffTree[x2].weight;
- haffTree[n + i].leftChild = x1;
- haffTree[n + i].rightChild = x2;
- }
- }
- void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[])
- //由n个结点的哈夫曼树haffTree构造哈夫曼编码haffCode
- {
- Code *cd = new Code;
- int child, parent;
- //求n个叶结点的哈夫曼编码
- for (int i = 0; i<n; i++)
- {
- //cd->start=n-1;//不等长编码的最后一位为n-1,
- cd->start = 0;//,----修改从0开始计数--morgan
- cd->weight = haffTree[i].weight;//取得编码对应权值的字符
- child = i;
- parent = haffTree[child].parent;
- //由叶结点向上直到根结点
- while (parent != 0)
- {
- if (haffTree[parent].leftChild == child)
- cd->bit[cd->start] = 0;//左孩子结点编码0
- else
- cd->bit[cd->start] = 1;//右孩子结点编码1
- //cd->start--;
- cd->start++;//改成编码自增--morgan
- child = parent;
- parent = haffTree[child].parent;
- }
- //保存叶结点的编码和不等长编码的起始位
- //for(intj=cd->start+1;j<n;j++)
- for (int j = cd->start - 1; j >= 0; j--)//重新修改编码,从根节点开始计数--morgan
- haffCode[i].bit[cd->start - j - 1] = cd->bit[j];
-
- haffCode[i].start = cd->start;
- haffCode[i].weight = cd->weight;//保存编码对应的权值
- }
- }
- int main()
- {
- int i, j, n = 4, m = 0;
- int weight[] = { 2,4,5,7 };
- HaffNode*myHaffTree = new HaffNode[2 * n - 1];
- Code*myHaffCode = new Code[n];
- if (n>MaxN)
- {
- cout << "定义的n越界,修改MaxN!" << endl;
- exit(0);
- }
- Haffman(weight, n, myHaffTree);
- HaffmanCode(myHaffTree, n, myHaffCode);
- //输出每个叶结点的哈夫曼编码
- for (i = 0; i<n; i++)
- {
- cout << "Weight=" << myHaffCode[i].weight << " Code=";
- //for(j=myHaffCode[i].start+1;j<n;j++)
- for (j = 0; j<myHaffCode[i].start; j++)
- cout << myHaffCode[i].bit[j];
- m = m + myHaffCode[i].weight*myHaffCode[i].start;
- cout << endl;
- }
- cout << "huffman's WPL is:";
- cout << m;
- cout << endl;
- return 0;
- }

1.基本介绍
B也是一种搜索树,说到B树,首先我们来复习一下AVL树,平衡二叉树任意结点左右子树的高度之差的绝对值最大为等于1,而且平衡二叉树中每个数据元素只能有一个数据元素一个关键字.但是B树就和平衡二叉树不同了,B树中每个数据元素可以有多个数据元素,多个关键字.B树也称为多路平衡查找树.B树中所有节点的孩子结点数的最大值就是B树的阶.下面我们看看B树有哪些特点:
特征:
ki:关键码,真实的数据,存放作为分割线的数据;Ai:指向孩子的指针;n:关键字的个数
相信很多同学会问二叉搜索树和折半查找的性能已经够高了,难道B树的性能更加高?答案是确定的.从算法逻辑上讲,二叉查找数据的查找速度和比较次数确实是最小的,但是我们不得不考虑一个很现实的问题磁盘的IO操作,数据库的索引是存放在磁盘上的,当数据量非常大的时候索引的大小可能就是几个G甚至更多.当我们使用索引进行查找的时候,难道要把整个索引算不加载到内存中,很明显是不可能的,能做的只能对其进行逐一加载,加载每一个磁盘页,这里的磁盘页我们可以看成是索引树的节点,很容易知道磁盘的IO是由树的高度决定的,既然如此,我们可能把树变的矮胖,这样不就减少了磁盘的读写次数了.这样便需要我们的B树.
2.查询流程
我们来举个例子:查询5这个数:
由图我们可以看出当数据量比较大的时候,B树在查询中比较的次数不比二叉树少,尤其是单一节点的元素数量很多的时候,相比磁盘IO的速度,内存的耗时几乎可以忽略,因此只要树的高度足够低,IO次数就足够少,就可以提升查找性能.
3.插入节点
我们要插入数值4
自顶向下查找4的节点位置,发现4应当插入到节点元素3,5之间。节点3,5已经是两元素节点,无法再增加。父亲节点 2, 6 也是两元素节点,也无法再增加。根节点9是单元素节点,可以升级为两元素节点。于是拆分节点3,5与节点2,6,让根节点9升级为两元素节点4,9。节点6独立为根节点的第二个孩子。
4.删除节点
删除数值11
删除11后,节点12只有一个孩子,不符合B树规范。因此找出12,13,15三个节点的中位数13,取代节点12,而节点12自身下移成为第一个孩子。(这个过程称为左旋)
5.应用
主要应用于文件系统,部分数据库的索引(MongoDB),大部分的关系型数据库一般使用的是B+树
1.基本概念
上面说了B树,下面我们来介绍一下B+树,B+树是B树的一种变体,查询的性能要比B树高.
B+树具有B树的特征,在此之外B+树还具有新的特征.一个m阶的B+树具有下面几个特征:
图解:
以后无论是增加元素还是修改元素,元素的最大值都在根节点.由于父节点的元素都出现在子节点,因此所有的叶子节点包含了全部的元素信息,并且每个叶子节点都带有指向下一个叶子节点的指针,形成了一个有序链表.B+树还有一个非常重要的特点,那就是卫星数据的定位,下面我们来了解一下:
卫星数据就是索引元素指向的数据记录,eg:在数据库中具体的某一行,在b树中,无论中间节点还是叶子节点都带有卫星数据:
在B+树中只有叶子节点带有卫星数据,其余中间节点都仅仅是索引,没有任何的数据关联,在数据库的聚集索引中,叶子节点直接包含卫星数据,在非聚集索引中,叶子节点带有指向卫星数据的指针.
2.B+树和B树查询的不同
B+树中间的节点没有卫星数据,同样大小的磁盘页可以容纳更多的节点元素,也就意味着数据量相同的情况下B+数据的结构比B-树更加矮胖,因此查询的IO次数也就更加的少.其次B+树查询必须最终查找到叶子节点,而B树只要找到匹配元素就可以了,也因此B-树的查询性能并不是很稳定,但是B+树的确实稳定的.
查询范围的不同.B树只能依靠中序遍历,我们来查询3-11之间的元素:
下面我们看看B+树的查找范围过程:
自顶向下,找到范围的下限,通过链表指针,遍历到元素6, 8;通过链表指针,遍历到元素9, 11,遍历结束:
3.B+树比B树的优势:
1.基本概念
B树是B+树的变种,而B+树又是B*树的变种,相对于B+树,B树又有他们的不同之处,首先说说他们的特征把:
特征:
评价:
1.基础概念
是一棵BST树,节点是红色或者黑色,根结点和所有叶子节点都是黑色(黑色表示稳定),每个红色节点必须有两个黑色的子节点,从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点
2.为什么要有红黑树?
二叉查找树效率尽管很高,但是还是存在一定的缺陷的.下面我们来看一个例子:
依次插入如下五个节点:7,6,5,4,3;
这样的二叉树不好吧!二叉查找树多次插入新节点导致二叉树不平衡.这样的二叉树性能大打折扣.因此我们需要红黑树来解决这样的问题,红黑树除了具有二叉树的基本特征外,还具有下面特征:
红黑树从根到叶子的最长路径不会超过最短路径的两倍,当插入或者删除节点的时候红黑树的规则可能就会别打破.这时候就需要作出一个调整来维持我们的规则.插入21这个值之后打破了上述红黑树的特征4
3.调整方法:变色和旋转
变色:
左旋转:
右旋转:
4.什么情况下变色?什么情况下旋转?
如果情况比较复杂的时候变色和旋转结合使用:变色 -> 左旋转 -> 变色 -> 右旋转 -> 变色,....一般情况下先使用变色,解决不了就使用旋转.
5.应用场景
其应用有很多,比如有JDK的集合类TreeMap
和TreeSet
底层就是红黑树,在Java8中,HashMap
也是用到了红黑树。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。