赞
踩
目录
先来看一下什么是二叉查找树
对于二叉查找树中的任意一个节点,其左子树中每个节点的值都要小于这个节点的值,而右子树中每个节点的值都要大于这个节点的值
我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。
- public TreeNode searchBST(TreeNode root, int val) {
- if(root==null)return null;
- if(root.val>val)return searchBST(root.left,val);
- if(root.val<val)return searchBST(root.right,val);
- return root;
- }
我们从根节点开始,依次比较要插入的数据和节点的大小关系
- public TreeNode insertIntoBST(TreeNode root, int val) {
- //找到空位置插入新节点
- if(root==null)return new TreeNode(val);
- if(root.val<val)root.right=insertIntoBST(root.right,val);
- if(root.val>val)root.left=insertIntoBST(root.left,val);
- return root;
- }
针对要删除节点的子节点个数的不同,我们需要分三种情况来处理:
A
恰好是末端节点,两个子节点都为空,那么它可以当场去世了。A
只有一个非空子节点,那么它要让这个孩子接替自己的位置。A
有两个子节点,麻烦了,为了不破坏 BST 的性质,A
必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。- public TreeNode deleteNode(TreeNode root, int key) {
- if(root==null)return null;
- if(root.val<key)root.right=deleteNode(root.right,key);
- if(root.val>key)root.left=deleteNode(root.left,key);
- if(root.val==key){
- //处理情况 1 和 2
- if(root.left==null)return root.right;
- if(root.right==null)return root.left;
- //处理情况 3
- TreeNode minNode=getMin(root.right);
- //删除 右子树的最小节点
- root.right=deleteNode(root.right,minNode.val);
- //用右子树的最小节点替换 root 节点
- minNode.left=root.left;
- minNode.right=root.right;
- //注意
- root=minNode;
- }
- return root;
- }
- public TreeNode getMin(TreeNode root){
- while (root.left!=null){
- root=root.left;
- }
- return root;
- }
针对包含值相同的节点的二叉查找树,有两种存储方式。
对于同一组数据,我们可以构造不同的二叉查找树
不管操作是插入、删除还是查找,时间复杂度其实都跟树的高度成正比
第一种二叉查找树,根节点的左右子树极度不平衡,已经退化成了链表,所以查找的时间复杂度就变成了O(n)。
最理想的情况下二叉查找树是一棵完全二叉树(或满二叉树),如上图第三种,插入、删除、查找操作的时间复杂度是O(logn)。
二叉查找树在频繁的动态更新过程中,可能会出现树的高度远大于log n的情况,从而导致各个操作的效率下降。为了避免时间复杂度的退化,针对二叉查找树,引出了一种更加复杂的树,平衡二叉查找树,时间复杂度可以做到稳定的O(logn)
平衡二叉树的严格定义:二叉树中任意一个节点的左右子树的高度相差不能大于1。
平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。
红黑树是平衡二叉树的一种,红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:
根节点是黑色的;
每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
当然红黑树同时还要满足二叉查找树的特点
平衡二叉查找树其实有很多,比如AVL(严格符合平衡二叉树的定义)、Splay Tree(伸展树)、Treap(树堆)等,但是在实际工程开发中,对于很多需要平衡二叉查找树的地方,更多会选择使用红黑树。
散列表的插入、删除、查找操作的时间复杂度可以做到常量级的O(1),非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找操作时间复杂度才是O(logn),相对散列表,好像并没有什么优势。那为什么还要用二叉查找树呢?
比如Java中:
有人可能会疑惑为什么这里会涉及到IO次数呢?假设给一亿个数据构建二叉查找树索引,那索引中会包含大约1亿个节点,每个节点假设占用16个字节,那就需要大约1GB的内存空间。给一张表建立索引,我们需要1GB的内存空间。如果我们要给10张表建立索引,那对内存的需求是无法满足的。所以文件系统的索引都是保存在磁盘当中的。
计算机存储设备一般分为两种:内存储器(main memory)和外存储器(external memory)
磁盘是一个扁平的圆盘(与电唱机的唱片类似)。盘面上有许多称为磁道的圆圈,数据就记录在这些磁道上。磁盘可以是单片的,也可以是由若干盘片组成的盘组,每一盘片上有两个面。如下图中所示的6片盘组为例,除去最顶端和最底端的外侧面不存储数据之外,一共有10个面可以用来保存信息。
磁盘上数据必须用一个三维地址唯一标示:柱面号(用来定位盘面上的磁道)、盘面号(选择盘面)、块号(定位是哪个扇区)
读/写磁盘上某一指定数据需要下面3个步骤:
经过上面三个步骤,指定数据的存储位置就被找到。这时就可以开始读/写操作
磁盘读取数据是以盘块(扇区)为基本单位的。位于同一盘块中的所有数据都能被一次性全部读取出来。而磁盘IO代价主要花费在查找时间上。因此我们应该尽量将相关信息存放在同一盘块,同一磁道中。或者至少放在同一柱面或相邻柱面上,以求在读/写信息时尽量减少磁头来回移动的次数,避免过多的查找时间Ts。
所以,在大规模数据存储方面,大量数据存储在外存磁盘中,而在外存磁盘中读取/写入块(block)中某数据时,首先需要定位到磁盘中的某块,如何有效地查找磁盘中的数据,需要一种合理高效的外存数据结构
当内存读取B文件索引时,程序需要将根节点从磁盘读取到内存中。如果下一个子节点也存储在磁盘中,程序需要从磁盘中读取该节点。为了减少磁盘I/O操作的次数,程序通常会将多个磁盘块读入内存中,这些块中至少包含下一个要访问的子节点。这个操作被称为预读。
如果B文件的节点和数据存储在不同的磁盘块中,程序可能需要多次从磁盘中读取数据才能获取完整的节点或数据。这会导致频繁的磁盘I/O操作,降低程序的性能。
程序在内存中访问B文件的节点和数据时,也需要考虑页的大小。如果一个节点或数据的大小超过了一页的大小,程序需要将其分成多个段,每个段存储在不同的页中。这个过程被称为分页或分块。
概念解析
页(datapage):页是内存操作的基本单位,页一般由操作系统决定是多大,一般是4k。我们在数据交互时,可以取页的整数倍来进行读取。
我们可以看一个word文档的属性
实际大小只有11.5KB,但是占用空间却是12.0KB(4的倍数)
磁盘块(扇区):磁盘块(扇区)是文件系统用来管理磁盘空间的基本单位。
为了降低树的高度,所以我们引出了B树
B树是一种多路搜索树,它的每个节点可以拥有多于两个孩子节点。M路的B树最多能拥有M个孩子节点(即最多有M-1个关键字)
特点(个人觉得这些特点不用死记,不想看的可以跳过)
- 除根节点外,其他节点至少有M/2(向上取整)个孩子节点
- 每个结点的值(索引) 都是按递增次序排列存放的,并遵循左小右大原则。
- 根结点 的 子节点 个数为 [2,M]。
- B树的所有叶子结点都位于同一层。
每个节点的结构
示例
这里推荐一个演示网站,非常形象:B-Tree Visualization (usfca.edu)
我们选择最大度数为5之后,插入一组数据: 100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250 。然后观察一些数据插入过程中,节点的变化情况。
一旦节点存储的key数量到达5,就会裂变,中间元素会向上分裂
B树中每个节点都可以存放表的行记录数据,每个节点的读取可以视为一次I/O读取,树的高度表示最多的I/O次数,在相同数量的总记录个数下,每个节点的记录个数越多,高度越低,查询所需的I/O次数越少;假设,硬盘一次I/O数据为16K,一条记录占1K,理论上一个节点最多可以放16个记录,16 ×16 ×16 = 2^12=4096条,当需要存100000条数据时,依然可能导致树高度剧增。
B树和B+树相比的一个特点是 内节点(非叶子节点)也存储了 表中的一行记录。我们可以发现节点的data部分其实占了很大的空间,相比之下指针部分和关键字部分占得却很少。为了进一步提高磁盘的访问效率,就产生了B+树
B+树与B树最大的不同是内部结点不保存记录数据,只保存关键字,用于查找,所有记录数据都保存在叶子结点中。由于每个非叶子节点只存放关键字,这样节点中能存放的关键字数量就更多,树的结构就更加矮小,访问磁盘的次数就更少。
上图是标准的B+Tree的数据结构,MySQL索引数据结构对经典的B+Tree进行了优化。在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能,利于排序。
特点(个人觉得这些特点不用死记,不想看的可以跳过)
B+树中的节点不存储数据,只是索引,而B树中的节点存储数据;
通过双向链表将叶子节点串联在一起,这样可以方便按区间查找;
每个节点中子节点的个数不能超过m,也不能小于m/2;
根节点的子节点个数可以不超过m/2,这是一个例外;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。