[Algorithms]查找算法
一、顺序表查找
时间复杂度:o(n)
/*顺序查找,参数分别为:数组a、数组长度n、关键字key*/
int SeqSearch(int *a,int n,int key)
{
int i;
for(i=1;i<=n;i++)
{
if(a[i]==key)
return i;
}
return 0;
}
// ****************
// 顺序查找优化:哨兵
// ****************
/*按哨兵顺序查找*/
int SeqSearch2(int *a,int n,int key)
{
int i;
a[0]=key;
i=n;
while(a[i]!=key)
{
i--;
}
return i;
}
二、有序表查找(初始的数据一定要是有序的)
1. 折半类查找
时间复杂度o(log n)
/*折半查找*/
int Binary_Search(int *a,int n,int key)
{
int low,high,mid;
low = 1;
high = n;
while(low <= high)
{
mid = (low+high)/2;
if(key < a[mid])
{
high = mid - 1;
}
else if(key > a[mid])
{
low = mid + 1;
}
else
{
return mid;
}
}
return 0;
}
下面几个改进不进行代码的书写,仅进行想法的说明
2. 插值查找
时间复杂度o(log n)
/*插值查找:对于折半查找算法的改进,主要用于分布均匀数据的查找,若分布不均匀性能会差折半查找很多*/
修改后的mid表达式如下
m
i
d
=
l
o
w
+
(
k
e
y
−
a
[
l
o
w
]
)
(
a
[
h
i
g
h
]
−
a
[
l
o
w
]
)
(
h
i
g
h
−
l
o
w
)
mid = low +\frac{(key - a[low])}{(a[high]-a[low])}(high-low)
mid=low+(a[high]−a[low])(key−a[low])(high−low)
原理解释:
折半查找中,可以将等式进行变换
m
i
d
=
l
o
w
+
h
i
g
h
2
=
l
o
w
+
h
i
g
h
−
l
o
w
2
mid = \frac{low+high}{2}=low+\frac{high-low}{2}
mid=2low+high=low+2high−low
因为是求中间值,所以是1/2,在均匀分布中这个1/2可以转换为(这里可以用线段长度比来理解)
m
i
d
=
a
[
m
i
d
]
−
a
[
l
o
w
]
a
[
h
i
g
h
]
−
a
[
l
o
w
]
mid = \frac{a[mid]-a[low]}{a[high]-a[low]}
mid=a[high]−a[low]a[mid]−a[low]
当我们要查找的值为key时,那么key在该表中的分布位置为
k
e
y
−
a
[
l
o
w
]
a
[
h
i
g
h
]
−
a
[
l
o
w
]
\frac{key-a[low]}{a[high]-a[low]}
a[high]−a[low]key−a[low]
3. 斐波那契查找
时间复杂度o(log n)
/*斐波那契查找:对于折半查找算法的改进,主要用于分布处于表右侧的数据,在左侧的查找性能差*/
原理解释:
首先介绍一个与之紧密相连的概念:黄金分割。黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。0.618被公认为最具有审美意义的比例数字,这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。因此被称为黄金分割。斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。
下面描述算法的步骤:
①首先设置一个计算好的斐波那契数列数组F,通过该数组进行定位,定义k为数组索引。
②计算表长度n位于斐波那契数列中的位置。
while(n>F[k]-1)
k++;
③将不满的数值进行补全
for(i=n;i<F[k-1];i++)
{
a[i]=a[n];
}
④当前分割的下标 mid = low + F[k-1] - 1;
⑤若key位于当前下标所对应值的左侧,k-1;若key位于当前下标所对应值的右侧,k-2;若k=mid时,有两种情况:当mid<=n时,mid即为key的位置;当mid>n时,n为key的位置。
三、线性索引查找
1. 稠密索引(索引项=关键字)
每个数据记录一个索引项,索引项是按照关键码有序的排列。
优点:顺序查找速度快
缺点:数据量大时索引数据集规模大,占用空间大,需要反复区访问磁盘,查找性能下降。
2. 分块索引(将索引项分块)
分块有序:满足块内无序(节省空间),块间有序
索引项:最大关键码、块长、块首指针
3. 倒排索引(索引项属性确定位置)
类似于搜索引擎
索引项:次关键码、记录表号
四、二叉排序树
若左子树不空,则左子树上所有结点的值均小于它的根节点的值。
若右子树不空,则右子树上所有结点的值均大于它的根节点的值。
时间复杂度:o(log n)~o(n)[最好时间复杂度为平衡二叉树]
/*二叉树二叉链表结点结构定义*/
typedef struct BiTNode
{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
/*二叉排序树*/
// 指针f指向T的双亲,其初始调用值为NULL;(f主要是为了方便后续的其他操作,设置p的位置为最后遍历的树结点)
bool SearchBST(BiTree T,int key,BiTree f,BiTree *p)
{
if(!T)
{
*p = f;
return false;
}
else if(key == T->data)
{
*p = T;
return true;
}
else if(key < T->data)
{
return SearchBST(T->lchild,key,T,p);
}
else
return SearcbBST(T->rchild,key,T,p);
}
二叉排序树的插入删除在这里只进行说明
bool InsertBST(BiTree *T,int key)
{
查找key,最后搜索的树结点为p
若找到key值返回false
若未找到:
树为空,设置为根节点;否则继续判断
如果 key<p->data: 插入为左孩子;反之插入为右孩子;
}
bool DeleteBST(BiTree *T,int key)
{
if:当前指向树结点的指针为空,返回删除失败,未找到结点data等于key的结点
else:
if:找到结点data等于key,删除该结点
【
删除的三种情况:(①②可采用相同算法实现)
①删除结点为叶子节点:直接删除
②删除结点只有左子树或右子树:删除当前结点,将子树的最上层结点移到当前位置
③删除结点有左子树也有右子树:
查找到待删结点的前驱(转左一次,查右到底);
将前驱数据复制到待删结点;
将待删结点的左孩子,接到待删结点的双亲结点的右孩子上。
】
else if:
没找到且key值小于当前结点data,查当前结点左孩子;
没找到且key值大于当前结点data,查当前结点右孩子;
}
五、平衡二叉树(AVL树)
平衡二叉树是一种二叉排序树,其中每个结点的左子树和右子树的高度差至多为1。
平衡因子BF=左子树深度-右子树深度
最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树。
实现平衡二叉树通过旋转最小不平衡子树构建。
/*平衡二叉树定义*/
typedef struct BiTNode
{
int data;
int bf;/*平衡因子*/
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
下面讨论几种旋转的情况:
1.右旋(BF值符号都为正)
右旋原理图如下
void R_Rotate(BiTree *p)
{
BiTree L;
L=(*p)->lchild; /*转换根结点*/
(*p)->lchild=L->rchild;/**/
L->rchild=(*p);
*p=L;
}
2.左旋(BF值符号都为负)
此操作与右旋对称
void R_Rotate(BiTree *p)
{
BiTree R;
R=(*p)->rchild; /*转换根结点*/
(*p)->rchild=R->lchild;/**/
R->lchild=(*p);
*p=R;
}
3.左平衡旋转处理(BF符号不同,且最小不平衡子树在左边)
左平衡旋转原理图如下:
#define LH +1 //左高
#define EH 0 //等高
#define RH -1 //右高
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
L=(*T)->lchild;
switch(L->bf)
{/*检查左子树的平衡度,并做相应平衡处理*/
case LH: /*新结点插入在T的左孩子的左子树上,要做单右旋处理*/
(*T)->bf=L->bf=EH;
R_Rotate(T);
break;
case RH: /*新结点插入在T的左孩子的右子树上,要做双旋处理*/
Lr=L->rchild; /*Lr 指向T的左孩子的右子树根*/
switch(Lr->bf)/*修改T及其左孩子的平衡因子*/
{
case LH:
(*T)->bf=RH;
L->bf=EH;
break;
case EH:
(*T)->bf=L->bf=EH;
break;
case RH:
(*T)->bf=EH;
L->bf=LH;
break;
}
Lr->bf=EH;
L_Rotate(&(*T)->lchild);/*对T的左子树做左旋平衡处理*/
R_Rotate(T);/*对T做右旋平衡处理*/
}
}
4.右平衡旋转处理(BF符号不同,且最小不平衡子树在右边)
类似于左平衡旋转处理,此处省略。
5.二叉平衡树插入函数
/*若在平衡二叉排序树T中不存在和e有相同关键字的结点,则插入*/
/*若因为插入使得二叉排序树失去平衡,则做平衡旋转处理,布尔变量taller反映T长高与否*/
bool InsertAVL(BiTree *T,int e,bool *taller)
{
if(!*T)
{/*插入新结点,树长高,置taller为true*/
*T=(BiTree)malloc(sizeof(BiTree));
(*T)->data=e;
(*T)->lchild=(*T)->rchild=NULL;
(*T)->bf=EH;
*taller=true;
}
else
{
if(e==(*T)->data)
{
/*如果树中有相同关键字则不再进行插入*/
*taller=false;
return false;
}
if(e<(*T)->data)
{
/*小于在左子树进行搜索*/
if(!InsertAVL(&(*T)->lchild,e,taller))/*子树中有相同关键字,不再插入*/
return false;
if(*taller)/*已插入*/
{
switch((*T)->bf)/*检查T的平衡度*/
{
case LH: /*左高,左平衡*/
LeftBalance(T);
*taller=false;
break;
case EH: /*等高,左增高*/
(*T)->bf=LH;
*taller=true;
break;
case RH: /*右高,现等高*/
(*T)->bf=EH;
*taller=false;
break;
}
}
}
else
{
/*大于在右子树进行搜索*/
if(!InsertAVL(&(*T)->rchild,e,taller))
return false;
if(*taller)
{
switch((*T)->bf)
{
case LH: /*左高,现等高*/
(*T)->bf=EH;
*taller=false;
break;
case EH: /*等高,右增高*/
(*T)->bf=RH;
*taller=true;
break;
case RH: /*右高,右平衡*/
RightBalance(T);
*taller=false;
break;
}
}
}
}
return true;
}
六、多路查找树(B树)
2-3树
2-3树是每个结点都具有两个(2结点)或三个孩子(3结点)的多路查找树。
2结点包含一个元素和两个孩子(或没有孩子),左子树包含的元素小于该元素,右子树包含的元素大于该元素。
3结点包含一小一大两个元素和三个孩子(或没有孩子),左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。
2-3树插入情况共分为3种:
①空树:插入一个2结点
②插入结点到一个2结点的叶子上
③插入一个新元素到3结点上,这里比较复杂分为几种情况进行讨论
第一种情况:向满2结点子树插入元素,如下图。由于一个结点最多存储2个元素,当5到来时,需要把元素4所在结点升级为3结点,拆分成满足3结点性质的结果得出右图所示情况。
第二种情况:向满3结点子树插入元素,如下图。同理由于一个结点存储元素达到上限,一直回溯到可以变为3结点的上级结点。
第三种情况:根节点也是满3结点,如下图。由于存储都达到了上限,这时需要将树完全展开,得到如右图所示情况。
2-3树删除情况共分为3种:
①所删除元素在一个3结点的叶子结点上,直接删除。
②所删除元素位于一个2结点上,这里分四种情况:
第一种情况:双亲2结点,有一个3结点孩子。将对应结点删除,相应进行左/右旋。【图中为删除左孩子】
第二种情况:双亲2结点,有一个2结点孩子。将根结点直接后继左旋(删除左孩子时)/前驱右旋(删除右孩子时)。【图中为删除左孩子】
第三种情况:双亲3结点,中间孩子为一个元素。将双亲拆分并合并。
个人认为中间孩子还应该有一种情况,书中并没有说明,即中间孩子有两个元素。此时将中间孩子左边的元素改为右图所示结构。
第四种情况:当前树是满二叉树时,需要进行合并处理,如下图。
③所删除元素不在叶子结点,将直接前驱或直接后继元素来补位。根据情况进行选择,改动最小优先。
2-3-4树
2-3树的扩展,多一个4结点。
4结点孩子的个数只能为0或4,左子树包含小于最小元素的元素;第二子树包含大于最小元素,小于第二元素的元素;第三子树包含大于第二元素,小于最大元素的元素;右子树包含大于最大元素的元素。
设初始化数组{7,1,2,5,6,9,8,4,3}插入删除原理图如下:
B树
B树是一种平衡多路查找树,结点最大的孩子数目称为B树的阶,2-3树是3阶B树,2-3-4树是4阶B树。
下面为n个关键字的m阶B树的查找次数
查
找
次
数
k
<
=
l
o
g
m
2
(
n
+
1
2
)
+
1
查找次数k<=log_\frac{m}{2}(\frac{n+1}{2})+1
查找次数k<=log2m(2n+1)+1
B+树
为了解决B树在遍历时候访问外存导致的多次访问导致的时间浪费,插入删除方式与B树类似。
B+树是B树的变形树,出现在分支结点的元素会被当作在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每一个叶子结点都会保存一个指向后一叶子结点的指针。
特别适合有范围的查找,先从根节点找到范围最小值,再从叶子结点该最小值处出发到最大值。
七、散列表查找
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。f称为散列函数,又叫哈希函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。
适合查找与给定值相等的记录。
- 散列函数构造(满足计算简单、散列地址分布均匀则为好的散列函数)
①直接定址法:适用于查找表较小且连续的情况
f
(
k
e
y
)
=
a
∗
k
e
y
+
b
(
a
,
b
为
常
数
)
f(key)=a*key+b(a,b为常数)
f(key)=a∗key+b(a,b为常数)
②数字分析法:处理关键字位数较大的情况,需要事先知道关键字的分布且关键字的若干分布较均匀。
抽取关键字通过反转、循环左/右移、计数叠加等方式进行函数生成。
③平方取中法:适用于不知道关键字分布,位数不大的情况。
关键字平方之后取中间的几位数进行构造。
④折叠法:不需要知道关键字分布,关键字位数较多。
⑤除留余数法:散列表表长m,需要选择合适的除数p。(p一般取接近m的最小质数或不包含小于20质因子的合数)
f
(
k
e
y
)
=
k
e
y
%
p
(
p
<
=
m
)
f(key)=key \% p (p<=m)
f(key)=key%p(p<=m)
⑥随机数法:适用于关键字长度不等。
- 如何解决散列冲突问题
①开放定址:一旦发生冲突就去寻找下一个空的散列地址,只要散列表足够大即可。
线性探测
f
i
(
k
e
y
)
=
(
f
(
k
e
y
)
+
d
i
)
%
m
(
d
i
=
1
,
2
,
3
,
.
.
.
,
m
−
1
)
f_i(key)=(f(key)+d_i)\%m(d_i=1,2,3,...,m-1)
fi(key)=(f(key)+di)%m(di=1,2,3,...,m−1)
二次探测
f
i
(
k
e
y
)
=
(
f
(
k
e
y
)
+
d
i
)
%
m
(
d
i
=
1
2
,
−
1
2
,
2
2
,
−
2
2
,
.
.
.
,
q
2
,
−
q
2
,
q
<
=
m
/
2
)
f_i(key)=(f(key)+d_i)\%m(d_i=1^2,-1^2,2^2,-2^2,...,q^2,-q^2,q<=m/2)
fi(key)=(f(key)+di)%m(di=12,−12,22,−22,...,q2,−q2,q<=m/2)
随机探测
f
i
(
k
e
y
)
=
(
f
(
k
e
y
)
+
d
i
)
%
m
(
d
i
是
一
个
随
机
数
列
)
f_i(key)=(f(key)+d_i)\%m(d_i是一个随机数列)
fi(key)=(f(key)+di)%m(di是一个随机数列)
②再散列函数:事先准备多个散列函数
③链地址:用链表存储关键字,冲突则给单链表增加结点
④公共溢出区:增加溢出表存储冲突
八、红黑树
红黑树也是一种平衡二叉树,红黑树具有特殊的着色属性,或红色或黑色。遵循下面几条规则:
①所有结点要么着红色,要么着黑色
②叶子结点都是黑色
③叶子结点不包含数据
④所有非叶子结点都有两个子结点
⑤如果一个结点是红色的,则它的子结点都是黑色的
⑥在一个结点到其他叶子结点的路径中,如果总是包含同样数目的黑色结点,则该路径相比其他路径是最短的
认识红黑树,首先认识下红黑树与AVL树的区别
[转自:https://www.jianshu.com/p/37436ed14cc6]
深入研究的一篇好文放在这里[https://zhuanlan.zhihu.com/p/93369069]
RB-Tree和AVL树作为BBST,其实现的算法时间复杂度相同,AVL作为最先提出的BBST,貌似RB-tree实现的功能都可以用AVL树是代替,那么为什么还需要引入RB-Tree呢?
红黑树不追求"完全平衡",即不像AVL那样要求节点的 |balFact| <= 1
,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。
就插入节点导致树失衡的情况,AVL和RB-Tree都是最多两次树旋转来实现复衡rebalance,旋转的量级是O(1)
删除节点导致失衡,AVL需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而RB-Tree最多只需要旋转3次实现复衡,只需O(1),所以说RB-Tree删除节点的rebalance的效率更高,开销更小!
AVL的结构相较于RB-Tree更为平衡,插入和删除引起失衡,如2所述,RB-Tree复衡效率更高;当然,由于AVL高度平衡,因此AVL的Search效率更高啦。
针对插入和删除节点导致失衡后的rebalance操作,红黑树能够提供一个比较"便宜"的解决方案,降低开销,是对search,insert ,以及delete效率的折衷,总体来说,RB-Tree的统计性能高于AVL.
故引入RB-Tree是功能、性能、空间开销的折中结果。
AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。
红黑树,读取略逊于AVL,维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。
基本上主要的几种平衡树看来,红黑树有着良好的稳定性和完整的功能,性能表现也很不错,综合实力强,在诸如STL的场景中需要稳定表现。
红黑树的查询性能略微逊色于AVL树,因为其比AVL树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较,但是,红黑树在插入和删除上优于AVL树,AVL树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于AVL树为了维持平衡的开销要小得多
总结:实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。对于进行随机性的频繁的插入删除操作,红黑树的性能更好。