rename dir
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
## Bitmap
|
||||
|
||||
也就是用1个(或几个)bit位来标记某个元素对应的value(如果是1bitmap,就只能是元素是否存在;如果是x-bitmap,还可以是元素出现的次数等信息)。使用bit位来存储信息,在需要的存储空间方面可以大大节省。应用场景有:
|
||||
|
||||
|
||||
1. 排序(如果是1-bitmap,就只能对无重复的数排序)
|
||||
2. 判断某个元素是否存在
|
||||
|
||||
比如,某文件中有若干8位数字的电话号码,要求统计一共有多少个不同的电话号码?
|
||||
|
||||
分析:8位最多99 999 999, 如果1Byte表示1个号码是否存在,需要95MB空间,但是如果1bit表示1个号码是否存在,则只需要 95/8=12MB 的空间。这时,数字k(0~99 999 999)与bit位的对应关系是:
|
||||
|
||||
```
|
||||
#define SIZE 15*1024*1024
|
||||
char a[SIZE];
|
||||
memset(a,0,SIZE);
|
||||
|
||||
// a[k/8]这个字节中的 `k%8` 位命中,置为1
|
||||
// 这里要注意 big-endian 和 little-endian的问题 ,假设这里是big-endian
|
||||
a[k/8] = a[k/8] | (0x01 << (k%8))
|
||||
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
|
||||
## Bloom filter(布隆过滤器)
|
||||
|
||||
Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。
|
||||
|
||||
|
||||
### Bloom filter 特点
|
||||
|
||||
为了说明Bloom Filter存在的重要意义,举一个实例:假设要你写一个网络蜘蛛(web crawler)。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”。为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL。给一个URL,怎样知道蜘蛛是否已经访问过呢?稍微想想,就会有如下几种方案:
|
||||
|
||||
1. 将访问过的URL保存到数据库。
|
||||
2. 用HashSet将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。
|
||||
3. URL经过MD5或SHA-1等单向哈希后再保存到HashSet或数据库。
|
||||
4. BitMap方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。
|
||||
|
||||
方法1~3都是将访问过的URL完整保存,方法4则只标记URL的一个映射位。以上方法在数据量较小的情况下都能完美解决问题,但是当数据量变得非常庞大时问题就来了。
|
||||
|
||||
方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?
|
||||
方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,就需要5GB内存。
|
||||
方法3:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。
|
||||
方法4消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。还记得数据结构课上学过的Hash表冲突的各种解决方法么?若要降低冲突发生的概率到1%,就要将BitSet的长度设置为URL个数的100倍。
|
||||
|
||||
实质上上面的算法都忽略了一个重要的隐含条件:允许小概率的出错,不一定要100%准确!也就是说少量url实际上没有没网络蜘蛛访问,而将它们错判为已访问的代价是很小的——大不了少抓几个网页呗。
|
||||
|
||||
|
||||
### Bloom filter 算法
|
||||
|
||||
Bloom filter可以看做是对bitmap的扩展。只是使用多个hash映射函数,从而减低hash发生冲突的概率。算法如下:
|
||||
|
||||
1. 创建 m 位的bitset,初始化为0, 选中k个不同的哈希函数
|
||||
2. 第 i 个hash 函数对字符串str 哈希的结果记为 h(i,str) ,范围是(0,m-1)
|
||||
3. 将字符串记录到bitset的过程:对于一个字符串str,分别记录h(1,str),h(2,str)...,h(k,str)。 然后将bitset的h(1,str),h(2,str)...,h(k,str)位置1。也就是将一个str映射到bitset的 k 个二进制位。
|
||||
|
||||
4. 检查字符串是否存在:对于字符串str,分别计算h(1,str)、h(2,str),...,h(k,str)。然后检查BitSet的第h(1,str)、h(2,str),...,h(k,str) 位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则“认为”字符串str存在。但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被Bloom Filter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为false positive 。
|
||||
|
||||
5. 删除字符串:字符串加入了就被不能删除了,因为删除会影响到其他字符串。实在需要删除字符串的可以使用Counting bloomfilter(CBF)。
|
||||
|
||||
|
||||
`Bloom Filter 使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。`
|
||||
|
||||
|
||||
|
||||
### 最优的哈希函数个数,位数组m大小
|
||||
|
||||
哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。
|
||||
|
||||
在原始个数位n时,那这里的k应该取多少呢?位数组m大小应该取多少呢?这里有个计算公式:`k=(ln2)*(m/n)`, 当满足这个条件时,错误率最小。
|
||||
|
||||
|
||||
假设错误率为0.01, 此时m 大概是 n 的13倍,k大概是8个。 这里的n是元素记录的个数,m是bit位个数。如果每个元素的长度原大于13,使用Bloom Filter就可以节省内存。
|
||||
|
||||
|
||||
### 错误率估计
|
||||
|
||||
|
||||
|
||||
### 实现示例
|
||||
|
||||
```
|
||||
#define SIZE 15*1024*1024
|
||||
char a[SIZE]; /* 15MB*8 = 120M bit空间 */
|
||||
memset(a,0,SIZE);
|
||||
|
||||
int seeds[] = { 5, 7, 11, 13, 31, 37, 61};
|
||||
|
||||
int hashcode(int cap,int seed, string key){
|
||||
int hash = 0;
|
||||
for (int i=0;i<key.length();i++){
|
||||
hash = (seed*hash +key.charAt(i));
|
||||
}
|
||||
return hash & (cap-1);
|
||||
}
|
||||
```
|
||||
|
||||
对每个字符串str求哈希就可以使用 `hashcode(SIZE*8,seeds[i],str)` ,i 的取值范围就是 (0,k)。
|
||||
|
||||
|
||||
## Bloom filter应用
|
||||
|
||||
* 拼写检查一类的字典应用
|
||||
* 数据库系统
|
||||
* 网络领域(爬虫,web cache sharing)
|
||||
|
||||
|
||||
## 参考
|
||||
http://www.cnblogs.com/heaad/archive/2011/01/02/1924195.html
|
||||
http://blog.csdn.net/jiaomeng/article/details/1495500
|
||||
http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html `哈希函数个数k、位数组大小m` 测试论证
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
## Hash映射,分而治之
|
||||
|
||||
这里的`Hash映射`是指通过一种映射散列的方式,将海量数据均匀分布在对应的内存或更小的文件中
|
||||
|
||||
使用hash映射有个最重要的特点是: `hash值相同的两个串不一定一样,但是两个一样的字符串hash值一定相等`。哈希函数如下:
|
||||
|
||||
```
|
||||
int hash = 0;
|
||||
for (int i=0;i<s.length();i++){
|
||||
hash = (R*hash +s.charAt(i)%M);
|
||||
}
|
||||
```
|
||||
|
||||
大文件映射成多个小文件。具体操作是,比如要拆分到100(M)个文件:
|
||||
|
||||
1. 对大文件中的每条记录求hash值,然后对M取余数,即 `hash(R)%M`,结果为K
|
||||
2. 将记录R按结果K分配到第K个文件,从而完成数据拆分
|
||||
|
||||
这样,两条相同的记录肯定会被分配到同一个文件。
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## 海量数据处理
|
||||
|
||||
所谓海量数据,就是数据量太大,要么在短时间内无法计算出结果,要么数据太大,无法一次性装入内存。
|
||||
|
||||
针对时间,我们可以使用巧妙的算法搭配合适的数据结构,如bitmap/堆/trie树等
|
||||
针对空间,就一个办法,大而化小,分而治之。常采用hash映射
|
||||
|
||||
|
||||
* Hash映射/分而治之
|
||||
* Bitmap
|
||||
* Bloom filter(布隆过滤器)
|
||||
* 双层桶划分
|
||||
* Trie树
|
||||
* 数据库索引
|
||||
* 倒排索引(Inverted Index)
|
||||
* 外排序
|
||||
* simhash算法
|
||||
* 分布处理之Mapreduce
|
||||
|
||||
|
||||
### 估算
|
||||
|
||||
在处理海量问题之前,我们往往要先估算下数据量,能否一次性载入内存?如果不能,应该用什么方式拆分成小块以后映射进内存?每次拆分的大小多少合适?以及在不同方案下,大概需要的内存空间和计算时间。
|
||||
|
||||
比如,我们来了解下以下常见问题`时间` 和 `空间` 估算 :
|
||||
|
||||
```
|
||||
8位的电话号码,最多有99 999 999个
|
||||
IP地址
|
||||
1G内存,2^32 ,差不多40亿,40亿Byte*8 = 320亿 bit
|
||||
|
||||
```
|
||||
|
||||
|
||||
海量处理问题常用的分析解决问题的思路是:
|
||||
|
||||
* 分而治之/Hash映射 + hash统计/trie树/红黑树/二叉搜索树 + 堆排序/快速排序/归并排序
|
||||
* 双层桶划分
|
||||
* Bloom filter 、Bitmap
|
||||
* Trie树/数据库/倒排索引
|
||||
* 外排序
|
||||
* 分布处理之 Hadoop/Mapreduce
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -0,0 +1,2 @@
|
||||
|
||||
## simhash算法
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## 倒排索引(Inverted Index)
|
||||
|
||||
|
||||
常规的索引是文档到关键词的映射,就是每个文档指向一个它所包含的索引项的序列,也就是文档文档指向了它包含的索引项序列,也就是文档指向它包含的哪些单词。
|
||||
|
||||
倒排索引也叫反向索引。是文档检索系统中最常用的数据结构。被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。
|
||||
常被应用于搜索引擎和关键字查询的问题中。比如文档检索系统中,查询哪些文件包含了某个单词。
|
||||
|
||||
而反向索引则是单词(关键词)指向包含了它的文档们。比如:
|
||||
|
||||
```
|
||||
T0 = "it is what it is"
|
||||
T1 = "what is it"
|
||||
T2 = "it is a banana"
|
||||
```
|
||||
|
||||
得到的反向文件索引是:
|
||||
|
||||
```
|
||||
"a": {2}
|
||||
"banana": {2}
|
||||
"is": {0, 1, 2}
|
||||
"it": {0, 1, 2}
|
||||
"what": {0, 1}
|
||||
```
|
||||
|
||||
当我们以”what” “is” “it”检索条件去查询时,结果集是:{0,1}∩{0,1,2}∩{0,1,2} = {0,1} 。
|
||||
也就是结果集为文档0和文档1,但其实精确匹配的是文档1。
|
||||
|
||||
|
||||
### 数据结构
|
||||
|
||||
- 将文章提炼关键词,且每个关键词都对应这篇文章
|
||||
- 在加入新文章时,我们需要为新文章中的关键词索引和之前的合并
|
||||
|
||||
|
||||
|
||||
### 参考
|
||||
|
||||
[Elasticsearch](https://github.com/elastic/elasticsearch) 就是使用倒排索引(inverted index)的结构来做快速的全文搜索; ElasticSearch 不仅用于全文搜索, 还有非常强大的统计功能 (facets),携程,58,美团的分享中都提到ES构建实时日志系统,帮助定位系统问题。
|
||||
|
||||
|
||||
[Elasticsearch权威指南](http://es.xiaoleilu.com/index.html)
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
### 分布处理之Mapreduce
|
||||
|
||||
MapReduce是Google提出的一个软件架构,用于大规模数据集(大于1TB)的并行运算。概念“Map(映射)”和“Reduce(归纳)”,及他们的主要思想,都是从函数式编程语言借来的,还有从矢量编程语言借来的特性。MapReduce的伟大之处就在于让不熟悉并行编程的程序员也能充分发挥分布式系统的威力。
|
||||
|
||||
|
||||
##### Mapreduce工作原理
|
||||
|
||||
举一个例子:10年内所有论文(当然有很多很多篇)里面出现最多的几个单词。
|
||||
|
||||
我们把论文集分层N份,一台机器跑一个作业。这个方法跑得快,但是有部署成本,需要把程序copy到别的机器,要把论文分N份,且还需要最后把N个运行结果整合起来。这其实就是Mapreduce本质。
|
||||
|
||||
|
||||
map函数和reduce函数是交给用户实现的,这两个函数定义了任务本身。
|
||||
|
||||
* map函数:接受一个键值对(key-value pair),产生一组中间键值对。MapReduce框架会将map函数产生的中间键值对里键相同的值传递给一个reduce函数。Map操作是可以高度并行的。
|
||||
* reduce函数:接受一个键,以及相关的一组值,将这组值进行合并产生一组规模更小的值(通常只有一个或零个值)。
|
||||
|
||||
|
||||
|
||||
##### Hadoop
|
||||
|
||||
谷歌技术有"三宝",GFS、MapReduce和大表(BigTable)。
|
||||
|
||||
Hadoop实际上就是谷歌三宝的开源实现,Hadoop MapReduce对应Google MapReduce,HBase对应BigTable,HDFS对应GFS。HDFS(或GFS)为上层提供高效的非结构化存储服务,HBase(或BigTable)是提供结构化数据服务的分布式数据库,Hadoop MapReduce(或Google MapReduce)是一种并行计算的编程模型,用于作业调度。
|
||||
|
||||
Hadoop 使用java实现。
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
|
||||
## 双层桶划分
|
||||
|
||||
双层桶不是一种数据结构,只是一种算法思维。分而治之思想。
|
||||
|
||||
当我们有一大推数据需要处理时,局限于各种资源限制(主要说内存)不能一次处理完成,这是需要将一大堆数据分成多个小段数据。通过处理各个小段数据完成最终任务。
|
||||
|
||||
双层这里是虚指,并不是一定把数据分成2份,也可能多份。比如下面几个问题:
|
||||
|
||||
1. 2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
|
||||
2. 5亿个int找它们的中位数
|
||||
|
||||
第一个问题,2.5亿(2^32=4,294,967,296)个数,我们将这2^32个数分到2^8=256个区域(文件中)。每个文件中的平均数字个数差不多 2^24个(1千7百万个)。
|
||||
0~2^24 第一个文件,2^24~2^25第二个文件
|
||||
|
||||
|
||||
假设32位机,装下这些数字需要的内存是 2^24*4=2^26=64MB,也可以不用将文件一次性读入内存而是采用流式读取。
|
||||
|
||||
然后对每个文件使用bitmap处理,每2bit(2-bitmap)表示一个整数,00表示整数未出现,01表示出现一次,10表示出现两次及其以上。这样,每个文件2^24个数字,最大数2^32/(8/2)=2^30=1GB内存
|
||||
|
||||
这个问题倒是更新是bitmap的应用,没有很好体现双层桶分治的优势。
|
||||
|
||||
|
||||
第二个问题,首先我们将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
|
||||
|
||||
|
||||
|
||||
适用问题领域是:`top-k,中位数,不重复或重复的数字`
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
## 外排序
|
||||
|
||||
|
||||
对磁盘文件的排序。将待处理的数据不能一次装入内存,先读入部分数据排序后输出到临时文件,采用「排序-归并」的策略。在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
|
||||
|
||||
`多路归并`,`最小堆`
|
||||
|
||||
比如,要对900 MB的数据进行排序,但机器上只有100 MB的可用内存时,外归并排序按如下方法操作:
|
||||
|
||||
1. 读入100 MB的数据至内存中,用某种常规方式(如快速排序、堆排序、归并排序等方法)在内存中完成排序。
|
||||
2. 将排序完成的数据写入磁盘。
|
||||
3. 重复步骤1和2直到所有的数据都存入了不同的100 MB的块(临时文件)中。在这个例子中,有900 MB数据,单个临时文件大小为100 MB,所以会产生9个临时文件。
|
||||
4. 读入每个临时文件(顺串)的前10 MB( = 100 MB / (9块 + 1))的数据放入内存中的输入缓冲区,最后的10 MB作为输出缓冲区。(实践中,将输入缓冲适当调小,而适当增大输出缓冲区能获得更好的效果。)
|
||||
5. 执行`九路归并`算法,将结果输出到输出缓冲区。一旦输出缓冲区满,将缓冲区中的数据写出至目标文件,清空缓冲区。一旦9个输入缓冲区中的一个变空,就从这个缓冲区关联的文件,读入下一个10M数据,除非这个文件已读完。这是“外归并排序”能在主存外完成排序的关键步骤 -- 因为“归并算法”(merge algorithm)对每一个大块只是顺序地做一轮访问(进行归并),每个大块不用完全载入主存。
|
||||
|
||||
|
||||
为了增加每一个有序的临时文件的长度,可以采用`置换选择排序`(Replacement selection sorting)。它可以产生大于内存大小的顺串。具体方法是在内存中使用一个`最小堆`进行排序,设该最小堆的大小为M。算法描述如下:
|
||||
|
||||
1. 初始时将输入文件读入内存,建立最小堆。
|
||||
2. 将堆顶元素输出至输出缓冲区。然后读入下一个记录:
|
||||
* 若该元素的关键码值不小于刚输出的关键码值,将其作为堆顶元素并调整堆,使之满足堆的性质;
|
||||
* 否则将新元素放入堆底位置,将堆的大小减1。
|
||||
3. 重复第2步,直至堆大小变为0。
|
||||
4. 此时一个顺串已经产生。将堆中的所有元素建堆,开始生成下一个顺串。[3]
|
||||
|
||||
此方法能生成平均长度为2M的顺串,可以进一步减少访问外部存储器的次数,节约时间,提高算法效率。
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
|
||||
## 数据库索引
|
||||
|
||||
索引使用的数据结构多是B树或B+树。B树和B+树广泛应用于文件存储系统和数据库系统中,mysql使用的是B+树,oracle使用的是B树,Mysql也支持多种索引类型,如b-tree 索引,哈希索引,全文索引等。
|
||||
|
||||
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
|
||||
|
||||
|
||||
### 磁盘数据查找过程
|
||||
|
||||

|
||||
|
||||
盘面:每一个盘片都有2个上下盘面,每个盘面都可以存储数据
|
||||
|
||||
柱面:所有盘面上的同一磁道构成一个圆柱,叫做柱面。磁盘读写按柱面进行;
|
||||
只在同一柱面所有的磁头全部读/写完毕后磁头才转移到下一柱面,因为选取磁头只需通过电子切换即可,而选取柱面则必须通过机械切换。电子切换相当快,比在机械上磁头向邻近磁道移动快得多,所以,数据的读/写按柱面进行,而不按盘面进行。也就是说,一个磁道写满数据后,就在同一柱面的下一个盘面来写,一个柱面写满后,才移到下一个扇区开始写数据。读数据也按照这种方式进行,这样就提高了硬盘的读/写效率。
|
||||
|
||||
磁道:磁盘在格式化时被划分出许多同心圆,这些同心圆轨迹叫做磁道 track。磁道从外向内从0开始编号。
|
||||
|
||||
扇区:信息以脉冲串的形式记录在这些轨迹中,这些同心圆不是连续记录数据,而是被划分成一段段圆弧,每段圆弧叫做一个扇区,扇区从“1”开始编号 。扇区也叫块号。
|
||||
|
||||
磁盘在物理上划分为柱面, 磁道,扇区。想要读取扇区的数据,需要将磁头放到这个扇区上方:
|
||||
|
||||
1. 先找到柱面,也就是寻道。磁头是不能动的,但可以沿着磁盘半径方向运动,耗时记为寻道事件 t(seek)
|
||||
2. 将目标扇区旋转到磁头下,这个过程耗时是旋转时间t(r)
|
||||
|
||||
一个磁盘扇区数据读取的时间t = t(seek)+t(r)+t(数据传输) , 在数据库查找数据时,查找时间与访问的磁盘盘块成正比,内存处理时间可以忽略不计。
|
||||
|
||||
|
||||
### B树
|
||||
|
||||
2-3树:一个节点最多有2个key,红黑树就是2-3树的一种实现。
|
||||
|
||||
B树又叫多路平衡查找树。B树可以看做是对2-3树的扩展,允许每个节点有M-1个key,并以升序排列,这里的M就是B树的阶。
|
||||
|
||||
|
||||
B树的度d(d>=2) ,有一些特征:
|
||||
|
||||
1. 根节点至少有2个子节点
|
||||
2. 所有的叶节点具有相同的深度 h,也就是树高
|
||||
3. 每个叶子节点至少包含一个key和2个指针,最多2d-1个key和2d个指针,叶节点的指针都是null。每个节点的关键字个数在【d-1,2d-1】之间
|
||||
4. 每个非叶子节点,key和指针互相间隔,节点两端是指针,因此节点中指针个数=key的个数+1
|
||||
5. 每个指针要么是null,要么指向另一个节点
|
||||
|
||||
如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。
|
||||
如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。
|
||||
如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。
|
||||
|
||||
|
||||
使用数据结构表示如下:
|
||||
|
||||
```
|
||||
typedef struct Item{
|
||||
int key;
|
||||
Data data;
|
||||
}
|
||||
|
||||
#define m 3 //B树的阶
|
||||
|
||||
typedef struct BTNode{
|
||||
int degree; //B树的度
|
||||
int keynums; //每个节点key的个数
|
||||
Item items[m];
|
||||
struct BTNode *p[m];
|
||||
}BTNode,* BTree;
|
||||
|
||||
typedef struct{
|
||||
BTNode *pt; //指向找到的节点
|
||||
int i; // 节点中关键字的序号 (0,m-1)
|
||||
int tag; //1:查找成功,0:查找失败
|
||||
}Result;
|
||||
|
||||
Status btree_insert(root,target);
|
||||
Status btree_delete(root,target);
|
||||
Result btree_find(root,target);
|
||||
|
||||
```
|
||||
|
||||
### 建立索引
|
||||
|
||||
当为一张空表创建索引时,数据库系统将为你分配一个索引页,该索引页在你插入数据前一直是空的。此页此时既是根结点,也是叶结点。每当你往表中插入一行数据,数据库系统即向此根结点中插入一行索引记录。
|
||||
|
||||
插入和删除新的数据记录都会破坏B-Tree的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持B-Tree性质
|
||||
|
||||
### 查找操作
|
||||
|
||||
从root节点出发,对每个节点,找到等于target的key,则查找成功;或者找到大于target的最小k[i], 到 k[i] 左指针指向的子节点继续查找,直到页节点,如果找不到,说明关键字target不在B树中。
|
||||
|
||||
分析下时间复杂度:
|
||||
|
||||
对于一个度为d的B-Tree,每个节点的索引key个数是d-1, 索引key个数为N,树高h上限是:
|
||||
|
||||
2d^h-1=N ==> h=logd^((N+1) /2) ???
|
||||
|
||||
因此,检索一个key,查找节点的个数的复杂度是O(logd^N)
|
||||
|
||||
```
|
||||
比如d=2,N=1,000,000 (1百万),h差不多20个
|
||||
d=3,N=1,000,000 (1百万) ,h差不多13个(3^11=1,594,323)
|
||||
d=4,N=1,000,000 (1百万) ,h差不多10个
|
||||
d=5,N=1,000,000 (1百万) ,h差不多9个 (5^9 = 1,953,125)
|
||||
d=6,N=1,000,000 (1百万) ,h差不多8个(6^8 = 1,679,616)
|
||||
d=7,N=1,000,000 (1百万) ,h差不多8个
|
||||
d=8,N=1,000,000 (1百万) ,h差不多7个
|
||||
d=9,N=1,000,000 (1百万) ,h差不多7个
|
||||
d=10,N=1,000,000 (1百万) ,h差不多6个
|
||||
d=100时,h差不多3个
|
||||
```
|
||||
|
||||
数据库系统在设计时,通常将一个节点的大小设为一个页大小(通常4k),这样保证一个节点在物理上也存储在一个页里,加上计算机存储分配都是按页对其,这样保证一个节点只需要一次I/O.
|
||||
|
||||
实际应用中,d都是比较大,通常超过100,因此1百万的数据通常最多访问3个节点,也就是3次I/O, 因此使用B树作为索引结构查询效率非常高。
|
||||
|
||||
|
||||
### 插入数据
|
||||
|
||||
插入数据时,需要更新索引,索引中也要添加一条记录。索引中添加一条记录的过程是:
|
||||
|
||||
沿着搜索的路径从root一直到叶节点
|
||||
|
||||
每个节点的关键字个数在【d-1,2d-1】之间,当节点的关键字个数是2t-1时,再加入target就违反了B树定义,需要对该节点进行分裂:已中间节点为界,分成2个包含d-1个关键字的子节点(另外还有一个分界关键字,2*(d-1)+1=2d-1),同时把该分界关键字提升到该叶子的父节点中,如果这导致父节点关键字个数超过2d-1,就继续向上分裂,直到根节点。
|
||||
|
||||
如下演示动画,往度d=2的B树中插入:` 6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4`
|
||||
|
||||

|
||||
|
||||
|
||||
### B树和B+树的区别
|
||||
|
||||
B树和B+树的区别在于:
|
||||
|
||||
1. B+树的非叶子节点只包含导航信息,不包含实际记录的信息,这可以保证一个固定大小节点可以放入更多个关键字,也就是更大的度d,从而树高h可以更小,从而相比B树有更优秀的查询效率
|
||||
2. 所有的叶子节点和相邻的节点使用链表方式相连,便于区间查找和遍历
|
||||
|
||||
|
||||
Reference in New Issue
Block a user