一、顺序查找(线性查找)
顺序表、链表都适用
(一)一般线性表的顺序查找
- n个元素的表,给定key=第i个元素,需要进行n-i+1次比较,查找成功的平均长度是(关键字比较次数平均值)ASL=(n+1)/2;查找失败时,比较n+1次,ASL=n+1;时间复杂度都是O(n)
- 缺点:当n较大时,平均查找长度较大,效率低
- 优点:对数据元素的存储没有要求,顺序存储或链式存储皆可;对表中记录的有序性没有要求
- 对链表只能进行顺序查找 2.有序线性表的顺序查找
typedef struct{ //查找表的数据结构
ElemType *elem; //元素存储空间基址,建表时按实际长度 分配,0号单元留空
int TableLen; //表的长度
}SSTable;
int Search_Seq(SSTable ST,ElemType key){
//在顺序表ST中顺序查找关键字为key的元素。若找到则返回该元 素在表中的位置
ST.elem[0]=key; //“哨兵”,不必判断数组是否越界
for (i=ST.TableLen;ST.elem[i]!=key; --i);//从后往前找
return i; //若表中不存在关键字为key的元素,将查找到i为0时退出for循环
}
(二)有序线性表的顺序查找
- 查找成功的长度是ASL=(n+1)/2;查找失败是ASL=n/2+n/(n+1)
- n个结点,有n+1个失败结点,到达第j个失败结点的概率为1/(n+1)
- 顺序存储或链式存储均可
二、折半查找(二分查找)
- 用二叉树(判定树)来描述,判定树是一棵平衡二叉树,只有最下面一层是不满的
- 查找成功时查找长度为从根结点到目的结点的路径上的结点数,ASL=log(2,(n+1))-1,时间复杂度O(log(2,n));查找失败时的查找长度为从根结点到对应失败结点的父结点的路径上的结点数;都是ASL<=h(树高)
- 每个结点值均大于其左子结点值,均小于其右子结点值
- 若有序序列有n个元素,则对应的判定树有n个圆形的非叶结点和n+1个方形的叶结点
- 要求线性表必须具有随机存取的特性,仅适合于顺序存储结构,不适合于链式存储结构,且要求元素按关键字有序排列
- 折半查找不一定比顺序查找更快(存在特殊情况)
int Binary_Search(SeqList L, ElemType key) {
//在有序表L中查找关键字为key的元素,若存在则分会其位置,不存在返回-1
int low = 0, high = L.length - 1, mid;
while (low <= high) {
mid = (low + high) / 2; //取中间位置
if (L.elem[mid] == key)
return mid; //查找成功则返回所在位置
else if(L.elem[mid] > key)
high = mid - 1; //从前半部分继续查找
else
low = mid + 1; //从后半部分继续查找
}
return -1;
}
//取中间位置时,向上/向下取整都可以,但要保证每次查找的取整方式相同
查找成功(圆形结点)的ASL=(1×1+2×2+3×4+4×4)/11=3,查找失败(方形结点)的ASL=(3×4+4×8)/12=11/3(次数x数量)
求判定树:先加右结点后左
若向上取整,则上图改成左-右=0或1
三、分块查找(索引顺序查找)
- 将查找表分为若干子块,块内的元素可以无序,但块间的元素是有序的,即第一个块中的最大关键字小于第二个块中的所有记录的关键字,第二个块中的最大关键字小于第三个块中的所有记录的关键字,以此类推。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列。
- 查找过程:第一步,在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表;第二步,在块内顺序查找。
- 折半查找索引:(见视频例子***)若索引表中不包含目标关键字,则折半查找最终停在low>high,要在low所指分块中查找
//索引表
typedef struct {
ElemType maxValue;
int low,high;
}Index;
//顺序表存储实际元素
ELemType List[100];
四、树形查找
(一)二叉排序树BST
- 是空树,或者 左子树的值均小于根节点均小于右子树的值,其左右子树也分别是二叉排序树
- 中序遍历为递增
- 二叉排序树的查找效率,取决于树的高度。若二叉排序树的左、右子树的高度之差的绝对值不超过1(平衡二叉树),它的平均查找长度和O(log₂n)成正比。在最坏情况下,即构造二叉排序树的输入序列是有序的,则会形成一个只有右孩子的单支树,此时二叉排序树的性能显著变坏,树的高度为n,则其平均查找长度为(n+1)/2
- 二分查找的判定树唯一,二叉排序树的查找不唯一
- 二叉排序树插入、删除的时间O(log₂n),二分查找插入、删除为O(n)
- 动态表用二叉排序树,静态表用二分查找
1.查找
//非递归,最坏空间复杂度O(1)
BSTNode *BST_Search(BiTree T,ElemType key){
while(T!=NULL && key!=T->data) {
if(key<T->data) T=T->lchild;
else T=T->rchild;
}
return T;
}
//递归:简单,效率低,最坏空间复杂度O(h)
BSTNode *BSTSearch(BSTree Tint key){
if(T==NULL)
return NULL;//查找失败
if (key==T->key)
return T;//查找成功
else if(key <T->key)
return BSTSearch(T->lchild,key);//在左子树中找
else
return BSTSearch(T->rchild,key);//在右子树中找
2.插入
- 新插入结点一定是叶子结点,且是查找失败时的查找路径上访问的最后一个结点的左孩子或右孩子
- 空树直接插入,关键字小于结点值则则插入到左子树,关键字大于根结点值,则插入到右子树。
//递归,最坏空间O(h)
int BST_Insert(BiTree &T,KeyType k){
if(T==NULL){//原树为空,新插入的记录为根结点
T=(BiTree)malloc(sizeof(BSTNode));
T->data=k;
T->lchild=T->rchild=NULL;
return 1;//返回1,插入成功
}
else if(k==T->data)//树中存在相同关键字的结点,插入失败
return 0;
else if(k<T->data)//插入T的左子树
return BST_Insert(T->lchild,k);
else //插入T的右子树
return BST_Insert(T->rchild,k);
}
//非递归?
3.构造
void Creat_BST(BiTree &T,KeyType str[],int n) {
T=NULL;//初始时T为空树
int i=0;
while(i<n){//依次将每个关键字插入二叉排序树
BST_Insert(T,str[i]);
i++;
}
}
4.删除
- 若被删除结点z是叶结点,则直接删除
- 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
- 若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况
- 删除并插入节点后,二叉排序树是否和原来相同?
(二)平衡二叉树(AVL树)
- 是空树,或者左、右子树都是平衡二叉树,且左、右子树高度之差(平衡因子=左高-右高)的绝对值不超过1
//结点
typedef struct AVLNode{
int key;//数据域
int balance;//平衡因子
struct AVLNode *lchild,*rchild;
}AVLNode,*AVLTree;
1.插入
- 插入一个结点时,调整最小不平衡子树,即以插入路径上离插入结点最近的平衡因子的绝对值大于1的结点作为根的子树,在保持二叉排序特性的条件下,使重新达到平衡
LR和RL旋转时,新结点究竟是插入C的左子树还是插入C的右子树不影响旋转过程
p289例题
2.删除
- 1)用二叉排序树的方法对结点w执行删除操作。
- 2)若导致了不平衡,则从结点w开始向上回溯,找到第一个不平衡的结点z(最小不平衡子树);y为结点z的高度最高的孩子;x是结点y的高度最高的孩子。
- 3)然后对以z为根的子树进行平衡调整,其中x、y和z可能的位置有4种情况:
- y是z的左孩子,x是y的左孩子(LL,右单旋转);
- y是z的左孩子,x是y的右孩子(LR,先左后右双旋转);
- y是z的右孩子,x是y的右孩子(RR,左单旋转);
- y是z的右孩子,x是y的左孩子(RL,先右后左双旋转)。
- 这四种情况与插入操作的调整方式一样。不同之处在于,插入操作仅需要对以z为根的子树进行平衡调整;而删除操作,先对以z为根的子树进行平衡调整,若调整后子树的高度减1,则可能需要对z的祖先结点进行平衡调整,甚至回溯到根结点(导致树高减1)。
***删除和插入的视频课没看
2.查找
- 关键字的比较次数不超过树的深度O(h)
- 含有n个结点的平衡二叉树的最大深度为O(log₂n),平均查找效率为O(log₂n)。
(三)红黑树
- 性质: ①每个结点或是红色,或是黑色的。 ②根结点是黑色的。 ③叶结点(虚构的外部结点、NULL结点)都是黑色的。 ④不存在两个相邻(父子不行,兄弟可以)的红结点(红结点的父结点和孩子结点均是黑色的)。 ⑤对每个结点,从该结点到任意一个叶结点的简单路径上,所含黑结点的数量相同。
- 红黑树是二叉排序树,左<根<右
- 引入了n+1个外部叶结点,以保证红黑树中每个结点(内部结点)的左、右孩子均非空
- 从某结点出发(不含该结点)到达一个叶结点的任意一个简单路径上的黑结点总数称为该结点的黑高bh。根结点的黑高称为红黑树的黑高。
- 结论1:从根到叶结点的最长路径不大于最短路径的2倍。
- 性质2:有n个内部节点的红照树高度h≤2log(2,(n+1))
- 根节点黑高为h的红黑树内部结点数(含有关键字的)至少有2^h-1个(内部结点数最少的情况:总共h层黑结点的满树形态)
//定义
struct RBnode(
int key;//关键字的值
RBnode* parent;// 父节点指针
RBnode* iChild;
RBnode* rChild;
int color;// 结点颜色,如:可用 0/1表示黑红,也可使用枚举型enum
};
1.查找
- 查找时间为O(log(2,n))
- 查找方法与AVL树相同
2.插入
- 新插入的结点,若是根节点就是黑色,若不是,就是红色
- 设结点z为新插入的结点。插入过程:
1)用二叉查找树插入法插入,并将结点z着为红色。若结点=的父结点是黑色的,无须做任何调整,此时就是一棵标准的红黑树,结束。
2)若结点z是根结点,则将z着为黑色(树的黑高增1),结束。
3)若结点z不是根结点,且z的父结点z.p是红色的,则分为下面三种情况,区别在于z的叔结点(父亲的兄弟)y的颜色不同,因z.p是红色的,插入前的树是合法的,根据性质②和④,爷结点Z.p.p必然存在且为黑色。性质④只在z和z.p之间被破坏了。
情况1:z的叔结点y是黑色的,且z是LL,则右单+父爷位置互换+父爷颜色调换
情况2:z的叔结点y是黑色的,且z是RR,则左单+父爷位置互换+父爷颜色调换
情况3:z的叔结点y是黑色的,且z是LR,则左右双旋+儿爷位置互换+儿爷颜色调换
情况4:z的叔结点y是黑色的,且z是RL,则右左双旋+儿爷位置互换+儿爷颜色调换
情况5:z的叔结点y是红色的,L或R不影响,则叔父爷变色,将爷视为新插入的结点,进行新插入处理
每棵子树T、T₂、T₃和T₄都有一个黑色根结点,且具有相同的黑高。
3.删除(大概率不考)
- 时间为O(log(2,n))
- 处理方式同二叉排序树。删除结点后,可能破坏“红黑树特性”,此时需要调整结点颜色、位置
(四)B树
- 高度h(不包括叶子结点),m阶,n个关键字。最小log(m,(n+1)),最大log(m/2,(n+1)/2)+1
1.查找
- ①在B树中找结点;②在结点内找关键字。
- B树常存储在磁盘上,因此前一查找操作是在磁盘上进行的,而后一查找操作是在内存中进行的,即在磁盘上找到目标结点后,先将结点信息读入内存,然后再采用顺序查找法或折半查找法。
- 在磁盘上进行查找的次数即目标结点在B树上的层次数,决定了B树的查找效率。
2.插入
- 1)定位。利用前述的B树查找算法,找出插入该关键字的终端结点(插入位置一定是最底层的非叶结点)
- 2)插入。每个非根结点的关键字个数都在[⌈m/2⌉-1,m-1]。若结点插入后的关键字个数小于m,可以直接插入;若结点插入后的关键字个数大于m-1,必须对结点进行分裂。
- 分裂的方法:取一个新结点,在插入key后的原结点,从中间位置⌈m/2⌉将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置⌈m/2⌉的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增1。
3.删除
- 当被删关键字k不在终端结点中时,可以用k的前驱(或后继)k',即k的左侧子树中“最右下”的元素(或右侧子树中“最左下”的元素),来替代k,然后在相应的结点中删除k',关键字k'必定落在某个终端结点中,则转换成了被删关键字在终端结点中的情形
- 只需讨论被删关键字在终端结点中的情形,有下列三种情况:
- 1)若被删关键字所在结点删除前的关键字个数≥[m/2],则直接删去该关键字
- 2)兄弟够借。若被删关键字所在结点删除前的关键字个数=[m/2]-1,且与该结点相邻的右(或左)兄弟结点的关键字个数≥[m/2],则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法),以达到新的平衡。即借右:父(当前的后继)放在被删的位置,兄弟(后继的后继)放在父的位置;借左则为前驱、前驱的前驱
- 3)兄弟不够借。若被删关键字所在结点删除前的关键字个数==[m/2]-1,且此时与该结点相邻的左、右兄弟结点的关键字个数都=[m/2]-1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。
- 在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少至0(根结点关键字个数为1时,有2棵子树),则直接将根结点删除,合并后的新结点成为根;若双亲结点不是根结点,且关键字个数减少到[m/2]-2,则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合8树的要求为止。
(四)B+树
五、散列表(Hash哈希表)
- 散列表:根据关键字可以计算出存储地址
- 哈希函数:将关键字映射成地址
- 查找时间O(1),与表中元素个数无关
- 会产生冲突
(一)散列函数构造方法
- 散列函数的定义域必须包含全部关键字,而值域的范围不超过散列表的地址范围。
- 散列函数计算出的地址应尽可能均匀地分布在整个地址空间,尽可能地减少冲突。
- 散列函数应尽量简单,能在较短的时间内计算出任意一个关键字对应的散列地址。下面介绍常用的散列函数。
1.直接定址法
直接取关键字的某个线性函数值为散列地址,散列函数为H(key)=key或H(key)=a×key+b
适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。
2.除留余数法
散列表表长为m,取一个不大于m但最接近或等于m的质数p,散列函数为H(key)=key%p
通用,适合关键字为整数的情况
3.数字分析法
设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较为均匀的若干位作为散列地址。
这种方法适合于已知的关键字集合,且关键字的某几个数码位分布均匀
4.平方取中法
取关键字的平方值的中间几位作为散列地址。具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀
适用于关键字的每位取值都不够均匀,或均小于散列地址所需的位数。
(二)处理冲突方法
1.拉链法
- 把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。
- 假设散列地址为i的同义词链表的头指针存放在散列表的第i个单元中,(头插、尾插均可,没规定就默认头插),因而查找、插入和删除操作主要在同义词链中进行。
- 拉链法适用于经常进行插入和删除的情况。
- 查找:根据函数算地址、跳到该地址、顺着链表查,查找长度为在链表中对比关键字的次数(不计对比空指针的次数)
- 删除:查找成功就删除成功
2.开放定址法
- 表中可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放
- H₁=(H(key)+d)%m,H(key)为散列函数;i=1,2,…,k(k≤m-1);m表示散列表表长;散列单元是0~m-1;di为增量序列。
- i∈[0,m-1]
- 查找成功,就能删除成功
- 采用开放定址法时,不能随便物理删除表中已有元素,否则会截断其他同义词元素的查找路径,删除元素时可以做一个删除标记,进行逻辑删除。但这样做的副作用是:执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用。查找效率低
(1)线性探测法
- di=1,2,…,m-1。
- 冲突发生时,顺序查看表中下一个单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。
- 线性探测法可能使第i个散列地址的同义词存入第i+1个散列地址,这样本应存入第i+1个散列地址的元素就争夺第i+2个散列地址的元素的地址……从而造成大量元素在相邻的散列地址上聚集(或堆积)起来,大大降低了查找效率。
(2)平方探测法(二次探测法)
- di=1²,-1²,2²,-2²,…,k²,-k²,其中k≤m/2,散列表长度m必须是一个可以表示成4k+3的素数。平方探测法是一种处理冲突的较好方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。
(3)双散列法
- di=ixHash₂(key),需要使用两个散列函数,当通过第一个散列函数H(key)得到的地址发生冲突时,则利用第二个散列函数Hash₂(key)计算该关键字的地址增量。具体函数是Hi=(H(key)+ixHash₂(key))%m;初始探测位置H₀=H(key)%m。i是冲突的次数,初始为0。
(4)伪随机序列法
di=伪随机数序列。