数据结构(四)查找
第四章 查找
4.1 查找概论
- 查找表:同一类型的数据构成的集合
- 关键字:数据元素种,某个数据项的值
- 静态查找表:只做查找操作
- 动态查找表:在查找的同时,做新增或删除操作
4.2 折半查找
-
折半查找有前提:
(1)线性表中的记录必须是关键字有序
(2)线性表必须采用顺序存储结构(不能是链表)/** 二分查找 * @param arr : 带查找数组 * @param n : arr[0]不做存储,待查数据产品那个arr[1]开始 * @param key :待查关键字 * @return :返回角标 */ int binerySearch(int *arr,int n,int key){ int low = 1; //从arr[1]开始查找 int high = n; int mid; while(low <= high){ mid = (low + high)/2; if(key < arr[mid]){ high = mid-1; }else if(key > arr[mid]){ low = mid + 1; }else{ return mid; } } return -1; }
4.3 线性索引查找
-
索引:把关键字与对应记录相关联的过程
(1)现行索引就是将索引项集合组织为现行结构
(2)现行索引结构称作索引表
(3)下面介绍3种索引表:稠密索引,分块索引,倒排索引 -
稠密索引
对数据表中的i每一项都做索引记录,使得索引表中的记录条数和数据表中的记录条数一样。 -
分块索引:
对数据项分块,使得快内数据无序,块间数据有序。索引表只对每块进行索引。 -
倒排索引:
搜索引擎的索引。
4.4 二叉排序树
-
二叉查找树:
又称二叉排序树,它是一个二叉树,具有如下性质:左子树结点上的值均小于根节点的值。右子树的值均大于根节点的值。
-
二叉排序树的递归查找
typedef struct BiTNode{ struct BiTNode *lchild, * rvhild; int data; }BiTNode,*BiTree; /** * 二叉排序树查找操作 * @param node : node所链接的树 * @param key :关键字值 * @param f : 指针f指向node结点的双亲。初始调用值为NULL * @param p :查找成功时,p指向该数据结点;查找失败时,指向查找路径上最后访问的节点。初始时,p是NULL * @return */ int serachBST(BiTree node,int key,BiTNode *f,BiTree *p){ if(node == NULL){ * p =f; return -1; }else if(key == node->data){ *p = node; return 0; }else if(key < node->data){ return serachBST(node->lchild,key,node,p); }else{ return serachBST(node->rvhild,key,node,p); } } /** * insertBST(tree,93) * @param tree * @param key * @return */ int insertBST(BiTree *tree,int key){ BiTree p, s; if(! serachBST(*tree,key,NULL,&p)){ // 查找不成功就开始插入将诶点 s = (BiTree) malloc(sizeof(BiTNode)); s->data = key; s->lchild = s->rvhild = NULL; if(p == NULL) // 开始没有根节点,创建s结点作为根节点 * tree = s; else if (key < p->data) p->lchild = s; // s作为做结点插入 else p->rvhild = s; // s作为右结点插入 return 1; }else{ return -1; // 树中已经有关键字的结点,无需插入 } } int main() { int a[10] = {1,2,45,234,12,6,78,123,43,111}; BiTree tree ; for (int i = 0; i < 10; i++) { insertBST(&tree ,a[i]); } }
-
二叉排序树的节点删除
(1)要删除的节点只有左子树/右子树
直接删除结点,并把其左子树/右子树代替删除结点的位置
(2)要删除的结点既有左子树,又有右子树
1)用二叉排序树书中该结点的直接前驱和直接后继元素代替需要删除的结点的位置
2)把替代元素的位置用其子树代替(此时替代元素只有一个子树,因为替代元素是最右结点或最左结点)
eg:要删除结点47,用他的前驱和后继结点
int deleteNode(BiTree *node); /** * 删除二叉排序树的结点 * @param tree * @param key * @return */ int deleteBST(BiTree *tree,int key){ if(* tree == NULL) return -1; if (key == (*tree)->data) return deleteNode(tree); else if(key < (*tree)->data) return deleteBST(&(*tree)->lchild,key); else return deleteBST(&(*tree)->rvhild,key); } /** * 链接断链 * @param node * @return */ int deleteNode(BiTree *node){ BiTree tmp,s; // tmp记录被删除的结点 if((*node)->rvhild == NULL) { // 右子树为空,只要链接左子树 tmp = *node; *node = (*node)->lchild; free(tmp); }else if((*node)->lchild == NULL){ // 左子树为空,只要链接右子树 tmp = *node; *node = (*node)->rvhild; free(tmp); }else{ // 被删除结点既有左子树又有右子树 tmp = *node; s = (*node)->lchild; while(s->rvhild != NULL){ // 找到左子树中的最右结点,就是被删除结点的前驱结点,把该结点代替删除结点的位置 tmp = s; // tmp指向s的前驱结点 s = s->rvhild; // s指向被删除结点左子树的最右结点 } (*node)->data = s->data; if(tmp != *node) tmp->rvhild = s->lchild; else tmp->lchild = s->lchild; free(s); } return 1; }
(3)二叉排序树的出现,是为了查找某个结点的时候,比较的次数很少。但是设想一种极端情况,带查找的数据是一个从小到大的数组,构建成二叉排序树后,是一个极端的右斜树。此时,虽然是一个二叉排序树,但是却不够“平衡”,层次太深。我们希望一个二叉排序树的深度像一个完全二叉树一样,为\(log_2n\),那么其查找的时间复杂度就为\(O(logn)\)。下面就开始介绍如何把一个二叉树变成一个平衡的二叉排序树
4.5 平衡二叉树(AVL树)
- 平衡二叉树:一种二叉排序树结构,其左右子树的深度最多相差1.所以,判断一个二叉树是否是平衡二叉树,首先看其是否是二叉排序树,其次看他的左右子树高度差。
- 左子树高度-右子树高度的值 = 平衡因子(平衡因子的取值只能是-1,1,0)
4.6 B树与B+树
-
B树
(1)B树和B+树都是完全平衡查找树,这种平衡,要求树种的每个结点的平衡因子都是0
(2)m阶B树(也称m路B树)的性质
a)所谓m阶或者m路,是说每个结点最多有m个孩子结点。(含有m个分支的意思)
b)为了增加B树查找的速度,他规定了每个结点的关键字个数要多,多个关键字之间不是相邻的整数,要有大小间隔。
这些间隔就指向了不同范围的孩子结点。(如下图,65与67不相邻)。
c)B数的节点结构:由三部分组成,开头是节点中关键字个数,然后依次交替的是指向孩子节点的指针与关键字的值
其中,关键字ki的值 > 指针p(i-1)所指向孩子节点的所有关键字的值。
d)B树规定:m路分支的B树,是说节点最多有m个分支,最少也要有\(\frac{m}{2}\)向上取整个分支。
也就意味着:m路分支的B树,其节点中的关键字最少有\(\frac{m}{2}\)向上取证-1个,最多有m-1个关键字
e)根节点例外于d中的条件,根节点最少只要2个分支即可,也就意味着,根节点可以只有一个关键字
(3)B树的查找过程
B树是查找硬盘数据的数据结构,他只能只有一个节点在内存中。
当要查找的数不在内存节点的记录中时,会找到节点中指向该范围的孩子节点的硬盘地址。
然后把这个地址上的节点加载到内存中进行查找。
(4)B树的最底层叶子节点,是NULL指针。他没有任何关键字。只是作为查找失败的标志:
只要查找到B树的叶子节点,证明记录不在B树中
最底层非叶子结点,有关键字,称作终端节点
(5)B树的高度范围:
若B树中存储的总关键字个数为n,且最多有m个分支(m阶)。则其高度满足公式:
\(【log_m(n+1)】 \leq h \leq 【log_\frac{m}{2}(\frac{(n+1)}{2})+1】\)
eg:3阶B树,共有8个关键字,其高度为\(log_39\leq h \leq 【log_{1.5}(\frac{9}{2})+1】\)
(6)B树的插入:(底层插,提中间点为父母)
a)B树的关键字插入,均在最底层的非叶子结点中
b)根据大小,找到位置插入。若发现插入后,节点的关键字个数 > m-1个。则把节点中中间的关键字,提到父节点中。然后拆分左右记录,挂到新增父节点关键字的两边。
(7)B树关键字删除
a)过删除的是非终端节点(非底层),删除关键字后,把两边指针指向的孩子节点合并
b)若删除的是终端节点:先直接删除,若删除后发现关键字个数太少,则向兄弟节点借关键字。若兄弟节点关键字不够借,则把双亲结点的关键字下落到终端节点上,再把下落后形成的节点挂在原节点的兄弟双亲结点上。一层层的双亲结点下落,重新调整树形。
(8)B树索引文件的例子:
1)文件的每一行称作文件的每一个记录。B树索引文件,就是对这些行进行索引
2)文件中的每一行会压缩成一个key(用来排序)和一个指向指针(指向文件记录真实内容)的关键字,存放在B树的节点中。
3)B树索引后形成的节点,最大只能占一个内存页。(这样,从磁盘拿节点时不必换入多个页)
4)B树节点开头的关键字个数字段,占2字节 -
B+树
(1)m个分支的B+树节点,有m个关键字
(2)m阶B+树:B+树的所有节点最多有m个分支。最少有\(\frac{m}{2}\)个分支
(3)B+树的叶子节点中,包含所有关键字。叶子节点的一层用指针链接形成一个链表,便于顺序查找所有关键字
4.7 散列表查找‘
-
散列表的引出
(1)我们一开始对无序列表进行查找的时候,需要挨个遍历比较。后来对有序的线性表查找时,采用折半查找的方式,比较待查找值与“中点”值大小进行查找。后来,我们为了进一步减少比较的次数,构建了二叉排序树,形成树形结构。接着,对二叉树形结构优化成平衡排序树,使得所有结点的比较次数都差不多相同。在后来,对于大量查找的数据,我们用B树和B+树对外存的数据建立索引进行查找。
(2)在折半查找顺序表结构时,我们可以进一步优化,直接通过待查找的值,计算出他可能存在的线性表位置,做到一次查找,就能判断是否命中。这种线性表就叫做散列表(Hash Table),而通过元素值计算元素位置的映射函数就是散列函数。
(3)因此,散列表即是存储方法,也是查找方法。
(4)装填因子:是散列表中装填的数据个数占表长的百分比 -
散列函数的构建
设计原则:计算简单,三列地址分布均匀
(1)直接定址法:\(f(key) = a*key + b\)(a,b均为常数)
a)直接定址法是key的线性函数
b)这种方法不会产生位置冲突,但只适合表小且连续的情况。
(2)数字分析法
a)当存储的每个元素是一个大长串数字时,可以截取一小段作为key,然后对key进行一个函数的映射
b)数字分析法的关键在于截取长串数字中的一小段
(3)平方取中法
a)平方取中法,先计算key平方的值,然后取这个值的中间n位作为散列地址。
b)eg:key=1234,key方=1522756,中间3位就是227,作为散列地址
c)平方取中法适合不知道关键字分布,且关键字位数不是很大的情况。
(4)折叠法
a)折叠法把关键字柘城等长的几段,然后进行相加的和作为散列地址
b)eg:9876543210:987+654+321+000 = 1962散列地址
c)适合不知道关键字分布,但关键字位数不很大的情况。
(5)除数留余法
a)\(F(key) = key (mod) p\)。用关键字对p取模作为散列地址。此时,散列地址为(0~p-1)
b)一般情况,p选择小于表长m的最大质数
(6) 随机数法:\(F(key) = random(Key)\) -
处理散列冲突的方法
(1)开放地址法
当发现f(key1)计算出的位置已经有元素的时候,就采用下式重新计算位置:
\(f_i(key) = (f(key) + d_i) MOD\) m (\(d_i\)=1,2,3 ... m-1)
(2)再散列法:
比如当采用数字分析法进行散列映射出现冲突后,改用平方取中法重新计算散列位置
(3)链地址法:
当出现散列冲突后,把该元素加在改为制的链表上 -
代码实现散列表查找
#define MAXSIZE 12 #define NULLKEY -32768 typedef struct{ int *elem; // 数组元素存储基址,动态分配数组 int count; // 当前数组元素的个数 }HashTable; /* 初始化散列表 */ int initHashTable(HashTable *hash){ hash->count = MAXSIZE; hash->elem = (int *)malloc(sizeof(int)); for (int i = 0; i < MAXSIZE; ++i) { hash->elem[i] = NULLKEY; } return 1; } /* 哈希映射函数:除数求余法 */ int hashFun(int key){ return key % MAXSIZE; } /* 插入散列表, 冲突解决采用开放定址法 */ void insertHashTable(HashTable *hash,int key){ int addr = hashFun(key); while(hash->elem[addr] != NULLKEY){ addr = (addr + 1) %MAXSIZE; } hash->elem[addr] = key; }