一、基本概念
1.查找
在数据集中寻找满足某种条件的数据元素的过程,分为查找成功和查找失败两种情况。
2.查找表(查找结构)
用于查找的数据集合称为查找表,由同一类型的数据元素或记录组成。
基本操作:①查找某个元素是否在表中;②检索满足条件的元素的属性;③插入元素;④删除元素。
3.静态查找表
仅允许查找和检索,而不允许更改的查找表,更注重索引速度。
4.动态查找表
需要进行插入删除操作的查找表,注重插入删除是否方便。
5.关键字
数据元素中唯一标识该元素的某个数据项的值。
6.平均查找长度
查找时,一次查找的长度 = 比较关键字的次数。
平均查找长度ASL = 所有查找过程中进行关键字比较次数的平均值。
二、顺序查找(线性查找)
适用于顺序表和链表,时间复杂度:O(n)
1.线性表顺序查找
从头到尾或从尾到头依此访问查找的一种方式。
typedef struct{
ElemType *elem; //元素存储的空间基址,可空出0号空间存放“哨兵”
int TableLen;
}SSTable;
int Search_Seq(SSTable ST, ElemType key)
{
ST.elem[0] = key; //哨兵(减少数据越界判断,非必需,对时间开销有一定但不多的帮助)
for(int i=ST.TableLen; ST.elem[i] != key; i--);
return i; //若不存在待查找的元素,则将返回0
}
平均查找长度----查找效率分析:
2.有序表顺序查找
已知查找表有序的情况下,查找失败时不再比较另一端则直接返回失败信息,降低查找失败的平均查找长度。即当待查元素大于第 i 个元素而小于第 i+1 个元素时,直接返回失败信息。
查找判定树:
平均查找长度-----查找效率分析:
顺序查找优化: 按照被查找频率按查找频率从高往低对数据元素依此排序,这样可使查找成功时ASL更小
三、折半查找(二分查找)
针对有序查找表的一种查找方式。
1.折半查找!!!!!!!!!
时间复杂度:O()
适用范围:有序的顺序表(不适用于链表,链表不可随机访问)
自己理解:将给定值与查找表中值进行比对,若小于中值则在中值以左再次查找,若大于中值则在中值以右进行查找,以此类推,直到返回被查值的下标或左右指针互换位置(查找失败)。
若目标值<mid值:high = mid-1; mid = (low+high)/2
若目标值>mid值:low = mid +1; mid = (low+high)/2
int Binary_Search(SSTable L, ElemType key)
{
int low = 0, high = L.TableLen, 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;
}
判定树的构造:
若mid选择向上取整,则左子树节点数-右子树节点数 = 0或1,左右子树情况对调:
当low和high之间有偶数个元素,则mid分隔后,左半部分比右半部分多一个元素
查找成功的ASL = 近似于
<=h
查找失败的ASL<=h
2.分块查找(主要考察思路,很少考察代码)
分块查找又称为索引顺序查找,既有动态结构又适用于快速查找。
基本思想:将查找表分块,块内可无序而块间元素必需有序。建立一个索引表,表中个元素含有的最大关键字和第一个元素地址,索引表按关键字有序排列。
查找过程:①在索引表中确定待查记录所在块,可以顺序查找或折半查找索引表;②在块内顺序查找。
若索引表中不包含目标关键字,则折半查找索引表最终停在low>high,要在low所指分块中查找,若low超出索引表范围则查找失败。
查找次数:满树情况:最多次数 = 最少次数 = 树高;不满树情况:最多次数 = 树高 = 最少次数+1
查找效率分析(ASL):查找失败情况复杂,一般不考(考就生算);设索引查找和块内查找的平均查找长度分别为和
,则平均查找长度为ASL =
+
若查找表为“动态查找表”,则分块查找最好使用索引表+链表形式实现。
四、树形查找
1.二叉排序树(BST)!!!!
1)定义
二叉排序树又称为二叉查找树,特点为:左子树节点值<根节点值<右子树节点值
因此对二叉排序树进行中序遍历,可得到一个递增的有序序列。
2)查找
从根节点开始,沿某个分支逐层向下比较的过程:若BST非空,则先将给定值与根节点的关键字比较,若相同则成功;若小于根节点关键字则沿其左子树根节点向下进行类似操作;若大于根节点关键字则沿其右子树根节点向下进行类似操作。
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//查找(非递归)
BSTNode *BST_Search(BiTree T, ElemType key)
{
while(T != NULL && key != T->data)
{
if(key<T->data)
T = T->lchild;
eles
T = T->rchild;
}
return T; //失败会返回NULL
}
//查找(递归)
BSTNode *BST_Search(BiTree T, ElemType key)
{
if(T == NULL)
return NULL;
if(key == T->key)
return T;
else if(key < T->key)
return BST_Search(T->lchild, key);
else if
return BST_Search(T->rchild, key);
}
3)插入
BST是一种动态生成的树表,通过查找比较不断生成。
步骤:
- 若BST为空则直接插入;
- 若BST非空,比较待插入数据的值与根节点的值,若小于则下移插入左子树,若大于则下移插入右子树;
- 在子树中将节点与子树根节点进行比对,进行类似步骤②的操作,直到节点插入到对应的叶节点位置上( = 查找失败路径上访问的最后一个节点的孩子)。
//最坏时间复杂度O(h)
int BST_Insert(BiTree &T, KeyType key)
{
if(T == NULL)
{
T = (BiTree)malloc(sizeof(BSTNode));
T->data = key;
T->lchild = T->rchild = NULL;
return 1;
}
else if(key == T->data)
return 0;
eles if(key<T->data)
return BST_Insert(T->lchild, key);
else
return BST_Insert(T->rchild, key);
}
4)构造
从空树开始以此输入元素调用插入函数,逐渐构造出一棵树。
注意:BST的生成与输入的顺序相关,相同数据按不同顺序输入得到的BST树也不同(不同的关键字序列可能得到相同的BST,也可能得到不同的BST)。
void Creat_BST(BiTree &T, KeyType str[], int n)
{
T = NULL;
int i = 0;
while(i<n)
{
BST_Insert(T, str[i++]);
}
}
5)删除
BST的节点删除相对麻烦,需要保证删除该节点后其左右子树接到原树,且仍保持BST特征。
共有三种情况:
- 叶节点直接删除
- 若删除节点z只有一棵左子树或右子树,则让子树根节点直接替代节点z的位置
- 若删除节点z有左右两棵子树,则令z的直接后继(或前驱)替代z,然后从二叉排序树中删除这个直接后继(或前驱),这样就转换成情况①②
ps:情况③中的直接后继或前驱为中序遍历中的后继或前驱,可能为左子树最右下节点或右子树最左下节点(值与根节点最接近,不打破大小排序关系)
6.效率分析(与树高相关)
BST中某个元素的比较次数 = 该节点所在层的层数。
理想状态下树高 = (向上取整)——看完全二叉树部分
重点:计算平均查找长度
7.对比二分查找判定树&二叉排序树
| 二分查找判定树 | 二叉排序树 |
查找平均时间性能 | 平均 = 最大 = O( | 平均 = O( 最大 = O(n) ”单支“ |
唯一性 | 唯一 | 不唯一,看输入 |
维护表有序性 | 针对有序表,插入删除需移动元素 | 无需移动节点,仅改变指针完成插入和删除 |
维护时间开销 | O(n) | O( |
适用性 | 适用于静态查找表,顺序结构进行存储 | 适用于动态表,用BST作为逻辑结构 |
2.平衡二叉树(AVL)!!!!
1)定义
在插入和删除节点时保证任意节点的左右子树高度差的绝对值小于1,这样的树称为平衡二叉树(AVL树)
平衡因子:节点左子树高 - 右子树的高(只能是-1,0,1)
2)插入
为保证树的平衡,在插入节点时需检查插入路径上的节点是否因为此次的插入而出现不平衡,如果发生则找到距离插入节点最近的不平衡节点A(最小不平衡子树),对以A为根的树进行调整使其达到平衡。
步骤:插入与BST相同(注意有个不同就是平衡二叉树的插入,一开始插入的是根),检查是否出现不平衡,若没有则继续插入,若出现需进行调整:
①LL型
右单旋——父变爷,爷变右兄:因为在节点A的左孩子(L)的左子树(L)上增加新节点打破了平衡,需要进行一次右旋操作,将A的左孩子B顶替A的位置成为新的父节点,A成为B的右孩子,B原本的右孩子挂在A的左指针下。
//伪代码思路——B是父节点,C是孩子,A是爷节点,gf为这棵子树的父节点
A->lchild = B-> rchild
B->rchild = A
gf->lchild/rchild = B
②RR型
左单旋——父变爷,爷变左兄:因为节点A的右孩子(R)的右子树(R)上增加新节点打破了平衡,需要进行一次左旋操作,将A的右孩子B顶替A的位置成为新的父节点,A成为B的左孩子,B原本的左孩子挂在A的右指针下。
//伪代码思路——B是父节点,C是孩子,A是爷节点,gf为这棵子树的父节点
A->rchild = B-> lchild
B->lchild = A
gf->lchild/rchild = B
③LR型
左右双旋——子变爷,父左子,爷右子:因为在节点A的左孩子(L)的右子树(R)上增加新节点打破了平衡,需要进行一次左旋操作,将A的左孩子B的右孩子C成为A的左孩子,再进行一次右旋,将C成为新的爷节点,A、B分别成为它的左右子树,C原本的左右子树分别挂在AB的右左子树。
//伪代码思路——B是父节点,C是孩子,A是爷节点,gf为这棵子树的父节点
//左旋
A->lchild = C
B->rchild = C->lchild
C->lchild = B
//右旋
A->lchild = C->rchild
C->rchild = A
gf->lchild/rchild = C
④RL型
右左双旋——子变爷,父右子,爷左子:因为在节点A的右孩子(R)的左子树(L)上增加新节点打破了平衡,需要进行一次右旋操作,将A的右孩子B的左孩子C成为A的右孩子,再进行一次左旋,将C成为新的爷节点,A、B分别成为它的右左子树,C原本的左右子树分别挂在AB的左右子树。
//伪代码思路——B是父节点,C是孩子,A是爷节点,gf为这棵子树的父节点
//右旋
A->rchild = C
B->lchild = C->rchild
C->rchild = B
//左旋
A->rchild = C->lchild
C->lchild = A
gf->lchild/rchild = C
3)删除
与插入操作类似,AVL的节点删除时也需要进行平衡性的检查。
步骤:
- 用BST方法对节点X进行删除;
- 若出现不平衡现象则从节点X向上进行回溯(一路向北找最小不平衡子树),找到第一个不平衡节点A,B为节点A最近的孩子节点,C是B最近的孩子节点(找“个头”最高的儿孙节点),然后以A为根,利用与插入相同的四种调整方法进行判断调整(LL、RR、LR、RL)
- 若不平衡向上传导,重复②
注意!!!!!!!!!!!!
删除与插入对节点的调整方式完全一致,但需注意,插入仅针对最小不平衡子树A,而删除时可能出现树A调整后仍需对A的祖先节点进行进一步调整,甚至回溯到根节点的情况。
4)查找
AVL中查找操作与BST相同,因此查找时与给定值比较的关键字个数不超过树高,因此,含n个节点的AVL树的最大深度为,因此AVL的平均查找长度为
3.红黑树(难点但不是重点!!)
1)定义
红黑树是以BST(注意红黑树不是AVL)为基础的,满足以下条件的树:
- 左根右(左<根<右)
- 根叶黑(根和叶节点是黑节点,叶节点非传统意义上的叶节点,是虚构的外部节点值为NULL,补充:如果树的全部节点都为黑节点,则树一定是满二叉树,因为黑路同)
- 不红红(不可出现两个连续的红色节点)
- 黑路同(每条从根节点到叶子节点的路径上拥有的黑色节点数量相同)
struct RBnode{
int ksy;
RBnode *parent;
RBnode *lchild, *rchild;
int color;
};
2)性质
- 从根节点到叶节点的最长路径不大于最短路径的2倍
- 有n个内部节点的红黑树的高度 h<=2
- 红黑树的黑高(根节点到叶节点路径上黑色节点数量)至少为h/2
- 红黑树查找操作时间复杂度 = O(
)
- 若根节点黑高为h,则内部节点最少为
3)插入(很难)
与AVL相似,红黑树在插入新节点时要对节点进行检查,并根据检查结果进行对应的调整,但红黑树相对AVL而言无需频繁调整树的形态,调整时也可在常数级时间内尽快完成,更适合用于频繁插入删除的场景。
插入步骤:
- 若新节点是根节点,染为黑色,否则染为红色;
- 若插入后仍满足红黑树定义(仅需查看是否满足不红红,其他三条都不会被打破),若满足则插入结束,若不满足,根据其叔叔(父节点的兄弟节点)的颜色进行相应调整:
(i) 黑叔:旋转+染色(染色就是换成和之前不同的颜色)
LL型:右单旋,父换爷+染色(指给父和原爷节点染色)
RR型:左单旋,父换爷+染色
LR型:左右双旋,儿换爷+染色(指给儿和原爷节点染色)
RL型:右左双旋,儿换爷+染色
(ii) 红叔:染色+变新——叔父爷染色,爷变成新节点
以下是一个具体实例,好好体会~
4)删除(考的几率很小很小)
红黑树的删除处理和AVL的节点删除一样,按照红黑树特性(插入的调整方法)进行调整。
时间复杂度:
五、B树和B+树(难点!!——绝对平衡树,链表存储,不是重点)
1.B树
1)定义
B树是基于平衡二叉树衍生出的m路平衡查找树,即每个节点可存放m-1个元素,这m-1个元素将区间划分为m份,继而可将后续元素插入到对应的区间内,方便后续查找使用,查找方法与平衡二叉树中元素查找相同,但需注意,B树中的“叶节点”并不存在,是虚拟设定的查找失败节点,没有具体关键字。
//5叉排序树
struct Node{
ElemType keys[4];
struct Node *child[5];
int num;
};
当节点内关键字较少,树高增高,为提高查找效率,要求B树中除根节点外,任何节点都要有至少⌈m/2⌉个分叉,至少有⌈m/2⌉-1个关键字。
m阶B树特点(不考虑空树情况):
- 树中每个节点至多有m棵子树,即至少有⌈m/2⌉-1个关键字
- 若根节点不是终端节点,则至少有两棵子树
- 除根节点外所有非叶节点至少有⌈m/2⌉棵子树,即含有⌈m/2⌉-1个关键字
- 所有叶节点都出现在同一层次上,且不带信息(等同于折半查找判定树上的失败节点,实际值为NULL)
- 非叶节点内结构为指针与数据交叉存放的状态,如图,P为指针,K为关键字
2)高度 (磁盘存取次数)
因此B树的高度范围为:
总结:
3)查找
与AVL的查找类似,先与根节点进行比较,根据左根右的性质进行进一步比对查找,若与某非叶节点关键字匹配则查找成功,若未匹配则进入失败节点,查找失败。
4.插入
以5阶B树为例,除根节点外每个非叶节点关键字个数不小于2,不超过4,按25,38,49,60,80,90,99,88,83,87,92,93,94,73,74,75,71,72,76,77的顺序进行插入,有以下几个关键步骤
①在节点未满时,按顺序(这里的顺序是满足BST规则的大小顺序,不等于输入顺序)正常插入,当一个节点已满,令其中间元素成为根节点,左右两侧元素成为其子树的节点内关键字,树的高度+1:
-->
②继续插入元素,注意新元素一定插入到最底层的“终端节点”,利用查找来确定插入位置,不可插入上层根节点内(根节点内是调整过去的关键字)
③当再次出现节点已满的情况,将中间元素上移至根节点,节点内剩余m-2个元素划分到两个节点内:
④当当前最大根节点已满时,与处理终端节点类似,将根节点分割为新的最大根节点和其两个子树节点,并改变指针指向,同时树的高度+1:
⑤最终结果树:
总结:
5.删除
以插入的最终结果树为例进行删除操作,初始树的形态如图:
删除顺序:77,82,38,49,82
①被删除关键字在终端节点(不是失败节点),直接删除该关键字;
i 若删除后节点内关键字个数低于最小的要求(如五阶每个节点要求关键字>=2) ,且与此关键字所在节点相邻的兄弟节点内关键字个数宽裕,则让父节点内对应元素落入该节点,让兄弟节点中的最小/最大元素升入父节点中;(就是拿节点后继/前驱,后继的后继/前驱的前驱来填补)
删除关键字38
ii 若兄弟节点内没有宽裕的关键字,则将删除关键字后的节点与父节点内对应关键字、兄弟节点内元素进行合并,形成新的节点。
删除关键字49:
iii 若删除根节点内最后一个关键字,则判断其孩子节点关键字个数和是否超出节点内允许的最大关键字个数,若未超出,则直接合并形成新的根节点:
iv 若孩子节点中有富裕的元素,则同之前一样,将其中合适的拿个提上来成为新的根节点内关键字(根节点的处理和其他节点处理没什么不同,只是不限制内部最小元素个数而已)
②被删除关键字在非终端节点,用直接前驱(左侧指针所指子树最“右下”元素)或直接后继(右侧指针所指子树最“左下”元素)代替被删除的关键字。
注意:①对非终端节点关键字的删除必然可转换为对终端节点的删除操作
2.B+树
1)定义
与B树类似AVL的特性不同,B+树更类似于分块索引,其所有关键字全在终端节点,每个终端节点连接一个记录(类似失败节点),在查找和删除时可选择利用索引表或直接对链表进行操作。
m阶B+树满足的条件:
- 每个分支节点最多有m棵子树(孩子节点),且所有关键字全在叶子节点上,满足绝对平衡条件
- 非叶根节点至少有两棵子树,其他每个分支节点至少有⌈m/2⌉棵子树
- 节点的子树个数与关键字个数相同
- 所有叶节点包含关键字及指向相应记录的指针,叶节点中将关键字按大小顺序排列,相邻节点按大小顺序相互链接(支持顺序查找)
- 所有分支节点中仅包含其各个子节点中关键字最大值和相应指针(和分块索引一样)
2)查找
B+树的查找与分块查找相同,遵循多路查找原则(当然也可以顺序查找),如下图查找关键字9,就可以从根节点进行比对,<15选择走左侧,<15选择走9和15中间指针所指引的区域,=9查找成功。
注意!!!!!!!!!!!:
查找成功时一定是停留在叶子节点上,可能分支节点内也有与关键字相同值的元素,但并非我们要查询的结果。
3)对比B树和B+树
m阶B树 | m阶B+树 |
节点中n个关键字对应n+1棵子树(失败节点) | 节点中n个关键字对应n棵子树(记录) |
根节点关键字个数1<=n<=m-1 其他节点关键字个数m/2(向上取整)-1<=n<=m-1 | 根节点关键字个数1<=n<=m 其他节点关键字个数m/2(向上取整)<=n<=m |
各节点内包含的关键字不重复且都包含信息 | 仅叶节点包含信息,其他节点仅用于索引(使磁盘块内包含更多关键字,使树更矮读盘次数更少) |
六、散列表(hash)
1.基本概念
散列表(哈希表,Hash Table)︰是一种数据结构。特点是∶可以根据数据元素的关键字计算出它在散列表中的存储地址
散列函数(哈希函数)︰Addr=H(key)建立了“关键字”→“存储地址”的映射关系
冲突(碰撞)︰在散列表中插入一个数据元素时,需要根据关键字的值确定其存储地址,若该地址已经存储了其他元素,则称这种情况为“冲突(碰撞)”
同义词:若不同的关键字通过散列函数映射到同一个存储地址,则称它们为“同义词”
尽可能地映射到不同的存储位置,从而减少“冲突”。
如何处理冲突?
拉链法(又称链接法、链地址法)︰把所有“同义词”存储在一个链表中。
开放定址法:如果发生“冲突”,就给新元素找另一个空闲位置。
2. 散列函数的构造
散列函数(哈希函数)︰Addr=H(key)建立了“关键字”→“存储地址”的映射关系。
注意:!!!!!!!!!!:
散列表的装填因子=散列表的关键字树目/散列表的长度
设计散列函数时应该注意:
1)除留余数法
H(key) = key % p
散列表表长为m,取一个不大于m但最接近或等于m的质数p
注︰质数又称素数。指除了1和此整数自身外,不能被其他自然数整除的数
适用场景:较为通用,只要关键字是整数即可
拓展:为什么除留余数法要对质数取余?
2)直接定址法
H(key) = key或H(key) = a*key + b
其中,a和b是常数。这种方法计算最简单,且不会产生冲突。若关键字分布不连续,空位较多,则会造成存储空间的浪费。
适用场景:关键字分布基本连续
3)数字分析法
选取数码分布较为均匀的若干位作为散列地址
设关键字是r进制数〈如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址。
适用场景:关键字集合已知,且关键字的某几个数码位分布均匀。
4)平方取中法
取关键字的平方值的中间几位作为散列地址。
具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀。
适用场景:关键字的每位取值都不够均匀。
3.冲突处理
1)拉链法
拉链法(又称链接法、链地址法)︰把所有“同义词”存储在一个链表中
如何在散列表(拉链法解决冲突)中插入一个新元素?
Step 1:结合散列函数计算新元素的散列地址
Step 2:将新元素插入散列地址对应的链表(可用头插法,也可用尾插法)
散列表的插⼊操作(拉链法解决冲突)
散列表的查找操作(拉链法解决冲突)
查找目标20,计算目标元素存储地址:20%13=7; 20查找成功,查找长度=1
查找目标66,计算目标元素存储地址:66%13=1; 66查找失败,查找长度=4
查找目标21,计算目标元素存储地址:21%13=8; 21查找失败,查找长度=0
查找长度——在查找运算中,需要对比关键字的次数称为查找长度.
注:有的教材会把“空指针的对比”也计入查找长度。但考试中默认只统计 “关键字对比次数”
散列表的删除操作(拉链法解决冲突)
删除目标:27 ,计算目标元素存储地址:27%13=1; 27查找成功,删除成功
删除目标:20 ,计算目标元素存储地址:20%13=7; 20查找成功,删除成功
删除目标:66 ,计算目标元素存储地址:66%13=1; 66查找失败,删除失败
2)开放定址法
开放定址法的基本原理
开放定址法︰如果发生“冲突”,就给新元素找另一个空闲位置。
注:d表示第i次发生冲突时,下一个探测地址与初始散列地址的相对偏移量。
根据散列函数H(key),求得初始散列地址。若发生冲突,如何找到“另一个空闲位置”?
如何删除一个元素:
Step 1∶先根据散列函数算出散列地址,并对比关键字是否匹配。若匹配,则“查找成功”
Step 2∶若关键字不匹配,则根据“探测序列”对比下一个地址的关键字,直到“查找成功”或“查找失败”
Step 3:若“查找成功”,则删除找到的元素
特别注意:关于删除操作(逻辑删除)
例︰长度为13的散列表状态如下图所示,散列函数H(key)=key%13,采用线性探测法解决冲突。
正确示范:查找元素1
计算元素1的初始散列地址=1%13=1。对比位置#1,关键字不等于1;
根据线性探测法的探测序列,继续对比位置#2,关键字不等于1;
根据线性探测法的探测序列,继续对比位置#3,该位置原关键字已删,继续探测后一个位置;
根据线性探测法的探测序列,继续对比位置#4,关键字等于1,查找成功。