数据结构 查找
基础概念:
查找:
根据给定的值,在查找表中确定一个其关键字等于给定值的数据元素。
关键字:
用于标识一个数据元素的数据项的值。
主关键字:
可以唯一的表示一个记录的关键字。
平均查找长度ASL
ASL(Average Search Length)
线性表的查找
顺序查找
优点: 简单,对逻辑次序无要求,且不同储存结构均可使用。
缺点: ASL长,时间效率低
ASL: \((1+2+..+n)/n=(n+1)/2\)
方法:
就是for循环
改进:
增加哨兵:将查找关键字存入表头,从而免去查找过程中每次都要检测是否查找完成。
int Search_Seq(SSTable ST,KeyType key){
ST.R[0].key=key;//设置监视哨
for(i=S+T.length;ST.R[i].key!=key;---i);
return i;
}
二分查找
前提:
只能是数组,不能是链表,并且数组有序
查找次数:
比较次数等于查找成功时位于的树的深度。
即 \(\lfloor log_2n \rfloor+1\)
ASL:
\(log_2 (n+1)-1\) (n>50)
实现:
int a[1000]; //已经从小到大排序
int find(int k, int L, int R)
{
if (L > R) return -1;//大于号
int mid = (L + R) / 2;
if (a[mid] == k) return k;
if (a[mid] > k) return find(k, L, mid - 1);//是返回 ...mid-1
if (a[mid] < k) return find(k, mid + 1, R);
return -1;
}
优点:
效率比顺序查找快
缺点:
只适用于有序表且限于顺序储存结构(对线性链表无效)
分块查找
分块查找(Blocking Search)又称索引顺序查找。它是一种性能介于顺序查找和二分查找之间的查找方法。
存储结构
二分查找表由"分块有序"的线性表和索引表组成。
-
(1)"分块有序"的线性表
表R[1..n]均分为b块,前b-1块中结点个数为 ,第b块的结点数小于等于s;每一块中的关键字不一定有序,但前一块中的最大关键字必须小于后一块中的最小关键字,即表是"分块有序"的。 -
(2)索引表
抽取各块中的最大关键字及其起始位置构成一个索引表ID[l..b],即:
IDi中存放第i块的最大关键字及该块在表R中的起始位置。由于表R是分块有序的,所以索引表是一个递增有序表。
查找方式:
- 首先查找索引表
-
- 索引表是有序表,可采用二分查找或顺序查找,以确定待查的结点在哪一块。
- 然后在已确定的块中进行顺序查找
-
- 由于块内无序,只能用顺序查找。
优点:
- 在表中插入或删除一个记录时,只要找到该记录所属的块,就在该块内进行插入和删除运算。
- 因块内记录的存放是任意的,所以插入或删除比较容易,无须移动大量记录。
缺点:
需要增加一个辅助数组的存储空间
将初始表分块排序的运算。
适应情况:
线性表既要快速查找,又经常动态变化。(链表和线性表都适用)
比较总结
树表的查找
动态查找表
优点:
当表插入,删除操作频繁时,用动态查找表(几种特殊的树)比较好。
二叉排序树
二叉查找树/二叉排序树/二叉搜索树。
(BSTree) (Binary Sort Tree)
定义:
或是一棵空树,
或是具有下列性质的二叉树:
1) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2) 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
3) 左、右子树也分别为二叉排序树;
4) 没有键值相等的节点。
简单来说就是:左儿子小于父亲,右儿子大于父亲
特点:
二叉有序树,使用中序遍历,是有序的.
操作
建立:
struct node{
int data;
struct node *l, *r;
};
struct node *creat(struct node *root,int x){
if(root == NULL) // 如果 root 是空,表示当前是可以把这个点加进去的,就可以放进去了
{
root = new node;
root -> data = x;
root -> l = NULL;
root -> r = NULL;
}
else { //如果不是,考虑两种情况
if(x > root -> data) //如果比当前的根节点大,去右子树找
root -> r = creat(root -> r, x);
else
root -> l = creat(root -> l, x); // 否则去左子树找
}
return root; //别忘记了返回树根!
};
查找:
// 查询元素(递归)
Node find(Node T, int key) {
if (!T || key==T->data) return T;
else
if (key < T->data)
return find(T->left, key);
else
return find(T->right, key);
}
删除结点:
删除要求:
删除后,仍然保持二叉排序树的性质。
概述:
由于中序遍历二叉排序树可以得到一个递增有序的序列。那么,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点,因此我们的操作就是:
- 将因删除结点而断开的二叉链表重新链接起来
- 防止重新链接后树的高度增加
删除节点共有三种情况:
目标元素为叶子节点
叶子节点最容易删除,过程如下:
找到,然后删除....
目标元素只有左子树,或只有右子树
删除过程如下
- 找到目标节点的父节点
- 判断目标节点是父节点的左子树还是右子树
- 将父节点的left/right指针设为目标节点不为空的子树。
目标元素即有左子树,也有右子树
该情况删除操作最为复杂
补充两个概念:
前驱: 左子树的最大结点。
后继: 右子树的最小结点。
有两种方法:
- 法1: 将结点前驱替换该结点,然后删除前驱结点。
- 法2:将结点后继替换该结点,然后删除后继结点。
题目
ASL:
含有n个结点的二叉排序树的平均查找长度和树的 形态有关。
最好情况: ASL=\(\lfloor log_2n \rfloor+1\)(形态比较平衡)
最坏情况: ASL=(n+1)/2 O(n) (单支树的形态)
因此为了提高效率,需要将树尽量平衡,
因此推出平衡二叉树的概念。
平衡二叉树/AVL树
目的:
让二叉树尽量平衡,提高查找效率。
定义:
- 可以是空树。
- 假如不是空树
-
- 是每个节点的左右两个子树的高度差的绝对值不超过 1的二叉树 。
-
- 任何一个结点的左子树与右子树都是平衡二叉树。
性质:
对于n个结点的AVL树,
树的高度保持在:\(\lfloor log_2n \rfloor+1\)
ASL也保持在:\(\lfloor log_2n \rfloor+1\)
方法:
- 任何一个结点的左子树与右子树都是平衡二叉树。
- 增加平衡因子(BF):结点左子树的高度 - 结点右子树的高度。
- 使每个结点的平衡因子的绝对值小于等于1。
查找操作
平衡二叉树的查找基本与二叉查找树相同。
插入操作
与二叉查找树不同:
插入时要随时保证插入后整棵二叉树是平衡的。
也就是说要实时调整不平衡树。
调整的基本方法是: 旋转 。
插入调整
需要平衡旋转的有4种情况:
在讨论前要先弄清楚,我们需要调节的树是哪棵:也就是:
最小不平衡子树的根结点是什么:
当进行插入操作时,
找到该需要插入结点的位置并插入后,从该结点起向上寻找(回溯),第一个不平衡的结点即平衡因子bf变为-2或2的结点。
调整的方式有四种:
虽然调整的步骤看似很麻烦,其实我们只需要记住调整原则即可:
保持二叉树的性质
和 降低高度
。
如果是人工调整,四种方式都可以总结为四步:
-
找到最小不平衡子树和其根节点.
-
从根节点出发,沿插入路径找三个节点。
-
调整这三个节点:找出中位数,让中位数作为根节点,其余两个一左一右
-
调节三个结点的子树:
-
- 调整后的为左右子树的结点子树位置保持不变,
-
- 如果还剩一个子树没有位置:人工插入位置。
下面具体讲四种调整方式:
~~不写了,没必要,贴下图示吧
LL型调整
RR调整
LR调整
RL调整
删除调节好像不需要掌握,不写了。
其他树:
** B-树(深度了解)
就是平衡的多路**查找树
B+树
(深度了解)
是B-树的一种变型
要求:
- 所有的叶子结点都是关键字
- 叶子结点要有序
散列表-哈希(HASH)函数
概述:
基本概念
基本思想:
记录的储存位置与关键字之间存在对应关系。
也就是以键-值对存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。
优点: 查找效率高
缺点: 空间效率低
我们很容易想到:
用数字数据举例
定义一个数组a[n],如果有该数据x,再a[x]=1.
但是稍加思索就会发现,上述思想是不可能在任意情况下都实现的:
- 不可能任意情况下都有单射的散列函数
比如数据关键字为任意整数时,关键字-a与a该如何映射? - 散列表的大小也不可能总是保证大于所有可能的散列值。
比如数据关键字为正整数,那么散列函数只需要令散列值等于数据关键字即可保证单射,但是如果数据总量为1000,而数据的可能最大值为10000000,难道我们创建一个大小为10000000的散列表吗?
也就是说,
我们实际实现散列表时,必须面对这两个问题:
- 构造尽可能“接近”单射的散列函数。(还要求函数简单,转化效率高)
- 找到处理冲突的好方案。
冲突:
不同的关键码映射到同一个散列地址。
同义词:
具有相同函数值的多个关键字。
总结一下构造一个号好的散列函数需要考虑:
①执行速度(即计算散列函数所需时间);
② 关键字的长度;
③ 散列表的大小;
④关键字的分布情况;
⑤ 查找频率。
散列函数构造方法
我们只需要知道:直接定址法和除留余数法
直接定址法:
H(key)=a*key+b
a 和 b均为常数
数字分析法:
分析关键字的各个位的构成,截取其中若干位作为散列函数值,尽可能使关键字具有大的敏感度。
平方取中法:
这种方法是先求关键字的平方值,然后在平方值中取中间几位为散列函数的值。因为一个数平方后的中间几位和原数的每一位都相关,因此,使用随机分布的关键字得到的记录的存储位置也是随机的。
折叠法:
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列函数的值,称为折叠法。
除留余数法:
Hash(key) = key % p
其中,p为不大于散列表表长m的整数
解决冲突
开放地址法
基本思想
有冲突时就去寻找下一个空的散列地址,只要散列表足够大, 空的散列地址总能找到,并将数据元素存入。
常用方法:
线性探测法: di为1,2,3...m-1线性序列
二次探测法 \(d_i为1^2,2^2,3^2...q^2\) 二次序列
伪随机探测法 di为伪随机数序列
链地址法
也称 拉链法
基本思想:
是将所有关键字为同义词的记录存储在同一个线性链表中。
实现:
m个散列地址就开设m个散列表,然后用数组将m个单链表的表头指针储存起来。
优点:
- 非同义词不会冲突,
- 链表空间动态申请,更适合表长不确定的情况。
性能分析
查找过程:
a为装填因子= n/m 其中n=存入的元素个数,m=哈希表的长度
结论
- 平均性能优秀。
- 链地址法优于开地址法。
- 除留余数法做散列函数优于其他类型函数。