阅读目录
查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。
查找定义:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
查找算法分类:
1)静态查找和动态查找;
注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
2)无序查找和有序查找。
无序查找:被查找数列有序无序均可;
有序查找:被查找数列必须为有序数列。
平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
Pi:查找表中第i个数据元素的概率。
Ci:找到第i个数据元素时已经比较过的次数。
1. 顺序查找
说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。
基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
复杂度分析:
查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
当查找不成功时,需要n+1次比较,时间复杂度为O(n);
所以,顺序查找的时间复杂度为O(n)。
C++实现源码:
//顺序查找 int SequenceSearch(int a[], int value, int n) { int i; for(i=0; i<n; i++) if(a[i]==value) return i; return -1; }
2. 二分查找
说明:元素必须是有序的,如果是无序的则要先进行排序操作。
基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);
注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》
C++实现源码:
//二分查找(折半查找),版本1 int BinarySearch1(int a[], int value, int n) { int low, high, mid; low = 0; high = n-1; while(low<=high) { mid = (low+high)/2; if(a[mid]==value) return mid; if(a[mid]>value) high = mid-1; if(a[mid]<value) low = mid+1; } return -1; } //二分查找,递归版本 int BinarySearch2(int a[], int value, int low, int high) { int mid = low+(high-low)/2; if(a[mid]==value) return mid; if(a[mid]>value) return BinarySearch2(a, value, low, mid-1); if(a[mid]<value) return BinarySearch2(a, value, mid+1, high); }
3. 插值查找
在介绍插值查找之前,首先考虑一个新问题,为什么上述算法一定要是折半,而不是折四分之一或者折更多呢?
打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。
同样的,比如要在取值范围1 ~ 10000 之间 100 个元素从小到大均匀分布的数组中查找5, 我们自然会考虑从数组下标较小的开始查找。
经过以上分析,折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:
mid=(low+high)/2, 即mid=low+1/2*(high-low);
通过类比,我们可以将查找的点改进为如下:
mid=low+(key-a[low])/(a[high]-a[low])*(high-low),
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
复杂度分析:查找成功或者失败的时间复杂度均为O(log2(log2n))。
C++实现源码:
//插值查找 int InsertionSearch(int a[], int value, int low, int high) { int mid = low+(value-a[low])/(a[high]-a[low])*(high-low); if(a[mid]==value) return mid; if(a[mid]>value) return InsertionSearch(a, value, low, mid-1); if(a[mid]<value) return InsertionSearch(a, value, mid+1, high); }
4. 斐波那契查找
在介绍斐波那契查找算法之前,我们先介绍一下很它紧密相连并且大家都熟知的一个概念——黄金分割。
黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。
0.618被公认为最具有审美意义的比例数字,这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。因此被称为黄金分割。
大家记不记得斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。
基本思想:也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。
相对于折半查找,一般将待比较的key值与第mid=(low+high)/2位置的元素比较,比较结果分三种情况:
1)相等,mid位置的元素即为所求
2)>,low=mid+1;
3)<,high=mid-1。
斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,及n=F(k)-1;
开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种
1)相等,mid位置的元素即为所求
2)>,low=mid+1,k-=2;
说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。
3)<,high=mid-1,k-=1。
说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。
复杂度分析:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。
C++实现源码:
// 斐波那契查找.cpp #include "stdafx.h" #include <memory> #include <iostream> using namespace std; const int max_size=20;//斐波那契数组的长度 /*构造一个斐波那契数组*/ void Fibonacci(int * F) { F[0]=0; F[1]=1; for(int i=2;i<max_size;++i) F[i]=F[i-1]+F[i-2]; } /*定义斐波那契查找法*/ int FibonacciSearch(int *a, int n, int key) //a为要查找的数组,n为要查找的数组长度,key为要查找的关键字 { int low=0; int high=n-1; int F[max_size]; Fibonacci(F);//构造一个斐波那契数组F int k=0; while(n>F[k]-1)//计算n位于斐波那契数列的位置 ++k; int * temp;//将数组a扩展到F[k]-1的长度 temp=new int [F[k]-1]; memcpy(temp,a,n*sizeof(int)); for(int i=n;i<F[k]-1;++i) temp[i]=a[n-1]; while(low<=high) { int mid=low+F[k-1]-1; if(key<temp[mid]) { high=mid-1; k-=1; } else if(key>temp[mid]) { low=mid+1; k-=2; } else { if(mid<n) return mid; //若相等则说明mid即为查找到的位置 else return n-1; //若mid>=n则说明是扩展的数值,返回n-1 } } delete [] temp; return -1; } int main() { int a[] = {0,16,24,35,47,59,62,73,88,99}; int key=100; int index=FibonacciSearch(a,sizeof(a)/sizeof(int),key); cout<<key<<" is located at:"<<index; return 0; }
5. 树表查找
5.1 最简单的树表查找算法——二叉树查找算法。
基本思想:二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:
1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3)任意节点的左、右子树也分别为二叉查找树。
二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。
不同形态的二叉查找树如下图所示:
有关二叉查找树的查找、插入、删除等操作的详细讲解,请移步浅谈算法和数据结构: 七 二叉查找树。
复杂度分析:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
下图为二叉树查找和顺序查找以及二分查找性能的对比图:
基于二叉查找树进行优化,进而可以得到其他的树表查找算法,如平衡树、红黑树等高效算法。
代码实现:
/*************************************************************************
这是一个二叉查找树,实现了以下操作:插入结点、构造二叉树、删除结点、查找、
查找最大值、查找最小值、查找指定结点的前驱和后继。上述所有操作时间复杂度
均为o(h),其中h是树的高度
注释很详细,具体内容就看代码吧
*************************************************************************/
#include<stdio.h>
#include<stdlib.h>
//二叉查找树结点描述
typedef int KeyType;
typedef struct Node
{
KeyType key; //关键字
struct Node * left; //左孩子指针
struct Node * right; //右孩子指针
struct Node * parent; //指向父节点指针
}Node,*PNode;
//往二叉查找树中插入结点
//插入的话,可能要改变根结点的地址,所以传的是二级指针
void inseart(PNode * root,KeyType key)
{
//初始化插入结点
PNode p=(PNode)malloc(sizeof(Node));
p->key=key;
p->left=p->right=p->parent=NULL;
//空树时,直接作为根结点
if((*root)==NULL){
*root=p;
return;
}
//插入到当前结点(*root)的左孩子
if((*root)->left == NULL && (*root)->key > key){
p->parent=(*root);
(*root)->left=p;
return;
}
//插入到当前结点(*root)的右孩子
if((*root)->right == NULL && (*root)->key < key){
p->parent=(*root);
(*root)->right=p;
return;
}
if((*root)->key > key)
inseart(&(*root)->left,key);
else if((*root)->key < key)
inseart(&(*root)->right,key);
else
return;
}
//查找元素,找到返回关键字的结点指针,没找到返回NULL
PNode search(PNode root,KeyType key)
{
if(root == NULL)
return NULL;
if(key > root->key) //查找右子树
return search(root->right,key);
else if(key < root->key) //查找左子树
return search(root->left,key);
else
return root;
}
//查找最小关键字,空树时返回NULL
PNode searchMin(PNode root)
{
if(root == NULL)
return NULL;
if(root->left == NULL)
return root;
else //一直往左孩子找,直到没有左孩子的结点
return searchMin(root->left);
}
//查找最大关键字,空树时返回NULL
PNode searchMax(PNode root)
{
if(root == NULL)
return NULL;
if(root->right == NULL)
return root;
else //一直往右孩子找,直到没有右孩子的结点
return searchMax(root->right);
}
//查找某个结点的前驱
PNode searchPredecessor(PNode p)
{
//空树
if(p==NULL)
return p;
//有左子树、左子树中最大的那个
if(p->left)
return searchMax(p->left);
//无左子树,查找某个结点的右子树遍历完了
else{
if(p->parent == NULL)
return NULL;
//向上寻找前驱
while(p){
if(p->parent->right == p)
break;
p=p->parent;
}
return p->parent;
}
}
//查找某个结点的后继
PNode searchSuccessor(PNode p)
{
//空树
if(p==NULL)
return p;
//有右子树、右子树中最小的那个
if(p->right)
return searchMin(p->right);
//无右子树,查找某个结点的左子树遍历完了
else{
if(p->parent == NULL)
return NULL;
//向上寻找后继
while(p){
if(p->parent->left == p)
break;
p=p->parent;
}
return p->parent;
}
}
//根据关键字删除某个结点,删除成功返回1,否则返回0
//如果把根结点删掉,那么要改变根结点的地址,所以传二级指针
int deleteNode(PNode* root,KeyType key)
{
PNode q;
//查找到要删除的结点
PNode p=search(*root,key);
KeyType temp; //暂存后继结点的值
//没查到此关键字
if(!p)
return 0;
//1.被删结点是叶子结点,直接删除
if(p->left == NULL && p->right == NULL){
//只有一个元素,删完之后变成一颗空树
if(p->parent == NULL){
free(p);
(*root)=NULL;
}else{
//删除的结点是父节点的左孩子
if(p->parent->left == p)
p->parent->left=NULL;
else //删除的结点是父节点的右孩子
p->parent->right=NULL;
free(p);
}
}
//2.被删结点只有左子树
else if(p->left && !(p->right)){
p->left->parent=p->parent;
//如果删除是父结点,要改变父节点指针
if(p->parent == NULL)
*root=p->left;
//删除的结点是父节点的左孩子
else if(p->parent->left == p)
p->parent->left=p->left;
else //删除的结点是父节点的右孩子
p->parent->right=p->left;
free(p);
}
//3.被删结点只有右孩子
else if(p->right && !(p->left)){
p->right->parent=p->parent;
//如果删除是父结点,要改变父节点指针
if(p->parent == NULL)
*root=p->right;
//删除的结点是父节点的左孩子
else if(p->parent->left == p)
p->parent->left=p->right;
else //删除的结点是父节点的右孩子
p->parent->right=p->right;
free(p);
}
//4.被删除的结点既有左孩子,又有右孩子
//该结点的后继结点肯定无左子树(参考上面查找后继结点函数)
//删掉后继结点,后继结点的值代替该结点
else{
//找到要删除结点的后继
q=searchSuccessor(p);
temp=q->key;
//删除后继结点
deleteNode(root,q->key);
p->key=temp;
}
return 1;
}
//创建一棵二叉查找树
void create(PNode* root,KeyType *keyArray,int length)
{
int i;
//逐个结点插入二叉树中
for(i=0;i<length;i++)
inseart(root,keyArray[i]);
}
int main(void)
{
int i;
PNode root=NULL;
KeyType nodeArray[11]={15,6,18,3,7,17,20,2,4,13,9};
create(&root,nodeArray,11);
for(i=0;i<2;i++)
deleteNode(&root,nodeArray[i]);
printf("%d\n",searchPredecessor(root)->key);
printf("%d\n",searchSuccessor(root)->key);
printf("%d\n",searchMin(root)->key);
printf("%d\n",searchMax(root)->key);
printf("%d\n",search(root,13)->key);
return 0;
}
5.2 平衡查找树之2-3查找树(2-3 Tree)
2-3查找树定义:和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:
1)要么为空,要么:
2)对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比key要大。
3)对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
2-3查找树的性质:
1)如果中序遍历2-3查找树,就可以得到排好序的序列;
2)在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。(这也是平衡树中“平衡”一词的概念,根节点到叶节点的最长距离对应于查找算法的最坏情况,而平衡树中根节点到叶节点的距离都一样,最坏情况也具有对数复杂度。)
性质2)如下图所示:
复杂度分析:
2-3树的查找效率与树的高度是息息相关的。
- 在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN
- 在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN
距离来说,对于1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。
对于插入来说,只需要常数次操作即可完成,因为他只需要修改与该节点关联的节点即可,不需要检查其他节点,所以效率和查找类似。下面是2-3查找树的效率:
代码实现:
5.3 平衡查找树之红黑树(Red-Black Tree)
2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。
基本思想:红黑树的思想就是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息。红黑树中将节点之间的链接分为两种不同类型,红色链接,他用来链接两个2-nodes节点来表示一个3-nodes节点。黑色链接用来链接普通的2-3节点。特别的,使用红色链接的两个2-nodes来表示一个3-nodes节点,并且向左倾斜,即一个2-node是另一个2-node的左子节点。这种做法的好处是查找的时候不用做任何修改,和普通的二叉查找树相同。
红黑树的定义:
红黑树是一种具有红色和黑色链接的平衡查找树,同时满足:
- 红色节点向左倾斜
- 一个节点不可能有两个红色链接
- 整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。
下图可以看到红黑树其实是2-3树的另外一种表现形式:如果我们将红色的连线水平绘制,那么他链接的两个2-node节点就是2-3树中的一个3-node节点了。
红黑树的性质:整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同(2-3树的第2)性质,从根节点到叶子节点的距离都相等)。
复杂度分析:最坏的情况就是,红黑树中除了最左侧路径全部是由3-node节点组成,即红黑相间的路径长度是全黑路径长度的2倍。
下图是一个典型的红黑树,从中可以看到最长的路径(红黑相间的路径)是最短路径的2倍:
红黑树的平均高度大约为logn。
下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。
红黑树这种数据结构应用十分广泛,在多种编程语言中被用作符号表的实现,如:
- Java中的java.util.TreeMap,java.util.TreeSet;
- C++ STL中的:map,multimap,multiset;
- .NET中的:SortedDictionary,SortedSet 等。
代码实现:
//一、左旋代码分析
/*-----------------------------------------------------------
| node right
| / / ==> / /
| a right node y
| / / / /
| b y a b //左旋
-----------------------------------------------------------*/
static rb_node_t* rb_rotate_left(rb_node_t* node, rb_node_t* root)
{
rb_node_t* right = node->right; //指定指针指向 right<--node->right
if ((node->right = right->left))
{
right->left->parent = node; //好比上面的注释图,node成为b的父母
}
right->left = node; //node成为right的左孩子
if ((right->parent = node->parent))
{
if (node == node->parent->right)
{
node->parent->right = right;
}
else
{
node->parent->left = right;
}
}
else
{
root = right;
}
node->parent = right; //right成为node的父母
return root;
}
//二、右旋
/*-----------------------------------------------------------
| node left
| / / / /
| left y ==> a node
| / / / /
| a b b y //右旋与左旋差不多,分析略过
-----------------------------------------------------------*/
static rb_node_t* rb_rotate_right(rb_node_t* node, rb_node_t* root)
{
rb_node_t* left = node->left;
if ((node->left = left->right))
{
left->right->parent = node;
}
left->right = node;
if ((left->parent = node->parent))
{
if (node == node->parent->right)
{
node->parent->right = left;
}
else
{
node->parent->left = left;
}
}
else
{
root = left;
}
node->parent = left;
return root;
}
//三、红黑树查找结点
//----------------------------------------------------
//rb_search_auxiliary:查找
//rb_node_t* rb_search:返回找到的结点
//----------------------------------------------------
static rb_node_t* rb_search_auxiliary(key_t key, rb_node_t* root, rb_node_t** save)
{
rb_node_t *node = root, *parent = NULL;
int ret;
while (node)
{
parent = node;
ret = node->key - key;
if (0 < ret)
{
node = node->left;
}
else if (0 > ret)
{
node = node->right;
}
else
{
return node;
}
}
if (save)
{
*save = parent;
}
return NULL;
}
//返回上述rb_search_auxiliary查找结果
rb_node_t* rb_search(key_t key, rb_node_t* root)
{
return rb_search_auxiliary(key, root, NULL);
}
//四、红黑树的插入
//---------------------------------------------------------
//红黑树的插入结点
rb_node_t* rb_insert(key_t key, data_t data, rb_node_t* root)
{
rb_node_t *parent = NULL, *node;
parent = NULL;
if ((node = rb_search_auxiliary(key, root, &parent))) //调用rb_search_auxiliary找到插入结
点的地方
{
return root;
}
node = rb_new_node(key, data); //分配结点
node->parent = parent;
node->left = node->right = NULL;
node->color = RED;
if (parent)
{
if (parent->key > key)
{
parent->left = node;
}
else
{
parent->right = node;
}
}
else
{
root = node;
}
return rb_insert_rebalance(node, root); //插入结点后,调用rb_insert_rebalance修复红黑树
的性质
}
//五、红黑树的3种插入情况
//接下来,咱们重点分析针对红黑树插入的3种情况,而进行的修复工作。
//--------------------------------------------------------------
//红黑树修复插入的3种情况
//为了在下面的注释中表示方便,也为了让下述代码与我的倆篇文章相对应,
//用z表示当前结点,p[z]表示父母、p[p[z]]表示祖父、y表示叔叔。
//--------------------------------------------------------------
static rb_node_t* rb_insert_rebalance(rb_node_t *node, rb_node_t *root)
{
rb_node_t *parent, *gparent, *uncle, *tmp; //父母p[z]、祖父p[p[z]]、叔叔y、临时结点*tmp
while ((parent = node->parent) && parent->color == RED)
{ //parent 为node的父母,且当父母的颜色为红时
gparent = parent->parent; //gparent为祖父
if (parent == gparent->left) //当祖父的左孩子即为父母时。
//其实上述几行语句,无非就是理顺孩子、父母、祖父的关系。:D。
{
uncle = gparent->right; //定义叔叔的概念,叔叔y就是父母的右孩子。
if (uncle && uncle->color == RED) //情况1:z的叔叔y是红色的
{
uncle->color = BLACK; //将叔叔结点y着为黑色
parent->color = BLACK; //z的父母p[z]也着为黑色。解决z,p[z]都是红色的问题。
gparent->color = RED;
node = gparent; //将祖父当做新增结点z,指针z上移俩层,且着为红色。
//上述情况1中,只考虑了z作为父母的右孩子的情况。
}
else //情况2:z的叔叔y是黑色的,
{
if (parent->right == node) //且z为右孩子
{
root = rb_rotate_left(parent, root); //左旋[结点z,与父母结点]
tmp = parent;
parent = node;
node = tmp; //parent与node 互换角色
}
//情况3:z的叔叔y是黑色的,此时z成为了左孩子。
//注意,1:情况3是由上述情况2变化而来的。
//......2:z的叔叔总是黑色的,否则就是情况1了。
parent->color = BLACK; //z的父母p[z]着为黑色
gparent->color = RED; //原祖父结点着为红色
root = rb_rotate_right(gparent, root); //右旋[结点z,与祖父结点]
}
}
else
{
// if (parent == gparent->right) 当祖父的右孩子即为父母时。(解释请看本文评论下第23楼,同时,感谢SupremeHover指正!)
uncle = gparent->left; //祖父的左孩子作为叔叔结点。[原理还是与上部分一样的]
if (uncle && uncle->color == RED) //情况1:z的叔叔y是红色的
{
uncle->color = BLACK;
parent->color = BLACK;
gparent->color = RED;
node = gparent; //同上。
}
else //情况2:z的叔叔y是黑色的,
{
if (parent->left == node) //且z为左孩子
{
root = rb_rotate_right(parent, root); //以结点parent、root右旋
tmp = parent;
parent = node;
node = tmp; //parent与node 互换角色
}
//经过情况2的变化,成为了情况3.
parent->color = BLACK;
gparent->color = RED;
root = rb_rotate_left(gparent, root); //以结点gparent和root左旋
}
}
}
root->color = BLACK; //根结点,不论怎样,都得置为黑色。
return root; //返回根结点。
}
//六、红黑树的删除
//------------------------------------------------------------
//红黑树的删除结点
rb_node_t* rb_erase(key_t key, rb_node_t *root)
{
rb_node_t *child, *parent, *old, *left, *node;
color_t color;
if (!(node = rb_search_auxiliary(key, root, NULL))) //调用rb_search_auxiliary查找要删除的
结点
{
printf("key %d is not exist!/n");
return root;
}
old = node;
if (node->left && node->right)
{
node = node->right;
while ((left = node->left) != NULL)
{
node = left;
}
child = node->right;
parent = node->parent;
color = node->color;
if (child)
{
child->parent = parent;
}
if (parent)
{
if (parent->left == node)
{
parent->left = child;
}
else
{
parent->right = child;
}
}
else
{
root = child;
}
if (node->parent == old)
{
parent = node;
}
node->parent = old->parent;
node->color = old->color;
node->right = old->right;
node->left = old->left;
if (old->parent)
{
if (old->parent->left == old)
{
old->parent->left = node;
}
else
{
old->parent->right = node;
}
}
else
{
root = node;
}
old->left->parent = node;
if (old->right)
{
old->right->parent = node;
}
}
else
{
if (!node->left)
{
child = node->right;
}
else if (!node->right)
{
child = node->left;
}
parent = node->parent;
color = node->color;
if (child)
{
child->parent = parent;
}
if (parent)
{
if (parent->left == node)
{
parent->left = child;
}
else
{
parent->right = child;
}
}
else
{
root = child;
}
}
free(old);
if (color == BLACK)
{
root = rb_erase_rebalance(child, parent, root); //调用rb_erase_rebalance来恢复红黑树性
质
}
return root;
}
//七、红黑树的4种删除情况
//----------------------------------------------------------------
//红黑树修复删除的4种情况
//为了表示下述注释的方便,也为了让下述代码与我的倆篇文章相对应,
//x表示要删除的结点,*other、w表示兄弟结点,
//----------------------------------------------------------------
static rb_node_t* rb_erase_rebalance(rb_node_t *node, rb_node_t *parent, rb_node_t *root)
{
rb_node_t *other, *o_left, *o_right; //x的兄弟*other,兄弟左孩子*o_left,*o_right
while ((!node || node->color == BLACK) && node != root)
{
if (parent->left == node)
{
other = parent->right;
if (other->color == RED) //情况1:x的兄弟w是红色的
{
other->color = BLACK;
parent->color = RED; //上俩行,改变颜色,w->黑、p[x]->红。
root = rb_rotate_left(parent, root); //再对p[x]做一次左旋
other = parent->right; //x的新兄弟new w 是旋转之前w的某个孩子。其实就是左旋后
的效果。
}
if ((!other->left || other->left->color == BLACK) &&
(!other->right || other->right->color == BLACK))
//情况2:x的兄弟w是黑色,且w的俩个孩子也
都是黑色的
{ //由于w和w的俩个孩子都是黑色的,则在x和w上得去掉一黑色,
other->color = RED; //于是,兄弟w变为红色。
node = parent; //p[x]为新结点x
parent = node->parent; //x<-p[x]
}
else //情况3:x的兄弟w是黑色的,
{ //且,w的左孩子是红色,右孩子为黑色。
if (!other->right || other->right->color == BLACK)
{
if ((o_left = other->left)) //w和其左孩子left[w],颜色交换。
{
o_left->color = BLACK; //w的左孩子变为由黑->红色
}
other->color = RED; //w由黑->红
root = rb_rotate_right(other, root); //再对w进行右旋,从而红黑性质恢复。
other = parent->right; //变化后的,父结点的右孩子,作为新的兄弟结点
w。
}
//情况4:x的兄弟w是黑色的
other->color = parent->color; //把兄弟节点染成当前节点父节点的颜色。
parent->color = BLACK; //把当前节点父节点染成黑色
if (other->right) //且w的右孩子是红
{
other->right->color = BLACK; //兄弟节点w右孩子染成黑色
}
root = rb_rotate_left(parent, root); //并再做一次左旋
node = root; //并把x置为根。
break;
}
}
//下述情况与上述情况,原理一致。分析略。
else
{
other = parent->left;
if (other->color == RED)
{
other->color = BLACK;
parent->color = RED;
root = rb_rotate_right(parent, root);
other = parent->left;
}
if ((!other->left || other->left->color == BLACK) &&
(!other->right || other->right->color == BLACK))
{
other->color = RED;
node = parent;
parent = node->parent;
}
else
{
if (!other->left || other->left->color == BLACK)
{
if ((o_right = other->right))
{
o_right->color = BLACK;
}
other->color = RED;
root = rb_rotate_left(other, root);
other = parent->left;
}
other->color = parent->color;
parent->color = BLACK;
if (other->left)
{
other->left->color = BLACK;
}
root = rb_rotate_right(parent, root);
node = root;
break;
}
}
}
if (node)
{
node->color = BLACK; //最后将node[上述步骤置为了根结点],改为黑色。
}
return root; //返回root
}
//八、测试用例
//主函数
int main()
{
int i, count = 100;
key_t key;
rb_node_t* root = NULL, *node = NULL;
srand(time(NULL));
for (i = 1; i < count; ++i)
{
key = rand() % count;
if ((root = rb_insert(key, i, root)))
{
printf("[i = %d] insert key %d success!/n", i, key);
}
else
{
printf("[i = %d] insert key %d error!/n", i, key);
exit(-1);
}
if ((node = rb_search(key, root)))
{
printf("[i = %d] search key %d success!/n", i, key);
}
else
{
printf("[i = %d] search key %d error!/n", i, key);
exit(-1);
}
if (!(i % 10))
{
if ((root = rb_erase(key, root)))
{
printf("[i = %d] erase key %d success/n", i, key);
}
else
{
printf("[i = %d] erase key %d error/n", i, key);
}
}
}
return 0;
}
5.4 B树和B+树(B Tree/B+ Tree)
平衡查找树中的2-3树以及其实现红黑树。2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key。
维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。
B树定义:
B树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。
-
根节点至少有两个子节点
-
每个节点有M-1个key,并且以升序排列
-
位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
-
其它节点至少有M/2个子节点
下图是一个M=4 阶的B树:
可以看到B树是2-3树的一种扩展,他允许一个节点有多于2个的元素。B树的插入及平衡化操作和2-3树很相似,这里就不介绍了。下面是往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树的差异在于:
- 有k个子结点的结点必然有k个关键码;
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
如下图,是一个B+树:
下图是B+树的插入动画:
B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点在于:
- 由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。
- B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
下面是B 树和B+树的区别图:
B/B+树常用于文件系统和数据库系统中,它通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部性从而减少IO操作。它广泛用于文件系统及数据库中,如:
- Windows:HPFS文件系统;
- Mac:HFS,HFS+文件系统;
- Linux:ResiserFS,XFS,Ext3FS,JFS文件系统;
- 数据库:ORACLE,MYSQL,SQLSERVER等中。
有关B/B+树在数据库索引中的应用,请看张洋的MySQL索引背后的数据结构及算法原理这篇文章,这篇文章对MySQL中的如何使用B+树进行索引有比较详细的介绍,推荐阅读。
思路与实现:
B树的接口主要是插入(包括空树插入一个元素)和删除操作,而插入和删除操作不可避免的都会用到查找操作,而查找的主要思路比较简单,主要是利用B树是一种排序树的原理,可以很快找到要插入位置或者要删除结点。这里的关键是注意返回内容包括查找结果所在结点,以及该元素所在位置,这是为了方便接下来的操作比较简单。
插入:
通过对B树进行遍历,找出要插入的结点以及结点位置,如果找到的key值在B树当中已经存在,则说明插入失败,否则,就可以进行插入操作。这里可以先不管是否超出M阶树的上限要求,因为我们在定义的时候会故意留下一个位置,可以存放多余的一个元素,插入之后,通过判断是否达到M阶树上限要求,再进行递归的分裂操作。
/***
* @name Status insertBTree(BTree &T, Record e)
* @description 插入实现元素的插入
* @return 成功返回OK,如果存在则返回FALSE,否则返回ERROR
* @notice
***/
Status insertBTree(BTree &T, Record e)
{
BTree p;
int index, temp;
Status find_flag;
if (NULL == T)//考虑B树为空树的情况
{
T = (BTree)malloc(BTLEN);
if (NULL == T) return OVERFLOW;
T->keynum = 1;
T->parent = NULL;
for (index = 0;index <= m; ++index)
{
T->ptr[index] = NULL;
T->key[index] = 0;
}
T->key[1] = e.key;
return OK;
}
find_flag = findBTree(T, p, temp, e.key);//寻找插入节点
if (find_flag == TRUE)
{
return FALSE;
}
if (find_flag == FALSE)
{ //不管怎样先直接插入
p->keynum++;
for (index = p->keynum;index > temp;--index)
{
p->key[index] = p->key[index - 1];
p->ptr[index] = p->ptr[index - 1];
}
p->ptr[temp] = NULL;
p->key[temp] = e.key;
if (p->keynum == m) //这种情况得分裂
{
splitBTree(p);
}
renewParent(T);
return OK;
}
return ERROR;
}
分裂:
分裂操作是插入操作过程中一个最重要的操作,因为这是处理“冲突”(即结点中的数据元素大于B树规则中要求的最大个数)的一个通用的处理方式,这种方式必须要对所有的情况都适用,而分裂是解决这一问题一个方法。当然这种方法只是考虑到效率,没有对兄弟可否借数据进行判断,但是另外一种方式比较麻烦,这里先不做讨论。
分裂的思路是让父亲结点先腾出一个位置(包括key和ptr)出来,然后在需要分裂的结点里面取中间的元素并且移动中间的元素key到父亲结点已经腾出来的key位置那里,然后把分裂出来的右部分接到腾出来的ptr那里。注意整个过程对左部分和右部分的都要改变元素的个数以及清空一些没用的空间。在往上分裂之后可能会造成一种情况,就是父亲结点也可能达到分裂的最大个数,所以,检查父亲结点是否需要分裂,需要的话,递归之。
/***
* @name status splitBTree(BTree T)
* @description 递归实现分裂节点操作
* @return 成功返回OK,否则返回ERROR
* @notice
***/
Status splitBTree(BTree T) //此时分裂的节点一定会是超出最大值的。
{
BTree t1, t2;
int index, index_1;
if (T->parent == NULL)
{
t1 = (BTree)malloc(BTLEN);
if (NULL == t1) return OVERFLOW;
t2 = (BTree)malloc(BTLEN);
if (NULL == t2) return OVERFLOW;
t1->keynum = m / 2;
t2->keynum = m - (m / 2) - 1;
t1->parent = T;
t2->parent = T;
for (index = 0;index <= m; ++index) //先全部初始化
{
t1->ptr[index] = NULL;
t1->key[index] = 0;
t2->ptr[index] = NULL;
t2->key[index] = 0;
}
for (index = 0;index <= m / 2; ++index) //初始化t1
{
t1->ptr[index] = T->ptr[index];
t1->key[index] = T->key[index];
}
t2->ptr[0] = T->ptr[(m / 2) + 1];
for (index = (m / 2) + 2;index <= m; ++index) //初始化t2
{
t2->ptr[index - ((m / 2) + 1)] = T->ptr[index];
t2->key[index - ((m / 2) + 1)] = T->key[index];
}
T->keynum = 1;
T->ptr[0] = t1;
T->ptr[1] = t2;
T->key[1] = T->key[m / 2 + 1];
for (index = 2;index <= m; ++index) //初始化T
{
T->ptr[index] = NULL;
T->key[index] = 0;
}
return OK;
}
删除:
B树元素的删除操作与插入操作类似,但是却要麻烦,因为得分两种情况处理。(1)寻找到存在这个元素,而且这个元素所在是叶子节点(即它的孩子为空),直接对其进行删除,之后再判断是否小于B树规则中要求的最小的子树个数。如果小于,那就调用合并函数。(2)如果寻找到的这个元素是非叶子节点的元素,通过寻找比该元素小的最大元素(该元素肯定为叶子节点),把该元素直接赋值给要删除的元素,再在叶子节点处进行(1)中的操作。
/***
* @name Status deleteBTreeRecord(BTree &T, Record e)
* @description 实现B树元素的删除
* @return 成功返回OK,否则返回ERROR
* @notice
***/
Status deleteBTreeRecord(BTree &T, Record e)
{
BTree p, q;
int num, temp, index;
Status find_flag;
if (T == NULL)
return ERROR;
find_flag = findBTree(T, p, temp, e.key);
if (find_flag == FALSE)
{
return FALSE;
}
if (find_flag == TRUE)
{
//deleteBTreeBNode(p,temp);
if (p->ptr[temp] == NULL) //如果是叶子节点的话
{
for (index = temp;index <= p->keynum;++index)
{
p->key[index] = p->key[index + 1];
p->ptr[index] = p->ptr[index + 1];
}
p->keynum--;
if (p->keynum == (m + 1) / 2 - 2)
{
//调用借兄弟的函数
if (borrowBNode(p) == EMPTY) T = NULL;
else renewParent(T);
}
return OK;
}
else //不是叶子结点的话
{
//遍历
findMax(p->ptr[temp - 1], q, num);//返回的q一定会是叶子节点
p->key[temp] = q->key[num];
q->key[num] = 0;
q->keynum--;
if (q->keynum == (m + 1) / 2 - 2)
{
//调用借兄弟的函数
if (borrowBNode(q) == EMPTY) T = NULL;
else renewParent(T);
}
return OK;
}
return OK;
}
return ERROR;
}
合并:
在此先声明,因为一开始只考虑B树的阶为4的情况,后来改为使用宏定义阶M的数值,所以这段代码存在BUG,只支持阶为3或4的B树= =。
思路还是挺清晰的,首先先向兄弟结点借元素,如果兄弟能够借给你元素的话(即借了你之后并不会小于最少的分支),那么直接从兄弟那里取元素,否则,和兄弟合并。
合并其实是分裂反过来的情况,从父亲结点那里取出一个key值介于要合并的两个结点之间的元素,插入左部分最末尾处,同时右部分插到左部分后面,然后父亲结点元素依次往前挪。从而实现合并操作。之后,也必须对父亲结点进行判断是否小于最小的分支数,如果也小于,对父亲节点进行递归操作。
/***
* @name Status borrowBNode(BTree &T)
* @description 递归实现,向兄弟借元素,否则和兄弟合并
* @return 成功返回OK,否则返回ERROR
* @notice 这种情况应该是T为单元素结点
***/
Status borrowBNode(BTree T)
{
int mynum, bronum, index;
BTree b = NULL, f = NULL;
if (T == NULL) return ERROR;
f = T->parent;
if (f == NULL)//考虑父亲结点不存在的情况
{
if (T->keynum == 0)
{
f = T->ptr[0];
if (f == NULL)
{
free(T);
return EMPTY;
}
for (index = 0;index <= f->keynum;index++)
{
T->key[index] = f->key[index];
T->ptr[index] = f->ptr[index];
}
T->keynum = f->keynum;
free(f);
renewParent(T);
}
return OK;
}
mynum = whichSon(T);
if (mynum == 0)
bronum = 1;
else
bronum = mynum - 1;
b = f->ptr[bronum];
if (b->keynum == (m + 1) / 2 - 1) //如果兄弟帮不了你了
{
//那么就和这个兄弟合体
if (bronum < mynum) //如果我不是第一个
{
b->keynum++;
b->key[b->keynum] = f->key[mynum];
b->ptr[b->keynum] = T->ptr[0];
for (index = 1;index <= T->keynum;index++)
{
b->key[index + b->keynum] = T->key[index];
b->ptr[index + b->keynum] = T->ptr[index];
b->keynum++;
}
free(T);
for (index = mynum;index <= f->keynum;index++)
{
f->key[index] = f->key[index + 1];
f->ptr[index] = f->ptr[index + 1];
}
f->keynum--;
}
else
{
T->keynum++;
T->key[T->keynum] = f->key[bronum];
T->ptr[T->keynum] = b->ptr[0];
for (index = 1;index <= b->keynum;index++)
{
T->key[index + T->keynum] = b->key[index];
T->ptr[index + T->keynum] = b->ptr[index];
T->keynum++;
}
free(b);
for (index = bronum;index <= f->keynum;index++)
{
f->key[index] = f->key[index + 1];
f->ptr[index] = f->ptr[index + 1];
}
f->keynum--;
}
renewParent(f);
if (f->keynum == (m + 1) / 2 - 2)
{
//调用借兄弟的函数
return borrowBNode(f);
}
}
else//如果兄弟能够帮你
{
if (bronum < mynum) //如果我不是第一个
{
for (index = 1;index <= T->keynum;index++)
{
T->key[index + 1] = T->key[index];
T->ptr[index + 1] = T->ptr[index];
}
T->ptr[1] = T->ptr[0];
T->key[1] = f->key[mynum];
T->ptr[0] = b->ptr[b->keynum];
T->keynum++;
f->key[mynum] = b->key[b->keynum];
b->key[b->keynum] = 0;
b->ptr[b->keynum] = NULL;
b->keynum--;
}
else //如果我是第一个
{
T->keynum++;
T->key[T->keynum] = f->key[1];
T->ptr[T->keynum] = b->ptr[0];
f->key[1] = b->key[1];
b->ptr[0] = b->ptr[1];
for (index = 1;index <= b->keynum;index++)
{
b->key[index] = b->key[index + 1];
b->ptr[index] = b->ptr[index + 1];
}
b->keynum--;
}
}
return OK;
}
遍历,输出:
为了让B树更容易看,代码更容易调试,我同时还用队列写了个层次遍历,这个看看就好,实现起来挺麻烦的。而且可能代码实现也存在问题
/***
* @name Status ergodic(BTree T, LinkList L, int newline, int sum)
* @description print需要用到的递归遍历程序
* @return 成功返回OK
* @notice 此处用到队列
***/
Status ergodic(BTree T, LinkList L, int newline, int sum)
{
int index;
BTree p;
if (T != NULL)
{
printf("[ ");
Enqueue_L(L, T->ptr[0]);
for (index = 1;index <= T->keynum; index++)
{
printf("%d ", T->key[index]);
Enqueue_L(L, T->ptr[index]);
}
sum += T->keynum + 1;
printf("]");
if (newline == 0)
{
printf("\n");
newline = sum - 1;
sum = 0;
}
else
{
--newline;
}
}
if (IfEmpty(L) == FALSE)
{
Dequeue_L(L, p);
ergodic(p, L, newline, sum);
}
return OK;
}
/***
* @name Status print(BTree T)
* @description 层次遍历并分层输出B树
* @return 成功返回OK
* @notice
***/
Status print(BTree T)
{
LinkList L;
if (T == NULL)
{
printf("[ ]\n");
return OK;
}
InitQueue_L(L);
ergodic(T, L, 0, 0);
DestroyQueue(L);
return OK;
}
B树与B+树的思路与实现:
main.h
#include "Btree.h"
#include <iostream>
#include <math.h>
using namespace std;
void main()
{
/*
int a=5;
cout<<ceil(a/2.0)<<endl;*/
/*B树*/
Btree b;
b.InsertBtree(33);
b.InsertBtree(44);
b.InsertBtree(28);
b.InsertBtree(73);
b.InsertBtree(52);
b.InsertBtree(218);
b.InsertBtree(71);
b.DeleteBtree(44);
b.DeleteBtree(33);
b.DeleteBtree(73);
b.DeleteBtree(28);
b.DeleteBtree(52);
b.DeleteBtree(218);
b.DeleteBtree(44);
b.DeleteBtree(71);
b.DeleteBtree(44);
/*B+树*/
/*
BPlusTree bp;
bp.InsertBplustree(2);
bp.InsertBplustree(3);
bp.InsertBplustree(32);
bp.InsertBplustree(13);
bp.InsertBplustree(62);
bp.InsertBplustree(93);
bp.InsertBplustree(27);
bp.InsertBplustree(83);
bp.InsertBplustree(392);
bp.InsertBplustree(173);
bp.InsertBplustree(652);
bp.InsertBplustree(923);
bp.DeleteBPlustree(13);
bp.DeleteBPlustree(27);
bp.DeleteBPlustree(3);
bp.DeleteBPlustree(2);
*/
}
Btree.h
#ifndef _BTREE_
#define _BTREE_
#define m 3 /*m 路Btree*/
#define L 3 /*B+ Tree 元素节点最大长度*/
#include <iostream>
#include <math.h>
#include <stack>
using namespace std;
class TreeNode
{
friend class Btree;
friend class Result;
friend class BPlusTree;
public:
TreeNode()/*用于区分是否为BPlus 树的叶子节点 即数据节点*/
{
keynum=0;/*是数据节点*/
}
TreeNode(TreeNode* p1,int k,TreeNode *p2)/*构造根节点*/
{
keynum=1;
key[1]=k;
parent=0;
fill(ptr,ptr+m+1,(TreeNode*)0);
ptr[0]=p1;
ptr[1]=p2;
/* 设置子树的父节点*/
if (p1)
p1->parent=this;
if(p2)
p2->parent=this;
}
TreeNode(TreeNode* p,int *keyarry,TreeNode**ptrarry)/*分裂的新节点*/
{
parent=p;
fill(ptr,ptr+m+1,(TreeNode*)0);
int temp=ceil(m/2.0);//向上取整
ptr[0]=ptrarry[temp];
if(ptr[0])/*分裂后节点的父节点改变*/
ptr[0]->parent=this;
for (int i=1;i+temp<=m;i++)
{
key[i]=keyarry[temp+i];
ptr[i]=ptrarry[temp+i];
if (ptr[i])/*分裂后节点的父节点改变*/
ptr[i]->parent=this;
keyarry[temp+i]=0;
ptrarry[temp+i]=0;
}
keyarry[temp]=0;
ptrarry[temp]=0;
keynum=m-temp;
}
protected:
int keynum;/*关键字个数 keynum <=m-1*/
TreeNode *parent;/*父节点指针*/
TreeNode *ptr[m+1];/*子树指针 0...m */
int key[m+1];/*1...m 多一个单元可以保证当溢出时也可直接插入 之后在进行分裂*/
};
class Result
{
friend class Btree;
public:
Result(TreeNode*p=0,int i=0,bool r=0)
{
ResultPtr=p;
index=i;
Resultflag=r;
}
TreeNode * ResultPtr;
int index;
bool Resultflag;
};
class Btree
{
public:
Btree()
{
root=0;
}
void InsertBtree(int k);
Result Find(int k);
void DeleteBtree(int k);
void Insert(int k,TreeNode* node,TreeNode* p);/*关键字:k 该关键字k的右子树指针 插入节点a */
protected:
void BorrowOrCombine(TreeNode *a,int i,int type,stack<int> &s);/*待处理关键字是a 节点的 i
type 标志此次操作的前一次是 对其左 -1 右 1 无0 孩子进行操作*/
//void Insert(int k,TreeNode* node,TreeNode* p);/*关键字:k 该关键字k的右子树指针 插入节点a */
TreeNode *root;
};
/*B+Tree */
class DataNode: public TreeNode
{
friend class BPlusTree;
public:
DataNode(int k)/*构造第一个数据节点*/
{
data[1]=k;
parent=0;
pre=next=0;
datanum=1;
};
DataNode(TreeNode* Parent,DataNode*Pre,DataNode* Next,int *dataArray)/*分裂数据节点*/
{
parent=Parent;
pre=Pre;
next=Next;
int temp=ceil(L/2.0);
// copy(dataArray+1+temp,dataArray+L+1,data+1);
for (int i=1;i<=L+1-temp;i++)
{
data[i]=dataArray[i+temp];
dataArray[i+temp]=0;
}
datanum=L+1-temp;
Pre->datanum=temp;
}
private:
DataNode *pre;
DataNode *next;
int data[L+2];
int datanum;
};
class BPlusTree :public Btree
{
public:
BPlusTree()
{
root=0;
header=0;
}
void InsertBplustree(int k);/*插入关键字*/
void InsertData(int k,DataNode* dn);
void DeleteBPlustree(int k);/*删除关键字k*/
private:
DataNode* header;
};
#endif
Btree.cpp
#include "Btree.h"
#include <iostream>
#include <math.h>
#include <stack>
using namespace std;
void Btree::InsertBtree(int k)
{
if (!root)
{
root=new TreeNode(0,k,0);
return ;
}
TreeNode *a=root;/*当前节点*/
int i=1;/*k关节字 要插入节点a 的位置索引*/
/*找到插入节点*/
while(a)
{
i=1;
/*在a 中找到第一个比关键字k大的关键字的位置 i */
while(i<=a->keynum)
{
if (k<=a->key[i])
break;
else
i++;
}
/* 判断是否继续向下 还是已经到达子节点 */
if (!a->ptr[i-1])/* 已是叶子节点无需向下 直接插入 */
break;
else/*不是叶子节点*/
a=a->ptr[i-1];
}
if (a->key[i]==k)/*该关键字节点已存在 */
return ;
Insert(k,0,a);/*在叶子节点中插入关键字k*/
}
void Btree::Insert(int k,TreeNode* node,TreeNode* a)/*关键字:k 该关键字k的右子树指针 插入节点a */
{
int i=1;
/*在a 中找到第一个比关键字k大的关键字的位置 i */
while(i<=a->keynum)
{
if (k<=a->key[i])
break;
else
i++;
}
/*插入节点为 a 索引为 i */
for (int j=a->keynum;j>=i;j--)/*向后移动以便插入新关键字*/
{
a->key[j+1]=a->key[j];/* 关键字*/
a->ptr[j+1]=a->ptr[j];/*子树指针*/
}
a->key[i]=k;
a->ptr[i]=node;
a->keynum++;
if (a->keynum<=m-1) return;
else
{/*分裂节点然后插入父节点 |1 2 3 ...|ceil(m)(向上取整)|... m| */
int midkey=a->key[(int)ceil(m/2.0)];/*中间关键字及 NewNode 要插入父节点*/
TreeNode* NewNode=new TreeNode(a->parent,a->key,a->ptr);/*和a同parent*/
/*
for (int i=0;i<=NewNode->keynum;i++)
{
if (NewNode->ptr[i])
NewNode->ptr[i]->parent=NewNode;
}*/
a->keynum=m-ceil(m/2.0);
TreeNode * tempa=a;/*记录当前节点*/
a=a->parent;/*父节点*/
if (!a)/*无父节点*/
{
TreeNode *NewRoot=new TreeNode(tempa,midkey,NewNode);
tempa->parent=NewRoot;
NewNode->parent=NewRoot;
root=NewRoot;
return;
}
else
Insert(midkey,NewNode,a);
}
}
Result Btree::Find(int k)
{
if (!root)
{
cout<<"the tree is null !"<<endl;
return Result(0,0,0);
}
TreeNode* a=root;
int i=1;
while(a)
{
i=1;
while(i<=a->keynum)
{
if (k<=a->key[i])
{
break;
}
else
{
i++;
}
}
if (k==a->key[i])
{
return Result(a,i,1);
}
else
{
if (!a->ptr[i-1])
{
return Result(a,i,0);
}
else
{
a=a->ptr[i-1];
}
}
}
}
void Btree::DeleteBtree(int k)
{
if (!root)
{
cout<<"The tree is null !"<<endl;
return;
}
/*转化为删除叶子节点中的关键字 找其右子树的最小关键字*/
stack<int> s;/*记录路径上的 所有 index */
TreeNode *delnode=root;//待删除关键字k所在节点
int i=1;
while (delnode&&delnode->keynum)/*delnode->keynum ==0 是对B+树而言*/
{
i=1;
while(i<=delnode->keynum)
{
if (k<=delnode->key[i])
{
break;
}
else
{
i++;
}
}
if (k==delnode->key[i])
{
break;/*找到了*/
}
else
{
if (delnode->ptr[i-1]==0)
{
/*无此关键字*/
cout<<"no this key :"<<k<<endl;
return ;
}
else
{
/*向下一层*/
delnode=delnode->ptr[i-1];
s.push(i-1);/*通过该索引的指针向下一层查找*/
}
}
}
/* delnode i parent可以提供回去的路 */
TreeNode *p=delnode;/*当前节点*/
if (delnode->ptr[i]&&delnode->ptr[i]->keynum)/*delnode 不是叶子节点*//*B+tree 的元素节点*/
{
s.push(i);
p=delnode->ptr[i];
while(p->ptr[0]&&p->ptr[0]->keynum)/* p到达delnode 的右子树中最小关键字节点*/
{
p=p->ptr[0];
if (!p->ptr[0]->keynum)
break;
s.push(0);
}
}
if (p!=delnode)
{
/*将删除操作到对叶子节点的关键字的删除*/
delnode->key[i]=p->key[1];
i=1;
}
/* p, i 删除关键字由delnode i 转换为 p i */
BorrowOrCombine(p,i,0,s);
}
void Btree::BorrowOrCombine(TreeNode *a,int i,int type,stack<int> &s)/*待处理关键字是a 节点的 i
type 标志此次操作的前一次是 对其左 -1 右 1 无0 孩子进行操作 即这是对叶子节点的操作*/
{
if (a==root&&root->keynum==1)
{
TreeNode * oldroot=root;
if (type==-1)
{
if (root->ptr[i])
root=root->ptr[i];
else
root=0;
}
else if (type==1)
{
if (root->ptr[i-1])
root=root->ptr[i-1];
else
root=0;
}
else/*不是由下层传递而来*/
{
root=0;
}
if(root)
root->parent=0;
delete oldroot;
return;
}
int minnum=ceil(m/2.0)-1;
TreeNode *la,*ra;/*a 的左右兄弟节点*/
// if (!a->ptr[0])/*a 为叶子节点*/
// {
TreeNode *pflag=a->ptr[i-1];/*对B+树 判断哪个元素节点被合并掉了 指针为0*/
if (a->keynum>minnum||a==root)
{
for (int j=i;j<a->keynum;j++)
{
a->key[j]=a->key[j+1];
if (type==-1)
{
a->ptr[j-1]=a->ptr[j];
}
else if (type==1)
{
a->ptr[j]=a->ptr[j+1];
}
else
{
/*这是对叶子节点的操作 B+树 而言*/
if (pflag)
{
a->ptr[j]=a->ptr[j+1];
}
else
{
a->ptr[j-1]=a->ptr[j];
}
}
}
if (!type&&!pflag)
{
a->ptr[j-1]=a->ptr[j];
}
if (type==-1)
{
a->ptr[j-1]=a->ptr[j];
}
a->key[j]=0;
a->ptr[j]=0;
a->keynum--;
return;
}
else
{/* aa->keynum=minnum */
int index=s.top();
s.pop();
/*能借则借 借优先*/
if (index)/*有左兄弟*/
{
la=a->parent->ptr[index-1];
if (la->keynum>minnum)/*左兄弟关键字足够多可以借*/
{
/* 从左兄弟借 */
/*向后移动覆盖 i */
for (int j=i;j>1;j--)
{
a->key[j]=a->key[j-1];
if (type==-1)
{
a->ptr[j-2]=a->ptr[j-1];
}
else if (type==1)
{
a->ptr[j-1]=a->ptr[j];
}
else
{
if (pflag)
{
a->ptr[j-1]=a->ptr[j];
}
else
{
a->ptr[j-2]=a->ptr[j-1];
}
}
}
if (!type&&pflag)
{
a->ptr[j-1]=a->ptr[j];
}
if (type==1)
{
a->ptr[j-1]=a->ptr[j];
}
a->key[j]=a->parent->key[index];
a->ptr[0]=la->ptr[la->keynum];/*左兄弟的最右子树*/
/*父节点改变*/
la->ptr[la->keynum]->parent=a;
a->parent->key[index]=la->key[la->keynum];
la->key[la->keynum]=0;
la->ptr[la->keynum]=0;
la->keynum--;
return;
}
}
if (index<a->keynum)/*有右兄弟index<=a->keynum*/
{
ra=a->parent->ptr[index+1];
if (ra->keynum>minnum)/*右兄弟关键字足够多可以借*/
{
/* 从右兄弟借 */
/*向前移动覆盖 i */
for (int j=i;j<a->keynum;j++)
{
a->key[j]=a->key[j+1];
if (type==-1)
{
a->ptr[j-1]=a->ptr[j];
}
else if (type==1)
{
a->ptr[j]=a->ptr[j+1];
}
else
{
if (pflag)
{
a->ptr[j]=a->ptr[j+1];
}
else
{
a->ptr[j-1]=a->ptr[j];
}
}
}
if (!type&&!pflag)
{
a->ptr[j-1]=a->ptr[j];
}
if (type==-1)
{
a->ptr[j-1]=a->ptr[j];
}
a->key[j]=a->parent->key[index+1];
a->ptr[j]=ra->ptr[0];/*右兄弟的最左子树*/
if (ra->ptr[0]) /*叶子节点的 -》ptr【0】==0*/
ra->ptr[0]->parent=a;
a->parent->key[index+1]=ra->key[1];
/*右兄弟关键字去头 前移*/
for (int t=1;t<ra->keynum;t++)
{
ra->ptr[t-1]=ra->ptr[t];
ra->key[t]=ra->key[t+1];
}
/*t= ra->keynum */
ra->ptr[t-1]=ra->ptr[t];
ra->key[t]=0;
ra->ptr[t]=0;
ra->keynum--;
return;
}
}
/*合并可能会使 不完善节点向上传递*/
if (index)/*有左兄弟*/
{
la=a->parent->ptr[index-1];
if (la->keynum==minnum)/*左兄弟关键字不够多*/
{
/* 合并到左兄弟 */
la->key[la->keynum+1]=a->parent->key[index];
/*a 中的关键字填充到其左兄弟中 0 1 2 .... i-1 | i | i+1 ...... kyenum */
for (int l=1;l<=i-1;l++)
{
la->key[la->keynum+l+1]=a->key[l];
la->ptr[la->keynum+l]=a->ptr[l-1];
/*子树的父节点改变*/
if(a->ptr[l-1])
a->ptr[l-1]->parent=la;
}
if (type==-1)
{
la->ptr[la->keynum+l]=a->ptr[l];
if(a->ptr[l])
a->ptr[l]->parent=la;
}
else if(type==1)
{
la->ptr[la->keynum+l]=a->ptr[l-1];
if(a->ptr[l-1])
a->ptr[l-1]->parent=la;
}
else
{
if (pflag)
{
la->ptr[la->keynum+l]=a->ptr[l-1];
if(a->ptr[l-1])
a->ptr[l-1]->parent=la;
}
else
{
la->ptr[la->keynum+l]=a->ptr[l];
if(a->ptr[l])
a->ptr[l]->parent=la;
}
}
for (l=i;l<a->keynum;l++)
{
la->key[la->keynum+l+1]=a->key[l+1];
la->ptr[la->keynum+l+1]=a->ptr[l+1];
if(a->ptr[l+1])
a->ptr[l+1]->parent=la;
}
la->keynum=m-1;
TreeNode *tempp=a->parent;
tempp->ptr[index]=0;
delete a;
BorrowOrCombine(tempp,index,1,s);
return;
}
}
if (index<a->keynum)/*有右兄弟index<=a->keynum*/
{
ra=a->parent->ptr[index+1];
if (ra->keynum==minnum)/*右兄弟关键字不够多*/
{
/*合并到右兄弟 */
/* 右兄弟关键字右移 让出合并位置*/
for (int k=ra->keynum;k>0;k--)
{
ra->key[k+a->keynum]=ra->key[k];
ra->ptr[k+a->keynum]=ra->ptr[k];
}
ra->ptr[a->keynum]=ra->ptr[0];
ra->key[a->keynum]=a->parent->key[index+1];
/*a 中的关键字填充到其右兄弟中 0 1 2 .... i-1 | i | i+1 ...... kyenum */
for (int l=1;l<=i-1;l++)
{
ra->ptr[l-1]=a->ptr[l-1];
ra->key[l]=a->key[l];
/*子树的父节点改变*/
if(a->ptr[l-1])
a->ptr[l-1]->parent=ra;
}
if (type==-1)
{
ra->ptr[l-1]=a->ptr[l];
if(a->ptr[l])
a->ptr[l]->parent=ra;
}
else if (type==1)
{
ra->ptr[l-1]=a->ptr[l-1];
if(a->ptr[l-1])
a->ptr[l-1]->parent=ra;
}
else
{
if (pflag)
{
ra->ptr[l-1]=a->ptr[l-1];
if(a->ptr[l-1])
a->ptr[l-1]->parent=ra;
}
else
{
ra->ptr[l-1]=a->ptr[l];
if(a->ptr[l])
a->ptr[l]->parent=ra;
}
}
/*叶子节点无所谓 都是 0 */
for (l=i+1; l<=a->keynum;l++)
{
ra->key[l]=a->key[l];
ra->ptr[l]=a->ptr[l];
if(a->ptr[l])
a->ptr[l]->parent=ra;
}
ra->keynum=m-1;
TreeNode *tempp=a->parent;
tempp->ptr[index]=0;
delete a;/*删除节点a */
/**/
BorrowOrCombine(tempp,index+1,-1,s);
return;
}
}
}
}
/*
* **************************************************************
* B+tree
*/
void BPlusTree::InsertBplustree(int k)
{
if (!root)
{
if (!header)
{
header=new DataNode(k);
}
else
{
InsertData(k,header);
}
return;
}
/*find the data node where to insert the key */
TreeNode *inode=root;
while(inode->keynum)/*不是叶子节点就继续向下*/
{
int i=1;
while(i<inode->keynum)
{
if (k>inode->key[i])
{
i++;
}
else
break;
}
if (k<inode->key[i])
{
/*左子树*/
inode=inode->ptr[i-1];
}
else
{
/*右子树*/
inode=inode->ptr[i];
}
}
/*找到相应的叶子节点 inode */
InsertData(k,(DataNode*)inode);
}
void BPlusTree::InsertData(int k,DataNode* dn)
{
int i=1;
while (i<=dn->datanum)
{
if (k>dn->data[i])
{
i++;
}
else
break;
}
/**/
if (k==dn->data[i])
{
/*关键字已存在*/
return ;
}
else
{
/*数据后移以便插入关键字*/
for (int j=L;j>=i;j--)
{
dn->data[j+1]=dn->data[j];
}
dn->data[i]=k;
dn->datanum++;
if (dn->datanum>L)/*溢出需分裂*/
{
/*分裂为前后 两段 dn NewNode*/
DataNode *NewNode=new DataNode(dn->parent,dn,dn->next,dn->data);
if (dn->next)/*存在下一个数据节点*/
dn->next->pre=NewNode;
dn->next=NewNode;
if(!root)/*第一次分裂*/
{
root=new TreeNode(dn,NewNode->data[1],NewNode);/*后半段的第一个关键字放在插入其父节点*/
dn->parent=root;
NewNode->parent=root;
}
else
{
Insert(NewNode->data[1],NewNode,NewNode->parent);
}
}
}
}
void BPlusTree::DeleteBPlustree(int k)
{
/*仅有header 无 root*/
if(!root)
{
if (!header)
{
cout<<"B+tree is null !"<<endl;
return;
}
else
{
int i=1;
while (i<=header->datanum)
{
if (k==header->data[i])
{
for (int j=i;j<header->datanum;j++)
{
header->data[j]=header->data[j+1];
}
header->datanum--;
if(!header->datanum)
header=0;
return;
}
else
{
i++;
if(i==header->datanum)
{
cout<<"No this key :"<<k<<endl;
return ;
}
}
}
}
}
/*找到关键字k在的元素节点 d(elete)node */
TreeNode *dnode=root;
int i=1;
while (dnode->keynum)/*不是元素节点则继续向下查找*/
{
i=1;
while(i<=dnode->keynum)
{
if (k>=dnode->key[i])
{
i++;
}
else
break;
}
/*左子树*/
dnode=dnode->ptr[i-1];
}
/* 找到相应的元素节点 dnode */
int index=i;
int minnum=ceil(L/2);/*元素节点最少关键字数目*/
DataNode *datanode=(DataNode*) dnode;
DataNode *la,*ra;/*该元素节点的左右节点*/
if (datanode->datanum>minnum)/*关键字足够多 ,移动覆盖即可*/
{
int i=1;
while (i<=datanode->datanum)
{
if (datanode->data[i]==k)
{
for (int j=i;j<datanode->datanum;j++)
{
datanode->data[j]=datanode->data[j+1];
}
datanode->data[j]=0;
datanode->datanum--;
return;
}
else
{
if (i==datanode->datanum)
{
cout<<"No this key"<<k<<endl;
return;
}
else
i++;
}
}
}
else/*元素不够多 从左右邻居节点借或者合并 由上面查找过程知*/
/*index 是该元素节点的索引*/
{
/*查找该关键字*/
int i=1;
while (i<=datanode->datanum)
{
/*找到并覆盖*/
if (datanode->data[i]==k)
{
for (int j=i;j<datanode->datanum;j++)
{
datanode->data[j]=datanode->data[j+1];
}
datanode->data[j]=0;
datanode->datanum--;
break;
}
else
{
if (i==datanode->datanum)
{
cout<<"No this key"<<k<<endl;
return;
}
else
i++;
}
}
/*删除后节点不完善*/
/*借*/
if (index-1)/*有左兄弟*/
{
la=(DataNode*)datanode->parent->ptr[index-2];
if (la->datanum>minnum)/*左兄弟的关键字足够多可以借*/
{
for (int j=datanode->datanum;j>=1;j--)
{
datanode->data[j+1]=datanode->data[j];
}
datanode->data[1]=la->data[la->datanum];/*最大关键字*/
datanode->parent->key[index-1]=la->data[la->datanum];
la->data[la->datanum]=0;
la->datanum--;
datanode->datanum++;
return;
}
}
if (index<=datanode->parent->keynum)/*有右兄弟*/
{
ra=(DataNode*)datanode->parent->ptr[index];
if (ra->datanum>minnum)/*右兄弟的关键字足够多可以借*/
{
/*加在datanode末尾*/
datanode->data[datanode->datanum+1]=ra->data[1];
datanode->datanum++;
datanode->parent->key[index]=ra->data[2];
/*右兄弟移动覆盖*/
for (int j=1;j<ra->datanum;j++)
{
ra->data[j]=ra->data[j+1];
}
ra->data[j]=0;
ra->datanum--;
return;
}
}
/*邻居节点的关键字不够多 合并*/
if (index-1)/*有左兄弟*/
{
/*关键字合并到左兄弟尾部 并且删除对应搜索节点的关键字*/
la=(DataNode*)datanode->parent->ptr[index-2];
for (int j=1;j<=datanode->datanum;j++)
{
la->data[la->datanum+1]=datanode->data[j];
}
la->parent->ptr[index-1]=0;
la->next=datanode->next;
if (datanode->next)/*是尾节点*/
datanode->next->pre=la;
delete datanode;
DeleteBtree(la->parent->key[index-1]);
return;
}
if (index<=datanode->parent->keynum)/*有右兄弟*/
{
/*关键字合并到右兄弟头部 并且删除对应搜索节点的关键字*/
ra=(DataNode*)datanode->parent->ptr[index];
/*右兄弟关键字后移出位置以便合并*/
for (int j=ra->datanum;j>=1;j--)
{
ra->data[j+datanode->datanum]=ra->data[j];
}
/*填充到右兄弟节点头部*/
for (int l=1;l<=datanode->datanum;l++)
{
ra[l]=datanode[l];
}
ra->datanum+=datanode->datanum;
/*数据节点链操作 注意首尾的处理*/
ra->pre=datanode->pre;
if (datanode->pre)
{
datanode->pre->next=ra;
}
else
{
/*datanode 为 header*/
header=ra;
}
ra->parent->ptr[index-1]=0;
delete datanode;
DeleteBtree(ra->parent->key[index]);
return;
}
}
}
树表查找总结:
二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,我们可以使用平衡查找树。平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。
除此之外,2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。
6. 分块查找
分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
算法思想:将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
算法流程:
step1 先选取各块中的最大关键字构成一个索引表;
step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。
代码实现:
#include <stdio.h>
#define MAXL 100 //数据表的最大长度
#define MAXI 20 //索引表的最大长度
typedef int KeyType;
typedef char InfoType[10];
typedef struct
{
KeyType key; //KeyType为关键字的数据类型
InfoType data; //其他数据
} NodeType;
typedef NodeType SeqList[MAXL]; //顺序表类型
typedef struct
{
KeyType key; //KeyType为关键字的类型
int link; //指向对应块的起始下标
} IdxType;
typedef IdxType IDX[MAXI]; //索引表类型
int IdxSearch(IDX I,int m,SeqList R,int n,KeyType k)
{
int low=0,high=m-1,mid,i;
int b=n/m; //b为每块的记录个数
while (low<=high) //在索引表中进行二分查找,找到的位置存放在low中
{
mid=(low+high)/2;
if (I[mid].key>=k)
high=mid-1;
else
low=mid+1;
}
//应在索引表的high+1块中,再在线性表中进行顺序查找
i=I[high+1].link;
while (i<=I[high+1].link+b-1 && R[i].key!=k) i++;
if (i<=I[high+1].link+b-1)
return i+1;
else
return 0;
}
int main()
{
int i,n=25,m=5,j;
SeqList R;
IDX I= {{14,0},{34,5},{66,10},{85,15},{100,20}};
KeyType a[]= {8,14,6,9,10,22,34,18,19,31,40,38,54,66,46,71,78,68,80,85,100,94,88,96,87};
KeyType x=85;
for (i=0; i<n; i++)
R[i].key=a[i];
j=IdxSearch(I,m,R,n,x);
if (j!=0)
printf("%d是第%d个数据\n",x,j);
else
printf("未找到%d\n",x);
return 0;
}
7. 哈希查找
什么是哈希表(Hash)?
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
什么是哈希函数?
哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
算法流程:
1)用给定的哈希函数构造哈希表;
2)根据选择的冲突处理方法解决地址冲突;
常见的解决冲突的方法:拉链法和线性探测法。详细的介绍可以参见:浅谈算法和数据结构: 十一 哈希表。
3)在哈希表的基础上执行哈希查找。
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
复杂度分析:
单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。
使用Hash,我们付出了什么?
我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV pair,经常使用Python的博友可能更有这种体会。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么?
Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
代码实现:
#include <malloc.h>
#include <stdio.h>
#include <assert.h>
typedef enum {EX,EM,DL}State;//储存位置的状态,是已存,空,还是删除过,在这个哈希表中删除过元素的存储空间是不可再用的
typedef int DataType;
typedef struct HTELEM {
DataType _data;//存入的关键字(关键字不一定是数值,当然目前我们做的这个表中存的是数值)
State _state;
}HTELEM;
typedef struct HashTable{
HTELEM* arr;//存放哈希表的数组
int _capacity;//数组的容量
int _size;//已用存储位置的大小
int _IsLineDetective;//标志着选择的哈希函数
}HT,HashTable;
void swap (HT*ht,HT*newht)//交换两个哈希表的内容
{
int temp=0;
temp=(int)ht->arr;
ht->arr=newht->arr;
newht->arr=(HTELEM*)temp;
temp=ht->_capacity;
ht->_capacity=newht->_capacity;
newht->_capacity=temp;
temp=ht->_size;
ht->_size=newht->_size;
newht->_size=temp;
temp=ht->_IsLineDetective;
ht->_IsLineDetective=newht->_IsLineDetective;
newht->_IsLineDetective=temp;
}
void HashTableInit(HT*ht ,int capacity,int IsLineDetectiev)//初始化哈希表
{
int i=0;
ht->arr=(HTELEM*)malloc(sizeof(HTELEM)*capacity);//给存放哈希表的数组动态开辟内存
for (i=0;i<capacity;i++)//所有位置的储存状态置为空
{
ht->arr[i]._state=EM;
}
ht->_size=0;
ht->_IsLineDetective=IsLineDetectiev;
ht ->_capacity=capacity;
}
int HashFunc (DataType data, HT*ht)
{
return data%ht->_capacity;//哈希函数;除留余数法
}
int DetectiveLine(int hashaddr,HT*ht)//一次线型探测
{
hashaddr++;
if (hashaddr==ht->_capacity)
hashaddr=0;
return hashaddr;
}
int Detective2(int hashaddr,int i,HT*ht)//二次(平方)探测
{
hashaddr=hashaddr+2*i+1;
if (hashaddr>=ht->_capacity)
hashaddr%=ht->_capacity;
return hashaddr;
}
void HashTableInsert (HT *ht ,DataType data)//哈希表的插入
{
int i=0;
int hashaddr=-1;
assert (ht);
CheckCapacity(ht);//查看并决定是否需要扩容
hashaddr=HashFunc(data,ht);//通过哈希函数计算应插位置
while (EM!=ht->arr[hashaddr]._state)
{
if (ht->arr[hashaddr]._state==EX)
{
if (ht->arr[hashaddr]._data==data)//已经有这个数据就不用插入,直接返回
return;
}
if (ht->_IsLineDetective)//当前的位置被占(被不是要插的数据占),或被删除过,则不可用(哈希冲突),向后探测
{
hashaddr=DetectiveLine(hashaddr,ht);
}
else {
++i;
hashaddr=Detective2(hashaddr,i,ht);
}
}
ht ->arr[hashaddr]._state=EX;//向已经找到的位置,放入数据
ht->arr[hashaddr]._data=data;
ht->_size++;
}
int HashTableFind (HT*ht,DataType data)//在哈希表中查找数据,与插入相似
{
int hashaddr=-1,startaddr=-1,i=0;
assert(ht);
hashaddr =HashFunc(data,ht);
startaddr=hashaddr;//记录开始查找位置(既不发生哈希冲突时应存位置)
while (EM!=ht->arr[hashaddr]._state)
{
if (ht->arr[hashaddr]._state==EX)//找到返回
if (ht->arr[hashaddr]._data==data)
return hashaddr;
if (ht->_IsLineDetective)//向后查找
{
hashaddr=DetectiveLine(hashaddr,ht);
if (hashaddr==startaddr)
return -1;
}
else {
++i;
hashaddr=Detective2(hashaddr,i,ht);
if (hashaddr==startaddr)
return -1;
}
}
return -1;
}
void HashTableDelete(HT* ht ,DataType data)//哈希表的删除
{
int ret=-1;
assert (ht);
ret =HashTableFind(ht,data);
if (ret!=-1)
{
ht->arr[ret]._state=DL;
ht->_size--;
}
}
int HashTableSize(HT*ht)//哈希表的大小
{
assert (ht);
return ht->_size;
}
int HashTableEmpty(HT*ht)//哈希表是否为空
{
assert (ht);
if ( 0==ht->_size)
return 1;
}
void HashTableDestory(HT*ht)//哈希表的销毁
{
int i=0;
free(ht->arr);
ht->arr=NULL;
ht->_size=0;
ht->_IsLineDetective=0;
ht ->_capacity=0;
}
int CheckCapacity(HT*ht)//哈希表的扩容
{
int i=0;
int capacity;
assert (ht );
if (ht->_size*10/ht->_capacity>7)//当已用空间占到总容量的70%时扩容
{
HT newht;
capacity=ht->_capacity*2;
HashTableInit(&newht ,capacity,ht->_IsLineDetective);//初始化一个新表,(新表的容量是老表的2倍)
for (i=0;i<ht->_capacity;i++)//把老表中的元素插入新表
{
if (EX==ht->arr[i]._state)
HashTableInsert (&newht ,ht->arr[i]._data);
}
swap(&newht,ht);//把新表的内容交给老表(新表只是一个函数体内的变量,除了函数体会被销毁)
HashTableDestory(&newht);//销毁新表
}
return 1;
}
void TestHashTable()
{
HashTable ht;
HashTableInit(&ht,6,1);
HashTableInsert (&ht ,23);
HashTableInsert (&ht ,14);
HashTableInsert (&ht ,74);
HashTableInsert (&ht ,33);
HashTableInsert (&ht ,19);
HashTableInsert (&ht ,29);
HashTableInsert (&ht ,29);
printf("size=%d\n",HashTableSize(&ht));
if (-1!=HashTableFind(&ht,33))
{
printf ("have\n");
}
else
printf ("no have\n");
if (-1!=HashTableFind(&ht,3))
{
printf ("have\n");
}
else
printf ("no have\n");
HashTableDelete(&ht,23);
printf ("size=%d\n",HashTableSize(&ht));
if (-1!=HashTableFind(&ht,33))
{
printf ("have\n");
}
else
printf ("no have\n");
}
int main ()
{
TestHashTable();
return 0;
}
-- INSERT -- 258,2 Bot