一、基本概念
1、常用概念
(1).查找表(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成。
(2).关键字:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,其查找结果应该是唯一的【id,学号,qq号等】
(3).对查找表的常见操作
a.查找符合条件的数据元素【无改动->静态查找表->仅关注查找速度】
b.插入、删除某个数据元素【有改动->动态查找表->还要关注增删的难度】
(4).查找算法的效率评价->平均查找长度ASL->通常考虑成功、失败两种情况下的ASL
二、查找办法
1.顺序查找
(1)两种实现办法【常用线性表、动态数组】:
typedef struct
{
int elem[3]; //动态数组
int TableLen; //数组长度
}SSTable;
/*1.不带哨兵的顺序查找【从前往后即可】*/
int Search_Seq(SSTable ST, int key) //key为需要寻找的那个值
{
int i;
for (i = 0; i < ST.TableLen && ST.elem[i] != key; ++i)
; //注意这里有个分号,如果下标还小,值又不是想要的,就什么也不干,只是i++
//两种跳出循环,跳入下一行:一是i超过数组长度,二是某一元素值恰好为key
return i==ST.TableLen? -1:i; //判断是第一类跳出还是第二类
}
/*2.带哨兵的顺序查找【从后往前】*/
int Search_Seq_with_guard(SSTable ST, int key)
{
ST.elem[0] = key; //数组0号位置存哨兵,具体数据从1号开始存
int i;
for (i = ST.TableLen; ST.elem[i] != key; --i)
;
return i; //查找成功时,返回数组下标,失败时返回哨兵下标0
}
int main()
{
SSTable T = { T.elem[3] = {0,2}, T.TableLen = 5 };
int target;
int result;
//for (int i = 0; i < T.TableLen; ++i)
// cin>>T.elem[i];
cout << "which num do you want to find?" << endl<<"target= ";
cin >> target;
result = Search_Seq(T, target);
cout<<"The num you find located in :"<<result;
return 0;
}
注:哨兵优点:无需判断是否越界,效率更高。【尚未实现,报错】
查找成功的ASL:
查找失败的ASL:O(n+1)
(2)顺序查找的优化【使用查找判定树】:
a.【顺序表有序】
b.【顺序表被差概率不等】
如果很容易查找成功且概率不等->用第二种办法
如果很容易失败->用第一种办法
2.二分查找/折半查找
(1)适用范围:
有序的顺序表,而无法用链表->需要有随机访问的特性,而链表没有
(2)实现方法:
typedef struct
{
int *elem; //动态数组
int TableLen; //数组长度
}SSTable;
/*折半查找*/
int Binary_Search(SSTable L, int key)
{
int low = 0, high = L.TableLen - 1, mid;
while (low <= high)
{
mid = (low + high) / 2;
if (key== L.elem[mid])
return mid;
else if (key<L.elem[mid])
high = mid - 1;
else if (key>L.elem[mid])
low = mid + 1;
}
return -1; //查找失败,返回-1
}
如果是向下取整,则右子树比左子树的结点相等or多一个。
折半查找的判定树中,只有最下一层是不满的,因此,元素个数为n时树高h=log2(n+1)取上界
所以时间复杂度:,一般情况下比顺序查找更优秀,但是也不能绝对地说,因为特殊情况下顺序查找更快,如 7 ,8 , 9, 10,11 中找“ 7 ”
3.分块查找
(1)定义:
也称索引顺序查找,算法过程如下:
a.在索引表中确定待查记录所属分块(可顺序,可折半)
b.折半查找时,需要在low的块内顺序查找
(2)查找效率分析(ASL)
运用查找的总次数/元素个数
4.二叉排序树的查找
(1)定义:
左子树<根节点<右子树 (的值)【左小右大】
(2)实现方法:
typedef struct BSTNode
{
int key; //结点值
struct BSTNode *lchild, * rchild;
}BSTNdode,*BSTree;
/*1.二叉排序树查找值为key的结点*/
BSTNode* BST_Search(BSTree T, int key)
{
while (T != NULL && key != T->key)
{
if (key < T->key)
T = T->lchild;
else T = T->rchild;
}
return T;
}
/*2.二叉排序树查找值为key的结点【递归式】*/
BSTNode* BST_Search(BSTree T, int 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 (key > T->key)
return BST_Search(T->rchild, key);
}
第一种的空间复杂度O(1),第二种是O(h),与高度有关。
(3)二叉排序树的插入操作
typedef struct BSTNode
{
int key; //结点值
struct BSTNode *lchild, * rchild;
}BSTNdode,*BSTree;
int BST_Insert(BSTree& T, int k)
{
if (T == NULL)
{
T = new BSTNode;
T->key = k;
T->lchild = T->rchild = NULL;
return 1;
}
else if (k == T->key)
return 0;
else if (k < T->key)
return BST_Insert(T->lchild, k);
else if (k > T->key)
return BST_Insert(T->rchild, k);
}
(4)二叉排序树的构造
void Create_BST(BSTree& T, int str[], int n)
{
T = NULL; //一开始是空树
for (int i = 0; i < n; i++)
BST_Insert(T, str[i]);
}
不同的关键字序列可能得到不同的二叉排序树,也可能相同。【先左后右和先右后左相同】
(5)二叉排序树的删除:
a.叶子结点直接删除
b.只有左子树/右子树,让其来补位即可
c.删除一个既有左子树又有右子树的根节点,需要用直接前驱/直接后继来补位。
【二叉排序树经过中序遍历,一定可以得到递增序列,若删除叶子结点:可以用前驱/后继来补位】【找前驱:即最右下结点,后用左子树代替该位置;找后继:即最左下结点,后用右子树代替该位置】
(6)查找效率分析
查找长度——对比关键字的次数,反映了操作的时间复杂度。
所以要左右子树深度相差至多为1是最好情况,时间复杂度最低。【即平衡二叉树】
5.平衡二叉树(AVL)
(1)定义:
每个结点的 |平衡因子|<=1的二叉排序树。
平衡因子:左子树高减右子树高的值。
(2)平衡二叉树的插入
举例:【注意代码,调整时自下而上】
(3)查找效率分析
(4)平衡二叉树的删除
6.红黑树(RBT)
(1)创建红黑树的原因:
平衡二叉树插入删除时很容易破坏“平衡特性”,需要频繁调整树的形态。而红黑树很多时候不会破坏红黑特性,无需调整/可以在常数级时间内调整。
所以:
平衡二叉树:适用于以查为主,很少插入删除的场景。
红黑树:适用于频繁插入、删除的场景,实用性更强。
(2)定义:
除了是二叉排序树以外,还需要:【左根右,根叶黑,不红红,黑路同】
思考:要让内部结点少,肯定全为黑最少,因为带红肯定可以,但是这样就多了。并且此时必须全满,比如bh=2,只有左结点的话,就不满足“黑路同”的特性了。
性质1:【最长:红黑相间;最短:全是黑结点】
性质2:最少的话,全是黑结点!【注意黑高是不算本身结点以后,带上“外部结点”时,下面的层数,但是内部结点是包括根节点的】
(3)红黑树的插入
7.B树 (Balance Tree)
(1)定义【逐字读】
注:1.根结点是唯一可以只有一个关键字的,但是也可以多个关键字,但是不能超过m-1.
2.B树上强制平衡,所以所有子树的高度必须相同。
(2)B树的插入
a.首先确定关键词数目,阶数为m的B树,除了根节点的结点,其关键字数目n满足:
如5阶B树的结点关键词个数:
b.先插入,如果某结点满了,就采用“中间起,两头分”的办法。变为父节点。
(3)B树的删除
a.删除根节点找前驱或者后继结点来补位。
b.如果删除的是叶子结点,则看兄弟够不够借【挪过来后会不会导致兄弟的关键字也不够】
如果不会导致这意外,则让后继及后继的后继【前驱及其前驱】换位。
如果会,则说明此时可以将兄弟俩带着父节点一起合并。
(4)与B+树的区别
8.散列查找
(1)哈希排列【处理同义词的冲突】
散列法,又称为hash法或者关键字地址计算法。时间复杂度为0(理想情况下),是一种key-value的存储方法。核心就是由hash函数决定关键字值和散列地址之间的关系,通过这种关系来组织存储并进行查找等操作。
散列法面临的问题:会发生地址冲突。
(1)如何恰当的构造hash函数,使得结点分布均匀,尽量少的减少冲突。
(2)冲突无可避免,怎样处理冲突?
装填因子a=表中记录数/散列表长度【直接影响查找效率】【装得越多,a越大,空间利用率越大但是查找效率越低】
(2)散列/哈希函数
(3)处理冲突的办法【让数据分布更合理,不冲突】
注意哈希函数是除以数据表的表长(下例为13),而开放定值法师除以存储表的表长(下例为16)。
线性探测法的缺点:容易造成同义词、非同义词的“聚集/堆积”现象,严重影响查找效率。【原因:冲突后再探测一定是放在某个连续的位置】
采用二次探测法/平方探测法:虽然都是开放定址法,但是当选择的增量序列(di=0,1,-1,4,-4,9,-9...)不同时,其位置有不同之处。缓解了堆积,提高了效率!
表长要求:散列表长度m必须是一个可以表示成4j+3的素数,才能探测到所有位置。(数论知识)
(4)查找操作
不仅查找同义词,也要查找非同义词。遇到数值为空的再停止【注意不是逻辑为空!】
缺点:线性探测法很容易造成
(5)删除操作
不能简单置空,而是做一个删除标记,进行逻辑删除,否则会影响到后续数据的查找。