对于数据的基本操做是增删查改(CRUD)(Create(建立)、Retrieve(检索)、Update(更新,更改)、Delete(删除)),咱们能够注意到对于删和改两个基本操做,通常在进行以前都会先进行查找操做。所以查找技术至关重要,也由此产生了专门面向查找技术的各类数据结构。查找技术是多元化的,好比数据库数据的查找,或者对于电脑文件的快速查找等等。这些查找技术一部分是须要咱们紧紧掌握的,一部分是须要咱们深入理解的。下面开始对部分查找技术进行介绍:web
在查找问题中,一般将数据元素称为记录。
关键码:能够标识一个记录的数据项称为关键码,关键码的值称为键值,若关键码能够惟一标识一个记录,则称此关键码为主关键码,反之称此关键码为次关键码。
广义地讲,查找是在具备相同类型的记录构成的集合中找出知足给定条件的记录。给定的条件多是多种多样的,便与讨论,咱们把给定条件限制为“匹配”,即查找的关键码等于给定值的记录。
静态查找与动态查找:不涉及插入和删除的查找称为静态查找,反之为动态查找,动态查找不成功时,须要将被查找的记录插入到查找集合中,查找的结果可能会改变查找集合。
查找结构:通常而言,各类数据结构都会涉及到查找操做,有些数据结构专门面向查找操做,称为查找结构。
查找算法性能计算:将查找算法进行关键码的比较次数的数学指望值定义为平均查找长度(average search length),对于查找成功的状况,计算公式为:ASL=pi*ci(i从1到n的和),其中n为问题规模,查找集合中的记录个数。pi为查找第i个记录的几率,ci为查找第i个记录须要关键码的比较次数。
下面主要介绍的查找结构为:
线性表:适用于静态查找,主要采用顺序查找技术,折半查找技术。
树表:适用于动态查找,主要采用二叉排序树的查找技术。
散列表:静态查找和动态查找均适用,主要采用散列技术。算法
线性表对应的顺序存储结构以及链式存储结构决定了线性表的不一样查找方式。
顺序查找,sequential search又称线性查找,是最基本的查找技术之一,基本思想:从线性表的一端向另外一端逐个将关键码与给定值进行比较。若相等,则查找成功,给出该记录在表中的位置,若整个表检测完仍未找到与给定值相等的关键码,则查找失败,给出失败信息。
顺序表的顺序查找:
设置哨兵(待查值)(将它放在查找方向的尽头处,从另外一端开始查找,减小了每次查找过程当中检查是否越界的状况,实验代表,这种设置在查找大于1000次时,顺序查找的平均时间几乎减少了一半。)
实际上:一切简化边界条件而引入的附加节点(或记录)都可称为哨兵,例如单链表中的头结点。
例:数据库
int SeqSearch(int r[],int n,int k){ r[0]=k; //利用下标0做为监视哨 i=n; while(r[i]!=k){ //不用判断下标是否越界 i--; return i; }
单链表的顺序查找算法:数据结构
int SeqSearch(Node<int>*firsdt,int k){ p=first->next; //工做指针p初始化为指向第一个元素节点 count=1; while(p!=NULL&&p->data!=k){ p=p->next; //工做指针后移 j++; } if(p->data==k){ return j; } else{ return 0; } }
比较:顺序查找与其余查找技术比较,平均查找长度较大,特别是n较大时,查找效率很低。
折半查找:
要求:线性表中的记录必须按关键码有序,而且采用顺序存储结构。折半查找技术通常只能用于静态查找。
例:
折半查找——非递归算法:svg
int BinSearch1(int r[ ], int n, int k){ int low=1; high=n,mid; while (low<=high) { mid=(low+high)/2; if (k<r[mid]) high=mid-1; else if (k>r[mid]) low=mid+1; else return mid; } return 0; }
折半查找——递归算法:函数
int BinSearch2(int r[ ], int low, int high, int k){ if (low>high) return 0; else { mid=(low+high)/2; if (k<r[mid]) return BinSearch2(r, low, mid-1, k); else if (k>r[mid]) return BinSearch2(r, mid+1, high, k); else return mid; } }
根据折半查找的过程,咱们能够想到二叉树的逻辑结构,咱们尝试用二叉树的结构来模拟折半查找的过程,由此造成了断定树:
树中的每一个结点对应有序表中的一个记录,结点的值为该记录在表中的位置。一般称这个描述折半查找过程的二叉树为折半查找断定树,简称断定树。
断定树的构造方法:
当n=0时,折半查找断定树为空;
当n>0时,折半查找断定树的根节点为mid=(n+1)/2;
根节点的左子树是与有序表r[1]r[mid-1]相对应的折半查找断定树,根节点的右子树是与r[mid+1]r[n]相对应的折半查找断定树。
具体结构以下:
性质:
任意结点的左右子树中结点个数最多相差1
任意结点的左右子树的高度最多相差1
任意两个叶子所处的层次最多相差1性能
查找成功:
在表中查找任一记录的过程,便是折半查找断定树中从根结点到该记录结点的路径,和给定值的比较次数等于该记录结点在树中的层数。
查找成功时的平均查找长度ASL:
查找不成功:
查找失败的过程就是走了一条从根结点到外部结点的路径,和给定值进行的关键码的比较次数等于该路径上内部结点的个数(失败状况下的平均查找长度等于树的高度)。设计
上述结构查找成功时:
该树的ASLsucc=(1+22+34+4*4)/11=33/11=3指针
上述结构查找失败时:
查找不成功时的ASLusucc= ( 34+48) /12=11/3
任意两棵折半查找断定树,若它们的结点个数相同,则它们的结构彻底相同
具备n个结点的折半查找树的高度为:
code
线性表查找是静态的查找,要在线性表上进行动态查找,存在如下的问题:
1、无序顺序表上进行动态查找,插入操做简单,但查找的复杂性高
2、有序顺序表上进行动态查找,查找的时间复杂性好,可是插入操做时间复杂性高
3、单链表上进行动态查找,插入操做简单,但查找操做复杂性高
解决办法:
采用二叉树这种数据结构,实现动态查找。
二叉排序树(也称二叉查找树)
(1)若它的左子树不空,则左子树上全部结点的值均小于根结点的值;
⑵若它的右子树不空,则右子树上全部结点的值均大于根结点的值;
⑶ 它的左右子树也都是二叉排序树。
中序遍历二叉排序树能够获得一个按关键码有序的序列。
二叉排序树的插入算法:
若二叉排序树为空树,则新插入的结点为新的根结点;
不然,若是插入的值比根节点值大,则在右子树中进行插入;不然,在左子树中进行插入。
递归实现。
例:
void BiSortTree::InsertBST(BiNode<int> * &root, BiNode<int> *s) { if (root==NULL) root=s; else{ if (s->data<root->data) InsertBST(root->lchild, s); else InsertBST(root->rchild, s); } }
二叉排序树的删除:
二叉排序树的删除分为如下三种状况考虑:
1.被删除的结点是叶子:操做:将双亲结点中相应指针域的值改成空。
2.被删除的结点只有左子树或者只有右子树:操做:将双亲结点的相应指针域的值指向被删除结点的左子树(或右子树)。
3.被删除的结点既有左子树,也有右子树:
查找结点p的右子树上的最左下结点s及s双亲结点par;
将结点s数据域替换到被删结点p的数据域;
若结点p的右孩子无左子树,则将s的右子树接到par的右子树上; 不然,将s的右子树接到结点par的左子树上;
删除结点s;
例:
void BiSortTree::DeleteBST(BiNode<int> *p, BiNode<int> *f ) { if (!p->lchild && !p->rchild) { if(f->child==p) f->lchild= NULL; else f->lchild= NULL; delete p; } else if (!p->rchild) { //p只有左子树 if(f->child==p) f->lchild=p->lchild; else f->rchild=p->lchild; delete p; } else if (!p->lchild) { //p只有右子树 if(f->child==p) f->lchild=p->rchild; else f->rchild=p->rchild; delete p; } else { //左右子树均不空 par=p; s=p->rchild; while (s->lchild!=NULL) //查找最左下结点 { par=s; s=s->lchild; } p->data=s->data; if (par==p) p->rchild=s->rchild; //处理特殊状况 else par->lchild=s->rchild; //通常状况 delete s; } //左右子树均不空的状况处理完毕 }
二叉排序树的查找:二叉排序树的查找性能取决于二叉排序树的形状,在O(log2n)和O(n)之间。
在二叉排序树中查找给定值k的过程是:
⑴ 若root是空树,则查找失败;
⑵ 若k=root->data,则查找成功;不然
⑶ 若k<root->data,则在root的左子树上查找;不然
⑷ 在root的右子树上查找。
上述过程一直持续到k被找到或者待查找的子树为空,若是待查找的子树为空,则查找失败。
二叉排序树的查找效率在于只需查找二个子树之一。
例:
BiNode *BiSortTree::SearchBST(BiNode<int> *root, int k) { if (root==NULL) return NULL; else if (root->data==k) return root; else if (k<root->data) return SearchBST(root->lchild, k); else return SearchBST(root->rchild, k); }
二叉排序树的判断(判断一棵树是不是二叉排序树):
若是是空树,返回true;
若是是叶子,返回true;
不然
bst=true
若是root->lchild 是BST
若是root->rchild是BST,
寻找root的最左的分支和root的最右分支并比较大小
若是不知足,bst= false
bst=false
例:
bool BiTree<T>:: Is_BST(BiNode<T> *root){ if(root==NULL) return true; if( root->lchild==NULL && root->rchild==NULL) return true; else { bool l_bst,r_bst,bst=true; l_bst=Is_BST(root->lchild); if(l_bst) { r_bst=Is_BST(root->rchild); if(r_bst) { BiNode <T> *l_child,*l_pre=NULL, *r_child,*r_pre=NULL; l_child=root->lchild; r_child=root->rchild; while(l_child) { l_pre=l_child; l_child=l_child->rchild; } while(r_child) { r_pre=r_child; r_child=r_child->lchild; } if(l_pre&&root->data<l_pre->data || r_pre&&root->data>r_pre->data) bst=false; } else bst=false; } else bst=false; return bst; } }
基于遍历的思想判断
在中序遍历的过程当中查看中序遍历中相邻的两个节点之间是否正序,是则继续遍历,不然,为非二叉排序树。
例:
template<class T> void BiTree<T>::In_BST(BiNode<T> *root,int &key,bool &bst){ if(root&&bst) { In_BST(root->lchild,key,bst); if(key<=root->data ) { key=root->data; } else bst=false; In_BST(root->rchild,key,bst); } } template<class T> bool BiTree<T>:: Is_BST_2(BiNode<T> *root) { int data; BiNode<T> * pre,*t_root; t_root=root; while(root!=NULL) { pre=root; root=root->lchild; } data=pre->data; //找着中序遍历的第一个节点; bool bst=true; In_BST(t_root,data,bst); if(bst) return true; else return false; }
平衡二叉树(AVL树)
或者是一棵空的二叉排序树,或者是具备下列性质的二叉排序树:
⑴ 根结点的左子树和右子树的深度最多相差1;
⑵ 根结点的左子树和右子树也都是平衡二叉树。
平衡因子:结点的平衡因子是该结点的左子树的深度与右子树的深度之差。 在平衡树中,结点的平衡因子能够是1,0,-1。(由平衡二叉树的定义性质决定)
最小不平衡子树:在平衡二叉树的构造过程当中,以距离插入结点最近的、且平衡因子的绝对值大于1的结点为根的子树。 这时咱们就要对最小不平衡子树做出相应调整使得该树仍然为平衡二叉树。所以在构造二叉排序树的过程当中,每插入一个结点时,首先检查是否因插入而破坏了树的平衡性。
设结点A为最小不平衡子树的根结点,对该子树进行平衡调整概括起来有如下四种状况:
1.LL型:
B=A->lchild; A->lchild=B->rchild; B->rchild=A; A->bf=0; B->bf=0; if (FA==NULL) root=B; else if (A==FA->lchild) FA->lchild=B; else FA->rchild=B;
B=A->rchild; A->rchild=B->lchild; B->lchild=A; A->bf=0; B->bf=0; if (FA==NULL) root=B; else if (A==FA->lchild) FA->lchild=B; else FA->rchild=B;
3.LR型:
4.RL型
概念:
前面接触到的查找技术(顺序查找,折半查找,二叉排序树等)都是经过一系列的给定值与关键码的比较,查找效率依赖于查找过程当中进行的给定值与关键码的比较次数。为了提升效率,能够采用不比较的方式,在存储位置与关键码之间创建一个肯定的对应关系。这样,不通过比较,一次读取就能获得所查元素的查找方法。
散列表:采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表。
散列函数:将关键码映射为散列表中适当存储位置的函数。
散列地址:由散列函数所得的存储位置址 。
关系以下:
由上图得:散列既是一种查找技术,也是一种存储技术。
是经过记录的关键码定位该记录,没有完整地表达记录之间的逻辑关系,因此,散列主要是面向查找的存储结构。
限制:散列技术通常不适用于容许多个记录有一样关键码的状况。散列方法也不适用于范围查找,不能查找最大值、最小值,也不可能找到在某一范围内的记录。
散列技术的关键问题:散列函数的设计,冲突的处理。
冲突:对于两个不一样关键码ki≠kj,有H(ki)=H(kj),即两个不一样的记录须要存放在同一个存储位置,ki和kj相对于H称作同义词。
设计散列函数通常应遵循如下原则:
⑴ 计算简单。散列函数不该该有很大的计算量,不然会下降查找效率。
⑵ 函数值即散列地址分布均匀。函数值要尽可能均匀散布在地址空间,这样才能保证存储空间的有效利用并减小冲突。
散列函数的构造:
直接定址法:H(key) = a key + b (a,b为常数)使用条件:事先知道关键码,关键码集合不是很大且连续性较好。
除留余数法:H(key)=key mod p 除留余数法是一种最简单、也是最经常使用的构造散列函数的方法,而且不要求事先知道关键码的分布。 通常状况下,选p为小于或等于表长(最好接近表长)的最小素数。
数字分析法:根据关键码在各个位上的分布状况,选取分布比较均匀的若干位组成散列地址。适用状况: 事先知道关键码的分布,关键码的分布均匀:
例:关键码为8位十进制数,散列地址为2位十进制数
平方取中法:对关键码平方后,按散列表大小,取中间的若干位做为散列地址(平方后截取)。 适用状况:事先不知道关键码的分布且关键码的位数不是很大。
折叠法(分段叠加法):将关键码从左到右分割成位数相等的几部分,将这几部分叠加求和,取后几位做为散列地址。 适用状况:关键码位数不少,事先不知道关键码的分布。
例:设关键码为2 5 3 4 6 3 5 8 7 0 5,散列地址为三位。
冲突处理方法:堆积:在处理冲突的过程当中出现的非同义词之间对同一个散列地址争夺的现象。
1、闭散列方法( closed hashing,也称为开地址方法,open addressing ,开放定址法)。由关键码获得的散列地址一旦产生了冲突,就去寻找下一个空的散列地址,并将记录存入。
方法:
(1)线性探测法:
当发生冲突时,从冲突位置的下一个位置起,依次寻找空的散列地址。
对于键值key,设H(key)=d,闭散列表的长度为m,则发生冲突时,寻找下一个散列地址的公式为:
Hi=(H(key)+di) % m (di=1,2,…,m-1)
线性探测法构造的散列表的查找算法:
int HashSearch1(int ht[ ], int m, int k) { j=H(k); if (ht[j]==k) return j; //没有发生冲突,比较一次查找成功 i=(j+1) % m; while (ht[i]!=Empty && i!=j) { if (ht[i]==k) return i; //发生冲突,比较若干次查找成功 i=(i+1) % m; //向后探测一个位置 } if (i==j) throw "溢出"; else ht[i]=k; //查找不成功时插入 }
(2)二次探测法:
当发生冲突时,寻找下一个散列地址的公式为:
Hi=(H(key)+di)% m
(di=12,-12,22,-22,…,q2,-q2且q≤m/2)
(3)随机探测法:
当发生冲突时,下一个散列地址的位移量是一个随机数列,即寻找下一个散列地址的公式为:
Hi=(H(key)+di)% m
(di是一个随机数列,i=1,2,……,m-1)
(4)再hash法
2、开散列方法( open hashing,也称为拉链法,separate chaining ,链地址法):
基本思想:将全部散列地址相同的记录,即全部同义词的记录存储在一个单链表中(称为同义词子表),在散列表中存储的是全部同义词子表的头指针。
设n个记录存储在长度为m的散列表中,则同义词子表的平均长度为n / m。
例:
在拉链法构造的散列表查找算法:
Node<int> *HashSearch2(Node<int> *ht[ ], int m, int k) { j=H(k); p=ht[j]; while (p && p->data!=k) p=p->next; if (p->data= =k) return p; else { q=new Node<int>; q->data=k; q->next= ht[j]; ht[j]=q; } }
开散列表与闭散列表的比较:
3、创建公共溢出区:散列表包含基本表和溢出表两部分(一般溢出表和基本表的大小相同),将发生冲突的记录存储在溢出表中。
查找时,对给定值经过散列函数计算散列地址,先与基本表的相应单元进行比较,若相等,则查找成功;不然,再到溢出表中进行顺序查找。
散列查找的性能分析:
因为冲突的存在,产生冲突后的查找仍然是给定值与关键码进行比较的过程。
在查找过程当中,关键码的比较次数取决于产生冲突的几率。而影响冲突产生的因素有:
(1)散列函数是否均匀
(2)处理冲突的方法
(3)散列表的装载因子
α=表中填入的记录数/表的长度
几种不一样处理冲突方法的平均查找长度: