一.知识框架
静态查找与动态查找
静态查找是指在查找过程中,数据集合在查找之前是固定不变的,也就是说,查找过程中数据不发生增删改操作。静态查找通常采用静态数据结构,如数组或有序数组,通过某些查找算法(例如二分查找)来进行查找。
动态查找则是指在查找过程中,数据集合可以随时发生增删改操作。动态查找通常采用动态数据结构,比如链表、平衡树、哈希表等,以支持在运行时对数据的操作。
如果数据量固定,并且频繁查找,则选择静态查找更为合适。
如果数据变化频繁,且查找操作不占主导,则动态查找更加灵活。
二.线形结构
2.1 顺序查找
代码来自:【数据结构与算法】查找(Search)【详解】_检索结构查找-优快云博客
int Sequential_Search(int *a, int n, int key){
int i;
a[0] = key; //设置a[0]为关键字,称之为“哨兵”
i = n; //循环从数组尾部开始
while(a[i] != key){
i--;
}
return i; //返回0则说明查找失败
}
这种在查找方向的尽头放置“哨兵”免去了在查找过程中每一次比较后都要判断查找位置是否越界,在数据较多时,效率提高很大。
顺序表查找时间复杂度是O(n)。
2.2 折半查找(二分)
折半(二分)适用于有序的顺序表。
int Binary_Search(SeqList L, ElemType key){
int low = 0, high = L.length - 1, mid;
while(low <= high){
mid = (low + hight)/2; //取中间位置
if(L.elem[mid] == key){
return mid; //查找成功返回所在位置
}else if(L.elem[mid] > key){
high = mid - 1; //从前半部分继续查找
}else{
low = mid + 1; //从后半部分继续查找
}
}
return -1; //查找失败,返回-1
}
折半(二分)的过程可用二叉树来描述,称判断树,折半查找时间复杂度为O(logn).仅适用于顺序存储结构,不适用于链式存储结构。
2.3 分块查找
是顺序查找的一种改进方法,在此查找法中,出表本身外,还要建立一个索引表。
查找过程:
1.先查找索引表,确定待查元素所属的分块(顺序或折半)
2.在分块内顺序查找法
把长度n的查找表均分为b块 每块s个元素。
ASL=L
+L
=(b+1)/2 + (s+1)/2 =(S^2 +2S +n)/2S
当S=时,ASL最小为
+1
如果用折半查找法索引表,则长度为ASL=L
+L
[log
(B+1)]+(S+1)/2
三.树形结构
3.1 二叉排序树
二叉排序树(二叉搜索树,二叉查找树)一棵非空的二叉排序树具有以下性质;
1.如果左子树不空,则左子树上所有的节点的值都小于根节点值。
2.如果右子树不空,则右子树上所有的节点的值大于根节点的值。
3.左右子树也分别是二叉排序树。
构造一棵二叉树的结构:
/*二叉树的二叉链表结点结构定义*/
typedef struct BiTNode
{
int data; //结点数据
struct BiTNode *lchild, *rchild; //左右孩子指针
} BiTNode, *Bitree;
查找操作:
/*
递归查找二叉排序树T中是否存在key
指针f指向T的双亲,其初始调用值为NULL
若查找成功,则指针p指向该数据元素结点,并返回TRUE
否则指针p指向查找路径上访问的最后一个结点并返回FALSE
*/
bool SearchBST(BiTree T, int key, BiTree f, BiTree *p){
if(!T){
*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); //在右子树继续查找
}
}
插入操作:
/*
当二叉排序树T中不存在关键字等于key的数据元素时
插入key并返回TRUE,否则返回FALSE
*/
bool 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; //插入s为新的根节点
}else if(key < p->data){
p->lchild = s; //插入s为左孩子
}else{
p->rchild = s; //插入s为右孩子
}
return TRUE;
}else{
return FALSE; //树种已有关键字相同的结点,不再插入
}
}
删除操作:
删除节点有三种情况:
1.叶子节点
2.仅有左或右子树的节点
3.左右子树都有的节点
/*
若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点,
并返回TRUE;否则返回FALSE
*/
bool 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);
}
}
}
/*从二叉排序树中删除结点p,并重接它的左或右子树。*/
bool 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; //先转左
while(s->rchild){//然后向右到尽头,找待删结点的前驱
q = s;
s = s->rchild;
}
//此时s指向被删结点的直接前驱,p指向s的父母节点
p->data = s->data; //被删除结点的值替换成它的直接前驱的值
if(q != p){
q->rchild = s->lchild; //重接q的右子树
}else{
q->lchild = s->lchild; //重接q的左子树
}
pree(s);
}
return TRUE;
}
二叉树比较平均时 即深度与完全二叉树相同 查找时间为O(logn) 近似于折半查找。
不平衡的最坏情况 查找时间为O(n)等同于顺序查找 最好构建称一棵平衡二叉树
3.2 平衡二叉树
树上任意节点的左子树和右子树的深度之差不超过1.
节点平衡因子 = 左子树的高度 - 右子树的高度
在二叉排序树中 插入和删除节点后,只需要调整 最小不平衡子树 整棵树将恢复平衡。
调整平衡的方法:旋转树
那边长就往放反向旋转(降低树深度),内侧的先往外侧旋。
3.3 红黑树
参考文章:平衡搜索二叉树之红黑树(拒绝死记硬背,拥抱理解记忆)_红黑二叉树-优快云博客
3.4 B树 和 B+树
B树
定义:m阶B树时所有节点的平衡因子均等于0的m路平衡查找树
特点:
1.每个节点最多m棵子树
2.关键字数范围 m/2(向上取整)-1 ~ m-1
3.所有叶节点在同一层,且不存在,即查找失败的点。
//m叉排序树的数据结构
struct Node{
ElemType key[m-1]; //最多有m-1个关键字
struct Node *child[m];//最多有m棵子树
int num; //节点中世界存在的关键字个数
};
一棵含有n个关键字的m阶B树:
叶节点: n-1
最小高度: 最胖的树最矮,让树中的每个节点有m-1个关键字。
最大高度:最瘦的最高,让树中每个节点有[m/2]-1 个关键字根节点只有一个关键字就行。
B+树
一棵m阶的B+树,如果不为空,阶必须满足以下特性:
1.树中每个节点至少有m个关键字,即m棵子树。
2.除根节点外,所有非叶节点 至少含有[m/2]个关键字,即[m/2]棵子树。
3.所有叶节点中包含了全部关键字指向记录的指针,叶节点内的关键字有序排列,叶节点之间也是有序排列,指针相连。
4.所有非终端节点可以看成索引,仅包含了其子树中最大或 最小 关键字的值。
B与B+树的差异 (5阶为例)
1.B+树由分块查找进化而来;B树由二叉排序树进化而来。
2.B+树中,每个非根节点关键字的取值范围3 <=n<= 5,由n棵子树;
在B树中,每个非根节点关键字的取值范围是 2<= n <= 4,有 n+1棵子树。
3.在B+树中,仅叶节点包含信息,非叶节点仅起索引作用;
在B树中,全部节点的关键字都包含信息。
4.在B+树中,叶节点包含了全部的关键字,非叶节点中出现的关键字一定会出现在叶节点中,在B树中,任何节点中的关键字都不重复。
5B+树中支持顺序查找和多线路查找,B树支支持多路查找。
四.散列(哈希)表
散列表又称哈希表,这种数据结构的特点是:数据元素的关键字和它在表中的村粗地址直接相关。关键字与存储地址对应关系的函数称为哈希函数.
哈希函数 Address = Hash(key)
1.哈希是一种以常数平均时间执行插入,删除和查找的技术,但是,不支持元素排序和查找最小(大)值的操作。
2.哈希函数是一种映射关系,很灵活,任何关键字通过它的计算,返回值都落在表允许的范围之内即可。
3.对于不同的关键字,可能得到相同的哈希地址,这种现象称为冲突;哈希地址相同的关键字称为同义词。
4.处理冲突的方法有四种:1.链地址法(数组+链表) 2.开放定址法3.再散列法 4.建立公共溢出区。
5.哈希表的填装因子(表中记录数/表长)越大,关键字冲突可能性越大,同义词越多,查找效率可能更低。
散列表设计原则
1.清楚关键字发布情况。
2.散列表的大小由合理,太大浪费空间,太小则产生太多的同义词。
3.散列表中的数据要均匀分布,不要形成堆积。
4.散列表函数代码要精简。
散列表的设计
除留余数法:Hash(key) = key%p;
如果散列表表长为m,p为小于等于m的最大质数,再一般情况下,对质数取余会让冲突更少,数据元素在散列表的分布更均匀。
数据集{5,10,15,20,25,30,35,40,45,50}
直接定址法
Hash(key)=a*key +b //a和b为常数
数字分析法:根据数字特点,从数值中截取发布比较均匀的若干位作为散列地址,如手机号码;
随机数法:选择一个随机函数,用关键字作为随机函数种子,返回为散列的值。
即Hash(Key) = radmom(key),可结合除留余数法一起用。
//哈希表中数据元素的结构体
typedef struct Element{
unsigned int key; //关键字
int value; //数据元素其他数据项,可以是任意数据类型
//char value[1001]; //数据元素其他数据项,可以是任意类型
}Element;
//数据元素单链表
typedef struct Node{
Element elem; //数据元素
struct Node *next; //next指针
}Node;
//哈希表
typedef struct HashTable{
Struct Node *head; //数据元素存储地址,动态分配数组
int tablesize; //哈希表当前大小,即表长
int count; //哈下表中数据元素的个数
}HashTable;
//初始化哈希表,tablesize 为哈希表的表长,返回哈希表的地址
HashTable *InitHashTable(const unsigned int tablesize){
//分配哈希表
HashTable *hh=(HashTable*)malloc(sizeof(HashTable));
hh->tablesize=tablesize; //哈希表长
hh->head=(Node*)malloc((hh->tablesize)*sizeof(Node));
memset(hh->head,0,(hh->tablesize)*sizeof(Node));
hh->count=0; //哈希表中数据元素个数置为0
}