文章目录
第八章 查找
定义
搜索引擎工作:
查找表:是由同一类型的数据元素构成的集合。
关键字:是数据元素中某个数据项的值。
主关键字:若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)。
次关键字:那些可以识别多个数据元素的关键字,称之为次关键词(Secondary Key)。
查找:根据给定的某个值,在查找表中确定一个关键字等于给定值的数据元素。
静态查找表:只作查找操作的查找表,主要操作有1. 查询某个“特定的”数据元素是否在表中;2. 检索某个“特定的”数据元素和各种属性。
动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者删除已经存在的元素。
顺序表查找
思路:从头到尾遍历比较。
实现代码:
int Sequential_Search(int *a, int n, int key)
{
int i;
for (i=1; i<=n; i++)
{
if (a[i] == key)
return i;
}
return 0;
}
优化顺序查找:
设置一个哨兵,可以不需要 i 每次都和 n 比较。
做一个长度为n+1的数组,把0位置的值设为key,倒着查找每次下标减一,一定会出现值为key的时候,若下标为0表示没找到,否则表示找到。
int Sequential_Search2(int *a, int n, int key)
{
int i;
a[0] = key;
i = n;
while (a[i] != key)
{
i--;
}
return i;
}
时间复杂度:
最好的情况为
O
[
1
]
O[1]
O[1]。
最坏的情况为
O
[
n
]
O[n]
O[n]。
关键字在任一位置的概率是相同的,所以平均查找次数为
n
+
1
2
\frac{n+1}{2}
2n+1,最终时间复杂度为
O
[
n
]
O[n]
O[n]。
JAVA实现顺序查找
public class OrderFine {
public static void main(String[] args) {
int[] a = {2,3,4,5,1,6};
int b = 4;
Find1(a, b);
// 第一个位置为空,写为0
int[] c = {0, 2,3,4,5,1,6};
Find2(c, b);
}
private static void Find2(int[] a, int b) {
a[0] = b;
int len = a.length;
while (a[len-1] != b){
len--;
}
if (len==1){
System.out.println("没找到");
} else {
System.out.println(len-2);
}
}
private static void Find1(int[] a, int b) {
for (int i = 0; i < a.length; i++) {
if (a[i] == b){
System.out.println(i);
break;
}
}
}
}
有序表查找
折半查找
思路:前提是线性表中的关键词有序,线性表示顺序结构。取中间记录作为比较对象,若给定值小于中间值,就在左边区间找;若大于中间值,就在右边区间找。
代码实现:
int Binary_Search(int *a, int n, int key)
{
int low, high, mid;
low = 1; // 最低下标为记录首位
high = n; // 最高下标为记录末位
while (low <= high)
{
mid = (low + high) / 2; // 折半
if (key < a[mid])
high = mid - 1;
else if (key > a[mid])
low = mid + 1;
else
return mid; // 相等表示找到
}
return 0;
}
插值查找
思路:优化二分查找法,根据key在数值域中大小比例,来确定在哪找。
推导:
m i d = l o w + h i g h 2 = l o w + 1 2 ( h i g h − l o w ) = l o w + k e y − a [ l o w ] a [ h i g h ] − a [ l o w ] ( h i g h − l o w ) mid = \frac{low + high}{2}=low + \frac{1}{2}(high-low)=low+\frac{key-a[low]}{a[high] - a[low]}(high-low) mid=2low+high=low+21(high−low)=low+a[high]−a[low]key−a[low](high−low)
核心代码:
mid = low + (high-low)*(key-a[low]) / (a[high]-a[low]);
斐波那契查找(没理解)
找数组的长度在F数组中的位置。
根据F中的数字来扩充a数组,后面的值用a数组中的最大值填充
mid的值是由F决定的,mid=low+F[k-1] - 1
比较后修改high,low,k// 为什么要这样改k呢
如果小了,k-1,大了k-2.high并不会影响mid的选择,low才会
k在逐渐变小,得到的F值也在变小,F值是a的下标,如果key比a大,那么low变大了,F也会变得很大,此时k-2的话,得到的F值仍然是大的,
没看懂啊,后面再看吧
int Fibonacci_Search(int *a, int n, int key)
{
int low, high, mid, i, k;
low = 1; // 最低小标为记录首位
high = n;
k = 0;
while (n > F[k]-1) // 计算n位于斐波那契数列的位置
k++;
for (i=n; i<F[k]-1; i++)
a[i] = a[n];
while (n > F[k]-1) // 看看长度位于F数组的什么位置
k++;
for (i=n; i<F[k]-1; i++) // a数组的长度顺着上面F的取值,要扩充
a[i] = a[n];
while (low <= high)
{
mid = low + F[k-1] - 1; // 计算当前分割下标
if (key < a[mid])
{
high = mid-1;
k = k-1;
}
else if (key > a[mid])
{
low = mid+1;
k = k-2;
}
else
{
if (mid <=n )
return mid;
else
return n;
}
}
return 0;
}
JAVA实现有序表查找
折半:
public class BinarySearch {
public static void main(String[] args) {
int[] a = {1, 2, 3, 4, 5, 6, 7, 8};
int key = 3;
BinarySc(a, key);
}
private static void BinarySc(int[] a, int key) {
int high = a.length-1;
int low = 0;
int min = 0;
while (low <= high){
min = (low + high) / 2;
if (a[min] > key){
high = min - 1;
} else if (a[min] < key){
low = min + 1;
} else {
System.out.println(min);
break;
}
}
}
}
线性索引查找
稠密索引
稠密索引:数据集中每个记录都有一个索引,索引项一定按照关键码有序排列。
分块索引
分块有序:将数据集分块,按块给索引,这些块要满足下面两个条件,这样的序列叫做分块有序:
- 块内无序:每个块内的元素不需要有序
- 块间有序:比如第二块的记录的关键字均大于第一块中所有记录的关键字。
分块索引表结构:
- 最大关键码,存储每一个块中的最大关键字,好处是可使下一块的最小关键字也能比上一块最大的关键字大。
- 存储了块中的记录个数,以便循环时使用。
- 用于指向首数据元素的指针,便于开始对这一块中记录进行遍历。
分块索引表查找步骤:
- 先用简单的算法找到位于哪个块
- 然后利用块的指针, 在块中顺序搜索即可
时间复杂度分析:
共有n个记录,设有m块,每块t条记录,所以块的查找假设为
m
+
1
2
\frac{m+1}{2}
2m+1次,
块中的查询设为
t
+
1
2
\frac{t+1}{2}
2t+1次,所以总查找为:
m + 1 2 + t + 1 2 = 1 2 ( m + t ) + 1 = 1 2 ( n t + t ) + 1 \frac{m+1}{2} + \frac{t+1}{2} = \frac{1}{2}(m+t) +1 = \frac{1}{2}(\frac{n}{t} + t) +1 2m+1+2t+1=21(m+t)+1=21(tn+t)+1
最好的情况是m与t相等,所以次数为 n + 1 \sqrt{n}+1 n+1,时间复杂度为 O [ n ] O[\sqrt{n}] O[n],比折半查找的 O [ log n ] O[\log n] O[logn]差不少。
倒排索引
索引项的通用结构:
- 次关键码,例如上面的英文单词
- 记录号表,例如上面的文章编号
倒排索引:就是和上面的分块索引相反,左边放元素,右边放在哪个块。记录号表存储具有相同次关键码的所有记录的记录号,这样的索引方法就是倒排索引。
因为生活中有时需要根据属性来查找记录,例如搜索引擎。
二叉排序树
定义:二叉排序树(Binary Sort Tree),又称为二叉查找树,它或者是一颗空树,或者是有下列性质的二叉树:
- 若左子树不为空,则左子树上所有的结点都小于根结点
- 若右子树不为空,则右子树上所有的结点都大于根结点
- 左右子树也分别为二叉排序树
- 使用中序遍历可得从小到大的序列
作用:并不是为了排序,而是为了提高查找和插入删除关键字的速度。
二叉树结构:
typedef struct BiTNode // 结点结构
{
int data;
struct BitNode *lchild, *rchild;
} BiTNode, *BitTree;
二叉排序树的查找
思路:f用来指向双亲,p用来保存结果,找到了就为此结点,到最后一直没找到就返回离该点最接近的一个结点。
代码实现:
Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
if (!T) // 大小比较完的最后,到了null就表示找不到了
{
*p = f; // 返回空的父结点,就是二叉树中最接近的一个顶点
return FALSE:
}
else if (key == T->data)
{
*p = T;
return TRUE;
}
else if (key < T->data)
return SearchBST(T->lchild, key, T, p); // 在左子树继续找
else
return SearchBST(T->rchild, key, T, p); // 在右子树继续找
}
二叉排序树插入操作
思路:先检查树里有没有和插入点重复的,有就不插,没有就找到最接近插入点的结点,根据该结点和插入点的大小关系来判断为左孩子还是右孩子。
代码实现:
Status InsertBST(BiTree *T, int key)
{
BiTree p, s;
if (!SearchBST(*T, key, NULL, &p)) // 没找到就添加
{
// 先把结点建好
s = (BiTree) malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
// 添加环节
if (!p) // 如果是个空树,就把插入的点当成根结点
*T = s;
else if (key < p-data)
p->lchild = s; // 插入s为左孩子
else
p->rchild = s;
return TRUE;
}
else
return FALSE; // 已经有了相同的关键点,不再插入
}
二叉排序树删除操作
思路: 找删除点的中序前驱结点来替换被删除点,前驱和该删除点在排序上是相邻,所以是最适合替换的,同理也可用后驱替换。所以应有三个元素,一是被删除点,二是前驱点,三是前驱的父结点,前驱的父结点用来把断的接上。
查找代码:
删除前要找到结点,找到后针对结点删除
Status DeleteBST(BiTree *T, int key)
{
if (!*T) // 不存在的话
return FALSE;
else
{
if (key == (*T)->data) // 找到关键字等于key的数据元素
// 此处和查找不同
return Delete(T);
else if (key < (*T)->data)
return DeleteBST(&(*T)->lchild, key);
else
return DeleteBST(&(*T)->rchild, key);
}
}
删除代码:
Status Delete(BiTree *p)
{
BiTree q, s;
if ((*p)->rchild == NULL) // 右子树空则只需要重接左子树
{
q = *p;
*p = (*p)->lchild;
free(q);
}
else if ((*p)->lchild == NULL) // 同理
{
q = *p;
*p = (*p)->rchild;
free(q);
}
else // 左右子树均不为空
{
q = *p;
s = (*p)->lchild;
// 找p的左孩子的右孩子的尽头,s
while (s->rchild) // s指向被删除结点的前驱,q指向s的父结点
{
q = s;
s = s->rchild;
}
(*p)->data = s->data; // 将前驱的值赋给被删除结点位置
// 当q的左孩子拥有右孩子的时候
if (q != *p)
q->rchild = s->lchild; // 重接q的右子树
// 当q的左孩子没有右孩子的时候,q=p,没动
else
// 删除点的左孩子接替删除点的位置
// 正因没有右孩子,所以不会有影响
q->lchild = s->lchild;
free(s);
}
return TRUE;
}
删除操作图示:
绿色的线表示结点的变动,蓝色的数字表示中序遍历顺序,黄色表示结点
二叉排序树总结
二叉树虽然插入删除比顺序表简单,但也存在问题,树的结构是很影响速度的。
同样的数据元素,不同的排列顺序,会有不同的树的结构:
查找结点99,左边只需比较两次,而右边需要比较10次。
所以希望二叉排序树是比较平衡的,深度与完全二叉树相同,均为
[
log
2
n
]
+
1
[\log_2n]+1
[log2n]+1,所以茶中的复杂度也为
O
[
log
n
]
O[\log n]
O[logn]。
JAVA实现二叉排序树查找、插入、删除
因为java中,没有Tree实例就不能操作,没有指针,所以定义了一个变量,来标记该结点是否存在。
结点类:
public class Tree {
private int data;
// 因为java中,没有Tree实例就不能操作,没有指针
// 所以再定义一个变量,来标记该结点是否存在
public boolean exist=false;
public Tree lchild, rchild;
public int getData() {
return data;
}
public void setData(int data) {
exist = true;
this.data = data;
}
public Tree() {
lchild = rchild = null;
}
public Tree(int data) {
exist = true;
this.data = data;
lchild = rchild = null;
}
public void equal(Tree t){
if (t != null){
this.exist = true;
this.data = t.getData();
this.lchild = t.lchild;
this.rchild = t.rchild;
} else {
this.exist = false;
}
}
}
测试类:
public class BaseBT {
public static void main(String[] args) {
int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
Tree T = new Tree();
// 创建二叉排序树,就是一直插入的过程
for (int i = 0; i < a.length; i++) {
SearchOrInsert(T, a[i], null, false);
}
System.out.println("中序遍历--------");
// 中序遍历即可得到升序
TraverseTree(T);
// 删除结点
int key = 52;
System.out.println("删除的值为:" + key);
DeleteNode(T, key);
System.out.println("中序遍历--------");
TraverseTree(T);
}
private static void DeleteNode(Tree t, int key) {
// 首先要找到位置
if (t.exist == false){
System.out.println("无此元素");
} else {
if (key == t.getData()){
Delete(t);
} else if (key < t.getData()){
DeleteNode(t.lchild, key);
} else {
DeleteNode(t.rchild, key);
}
}
}
private static void Delete(Tree t) {
// 如果左右孩子存在空,是最简单的
// 如果左孩子为空,那么可能就是只有右孩子或都没有
if (t.lchild == null){
t.equal(t.rchild);
} else if (t.rchild == null){
t.equal(t.lchild);
} else {
// 找前驱
Tree q = t;
// 先找一个左孩子
Tree s = q.lchild;
// 再一直找左孩子的右孩子
while (s.rchild!=null){
q = s;
s = s.rchild;
}
// 此时可以确定t的新值
t.setData(s.getData());
// 此时要看s是否有右孩子,没有就直接接上
if (q == t){
// 如果左孩子没有右孩子,那么t的值就是左孩子,所以去掉左孩子
t.lchild = s.lchild;
} else {
q.rchild.equal(s.lchild);
}
}
}
private static void TraverseTree(Tree t) {
if (t!=null && t.exist){
TraverseTree(t.lchild);
System.out.println(t.getData());
TraverseTree(t.rchild);
}
}
// 查询插入一体化
private static boolean SearchOrInsert(Tree t, int i, Tree f, boolean search){
// 考虑根结点为空的情况
// tree不为空但exits为false,只有这一种情况
if (t!=null && t.exist==false){
System.out.println("插入了:" + i);
t.setData(i);
return false;
}
// t是树的根结点,i是被查找元素,f是当前结点的父结点
if (t == null){
if (search){
System.out.println("没找到");
} else {
System.out.println("插入了:" + i);
if (i > f.getData()){
f.rchild = new Tree(i);
} else {
f.lchild = new Tree(i);
}
}
return false;
}
// 找到了
if (t.getData() == i){
System.out.println("找到了");
return true;
}
// 往右子树走
if (t.getData() < i){
return SearchOrInsert(t.rchild, i, t, search);
} else {
return SearchOrInsert(t.lchild, i, t, search);
}
}
}
平衡二叉树AVL
定义:平衡二叉树是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。
平衡因子BF:将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF。
例子:
最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,称为最小不平衡子树。
案例:
插入了37,58的高度变成了2,BF也变成了2。
平衡二叉树实现原理
思想:在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。再保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之称为新的平衡子树。
案例:数组为{3,2,1,4,5,6,7,10,9,8},最终结果为下图,步骤就不上了,太多了。
方法:
- 出现不平衡问题的时候要立即修正
- 如果最小不平衡树的根结点为负数,该最小不平衡树就左旋,正数就右旋
- 如果最小不平衡树的根结点和孩子结点的BF符号不一样,就得调整到符号一样,调整的方法可能是改变顺序(11和12),也可能是旋转(14和15)
平衡二叉树实现算法
改进结点,添加BF因子:
typedef struct BiTNode // 结点结构
{
int data;
int bf;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
旋转可行的原因:
二叉排序树的构建和元素的输入顺序有很大关系,而下面的旋转操作并不像影响排序的正确性,所以旋转相当于把输入的顺序重新排了,数还是那些数,中序遍历起来也还是升序的,所以没问题。
右旋:
- 让原本根结点的左孩子作新的根结点,原本根结点作为新根结点的右孩子,重点是,旋转后的中序遍历顺序不能变。
- BF在别的地方改变,所以不需要在这里考虑BF
// 对以p为根的二叉排序树作右旋
// 之后p指向新的结点
void R_Rotate(BiTree *p)
{
BiTree L;
L = (*p)->lchild;
// 可能存在,也可能不存在
// 如果存在的话,为了保证正确的的大小顺序,具体看下图
(*p)->lchild = L->rchild;
// 重点就在这,让p的左孩子成为新的根结点
L->rchild = (*p);
*p = L;
}
右旋中的排序理解:
红色为BF,蓝色为中序顺序,注意,此处的BF只是一个参考。
右旋案例:
左旋:
void L_Rotate(BiTree *p)
{
BiTree R;
R = (*p)->rchlid;
(*p)->rchild = R->lchild;
R->lchild = (*p);
*p = R;
}
左平衡旋转处理:
思路:这里已经知道是要处理左平衡,所以知道T的BF大于0,直接从根结点的左孩子下手,先判断左孩子的BF,如果是同号,那么做简单的右旋并修改各个结点的BF即可;如果是异号,则以左孩子为根结点左旋,变为正BF,再对根结点右旋,同时修改各个结点的BF,这里的BF修改还跟插在了哪颗子树相关。
下图算是比较清晰例子:
#define LH +1 // 左高
#define EH 0 // 等高
#define RH -1 // 右高
// 对T所指结点为根的二叉树作左平衡旋转处理
// 结束时T指向新的根结点
void LeftBalance(BiTree *T)
{
BiTree L, Lr;
L = (*T)->lchild; // 处理左平衡,所以直接左子树
// 判断同号还是异号
switch(L->bf)
{
case LH: // 同号,新点插在了T的左孩子的左子树上,做单右旋处理
(*T)->bf = L->bf = EH;
R_Rotate(T);
break;
case RH: // 异号,插在了左孩子的右子树上,做双旋处理
Lr = L->rchild; // 左孩子的右子树根
// 判断是在右子树的何处,借此修改各结点bf
// 不过=0没看懂
switch(Lr->bf)
{
case LH:
(*T)->bf = RH;
L->bf = EH;
break;
case EH:
(*T)->bf = L->bf = EH;
break;
case RH:
(*T)->bf = EH;
L->bf = LH;
break;
}
Lr->bf = EH;
L_Rotate(&(*T)->lchild); // 对T的左子树做左旋
R_Rotate(T); // 对T做右旋
}
}
左平衡的三种case图例:
对应异号部分的三种情况,蓝色为中序顺序,红色为BF,N为新插入的点,在EH情况下,Lr就是N。
- EH:
- LH:
- RH:
左平衡规律总结:
- Lr会成为新的根结点,且bf为0。
- 因为是左平衡且异号,所以变化很固定,先根结点的左孩子左旋,再根结点右旋。
- T和L的bf和孩子取决于,N是Lr的左孩子还是又孩子。如果是左孩子,那么会小于根结点Lr,所以就会分配给L,所以此时L的bf=0,T的bf=-1。
- 如果N是Lr的右孩子,那么会大于根结点Lr,就会分配给T,所以此时L的bf=1,T的bf=0。
主函数:
思路:
- 先说插入:这是一个插入函数,每次插入调用一次。想插入就得先找到位置,所以该函数用了递归来寻找位置,使用
InsertAVL(&(*T)->lchild...
来递归查找,如果输入的参数T变成了Null,说明找到了位置,即可在该指针处创建结点。 - 再说平衡:由
if (e<(*T)->data)
这个判断可知道,插入后紧接着就会检查新插入结点的父结点的BF,并根据BF的值来判断是否需要对父结点进行平衡并该BF操作,不需要的话就把父结点的BF改了,毕竟插入了,所以一定会变。如果原来的父结点的BF为0的话,此时高度就会变,所以taller会变成TRUE,此时回到递归的上一次,即父结点为T的时候,此时因为taller变了,所以还要再判断是否平衡。因此只要高度变了,就会从下往上根据判断条件平衡。从下往上的话即不会漏掉,也可以在第一时间平衡。 - 这里面最近T是被插入的结点的父节点,然后T会一层一层再往上移动,然后层层改变BF。
- 做了左、右平衡后,
taller=False
,此时就不需要再网上判断了,所以说每插入一次,最多做一次平衡就行了
// 若不存在和e相同的,则插入并返回1,否则返回0
// 若插入后使二叉排序树失去平衡,则作平衡旋转处理
// taller反应是否长高
Status InsertAVL(BiTree *T, int e, Status *taller)
{
if (!*T)
{
// 插入新结点,树长高
*T = (BiTree) malloc(sizeof(BiTNode));
(*T)->data = e;
(*T)->lchild = (*T)->rchild = NULL:
(*T)->bf = EH;
*taller = TRUE;
}
else
{
if (e == (*T)->data)
{ // 已有,不再插入
*taller = FALSE;
return FALSE;
}
if (e < (*T)->data)
{ // 在T的左子树继续搜索
if (!InsertAVL(&(*T)->lchild, e, taller))
// 插入失败
return FALSE;
// 长高了,就得修改BF并考虑平衡问题
if (taller)
{
switch((*T)->bf)
{
case LH: // 原来为左高,左边再加一个就不平衡了
LeftBalance(T);
*taller = FALSE;
break;
case EH: // 原来一样高
(*T)->bf = LH;
*taller = TRUE;
break;
case RH:
// 为什么这个也有False?
// 左右变平衡,说明是在短的一边加的,所以高度不变
(*T)->bf = EH;
*taller = FALSE;
break;
}
}
}
else
{
if (!InsertAVL(&(*T)->rchild, e, taller))
return FALSE;
if (taller)
{
switch((*T)->bf)
{
case LH:
(*T)->bf = EH;
*taller = FALSE;
break;
case EH:
(*T)->bf = RH;
*taller = TRUE;
break;
case RH:
LeftBalance(T);
*taller = FALSE;
break;
}
}
}
}
return TRUE;
}
生成平衡二叉树:
int i;
int a[10] = {...};
BiTree T = NULL;
Status taller;
for (i=0; i<10; i++)
{
InsertAVL(&T, a[i], &taller);
}
时间复杂度:
查找、删除、插入都是
O
[
log
n
]
O[\log n]
O[logn]。
JAVA实现平衡二叉树相关
结点类:
public class BitNode {
public int BF;
public BitNode lchild, rchild;
private int data;
public boolean exist=false;
public int getData() {
return data;
}
public void setData(int data) {
exist = true;
this.data = data;
}
public BitNode() {
lchild = rchild = null;
}
public BitNode(int data) {
exist = true;
this.data = data;
lchild = rchild = null;
}
public void equal(BitNode bitNode){
if (bitNode!=null)
{
exist = bitNode.exist;
data = bitNode.data;
BF = bitNode.BF;
lchild = bitNode.lchild;
rchild = bitNode.rchild;
} else {
exist = false;
}
}
}
方法类:
public class AVLutils {
public static void L_Rotate(BitNode T) {
// T的分身
BitNode L = new BitNode();
L.equal(T);
// T的右孩子上位新跟结点
T.equal(T.rchild);
L.rchild = T.lchild;
T.lchild = L;
}
public static void R_Rotate(BitNode T) {
// T的分身
BitNode L = new BitNode();
L.equal(T);
// T的右孩子上位新跟结点
T.equal(T.lchild);
L.lchild = T.rchild;
T.rchild = L;
}
public static void LeftBalance(BitNode T){
// 做平衡,T.bf=1,是在变化之前传进函数的
BitNode L = T.lchild;
// 检查同号或异号
switch (L.BF){
// 同号的情况
case 1:
// 右转T
R_Rotate(T);
T.BF = L.BF = 0;
break;
// 异号的情况,这里要考虑Lr的符号
case -1:
BitNode Lr = L.rchild;
switch (Lr.BF){
// 插在了Lr的右边,此时新结点跟T走
case -1:
T.BF = 0;
L.BF = 1;
break;
// 插在了Lr的左边,此时新结点跟L走
case 1:
T.BF = -1;
L.BF = 0;
break;
case 0:
T.BF = L.BF = 0;
break;
}
// 根据规律可得以下固定内容
Lr.BF = 0;
L_Rotate(L);
R_Rotate(T);
}
}
public static void RightBalance(BitNode T){
BitNode R = T.rchild;
switch (R.BF){
// 此时同号为负
case -1:
L_Rotate(T);
T.BF = R.BF = 0;
case 1:
BitNode Rl = R.lchild;
switch (Rl.BF){
case 0:
T.BF = R.BF = 0;
// 插在了左边,跟着T
case 1:
T.BF = 0;
R.BF = -1;
case -1:
T.BF = 1;
R.BF = 0;
}
Rl.BF = 0;
R_Rotate(R);
L_Rotate(T);
}
}
// 通过比较来找位置
public static boolean InsertAVL(BitNode T, BitNode f, int e, Status sta){
// 不能存在说明找到要插入的位置了,假设只要插入了就变高
// 其实就是个检查机制,有插入就检查
if (T==null){
// 父结点不为空
if (e > f.getData()){
f.rchild = new BitNode(e);
} else {
f.lchild = new BitNode(e);
}
System.out.println("插入结点:" + e);
sta.taller = true;
return true;
} else {
// f表示父结点,f和T都为空说明是根结点
if (f==null && T.exist==false){
T.setData(e);
System.out.println("插入结点:" + e);
return true;
}
if (e == T.getData()){
System.out.println("重复了:" + e);
sta.taller = false;
return false;
} else if (e > T.getData()){
// 往右子树找
// 如果插入失败,则跳过
if (!InsertAVL(T.rchild, T, e, sta)){
return false;
}
// 插入成功后,检查高度
if (sta.taller){
// 现在是插到了右边
switch (T.BF){
case 0:
// 插入打破了平衡,说明最高高度变了
T.BF = -1;
sta.taller = true;
break;
case 1:
// 左右变平衡,说明是在短的一边加的,所以最高高度不变
T.BF = 0;
sta.taller = false;
break;
case -1:
RightBalance(T);
sta.taller = false;
break;
}
}
} else {
// 往左子树找
if (!InsertAVL(T.lchild, T, e, sta)){
return false;
}
if (sta.taller){
switch (T.BF){
case 0:
// 插入打破了平衡,说明最高高度变了
sta.taller = true;
T.BF = 1;
break;
case -1:
// 左右变平衡,说明是在短的一边加的,所以最高高度不变
sta.taller = false;
T.BF = 0;
break;
case 1:
LeftBalance(T);
sta.taller = false;
break;
}
}
}
}
// 能走到这里就说明插入成功了,否则在前面就返回false了
return true;
}
public static void TraverseTree(BitNode T){
if (T!= null){
TraverseTree(T.lchild);
System.out.println(T.getData());
TraverseTree(T.rchild);
}
}
}
高度标记类:
public class Status {
public boolean taller;
public Status(boolean taller) {
this.taller = taller;
}
}
测试类:
public class main {
public static void main(String[] args) {
int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
BitNode T = new BitNode();
Status sta = new Status(false);
for (int i = 0; i < a.length; i++) {
AVLutils.InsertAVL(T, null, a[i], sta);
}
System.out.println("中序遍历-------------");
AVLutils.TraverseTree(T);
}
}
多路查找树(B树)
多路查找树(mutil-way search tree):其每一个结点的孩子数可以多与两个,且每一个结点处可以存储多个元素。
由于它是查找树,所有元素之间存在某种特定的排序关系。
2-3树
2-3:每一个结点都有两个孩子(称它为2结点)或三个孩子(称它为3结点)
2结点:包含一个元素和两个孩子,排序和二叉排序树类似,但2结点要么没有孩子,要么有两个孩子。
3结点:包含一大一小两个元素和三个孩子,左子树包含小于较小元素的元素,中间子树包含介于较小较大之间的元素,右子树包含大于较大元素的元素。3结点要么没有孩子,要么有两个孩子。
性质:2-3树中所有叶子都在同一层次。
2-3树的插入:
- 空树插入2结点即可
- 插入元素到2结点中。如下图所示,3介于1,4之间,将左下角的2结点1改为3结点1、3。
- 插入元素到3结点中。此时3结点元素已经满了,所以要求修改3结点的父结点,把父结点改造成3结点。
4.插入元素到3结点的其他情况。如果父结点已经是3结点,那就继续找父结点的父结点,直到找到2结点。
2-3树的删除:
- 删除3结点上的叶子结点,直接和删除即可
- 删除2结点的叶子结点,可能会破坏2结点的定义,有四种情况在后面介绍
- 所删除的元素位于非叶子的分支结点,通常是将树按中序遍历后得到此元素的前驱或后继,考虑让他们补位
删除2结点的叶子结点有四种情况:
- 双亲是2结点,且拥有3结点的右孩子
- 双亲是2结点,右孩子也是2结点
- 双亲是一个3结点
- 如果当前树是一个满二叉树的情况,此时删除任何一个结点都不会满足2-3结点的定义
2-3-4树
概念:2-3树的拓展,包含了4结点的使用,包含大中小三个元素和四个孩子,也是要么四个要么没有。然后根据三个数分成四个区间,对应区间的数分配到对应的子树中。
案例:
数组:{7,1,2,5,6,9,8,4,3}。
创建流程:
删除流程,删除顺序是1、6、3、4、5、2、9:
B树
B树:B树是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。
B树的阶:结点最大的孩子数目。所以2-3树是3阶B树,2-3-4树是4阶B树。
一个m阶的B树有以下属性:
- 如果根结点不是叶结点,则至少有两棵子树。
- 每一个非根的分支结点都有k-1个元素和k个孩子,其中 [ m / 2 ] ≤ k ≤ m [m/2] \le k \le m [m/2]≤k≤m;每一个叶结点n都有k-1个元素,其中 [ m / 2 ] ≤ k ≤ m [m/2] \le k \le m [m/2]≤k≤m。
- 所有叶子结点都位于同一层次。
B+树(没看)
这个没代码也不知道如何用,没耐心看,先跳过
散列表(哈希表)查找概述
散列技术:在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。
散列函数/哈希函数:把这种对应关系f称为散列函数,又称哈希函数。
散列表/哈希表:采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。
散列表查找步骤:
- 根据散列函数计算地址,然后按地址存储该记录。
- 查找记录时,根据同样的散列函数计算记录的地址,直接访问地址。
适合场景:适合的求解问题是查找与给定值相等的记录。
冲突: key 1 ≠ key 2 \text{key}_1 \not = \text{key}_2 key1=key2的时候,却又 f ( key 1 ) = f ( key 2 ) f(\text{key}_1) = f(\text{key}_2) f(key1)=f(key2),这种现象称为冲突,把这两个关键字称为这个散列函数的同义词。
散列函数的构造方法
散列函数的设计原则:
- 计算简单,计算时间不应超过其他查找技术与关键字比较的时间。
- 散列地址分布均匀,让散列地址均匀分布在存储空间中,保证存储空间的有效利用。
直接定址法:
取关键词的某个线性函数值为散列地址:
f
(
k
e
y
)
=
a
x
k
e
y
+
b
(
a
、
b
为
常
数
)
f(key) = a x key + b (a、b为常数)
f(key)=axkey+b(a、b为常数)
使用场景:需事先知道关键字分布情况,适合查找表较小且连续的情况,由于这样的限制,并不常用。
数组分析法:
抽取一部分,再进行反转、移位、叠加等操作。
使用场景:处理关键字位数较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。
平方取中法:
假设关键字是1234,平方就是1522756,取中间三位是227,用作散列地址,也可以是275。
使用场景:不知道关键字的分布,而位数又不是很大的情况。
折叠法:
从左到右分割成位数相等的几部分,最后一部分位数不够可以短些,然后将这几部分叠加求和,并按散列表表长,取后记为做散列地址。
比如关键字是9876543210,散列表表长为3为,分成四组,987、654、321、0,然后叠加求和987+654+312+0=1962,再求后三位得散列地址962。
此时还不能保证分布均匀,可以折叠后再相加,如变成789+654+123+0=1566,此时地址为566。
使用场景:事先不需要知道关键字的分布,适合关键字位数较多的情况。
除留余数法:
对于散列表长为m的散列函数公式:
f
(
k
e
y
)
=
k
e
y
m
o
d
p
(
p
小
于
等
于
m
)
f(key)=key mod p (p小于等于m)
f(key)=keymodp(p小于等于m)
mod是取模的意思,不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。
比如 29 mod 12 = 5,就放在下标为5的位置:
使用经验:若表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
随机数法:
f ( k e y ) = r a n d o m ( k e y ) f(key)=random(key) f(key)=random(key)
不同方法的考虑因素:
- 计算地址所需时间
- 关键字的长度
- 散列表的大小
- 关键字的分布情况
- 记录查找的频率
处理散列冲突的方法
开放定址法:
一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
冲突了就用下面的公式找新地址:
f i ( k e y ) = ( f ( k e y ) + d i ) M O D m ( d i = 1 , 2 , 3 , . . . , m − 1 ) f_i(key) = (f(key) +d_i) MOD m (d_i=1,2,3,...,m-1) fi(key)=(f(key)+di)MODm(di=1,2,3,...,m−1)
上面是线性探测法,但是会出现本来不是同义词(第一次f的时候得到的值不相同)却要争夺一个地址的情况,这种现象称为堆积,但是堆积会大大降低存入和查找的效率。
因此提出二次探测法,目的是为了不让关键字都聚集在某一块区域:
f i ( k e y ) = ( f ( k e y ) + d i ) M O D m ( d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , q 2 , − q 2 , q ≤ m / 2 ) f_i(key) = (f(key) +d_i) MOD m (d_i=1^2,-1^2,2^2,-2^2,...,q^2, -q^2, q \le m/2) fi(key)=(f(key)+di)MODm(di=12,−12,22,−22,...,q2,−q2,q≤m/2)
还有一种随机探测法,因为可设定随机数种子,所以随机数不会重复:
f i ( k e y ) = ( f ( k e y ) + d i ) M O D m ( d i 是 一 个 随 机 数 列 ) f_i(key) = (f(key) +d_i) MOD m (d_i是一个随机数列) fi(key)=(f(key)+di)MODm(di是一个随机数列)
再散列函数法:
准备多个散列函数一起用,每当发生散列地址冲突的时候,就换一个计算方法,这样做消耗的时间比较多:
f i ( k e y ) = R H i ( k e y ) ( i = 1 , 2 , . . . , k ) f_i(key) = RH_i(key) (i=1,2,...,k) fi(key)=RHi(key)(i=1,2,...,k)
链地址法:
将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中值存储所有同义词子表的头指针。
该方法一定会为元素提供地址,但是会多出查找时遍历单链表的性能损耗。
例如集合{12,67,56,16,25,37,22,29,15,47,48,34},以12为出书,得表:
公共溢出区法:
创建一个地方专门存放冲突的关键字。
使用场景:适合冲突数据很少的情况,该结构对查找很友好。
散列表查找实现
定义散列表结构:
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 // 定义散列表长为数组的长度
#define NULLKEY -32768
typedef struct HashTable
{
int *elem; // 元素存储基址,动态分配数组
int count; // 当前数据元素个数
}
int m=0; // 散列表表长,全局变量
散列表的初始化:
Status Init HashTable(HashTable *H)
{
int i;
m = HASHSIZE;
H->count = m;
H->elem = (int *)malloc(m * sizeof(int));
for (i=0; i<m; i++)
H->elem[i] = NULLKEY;
return OK;
}
散列函数:
int Hash(int key)
{
return key % m; // 除留余数法
}
插入操作:
插入的关键字集合:{12、67、56、16、25、37、22、29、15、47、48、34}
void InsertHash(HashTable *H, int key)
{
int addr = Hash(key);
while (H->elem[addr] != NULLKEY) // 不为空,表冲突
addr = (addr + 1) %m; // 开放定址法的线性探测
H->elem[addr] = key; // 有空后插入关键字
}
查找记录:
Status SearchHash(HashTable H, int key, int *addr)
{
*addr = Hash(key);
while(H.elem[*addr] != key) // 不相等则说明发生了冲突
{
*addr = (*addr + 1) % m;
if(H.elem[*addr]==NULLKEY || *addr==Hash(key)
{
//如果一直没找到到了空地址,或者地址又转回来了
return UNSUCCESS; // 说明关键字不存在
}
}
return SUCCESS;
}
散列表查找性能分析
如果没有冲突的话复杂度就是 O [ 1 ] O[1] O[1],但是冲突是无法避免的,平均查找长度取决于以下几个因素:
- 散列函数是否均匀。
- 处理冲突的方法。性能比较:链地址法 > 二次探测法 > 线性探测法
- 散列表的装填因子。装填因子 α \alpha α=记录个数 / 三列表长度。记录越多, α \alpha α越大,产生冲突的可能性就越大。
总结:通常将散列表的空间设置得比查找集合大,虽然浪费了一定的空间,但是换来查找效率的大大提升。
JAVA实现散列表查找
public class main {
public static int HashSize = 12;
public static int NullKey = 65535;
public static void main(String[] args) {
// 初始化存储数组
int[] save = new int[12];
for (int i = 0; i < HashSize; i++) {
save[i] = NullKey;
}
// 存放数组
int[] a = {2, 4, 55, 22, 550};
SaveIntoHash(save, a);
// 查找数组
System.out.println("------------开始查找----------");
int[] b = {2, 4, 55, 22, 550, 11};
for (int i = 0; i < b.length; i++) {
SearchInHash(save, b[i]);
}
}
private static void SearchInHash(int[] save, int key) {
boolean flag = true;
int add = getHash(key);
while (save[add]!=key){
add = (add + 1) % HashSize;
// 如过转了一圈都没找到
// 或者说add没有再指向值,说明不是冲突而是不存在
if (save[add]==NullKey || add==getHash(key))
{
System.out.println("数组中不存在:" + key);
flag = false;
break;
}
}
if (flag){
System.out.println("找到了:" + key + " 地址为:" + add);
}
}
private static void SaveIntoHash(int[] save, int[] a) {
for (int i = 0; i < a.length; i++) {
int add = getHash(a[i]);
// 如果发生冲突的话
while (save[add]!=NullKey){
add = (add + 1) % HashSize;
}
save[add] = a[i];
System.out.println("地址:" + add + " 值:" + a[i]);
}
}
public static int getHash(int k){
return k % HashSize;
}
}