数据结构--树
Posted ppzhang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构--树相关的知识,希望对你有一定的参考价值。
一、树的概念
1、树的特性
1)一棵树中的任意两个结点有且仅有唯一的一条路径连通;
2)一棵树如果有n个结点,则它一定有n?1条边;
3)在一棵树中加一条边将会构成一个回路。
2、二叉树
1)二叉树是一种特殊的树,二叉树的特点是每个结点最多有两个儿子。
2)二叉树使用范围最广,一颗多叉树也可以转化为二叉树。
3、满二叉树
1)二叉树中每个内部节点都有两个儿子,满二叉树所有的叶节点都有相同的深度。
2)满二叉树是一棵深度为h且有2h?1个结点的二叉树。
4、完全二叉树
1)若设二叉树的高度为h,除了第h层外,其他层的结点数都达到最大个数,第h层从右向左连续 缺若干个结点,则为完全二叉树。
5、树的特点
1. 如果一棵完全二叉树的父节点编号为K,则其左儿子的编号是2K,右儿子的结点编号为2K+1
2. 已知完全二叉树的总节点数为n求叶子节点个数:
当n为奇数时:(n+1)/2
当n为偶数时 : (n)/2
3. 已知完全二叉树的总节点数为n求父节点个数:为:n/2
4. 已知完全二叉树的总节点数为n求叶子节点为2的父节点个数:
当n为奇数时:n/2
当n为偶数时 : n/2-1
5、如果一棵完全二叉树有N个结点,那么这棵二叉树的深度为【log2(N+1)log2(N+1)】(向上取整)
二、二叉树的操作。
参考博客: https://www.cnblogs.com/freeman818/p/7252041.html
1、生成树结构
1. 前序遍历: DBACEGF(根节点排最先,然后同级先左后右)
2. 中序遍历: ABCDEFG (先左后根最后右)
3. 后序遍历: ACBFGED (先左后右最后根)
#! /usr/bin/env python # -*- coding: utf-8 -*- class Node: def __init__(self,value=None,left=None,right=None): self.value=value self.left=left #左子树 self.right=right #右子树 if __name__==‘__main__‘: root=Node(‘D‘,Node(‘B‘,Node(‘A‘),Node(‘C‘)),Node(‘E‘,right=Node(‘G‘,Node(‘F‘))))
#! /usr/bin/env python # -*- coding: utf-8 -*- class Node: def __init__(self,value=None,left=None,right=None): self.value=value self.left=left #左子树 self.right=right #右子树 def preTraverse(root): ‘‘‘ 前序遍历 ‘‘‘ if root==None: return print(root.value) preTraverse(root.left) preTraverse(root.right) if __name__==‘__main__‘: root=Node(‘D‘,Node(‘B‘,Node(‘A‘),Node(‘C‘)),Node(‘E‘,right=Node(‘G‘,Node(‘F‘)))) print(‘前序遍历:‘) preTraverse(root) # DBACEGF
#! /usr/bin/env python # -*- coding: utf-8 -*- class Node: def __init__(self,value=None,left=None,right=None): self.value=value self.left=left #左子树 self.right=right #右子树 def midTraverse(root): ‘‘‘ 中序遍历 ‘‘‘ if root == None: return midTraverse(root.left) print(root.value) midTraverse(root.right) if __name__==‘__main__‘: root=Node(‘D‘,Node(‘B‘,Node(‘A‘),Node(‘C‘)),Node(‘E‘,right=Node(‘G‘,Node(‘F‘)))) print(‘中序遍历:‘) midTraverse(root) # ACBFGED
#! /usr/bin/env python # -*- coding: utf-8 -*- class Node: def __init__(self,value=None,left=None,right=None): self.value=value self.left=left #左子树 self.right=right #右子树 def afterTraverse(root): ‘‘‘ 后序遍历 ‘‘‘ if root == None: return afterTraverse(root.left) afterTraverse(root.right) print(root.value) if __name__==‘__main__‘: root=Node(‘D‘,Node(‘B‘,Node(‘A‘),Node(‘C‘)),Node(‘E‘,right=Node(‘G‘,Node(‘F‘)))) print(‘后序遍历:‘) afterTraverse(root) # ACBFGED
前序排列原理: #####此时执行preTraverse(root.left) 函数 1、第一步 root=Node(D) print D,D入栈[D] 2、第二步 root=Node(D).left=Node(B) print B, B入栈[D,B] 3、第三步 root=Node(B).left=Node(A) print A, A入栈[D,B,A] 4、第四步 root=Node(A).left=None,函数return到上一层 root=Node(A) A出栈[D,B] ####此时执行preTraverse(root.right) 5、第五步 preTraverse(root.right)即Node(A).right==None,函数return到上一层root=Node(B) B出栈 [D] 6、第六步 root=Node(B).right==Node(C),print C C入栈[D,C],打印C后顺序执行preTraverse(root.left) #####此时执行preTraverse(root.left) 函数 7、第七步 root=Node(C).left==None C出栈 [D] ####此时执行preTraverse(root.right) 8、第八步 root=Node(C).right==None D出栈,此时栈为空[] root=Node(D),顺序执行preTraverse(root.right) 9、root=Node(D).right=Node(E) print E ‘‘‘此时输出结果:DBACE‘‘‘
三、哈希树
1、hash树描述(就是散列树)
1. 散列树选择从2开始的连续质数来建立一个十层的哈希树。
2. 第一层结点为根结点,根结点下有2个结点;
3. 第二层的每个结点下有3个结点;
4. 依此类推,即每层结点的子节点数目为连续的质数。
2、hash树特点
注:关系型数据库中,索引大多采用B/B+树来作为存储结构,而全文搜索引擎的索引则主要采用hash的存储结构,这两种数据结构有什么区别?
1. 如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;
2. 当然了,这个前提是,键值都是唯一的,如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;
3. 如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,
有可能变成不连续的了,就没办法再利用索引完成范围查询检索;
4. 同理,哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);
3、建立hash树
1. 选择从2开始的连续质数来建立一个十层的哈希树。
2. 第一层结点为根结点,根结点下有2个结点;第二层的每个结点下有3个结点;
3. 依此类推,即每层结点的子节点数目为连续的质数。到第十层,每个结点下有29个结点。
4. 同一结点中的子结点,从左到右代表不同的余数结果。
例如:第二层结点下有三个子节点。那么从左到右分别代表:除3余0,除3余1,除3余2.对质数进行取余操作得到的余数决定了处理的路径。
5. 以随机的10个数的插入为例,来图解HashTree的插入过程。
6. 其实也可以把所有的键-值节点放在哈希树的第10层叶节点处,这第10层的满节点数就包含了所有的整数个数,
但是如果这样处理的话,所有的非叶子节点作为键-值节点的索引,这样使树结构庞大,浪费空间。
4、查找编辑
1. 哈希树的节点查找过程和节点插入过程类似,就是对关键字用质数序列取余,根据余数确定下一节点的分叉路径,直到找到目标节点。
2. 如上图,最小”哈希树(HashTree)在从4G个对象中找出所匹配的对象,比较次数不超过10次,也就是说:最多属于O(10)。
3. 在实际应用中,调整了质数的范围,使得比较次数一般不超过5次。
4. 也就是说:最多属于O(5),因此可以根据自身需要在时间和空间上寻求一个平衡点。
5、删除编辑
1. 哈希树的节点删除过程也很简单,哈希树在删除的时候,并不做任何结构调整。
2. 只是先查到到要删除的节点,然后把此节点的“占位标记”置为false即可(即表示此节点为空节点,但并不进行物理删除)。
6、hash树优点
1)结构简单
1. 从哈希树的结构来说,非常的简单,每层节点的子节点个数为连续的质数。
2. 子节点可以随时创建,因此哈希树的结构是动态的,也不像某些哈希算法那样需要长时间的初始化过程。
3. 哈希树也没有必要为不存在的关键字提前分配空间。
2)查找迅速
1. 从算法过程我们可以看出,对于整数,哈希树层级最多能增加到10。
2. 因此最多只需要十次取余和比较操作,就可以知道这个对象是否存在,这个在算法逻辑上决定了哈希树的优越性。
3)结构不变
1. 从删除算法中可以看出,哈希树在删除的时候,并不做任何结构调整。
2. 常规树结构在增加元素和删除元素的时候都要做一定的结构调整,否则他们将可能退化为链表结构,而导致查找效率的降低。
3. 哈希树采取的是一种“见缝插针”的算法,从来不用担心退化的问题,也不必为优化结构而采取额外的操作,因此大大节约了操作时间。
7、缺点编辑
1. 哈希树不支持排序,没有顺序特性。
2. 如果在此基础上不做任何改进的话并试图通过遍历来实现排序,那么操作效率将远远低于其他类型的数据结构。
8、hash索引使用范围
总结:哈希适用在小范围的精确查找,在列数据很大,又不需要排序,不需要模糊查询,范围查询时有用
1、hash索引仅满足“=”、“IN”和“<=>”查询,不能使用范围查询
因为hash索引比较的是经常hash运算之后的hash值,因此只能进行等值的过滤,不能基于范围的查找,
因为经过hash算法处理后的hash值的大小关系,并不能保证与处理前的hash大小关系对应。
2、hash索引无法被用来进行数据的排序操作
由于hash索引中存放的都是经过hash计算之后的值,而hash值的大小关系不一定与hash计算之前的值一样,
所以数据库无法利用hash索引中的值进行排序操作。
3、对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,
而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
4、Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。
对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。
这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
四、二分查找树和平衡二叉树 (AVL Tree)
1、二叉查找树
二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。
对该二叉树的节点进行查找发现深度为1的节点的查找次数为1,深度为2的查找次数为2,深度为n的节点的查找次数为n。
一个极端的二叉树:
这棵二叉树的查询效率就低了。因此若想二叉树的查询效率尽可能高,需要这棵二叉树是平衡的,从而引出新的定义——平衡二叉树,或称AVL树。
2.平衡二叉树(VAL Tree)
平衡二叉树的定义:
平衡二叉树(AVL树)在符合二叉查找树的条件下,还满足任何节点的两个子树的高度最大差为1
1.左边是AVL树,它的任何节点的两个子树的高度差<=1;
2.右边的不是AVL树,其根节点的左子树高度为3,而右子树高度为1.
平衡二叉树的插入或者删除:
AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。
LL:LeftLeft,也称“左左”。插入或删除一个节点后,根节点的左孩子(Left Child)的左孩子(Left Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
RR:RightRight,也称“右右”。插入或删除一个节点后,根节点的右孩子(Right Child)的右孩子(Right Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
LR:LeftRight,也称“左右”。插入或删除一个节点后,根节点的左孩子(Left Child)的右孩子(Right Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
RL:RightLeft,也称“右左”。插入或删除一个节点后,根节点的右孩子(Right Child)的左孩子(Left Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
让平衡二叉树重新保持平衡的方法:
LL的旋转。LL失去平衡的情况下,可以通过一次旋转让AVL树恢复平衡。步骤如下:
- 将根节点的左孩子作为新根节点。
- 将新根节点的右孩子作为原根节点的左孩子。
- 将原根节点作为新根节点的右孩子。
RR的旋转:RR失去平衡的情况下,旋转方法与LL旋转对称,步骤如下:
- 将根节点的右孩子作为新根节点。
- 将新根节点的左孩子作为原根节点的右孩子。
- 将原根节点作为新根节点的左孩子。
LR的旋转:LR失去平衡的情况下,需要进行两次旋转,步骤如下:
- 围绕根节点的左孩子进行RR旋转。
- 围绕根节点进行LL旋转。
RL的旋转:RL失去平衡的情况下也需要进行两次旋转,旋转方法与LR旋转对称,步骤如下:
- 围绕根节点的右孩子进行LL旋转。
- 围绕根节点进行RR旋转。
五、平衡多路查找树(B-TREE)
B-Tree是为磁盘等外存储设备设计的一种平衡查找树。
磁盘的读取与B-TREE
1.系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么
2.InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB。
3.系统一个磁盘块的存储空间往往没有这么大,因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB。
4.InnoDB在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。
5.B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。
B-TREE的数据结构与特性
B-Tree的数据结构:
1.定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。
2.对于不同的记录,key值互不相同。
一棵m阶的B-Tree有如下特性:
1. 每个节点最多有m个孩子。
2. 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
3. 若根节点不是叶子节点,则至少有2个孩子
4. 所有叶子节点都在同一层,且不包含其它关键字信息。
5. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
6. 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
7. ki(i=1,…n)为关键字,且关键字升序排序。
8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)
B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个3阶的B-Tree:
1.每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。
2.两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。
3.以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】 比较关键字29在区间(17,35),找到磁盘块1的指针P2。 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】 比较关键字29在区间(26,30),找到磁盘块3的指针P2。 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】 在磁盘块8中的关键字列表中找到关键字29。
B-TREE的性能分析:
1.分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。
2.由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。
3.3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。
4.B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。
六、B+TREE
B+TREE的定义和使用场景:
B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。
B-TREE和B+TREE的比较:
1.从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。
2.每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小。
3.当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。
4.在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,
5.这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度
B+Tree相对于B-Tree有几点不同:
a.非叶子节点只存储键值信息。
b.所有叶子节点之间都有一个链指针。
c.数据记录都存放在叶子节点中。
B+Tree后其结构如下图所示
1.B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点
2.所有叶子节点(即数据节点)之间是一种链式环结构。
3.因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
1.InnoDB存储引擎中页的大小为16KB, 2.一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节), 3.指针类型也一般为4或8个字节, 4.也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。 5.也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。
聚集索引(clustered index)和辅助索引(secondary index)
聚集索引:上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。
辅助索引:1.辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。
2.当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。
B-tree与哈希索引的区别
1)B-tree的索引:
是按照顺序存储的,所以,如果按照B-tree索引,可以直接返回,带顺序的数据,但这个数据只是该索引列含有的信息。因此是顺序I/O
适用于: 精确匹配 、范围匹配 、最左匹配
2)Hash索引:
索引列值的哈希值+数据行指针:因此找到后还需要根据指针去找数据,造成随机I/O
适合: 精确匹配
不适合: 模糊匹配 、范围匹配 、不能排序
以上是关于数据结构--树的主要内容,如果未能解决你的问题,请参考以下文章
LeetCode810. 黑板异或游戏/455. 分发饼干/剑指Offer 53 - I. 在排序数组中查找数字 I/53 - II. 0~n-1中缺失的数字/54. 二叉搜索树的第k大节点(代码片段