1.顺序表查找:
从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个 )记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。顺序表查找的时间复杂度是O(n)。
2.有序表查找-二分查找:
如果数组是有序的,首选二分查找。二分查找的时间复杂度是O(logn)。
//给定一个排序数组,从中找出某个数值的出现次数
//排序数组,肯定使用二分查找,查找出第一个和最后一个,然后取下标差
int AppearTimesOfNumber(intSrcArray[],int length,int number)
{
assert(SrcArray!=NULL);
int FirstAppear=0,LastAppear=0;
int Begin=0;
int End=length-1;
int Mid=0;
//先找第一次出现的坐标
while(Begin<End-1)
{
Mid=Begin+(End-Begin)/2;
if(SrcArray[Mid]>=number)
End=Mid;
else
Begin=Mid;
}
if(SrcArray[Begin]==number)
FirstAppear=Begin;
else if(SrcArray[End]==number)
FirstAppear=End;
else
return 0;
//接着寻找最后一次出现的坐标
Begin=0;
End=length-1;
while(Begin<End-1)
{
Mid=Begin+(End-Begin)/2;
if(SrcArray[Mid]>number)
End=Mid;
else
Begin=Mid;
}
if(SrcArray[End]==number)
LastAppear=End;
else
LastAppear=Begin;
return (LastAppear-FirstAppear+1);
}
3.线性索引查找-稠密索引,分块索引,倒排索引(搜索引擎)
1)稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项。因此,对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大
2)分块有序,是把数据集的记录分成了若干块,并且这些块需要满足两个条件:
• 块内无序,即每一块内的记录宋要求有序。当然,你如果能够让块内有序对查找来说更理想,不过这就要付出大量时间和空间的代价,因此通常我们不要求块内有序 。
• 块间有序,例如,要求第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块的所有记录的关键字均要大于第二块的所有记录关键字……因为只有块间有序,才有可能在查找时带来放率。
对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。
在分块索引表中查找,就是分两步进行:
1 . 在分块索引表中查找要查关键字所在的块。由于分块索引表是块间有序的,因此很容易利用折半、插值等算法得到结果。
2 . 根据块首指针找到相应的块,并在块中顺序查找关键码。因为块中可以是无序的,因此只能顺序查找。
3)搜索引擎技术-倒排索引。
4.二叉排序树(平衡二叉树AVL树)
二叉排序树 ( Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。
• 若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
• 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
• 它的左、右子树也分别为二叉排序树。
从二叉排序树的定义也可以知道,它前提是二叉树,然后它采用了递归的定义方法,再者,它的结点间满足一定的次序关系,左子树结点一定比其双亲结点小,右子树结点一定比其双亲结点大。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。具体代码如下:
//二叉排序树定义
typedef struct BiTreeNode
{
int m_nData;
struct BiTreeNode *m_pLeft;
struct BiTreeNode *m_pRight;
}BiNode,*BiTree;
//二叉排序树查找操作
//如果key值存在,指向key值所在的结点,如果不存在指向上一个结点
bool SearchBST(BiTree T,intkey,BiTree p_Prev,BiTree *p_Return)
{
if(!T)
{
*p_Return=p_Prev;
return false;
}
//查找成功,返回true
if(T->m_nData==key)
{
*p_Return=T;
return true;
}
//根节点大于查找值,在左子树中查找
else if(T->m_nData>key)
return SearchBST(T->m_pLeft,key,T,p_Return);
//根节点小于查找值,在右子树中查找
else
return SearchBST(T->m_pRight,key,T,p_Return);
}
//二叉排序树的插入操作,检查是否存在插入值,不存在插入
//插入返回true,否则返回false
bool InsertIntoBST(BiTree*T,int key)
{
BiTree p_PrevNode;
//如果不存在,在结尾结点出插入
if(!SearchBST(*T,key,NULL,&p_PrevNode))
{
BiTree q=(BiTree)malloc(sizeof(BiNode));
q->m_nData=key;
q->m_pLeft=q->m_pRight=NULL;
//空树,结点直接就是根节点
if(!p_PrevNode)
*T=q;
else if(p_PrevNode->m_nData>key)
p_PrevNode->m_pLeft=q;
else
p_PrevNode->m_pRight=q;
return true;
}
else
return false;
}
//有了二叉排序树的插入操作,那么建立一个二叉排序树就简单多了
//根据数组建立一个二叉排序树,返回树的根节点
BiTree BuildBST(BiTree *T,intsrc[],int length)
{
assert(src!=NULL&&length>0);
for(int i=0;i<length;i++)
InsertIntoBST(T,src[i]);
return *T;
}
平衡二叉树:
平衡二叉树(AVL树),是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于 1 。
5.多路查找树(B树)
多路查找树 ( muitl-way search tree ) ,其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。由于它是查找树,所有元素之间存在某种特定的排序关系。
B 树是一种平衡的多路查找树,2 -3 树和 2-3-4 树都是 B 树的特例。结点最大的孩子数目称为 B 树的阶 (order) ,因此, 2-3 树是 3 阶 B 树, 2-3 - 4树是 4阶B树。
一个 m 阶的 B 树具有如下属性:
Ø 如果根结点不是叶结点,则其至少有两棵子树。
Ø 每一个非根的分支结点都有 k-l 个元素和 k 个孩子,其中。
每一个叶子结点 n 都有 k-l 个元素,其中
Ø 所有叶子结点都位于同一层次。
Ø
我们知道,如果内存与外存交换数据次数频繁,会造成了时间效率上的瓶颈,那么 B 树结构怎么就可以做到减少次数呢?
在一个典型的 B 树应用中,要处理的硬盘数据量很大,因此无法一次全部装入内存。因此我们会对 B 树进行调整,使得 B 树的阶数(或结点的元素)与硬盘存储的页面大小相匹配。比如说一棵 B 树的阶为 1001 (即 1 个结点包含 1 000 个关键字) ,高度为 2,它可以储存超过 10 亿个关键字,我们只要让根结点持久地保留在内存中,那么在这棵树上,寻找某一个关键字至多需要两次硬盘的读取即可。
通过这种方式,在有限内存的情况下,每一次磁盘的访问我们都可以获得最大数量的数据。由于 B 树每结点可以具有比二叉树多得多的元素,所以与二叉树的操作不同,它们减少了必须访问结点和数据块的次数 ,从而提高了性能。可以说, B 树的数据结构就是为内外存的数据交互准备的。
6.散列表查找(哈希表)
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字 key 对应一个存储位置 f (key)。查找时,根据这个确定的对应关系找到给定值 key 的映射 f (key),若查找集合中存在这个记录,则必定在 f ( key) 的位置上。
这里我们把这种对应关系 f 称为散列函数,又称为哈希 ( Hash)函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表 (HashTable)。那么关键字对应的记录存储位置我们称为散列地址。散列技术的记录之间不存在什么逻辑关系,它只与关键字有关联。因此,散列主要是面向查找的存储结构。
1. 散列函数的构造方法
两个原则:
1) 计算简单。
如果设计的一个算法可以保证所有的关键字都不会产生冲突,但是这个算法需要很复杂的计算,会耗费很多时间,这对于需要频繁地查找来说,就会大大降低查找的效率。因此散列函数的计算时间不应该超过其他查找技术与关键字比较的时间。
2) 散列地址分布均匀
我们刚才也提到冲突带来的问题,最好的办法就是尽量让散列地址均匀地分布在存储空间中,这样可以保证存储空间的有效利用,并减少为处理冲突而耗费的时间。
几种设计方法:
a) 直接定址法
我们可以取关键字的某个线性函数值为散列地址,即
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。
b) 数字抽取法
数字抽取法就是利用关键字的一部分来计算散列表的位置的方法。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。
c) 平方取中法
这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的 3 位就是227,用做散列地址。再比如关键字是 4321 ,那么它的平方就是18671041,抽取中间的 3 位就可以是 671 ,也可以是 71 0 ,用做散列地址。平方取中法比较适合子不知道关键字的分布,而位数又不是很大的情况。
d) 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如我们的关键字是 9876543210 ,散列表表长为三位,我们将它分为四组,9871654132110,然后将它们叠加求和987+654+321+0=1962,再求后 3 位得到散列地址为 962 。有时可能这还不能够保证分布均匀 ,不妨从一端向另一端来回折叠后对齐相加。比如我们将 987 和 321 反转,再与 654 和 0 相加, 变成789+654+123+0=1566 ,此时散列地址为 566。折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
e) 除留余数法
此方法为最常用的构造散列函数方法。对于散列表长为 m 的散列函数公式为:
事实上,这方法不仅可以对关键宇直接取模,也可在折叠、 平方取中后再取模。
很显然,本方法的关键就在于选择合适的P,P 如果选得不好,就可能会容易产生同义词。因此根据前辈们的经验,若散列表表长为 m ,通常 p 为小子或等于表长(最好接近 m ) 的最小质数或不包含小子 20 质因子的合数。
f) 随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。也就是 f (key)
=random ( key ) 。这里 random 是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
那如果关键字是字符串如何处理?其实无论是英文字符,还是中文字符,也包括各种各样的符号,它们都可以转化为某种数字来对待,比如 ASCII 码或者Unicoæ 码等,因此也就可以使用上面的这些方法。
总之,现实中,应该视不同的情况采用不同的散列函数。我们只能给出一些考虑的因素来提供参考:
1. 计算散列地址所需的时间。
2. 关键字的长度 。
3. 散列袤的大小。
4. 关键字的分布情况。
5 . 记录查找的频率。综合这些因素,才能决策选择哪种散到函数更合适。
处理散列冲突的方法:
如果两个关键字,但却有,我们说此散列函数存在冲突。处理冲突的方法包括:
i. 开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
它的公式是:
我们把这种开放定址法成为线性探测法。这种方法容易产生堆积。因此,我们考虑扩大每次移动的距离,因此有二次探测法,此时。还有一种方法是,在冲突时,对于位移量,采用随机函数计算得到,我们称之为随机探测法。
ii. 再散列函数法
我们事先准备多个散列函数,每当散列地址出现冲突时,我们就换一个散列函数,最后得到不冲突的结果。这种方法能够使得关键字不产生聚集,当然,相应地也增加了计算的时间。
iii. 链地址法
我们还可以进一步变换思路,将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链装的性能损耗。
iv. 公共溢出区法
我们为所有冲突的关键字建立了一个公共的溢出区来存放。在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功。如果不相等,则到溢出表去进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。