如何在大量的信息中找到给定的信息元素?
顺序查找、二分查找、插值查找、斐波那契查找、树表查找、分块查找、哈希查找
查找+数据的存储结构是线性表(顺序存储或者链式存储)
基本思想:
从数据结构线性表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值K相比较,若相等则表示查找成功;若扫描结束没有找到关键字等于k的结点,表示查找失败。
规则:
基本流程:
代码:
//顺序查找
int SequenceSearch(int a[], int value, int n)
{
int i;
for(i=0; i
当查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
(1)是一种线性查找。
(2)是一种无序查找。
(1)算法十分简单。
(2)对静态查找表的记录没有任何要求,在一些小型数据的查找时,是可以适用的。
(1)当n很大时,查找效率极为低下。
(1)查找+序列是有序的。
(2)线性表中的记录必须是关键码有序。
(3)线性表必须采用顺序存储。
在有序表中,中间结点把线性表分成两个子表。用给定的k值和中间结点的关键字比较,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找成功或查找结束发现表中没有这样的结点。
(1)判断查找的序列是否有序,如果无序,则先进行排序操作。
(2)将待比较的key值和第mid=(low+high)/2位置的元素比较,比较结果分为三种情况:
1)相等,mid位置的元素就是需要寻找的元素。
2)大于,low=mid+1;
3)小于,high=mid-1.
在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区域继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区域继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录 ,查找失败为止。
非递归二分查找法:
//二分查找(折半查找),版本1
int BinarySearch1(int a[], int value, int n) //int BinarySearch1(int *a,int n,int value)也可。
{
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]
递归二分查找法:
//二分查找,递归版本
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]
最好的情况下,只有查找1次。
最坏的情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);
(1)二分查找的前提条件是处理的数据结构是有序表的顺序存储。对于静态查找表,一次排序后不再变化,二分查找能够得到不错的效率。但是对于需要频繁执行插入或者删除操作的数据集来说,维护有序的排序会带来不小的工作量。这种情况下就不建议使用二分查找法。
(2)二分查找方法不是自适应的算法(即傻瓜算法),一定要对半查找,不能根据实际情况调整下一次查找的位置。--解决方法:插值查找。
(3)是一种每次取的都是中间记录的查找方法。
(1)二分查找就是折半查找。
(2)折半查找可以理解为查找过程是在一颗二叉树上进行查找。二叉树的根结点是所有记录的中间记录。折半查找等于是把静态有序查找表分成了两颗子树,即查找结果只需要找其中的一半数据记录即可。
查找+二分查找不能调整查找点,不能根据实际情况进行自适应查找。
基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
//插值查找
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]
复杂度分析:查找成功或者失败的时间复杂度均为O(log2(log2n))。
(1)差值查找也属于有序查找。
(2)是一种自适应的查找算法。
(3)对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
查找+折半查找的中间点的选取不太恰当。
1.基本的思想?
通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。
2.规则?
将k值和第F(k-1)位置的记录进行比较(即mid=low+F(k-1)-1),比较结果分为三种。
1)相等,mid位置的元素就是要找的元素。
2)大于,令low=mid+1,k=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个,所以可以递归 的应用斐波那契查找。 |
3.流程?
4.代码
// 斐波那契查找.cpp
#include "stdafx.h"
#include
#include
using namespace std;
const int max_size=20;//斐波那契数组的长度
/*构造一个斐波那契数组*/
void Fibonacci(int * F)
{
F[0]=0;
F[1]=1;
for(int i=2;iF[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;itemp[mid])
{
low=mid+1;
k-=2;
}
if(key==tem[mid])
{
if(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<
1.复杂度分析?
2.斐波那契算法的特点?
(1)是一种有序查找算法。
(2)根据斐波那契序列的特点对有序表进行分割。
(3)要求表中记录的个数为某个斐波那契数小1(即n=F(k)-1).如果不满足该要求,则使用某种方法,使得表的记录个数满足该要求。
3.斐波那契算法的优点?
4.斐波那契算法的缺点?
1.基本的思想?
二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后和每个结点的父节点比较大小,查找最适合的范围。
2.规则?
3.流程?
4.代码?
#include
using namespace std;
typedef struct BSTNode
{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
//二叉排序树的插入——递归实现
void InsertBST(BSTree &DT,BSTNode *p)
{
if(DT==NULL)
DT=p;
else if((DT->key) > (p->key))
InsertBST(DT->lchild,p);
else
InsertBST(DT->rchild,p);
}
//二叉排序树结点的删除
void DeleteBST(BSTree &DT,BSTNode *p)
{//要删除结点p,f是p的双亲
BSTNode *f;
BSTNode *q,*fq;
if(!(p->lchild)&&!(p->rchild))//第一种情况:p是叶子结点
{
if(f->lchild==p)//p是左孩子
f->lchild=NULL;
else//p是右孩子
f->rchild=NULL;
q=p;
}
else if(!(p->rchild))//第二种情况:(1)p只有左子树
{
if(f->lchild==p)
f->lchild=p->lchild;
else
f->rchild=p->lchild;
q=p;
}
else if(!(p->lchild))//第二种情况:(2)p只有右子树
{
if(f->lchild==p)
f->lchild=p->rchild;
else
f->rchild=p->rchild;
q=p;
}
else //第三种情况:p既有左子树又有右子树
{//用p的中序后继来代替p
fq=p;//fq是q的双亲
q=p->lchild;
while(q->lchild)
{//遍历找到p的中序后继
fq=q;
q=q->lchild;
}
p->key=q->key;
if(fq==p)
fq->rchild=q->rchild;
else
fq->lchild=q->rchild;
}
delete q;
}
//二叉排序树的构造
void CreateBST(BSTree &DT,int n)
{
int i,j;
int r[100];
BSTNode *s;
DT=NULL;//这里一定要将DT置空,表示刚开始的时候是空树,不置空的话,编译器分配的DT是非空的
for(j=0;j>r[j];
for(i=0;ikey=r[i];
s->lchild=NULL;
s->rchild=NULL;
InsertBST(DT,s);
}
}
//二叉排序树的搜索——递归实现
BSTNode * SearchBST(BSTree &DT,int k)
{
BSTNode *p;
p=DT;
if(DT==NULL)
return NULL;
else if(p->key==k)
return p;
else if(p->key>k)
return SearchBST(p->lchild,k);
else
return SearchBST(p->rchild,k);
}
void main()
{
freopen("in.txt","r",stdin);
BSTree DT;
BSTNode *p;
int k;
CreateBST(DT,13);
cin>>k;
p=SearchBST(DT,k);
cout<key<
1.二叉搜索树的复杂度?
它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
2.二叉搜索树的特点?
(1)无序链表在插入的时候具有较高的灵活性,而有序数组在查找时具有较高的效率,二叉查找树(Binary Search Tree,BST)这一数据结构综合了以上两种数据结构的优点。
3.二叉搜索树的优点?
4.二叉搜索树的缺点?
5.二叉搜索树的性质:
对二叉查找树进行中序遍历,即可得到有序的数列。
1.二叉搜索树的形状上有什么特点?
特点:二叉搜索树要么是一棵空树,要么是具有以下性质的二叉树。
(1)若任意结点的左子树不为空,则左子树上所有结点的值均小于它的根结点的值。
(2)若任意结点的右子树不为空,则右子树上所有结点的值大于它的根结点的值;
(3)任意结点的左、右子树也分别为二叉查找树。
2.二叉树可以进行哪些扩展?
基于二叉查找树进行优化,进而可以得到其他的树表查找算法,如平衡树、红黑树等高效算法。二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,我们可以使用平衡查找树。平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。
除此之外,2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。
将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
step1 先选取各块中的最大关键字构成一个索引表;
step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。
//分块查找——索引查找
typedef struct
{
int key;
int stadr;
}IndexItem;
typedef struct
{
IndexItem elem[51];
int length;
}IndexTable;//索引表
int Search_Index(SSTable &ST,IndexTable &ID,int k)
{
int low,high,mid;
int p;//p用来保存查找的关键字所属的索引中的位置
int s,t;//s,t分别用来保存查找的关键字所在块的起始和终点位置
low=0;
high=ID.length-1;
while(low<=high && p<0)
{//该循环是用对半查找的方法,对索引表进行查找,从而定位要查找的元素所在的块
mid=(low+high)/2;
if(k>ID.elem[mid-1].key && kID.elem[mid].key)
low=mid+1;
else
p=mid;
}
}
s=ID.elem[p].stadr;
if(p==ID.length-1)
t=ST.length;//这里对p的判断很重要,p若是索引中最后一个位置,则t应该为ST的表长
else
t=ID.elem[p+1].stadr-1;
while(k!=ST.r[s]&&s<=t)//这里在块里进行顺序查找
s++;
if(s>t)
return 0;
else
return s;
}
//建立需要查找的表,和对半查找用的索引表
void CreateTable(SSTable &ST,IndexTable &ID,int n,int m)
{
int i;
cin>>ST.length;
for(i=1;i<=n;i++)
cin>>ST.r[i];
cin>>ID.length;
for(i=0;i>ID.elem[i].key>>ID.elem[i].stadr;
}
void main()
{
freopen("in.txt","r",stdin);
//freopen("in1.txt","r",stdin);
int i,j,k;
SSTable ST;
IndexTable ID;
CreateTable(ST,ID,34,7);
i=Search_Index(ST,ID,80);
cout<
xx
2.分块查找的特点?xx
3.分块查找的优点?33
4.分块查找的缺点?哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
1)用给定的哈希函数构造哈希表;
#include "stdio.h"
#include "stdlib.h"
#define HASHSIZE 7 // 定义散列表长为数组的长度
#define NULLKEY -32768
typedef int Status;
typedef struct{
int *elem; // 数据元素存储地址,动态分配数组
int count; // 当前数据元素个数
}HashTable;
// 散列表表长,全局变量
int m = 0;
void InitHashTable(HashTable *hashTable);
Status Hash(int key);
void Insert(HashTable *hashTable,int key);
Status Search(HashTable *hashTable,int key);
void DisplayHashTable(HashTable *hashTable);
int main(int argc, const char * argv[]) {
int result;
HashTable hashTable;
int arr[HASHSIZE] = {13,29,27,28,26,30,38};
//初始化哈希表
InitHashTable(&hashTable);
/**
* 向哈希表中插入数据;
也就是把元素使用哈希函数映射到哈希表中;
*/
for (int i = 0;i < HASHSIZE;i++){
Insert(&hashTable,arr[i]);
}
//数据已存到哈希表中,打印观察哈希表,元素的位置和原数组是完全不一样的
DisplayHashTable(&hashTable);
//查找数据
result = Search(&hashTable,30);
if (result == -1){
printf("没有找到!");
}else{
printf("在哈希表中的位置是:%d\n",result);
}
return 0;
}
//初始化一个空的哈希表
void InitHashTable(HashTable *hashTable){
m = HASHSIZE;
hashTable->elem = (int *)malloc(m * sizeof(int)); //申请内存
hashTable->count = m;
for(int i = 0;i < m;i++){
hashTable->elem[i] = NULLKEY;
}
}
//哈希函数(除留余数法)
Status Hash(int key){
return key % m;
}
//插入
void Insert(HashTable *hashTable,int key){
/**
* 根据每一个关键字,计算哈希地址hashAddress;
*/
int hashAddress = Hash(key); //求哈希地址
/**
* 发生冲突,表示该位置已经存有数据
*/
while(hashTable->elem[hashAddress] != NULLKEY){
//利用开放定址的线性探测法解决冲突
hashAddress = (hashAddress + 1) % m;
}
//插入值
hashTable->elem[hashAddress] = key;
}
//查找
Status Search(HashTable *hashTable,int key){
//求哈希地址
int hashAddress = Hash(key);
//发生冲突
while(hashTable->elem[hashAddress] != key){
//利用开放定址的线性探测法解决冲突
hashAddress = (hashAddress + 1) % m;
if (hashTable->elem[hashAddress] == NULLKEY || hashAddress == Hash(key)){
return -1;
}
}
//查找成功
return hashAddress;
}
//打印结果
void DisplayHashTable(HashTable *hashTable){
for (int i = 0;i < hashTable->count;i++){
printf("%d ",hashTable->elem[i]);
}
printf("\n");
}
单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。
(1)直接定址法
函数公式:f(key) = a * key + b(a,b为常数)
这种方法的优点是:简单、均匀,不会产生冲突。但是需要事先知道关键字的分布情况,适合查找表较小并且连续的情况。
(2)数字分析法
也就是取出关键字中的若干位组成哈希地址。比如我们的11位手机号是“187****1234”,其中前三位是接入号,一般对应不同的电信公司。中间四位表示归属地。最后四位才表示真正的用户号。
如果现在要存储某个部门的员工的手机号,使用手机号码作为关键字,那么很有可能前面7位都是相同的,所以我们选择后面的四位作为哈希地址就不错。
(3)平方取中法
取关键字平方后的中间几位作为哈希地址。由于一个数的平方的中间几位与这个数的每一位都有关,所以平方取中法产生冲突的机会相对较小。平方取中法所取的位数由表长决定。
如:K=456,K^2=207936,如果哈希表的长度为100,则可以取79(中间两位)作为哈希函数值。
(4)折叠法
折叠法是将关键字从左到右分割成位数相等的几个部分(最后一部分位数不够可以短),然后将这几部分叠加求和,并按哈希表表长,取后几位作为哈希地址。当关键字位数很多,而且关键字中每一位上数字分布大致均匀时,可以使用折叠法。
如:我们的关键字是9876543210,哈希表表长三位,我们可以分为四组:987 | 654 | 321 | 0,然后将他们叠加求和:987+654+321+0 = 1962,再取后三位就可以得到哈希地址为962.
(5)除留余数法
选择一个适当的正整数p(p<=表长),用关键字除以p,所得的余数可以作为哈希地址。即:H(key) = key % p(p<=表长),除留余数法的关键是选取适当的p,一般选p为小于或等于哈希表的长度(m)的某个素数。
如:m = 8,p=7.
m = 16,p = 13.
m = 32,p = 31.
(6)随机数法
函数公式:f(key) = random(key). 这里的random是随机函数,当关键字的长度不等时,采用这种方式比较合适。
总之,哈希函数的规则就是:通过某种转换关系,使关键字适度的分散到指定大小的顺序结构中。越分散,查找的时间复杂度就越小, 空间复杂度就越高。哈希查找明显是一种以空间换时间的算法。
(1)开放定址法
当冲突发生时,使用某种方法在哈希表中形成一探查序列。然后沿着该探查序列逐个单位的查找,直到找到一个开放的地址(即该地址单元为空)为止。对于哈希表中形成一探查序列时,可以有3种不同的方法:
1.线性探测法
将散列看成一个环形表,探测序列是(假设表长为m):
H(k),H(k)+1,H(k)+2.....m-1,0,1......H(k)-1。用线性探测法解决冲突时,求下一个开放地址的公式为:Hi = (H(k)+i) MOD m.
2.二次探测法
二次探测法的探测序列依次是12,-12,22,-22等等。当发生冲突时,求下一个开放地址的公式为:
H2i-1=(H(k)+i2)MODm
H2i=(H(k)-i2)MODm(1=优点:减少了堆集发生的可能性;
缺点:不容易探测到哈希表空间。
3.伪随机探测法
采用随机探测法解决冲突时,下一个开放地址的公式为:Hi=(H(k)+Ri)MODm。
其中R1,R2,...,Rm-1是一个随机排列。
(2)再哈希法
当冲突发生时,使用另一个函数计算得到一个新的哈希地址,直到冲突不再发生时为止。Hi=RHi(key)i=1,2,…,k 。其中RHi均是不同的哈希函数。优点是不易产生聚集,缺点是增加了计算时间。
(3)链地址法
将所有关键字为同义词的结点链接在同一个单链表中。若选定的哈希函数所产生的哈希地址为0~m-1,则可以将哈希表定义成一个由m个链表头指针组成的指针数组。优点是:不产生聚集;由于结点空间是动态申请的,故更适合造表前无法确定表长的情况;从表中删除节点容易。
(4)公共溢出区法
假设哈希函数的值域为[0...m-1],则设向量HashTable[0...m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0..v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都被填入溢出表中。
在哈希表上进行查找的过程和建表的过程基本一致。假设给定的值为k,根据建表时设定的哈希函数H,计算出哈希地址H(k),若表中该地址对应的空间未被占用。则查找失败。否则将该地址中的节点与给定值k比较,若相等则查找成功,否则按建表时设定的处理冲突方法找下一个地址,如此反复下去,直到找到某个地址空间未被占用(查找失败)或者关键字比较相等(查找成功)为止。
我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV pair。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么?
Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
1.什么是查找?
#定义:查找是指给定某个值,在查找表中确定一个关键字等于给定值的数据元素(或记录)。
#其他描述(解释和理解):
(1)查找是在大量的信息中寻找一个特定的信息元素。
(2)查找是常用的基本运算。
2.查找算法的分类?
=>第一种分类:静态查找和动态查找。(针对对查找表的操作而言的)
(1)静态查找:
(2)动态查找:
#总结:
#对静态查找和动态查找的认识(理解):
(1)静态查找和动态查找都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
=>第二种分类:无序查找和有序查找(针对查找的对象的有序或无序)
(1)无序查找:被查找的数列可以是有序也可以是无序。
(2)有序查找:被查找的数列必须为有序数列。
3.什么是平均查找长度(Average Search Length,ASL)?
=>定义:需要和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
=>公式:。
公式整体的意义:ASL表示的是对于含有n个数据元素的查找表,查找成功的平均查找长度。
每个分量的意义:
Pi:查找表中第i个数据元素的概率。
Ci:找到第i个数据元素时已经比较过的次数。
=>理解:
(1)
哈希算法:http://touch-2011.iteye.com/blog/1090305
===============================================================
参考资料:
https://www.cnblogs.com/yw09041432/p/5908444.html
https://www.cnblogs.com/QG-whz/p/4536875.html