顺序查找,又叫“线性查找”,通常用于线性表
算法思想:从最开始或者最后一个数据元素位置查找,直到找到或者找不到
以顺序表为例,查找表是单链表或者双链表当然也行,循环和结束的写法有些逻辑差别
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST,ElemType key){
int i;
for(i=0;i<ST.TableLen&&ST.elem[i]!=key;++i);
//查找成功,则返回元素下标;查找失败,则返回-1
return i==ST.TableLen?-1:i;
}
增加了一个”哨兵“,把0号位置空出来,实际的数据从1这个位置开始存放。当我们要查找某一个关键字的时候,会把关键字放在0号位置,这就是所谓的哨兵。
查找时,是从最后开始查找,直到遇到哨兵停止
差别在于无需同从最开始开始查找一样一直判断是否越界
这种写法在每一轮for循环的时候,只需要判断当前指向的元素与要查找的关键字是否相等而无需判断是否越界,效率更高
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST,ElemType key){
ST.elem[0]=key; //哨兵
int i;
for(i=ST.TableLen,ST.elem[i]!=key;--i); //从后往前找
return i; //查找成功,则返回元素下标;查找失败,则返回0
}
若表中元素是递增的,则每一次对比时若等于则查找成功结束,若小于则查找失败,若大于则继续对比下一个。
从下图中可以看到有n + 1个失败结点,即n + 1种失败情况,如21落在( 19 , 29 ) 区间内,是第4个失败结点,就要对比4次发现查找失败。
分子上最后面加了两个n,因为最下面的两种失败情况都需要把最前面的n个元素全部对比一遍,这两种情况都要对比n次关键字
折半查找,又称“二分查找”,仅适用于有序的顺序表
顺序表其实就是说些元素得是用数组存放起来的,只有顺序表才拥有随机访问的特性(就是只有这样才方便折半),链表没有这个特性
左右指针,low high; 中间指针mid,不断比较mid指针与查找元素的大小,左右修改指针,再次折半,直到找到
注:以下代码基于升序排列的线性表,如果是降序排列会略有不同
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//折半查找
int Binary_Search(SSTable L,ElemType key){
int low=0,high=L.TableLen-1,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; //查找失败。返回-1
}
折半查找大部分情况比顺序快(要查找的数据就在第一个就会顺序查找最快)
给定一组元素,进行分块查找前将元素分块并保证块内无序,块间有序。对其建立索引表
//索引表
typedef struct{
ElemType maxValue;
int low,high;
}Index;
//顺序表存储实际元素
ElemType List[100];
注意上边应该是high左移一位,因为mid=high,所以high=mid-1,导致high左移。
所以low>high查找失败的时候,low一定大于key
二叉排序树,又称二叉查找树(BST)。一棵二叉排序树或者是空二叉树,或者是具有如下性质的二叉树:
利用二叉排序树左子树结点值<根结点值<右子树结点值的特点实现查找操作
若树非空,目标值与根结点的值相比较:
若相等,则查找成功;
若小于根结点,则在左子树上查找,否则在右子树上查找。
查找成功,返回结点指针;查找失败返回NULL
//二叉排序树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree
//在二叉排序树中查找值为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;
}
最坏空间复杂度为O ( 1 ) ,也有如下递归实现的版本,最坏空间复杂度为O ( h ),不如这个好
//在二叉排序树中查找值为key的结点(递归实现)
BSTNode *BSTSearch(BSTree T,int key){
if(T==NULL){
return NULL; //查找失败
}
if(key==T->key){
return T; //查找成功
}else if(key<T->key){
return BSTSearch(T->lchild,key); //在左子树中找
}else{
return BSTSearch(T->rchild,key); //在右子树中找
}
}
新插入的一定是叶子结点
//在二叉排序树中插入关键字为k的新结点(递归实现)
int BST_Insert(BSTree &T,int k){
if(T==NULL){ //原树为空,新插入的结点为根结点
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1; //返回1,插入成功
}else if(k==T->key){ //树中存在相同关键字的结点,插入失败
return 0;
}else if(k<T->key){ //插入到T的左子树
return BST_Insert(T->lchild,k);
}else{ //插入到T的右子树
return BST_Insert(T->rchild,k);
}
}
时间复杂度为O ( h ),也有空间复杂度更低的用循环实现的非递归的实现方式
二叉树中不允许两个结点的值相等,如果我们要插入的关键字本来已经存在就不应该被插入
插入新结点的过程
//按照str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T,int str[],int n){
T=NULL; //初始时T为空树
int i=0;
while(i<n){
BST_Insert(T,str[i]); //依次将每个关键字插入到二叉排序树中
i++;
}
}
序列不同的关键字序列可能得到同款二叉排序树,也可能得到不同款二叉排序树
先搜索找到目标结点
若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质
若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
若结点z有左右两棵子树,则令z的中序遍历的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一种或第二种情况
查找长度直接反映了查找操作时间复杂度,时间复杂度的数量级应该和查找长度的数量级相同
平均查找长度的数量级就是查找操作的时间复杂度,我们要追求查找操作的时间复杂度尽可能地低,因为插入和删除都需要建立在查找基础上
平衡二叉树(Balanced Binary Tree)简称平衡树(AVL树)
树上任一结点的左子树和右子树高度之差不超过1
结点的平衡因子=左子树高-右子树高
-1,0,1
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild,*rchild;
}AVLNode,*AVLTree;
上一小节的末尾提过让一棵二叉排序树保持平衡就能保证其查找效率达到O ( l o g 2 n ) 数量级,因此我们着重关心在二叉排序树中插入新结点后如何保持平衡
每次调整的对象都是“最小不平衡子树”。在插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡
这里讨论的是先假定插入后A是最小不平衡子树的根结点且插入新结点之后刚好导致不平衡的情况,所以要假定所有子树的高度都是H,尽管让AR高度为H+1和BR高度为H-1都能满足初始状态的树是平衡的
1.f->lchild=p->rchild //B的右子树变为A的左子树
2.p->rchild=f; //A结点变为B的右孩子
3.gf->lchild/rchild=p //让A的父结点原本指向A的孩子指针指向B
1.f->rchild=p->lchild //B结点左孩子变为A的右孩子
2.p->lchild=f; //A结点变为B的左孩子
3.gf->lchild/rchild=p; //让A的父结点原本指向A的孩子指针指向B
对于LR RL 只要把旋转的结点直接放在不平衡的根节点,再考虑把旋转结点的左子树和右子树分别放在不平衡根节点的左边或者右边(左边就放在左边的右边,右边就放在右边的左边)
总结出只有左孩子才能右上旋,只有右孩子才能左上旋,每一次旋转都能导致它和它的父结点父子关系互换的规律。在LR中处理A的左孩子的右孩子,它首先是一个右孩子,所以第一步只能让它左上旋替代A以前的左孩子,然后作为左孩子它只能右旋,RL也遵循相同的规律
插入操作导致“最小不平衡子树”高度+1,经过调整后高度恢复,所以在插入操作中只要将最小不平衡树调整平衡则其他祖先结点的平衡因子都会恢复
平衡二叉树 AVL:插入/删除 很容易破坏 ”平衡“ 特性,需要频繁调整树的形态。如:插入操作导致不平衡,则需要先计算平衡因子,找到不平衡子树(时间开销大),再进行 LL/RR/LR/RL 调整
红黑树 RBT:插入/删除 很多时候不会破坏 ”红黑“ 特性,无需频繁调整树的形态。即便需要调整,一般都可以在常数级时间内完成
红黑树是二叉排序树 左子树结点值 ≤ 根结点值 ≤ 右子树结点值
与普通BST相比,有什么要求
1.每个结点或是红色,或是黑色的
2.根节点是黑色的(根叶黑) ”根叶黑“
3.叶结点(外部结点、NULL结点、失败结点)均是黑色的
红黑树中的叶子结点并不是包含关键字的实际结点而是查找失败的结点,对应的是一个空指针
4.不存在两个相邻的红结点(即红结点的父节点和孩子结点均是黑色) ”不红红“
5.对每个结点,从该节点到任一叶结点的简单路径上,所含黑结点的数目相同 ”黑叶同“
struct RBnode { // 红黑树的结点定义
int key; // 关键字的值
RBnode* parent; // 父节点指针
RBnode* lchild; // 左孩子指针
RBnode* rchild; // 右孩子指针
int color; // 结点颜色,如:可用 0/1 表示 黑/红,也可使用枚举型enum表示颜色
};
结点的黑高 bh —— 从某结点出发(不含该结点)到达任一空叶结点的路径上黑结点总数
左右子树高度只差不超过2倍,所以比avl树要求更低,就更不容易修改,删除插入效率更高
根节点黑高为h的红黑树,内部结点数(关键字)至少有多少个?至多有多少个?
内部结点数最少的情况 —— 总共h层黑结点的满树形态
内部结点数最多的情况 —— h层黑结点,每一层黑结点下面都铺满一层红结点。
共2h层的满树形态
与BST、AVL相同,从根出发,左小右大,若查找到一个空叶节点,则查找失败
B树,⼜称多路平衡查找树,B树中所有结点的孩⼦个数的最⼤值称为B树的阶,通常⽤m表示。⼀棵m阶B树或为空树,或为满⾜如下特性的m叉树:
对非终端结点关键字的删除,必然可以转化为对终端结点的删除操作。若被删除关键字在非终端节点,则用直接前驱或直接后继来替代被删除的关键字
可以理解为:要追求 “绝对平衡”,即所有子树高度要相同
结点的子树个数与关键字个数相等
这也是与B树最大的不同
B+树同时支持多路查找和顺序查找
B树查找成功,可能停在任何一层。B+树中,如果只是在分支结点找到要查找的关键字,查找并没有结束,只有找到最下层的叶子结点后才可以找到某一个关键字实际对应的记录的存放位置。无论查找成功与否,最终一定都要走到最下面一层结点
操作系统对于磁盘的读写一般以磁盘块为单位,一般B+树和B树的不同结点就存放在不同磁盘块当中,对于B+树的查找就是反复将各个结点对应的磁盘块读入内存处理,最终得到要查找的关键字对应记录的存放位置
一层就是一次读磁盘
在B+树中,非叶结点不含有该关键字对应记录的存储地址。可以使一个磁盘块可以包含更多个关键字,使得B+树的阶更大,树高更矮,读磁盘次数更少,查找更快。典型应用:关系数据库的“索引”(如MySQL)
哈希表。是一种数据结构,特点是:数据元素的关键字与其存储地址直接相关