目录
需求分析: 4
1.1问题描述: 4
1.2问题要求: 4
概要设计 4
2.1抽象数据类型定义 4
2.2设计思路 4
2.2.1模块调用: 4
3详细设计 6
3.1存储结构设计 6
3.1.1顺序查找的基本思想 6
3.1.2二分法查找(折半查找)的基本思想 7
3.1.3斐波那契查找的前提是待查找的查找表必须顺序存储并且有序 9
3.1.4二叉排序树简介 10
3.2二叉排序树相关操作 11
3.3功能模块设计 20
4.运行与测试 27
5.总结 32
6.附录 32
需求分析:
1.1问题描述:
设计一个实现①顺序查找②二分查找(折半查找)、④二叉排序树、⑤平衡二叉树、③哈希查找算法的程序,并具有人机交互界面。
基本要求:
(1)设计一个菜单将实现的查找算法的名字显示出来,并提示用户对查找算法进行选择;
(2)分别实现顺序查找、二分查找(折半查找)、二叉排序树、哈希查找;
(3)哈希函数采用除留余数发,解决冲突的方法大家任选择一种;
(4)二叉排序树必须实现构建、查找、插入、删除四个基本操作;
(5)输出各种排序的结果并进行比较。
要求:如何用多种数据结构来求解问题。同时要求实现对应数据结构的所有基本操作。
1.2问题要求:
(1) 建立顺序表,设置关键字来实现顺序查找。
(2) 运用上述建立的顺序表,来实现二分查找。
(3) 建立链表结构,综合运用队列的相关知识,实现二叉树的排序。
(4) 运用除留余数法来分配空间,线性探测法来解决冲突实现哈希查找
(5) 在主函数中调用用户自定义函数,输出相应结果
(6) 本程序是对几种查找算法的综合比较
概要设计
2.1抽象数据类型定义
查询数据类型定义:
ADT chaxun{
数据对象:D={aij|aij属于{1,2,3…},i,j>0}
数据关系:R={
2.2设计思路
2.2.1模块调用:
3详细设计
3.1存储结构设计
3.1.1顺序查找的基本思想
从表的一端开始,顺序扫描表,依次将扫描到的结点关键字和给定值(假定为a)相比较,若当前结点关键字与a相等,则查找成功;若扫描结束后,仍未找到关键字等于a的结点,则查找失败。
说白了就是,从头到尾,一个一个地比,找着相同的就成功,找不到就失败。很明显的缺点就是查找效率低。
适用于线性表的顺序存储结构和链式存储结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
计算平均查找长度。 3.1.3斐波那契查找的前提是待查找的查找表必须顺序存储并且有序 void fibonacci(int *f)//斐波那契数 斐波那契查找的核心是: 关于斐波那契查找, 如果要查找的记录在右侧,则左侧的数据都不用再判断了,不断反复进行下去,对处于当众的大部分数据,其工作效率要高一些。所以尽管斐波那契查找的时间复杂度也为O(logn),但就平均性能来说,斐波那契查找要优于折半查找。可惜如果是最坏的情况,比如这里key=1,那么始终都处于左侧在查找,则查找效率低于折半查找。 3.2.2二叉排序树的插入操作 二叉排序树的生成 BSTNode *CreateBST(KeyType A[], int n) 3.2.4二叉排序树的删除操作 方法一:设s为p结点在中序序列中的直接前驱,将p的左子树改为f的左子树,将p的右子树改为s的右子树。 方法二:用p结点在中序序列中的直接前驱(或后继)s代替p,然后再从二叉排序树中将s删除。这时如果s为p的直接前驱,则s只有左子树(或者没有孩子),则删除s可以按照删除p的其余两种情况处理。如果s为p的直接后继,则s只有右子树(或者没有孩子),删除s同理可以按照删除p的其余两种情况处理。 平衡二叉树大部分操作和二叉查找树类似,主要不同在于插入删除的时候平衡二叉树的平衡可能被改变,并且只有从那些插入点到根结点的路径上的结点的平衡性可能被改变,因为只有这些结点的子树可能变化。 调整措施: ② 双旋转 冲突 (2)数字分析法 (3)平方取中法 (4)除留余数法 (5)随机数法 1、线性探查 创建哈希表: 哈希表的查找: 4.运行与测试 平衡二叉树: 5.总结 https://download.csdn.net/download/whj707216853/10532744
例如上表,查找1,需要1次,查找2需要2次,依次往下推,可知查找16需要16次,
可以看出,我们只要将这些查找次数求和(我们初中学的,上底加下底乘以高除以2),然后除以结点数,即为平均查找长度。
设n=节点数
平均查找长度=(n+1)/2
int Find(int[] ary, int target)
{
for (int i = 0; i < ary.Length; i++)
{
if (ary[i] == target)
{
//找到了就返回找到的位置
return i;
}
}
//没找到就返回-1,表示没找到
return -1;
}
3.1.2二分法查找(折半查找)的基本思想
前提:
(1)确定该区间的中点位置:mid=(low+high)/2
min代表区间中间的结点的位置,low代表区间最左结点位置,high代表区间最右结点位置
(2)将待查a值与结点mid的关键字(下面用R[mid].key)比较,若相等,则查找成功,否则确定新的查找区间:
如果R[mid].key>a,则由表的有序性可知,R[mid].key右侧的值都大于a,所以等于a的关键字如果存在,必然在R[mid].key左边的表中。这时high=mid-1
如果R[mid].key 如果R[mid].key=a,则查找成功。
(3)下一次查找针对新的查找区间,重复步骤(1)和(2)
(4)在查找过程中,low逐步增加,high逐步减少,如果high
平均查找长度=Log2(n+1)-1
注:虽然二分法查找的效率高,但是要将表按关键字排序。而排序本身是一种很费时的运算,所以二分法比较适用于顺序存储结构。为保持表的有序性,在顺序结构中插入和删除都必须移动大量的结点。因此,二分查找特别适用于那种一经建立就很少改动而又经常需要查找的线性表。
int Binary_Search(int key,int low,int high)//折半查找
{
int mid;
if(low == high)
{
if(data[low] == key)
return low;
else
return -1;
}
else
{
mid = (low + high) / 2;
if(mid == low)
mid++;
if(key < data[mid])
return Binary_Search(key, low, mid - 1);
else
return Binary_Search(key, mid, high);
}
}
相对于折半查找,一般将待比较的key值与第mid=(low+high)/2位置的元素比较,比较结果分三种情况
1)相等,mid位置的元素即为所求
2)> ,low=mid+1;
3) < ,high=mid-1;
斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,及n=Fk-1;
开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种
1)相等,mid位置的元素即为所求
2)> ,low=mid+1,k-=2;
说明:low=mid+1说明待查找的元素在[mid+1,hign]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找
3)< ,high=mid-1,k-=1;
说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1
个,所以可以递归 的应用斐波那契查找
斐波那契查找的算法如下:
{
f[0] = 1;
f[1] = 1;
for(int i = 2;i < 20;++i)
f[i] = f[i - 2] + f[i - 1];
}
int fibonacci_search(int *a,int key,int n)//斐波那契查找
{
int low = 0,high = n - 1;
int mid = 0;
int k = 0;
int F[20];
fibonacci(F);
while(n > F[k] - 1) //计算出n在斐波那契中的数列
++k;
for(int i = n;i < F[k] - 1;++i) //把数组补全
a[i] = a[high];
while(low <= high)
{
mid = low + F[k-1] - 1; //根据斐波那契数列进行黄金分割
if(a[mid] > key)
{
high = mid - 1;
k = k - 1;
}
else if(a[mid] < key)
{
low = mid + 1;
k = k - 2;
}
else{
if(mid <= high) //如果为真则找到相应的位置
return mid;
else
return -1;
}
}
return -1;
}
1)当key=a[mid]时,查找成功;
2)当key 3)当key>a[mid]时,新的查找范围是第mid+1个到第high个,此时范围个数为F[k-2] - 1个,即数组右边的长度,所以要在[F[k - 2] - 1]范围内查找。
还有关键一点,折半查找是进行加法与除法运算的(mid=(low+high)/2),插值查找则进行更复杂的四则运算(mid = low + (high - low) * ((key - a[low]) / (a[high] - a[low]))),而斐波那契查找只进行最简单的加减法运算(mid = low + F[k-1] - 1),在海量数据的查找过程中,这种细微的差别可能会影响最终的效率。
3.1.4二叉排序树简介
二叉排序树(Binary Sort Tree,简称BST),又称二叉查找树,是红黑树、AVL树等的基础。它或是一棵空树,或者是具有下列性质的一棵二叉树:
1、若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2、若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3、它的左右子树也分别为二叉排序树。
下面的一棵树即为二叉排序树:
很明显,对二叉排序树进行中序遍历,便可得到一个有序序列,该有序序列中的各元素按照从小到大的顺序排列,因此一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列。
3.2二叉排序树相关操作
3.2.1二叉排序树的操作介绍
二叉排序树通常有查找、插入、删除等操作。查找操作很简单,无非就是递归查找,有点类似二叉树遍历的过程。插入操作也不难,一般是先在二叉排序树pTree中查找,看是否存在有等于给定值的元素,如果查找不到,则将给定值插入到该二叉排序树中,但是要保证插入后的树依然是二叉排序树。这样,新节点插入的位置便是唯一的,而且新插入的节点一定是一个新添加的叶子节点,并且是查找不成功时查找路径上访问的最后一个节点的左孩子或右孩子。正是由于其在查找过程中插入节点的特性,二叉排序树是一种动态树。
在给出各操作实现的具体代码前,要详细看下二叉排序树的删除操作,删除操作相比于二叉排序树的其他操作要难些,但也只是相对于其本身的其他操作而已,真正理解了也就很容易了。闲话少说,下面就来具体分析二叉排序树的删除操作。
我们假设在二叉排序树中要被删除的节点为p(即p指向的节点,下同),其父节点为f,当然节点p可能是节点f的的左孩子或右孩子,但在下面各种情况的分析中,你会发现,无论是左孩子还是右孩子,都不影响删除操作的通用性。很明显,删除操作要分为如下3种情况:
1、若待删节点p为叶子节点,则删除p后,并不会破坏整棵树的结构,因此只需令p=NULL即可。
2、若待删节点p只有左子树或右子树,则只需将左子树或右子树重接到p的父节点上即可,即执行如下操作:p=p->lchild或p=p->rchild。
3、若待删节点p既有左子树又有右子树,显然就不如上面两种情况那么简单了。我们要使节点p被删除后,二叉排序树的结构不变,就需要对它的子树做一些操作,而且只需操作一个子树即可,操作左子树和操作右子树的思路相似。
二叉排序树的类型定义:typedef struct BSTNode
{
KeyType key; //数据域
BSTNode *lchild;
BSTNode *rchild;
}
(1)如果二叉排序树T为空,则创建一个关键字为k的结点,将其作为根结点。
(2)否则将k和根结点的关键字进行比较,如果相等则返回,如果k小于根结点的关键字则插入根结点的左子树中,否则插入根结点的右子树中。
二叉排序树的插入算法: int InsertBST(BSTNode *p, KeyType k)
{
if(p==NULL)
{
p=(BSTNode*)malloc(sizeof(BSTNode));
p->key=k;
p->lchild=p->rchild=NULL;
return 1;
}
else if(k==p->key)
return 0;
else if(k
{
BSTNode *bt=NULL;
int i=0;
while(i
InsertBST(bt, A[i]);
i++;
}
return bt;
}输入{50,16,56,52,8}生成二叉排序树
3.2.3二叉排序树的查找操作
首先将需要查找的值与根结点比较,如果相等则查找成功,算法终止;如果比根结点小则左子树中查找,如果比根结点大则到右子树查找。
二叉排序树的查找算法的递归形式:
BSTree SearchBST(BSTree t, int k)
{
if(tnull || kt->key)
return t;
else if(kkey)
return SearchBST(t->lchild, k);
else
return SearchBST(t->rchild, k);
}
叉排序树的查找算法的非递归形式:
BSTree SearchBST2(BSTree t, int k)
{
BSTree p=t;
while(p!=null && p->key!=k)
{
if(kkey)
p=p->lchild;
else
p=p->rchild;
}
return p;
}
查找过程演示图: ![这里写图片描述](https://img-blog.csdn.net/20180710145514842?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3doajcwNzIxNjg1Mw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
删除二叉排序树的某一个结点的步骤如下:
1)查找待删除的结点
查找结点时,令p指向其访问到的结点,f指向其双亲结点。若树中找不到被删结点时返回NULL,否则被删除结点是p,返回p。
2)删除结点
假设要删除二叉排序树中的一个结点p,其双亲结点为f,则删除结点*p时,需考虑以下3种情况:
(1)p为叶子结点。
在这种情况下,可以将p结点直接删除。
p为左子树:
f->lchild=NULL;
free§;
p为右子树:
f->rclild=NULl; free(p);
操作示意图如下:![这里写图片描述](https://img-blog.csdn.net/20180710145547295?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3doajcwNzIxNjg1Mw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
(2)
*p只有左子树,或只有右子树。
对于这种情况,可以直接将*p的左子树或右子树与其双亲结点*f相连,然后删除*p。
p为f的左孩子,p的左子树非空:
f->lchild=p->lchild;
free(p);
p为f的左孩子,p的右子树非空:
f->lchild=p->rchild;
free(p);
p为f的右孩子,p的左子树非空:
f->rchild=p->lchild;
free(p);
p为f的右孩子,p的右子树非空:
f->rchild=p->rchild;
free(p);
操作示意图如下:
![这里写图片描述](https://img-blog.csdn.net/20180710145636221?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3doajcwNzIxNjg1Mw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
(3)*p有左右子树。
f->lchild=p->lchild;
s->rchild=p->rchild; free(p);
操作示意图如下:
![这里写图片描述](https://img-blog.csdn.net/20180710145704781?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3doajcwNzIxNjg1Mw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
平衡二叉树:
平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
平衡二叉树不平衡的情形:
把需要重新平衡的结点叫做α,由于任意两个结点最多只有两个儿子,因此高度不平衡时,α结点的两颗子树的高度相差2.容易看出,这种不平衡可能出现在下面4中情况中:
1.对α的左儿子的左子树进行一次插入
2.对α的左儿子的右子树进行一次插入
3.对α的右儿子的左子树进行一次插入
4.对α的右儿子的右子树进行一次插入
情形1和情形4是关于α的镜像对称,二情形2和情形3也是关于α的镜像对称,因此理论上看只有两种情况,但编程的角度看还是四种情形。
第一种情况是插入发生在“外边”的情形(左左或右右),该情况可以通过一次单旋转完成调整;第二种情况是插入发生在“内部”的情形(左右或右左),这种情况比较复杂,需要通过双旋转来调整。
① 单旋转
上图是左左的情况,k2结点不满足平衡性,它的左子树k1比右子树z深两层,k1子树中更深的是k1的左子树x,因此属于左左情况。
为了恢复平衡,我们把x上移一层,并把z下移一层,但此时实际已经超出了AVL树的性质要求。为此,重新安排结点以形成一颗等价的树。为使树恢复平衡,我们把k2变成这棵树的根节点,因为k2大于k1,把k2置于k1的右子树上,而原本在k1右子树的Y大于k1,小于k2,就把Y置于k2的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。
这种情况称为单旋转。
对于左右和右左两种情况,单旋转不能解决问题,要经过两次旋转。
对于上图情况,为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以k2为根的平衡二叉树。
AVL树的删除操作:
同插入操作一样,删除结点时也有可能破坏平衡性,这就要求我们删除的时候要进行平衡性调整。
删除分为以下几种情况:
首先在整个二叉树中搜索要删除的结点,如果没搜索到直接返回不作处理,否则执行以下操作:
① .要删除的节点是当前根节点T。
如果左右子树都非空。在高度较大的子树中实施删除操作。
分两种情况:
(1)、左子树高度大于右子树高度,将左子树中最大的那个元素赋给当前根节点,然后删除左子树中元素值最大的那个节点。
(2)、左子树高度小于右子树高度,将右子树中最小的那个元素赋给当前根节点,然后删除右子树中元素值最小的那个节点。
如果左右子树中有一个为空,那么直接用那个非空子树或者是NULL替换当前根节点即可。
② 、要删除的节点元素值小于当前根节点T值,在左子树中进行删除。
递归调用,在左子树中实施删除。
这个是需要判断当前根节点是否仍然满足平衡条件,
如果满足平衡条件,只需要更新当前根节点T的高度信息。
否则,需要进行旋转调整:
如果T的左子节点的左子树的高度大于T的左子节点的右子树的高度,进行相应的单旋转。否则进行双旋转。
③ 、要删除的节点元素值大于当前根节点T值,在右子树中进行删除。
3.3功能模块设计
哈希表和哈希函数
在记录的存储位置和它的关键字之间是建立一个确定的对应关系(映射函数),使每个关键字和一个存储位置能唯一对应。这个映射函数称为哈希函数,根据这个原则建立的表称为哈希表(Hash Table),也叫散列表。
以上描述,如果通过数学形式来描述就是:
若查找关键字为 key,则其值存放在 f(key) 的存储位置上。由此,不需比较便可直接取得所查记录。
注:哈希查找与线性表查找和树表查找最大的区别在于,不用数值比较。
若 key1 ≠ key2 ,而 f(key1) = f(key2),这种情况称为冲突(Collision)。
根据哈希函数f(key)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这一映射过程称为构造哈希表。
构造哈希表
常见的构造哈希表的方法有 5 种:
(1)直接定址法
说白了,就是小学时学过的一元一次方程。
即 f(key) = a * key + b。其中,a和b 是常数。
假设关键字是R进制数(如十进制)。并且哈希表中可能出现的关键字都是事先知道的,则可选取关键字的若干数位组成哈希地址。
选取的原则是使得到的哈希地址尽量避免冲突,即所选数位上的数字尽可能是随机的。
取关键字平方后的中间几位为哈希地址。通常在选定哈希函数时不一定能知道关键字的全部情况,仅取其中的几位为地址不一定合适;
而一个数平方后的中间几位数和数的每一位都相关, 由此得到的哈希地址随机性更大。取的位数由表长决定。
取关键字被某个不大于哈希表表长 m 的数 p 除后所得的余数为哈希地址。
即 f(key) = key % p (p ≤ m)
这是一种最简单,最常用的方法,它不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。
注意:p的选择很重要,如果选的不好,容易产生冲突。根据经验,一般情况下可以选p为素数。
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 f(key) = random(key)。
通常,在关键字长度不等时采用此法构造哈希函数较为恰当。
解决冲突
(1)开放定址法
如果两个数据元素的哈希值相同,则在哈希表中为后插入的数据元素另外选择一个表项。
当程序查找哈希表时,如果没有在第一个对应的哈希表项中找到符合查找要求的数据元素,程序就会继续往后查找,直到找到一个符合查找要求的数据元素,或者遇到一个空的表项。
例子
若要将一组关键字序列 {1, 9, 25, 11, 12, 35, 17, 29} 存放到哈希表中。
采用除留余数法构造哈希表;采用开放定址法处理冲突。
不妨设选取的p和m为13,由 f(key) = key % 13 可以得到下表。
需要注意的是,在上图中有两个关键字的探查次数为 2 ,其他都是1。
这个过程是这样的:
a. 12 % 13 结果是12,而它的前面有个 25 ,25 % 13 也是12,存在冲突。
我们使用开放定址法 (12 + 1) % 13 = 0,没有冲突,完成。
b. 35 % 13 结果是 9,而它的前面有个 9,9 % 13也是 9,存在冲突。
我们使用开放定址法 (9 + 1) % 13 = 10,没有冲突,完成。
2)拉链法
将哈希值相同的数据元素存放在一个链表中,在查找哈希表的过程中,当查找到这个链表时,必须采用线性查找方法。
在这种方法中,哈希表中每个单元存放的不再是记录本身,而是相应同义词单链表的头指针。
例子
如果对开放定址法例子中提到的序列使用拉链法,得到的结果如下图所示:
设给出一组元素,它们的关键码为:37,25,14,36,49,68,57,11,散列表为HT[12],表的大小m = 12,假设采用Hash(x) = x % p; // (p = 11) 11是接近m的质数,就有:
Hash(37) = 4 Hash(25) = 3 Hash(14) = 3 Hash(36) = 3 Hash(49) = 5 Hash(68) = 2 Hash(57) = 2 Hash(11) = 0
采用线性探查法处理冲突
需要加入一个元素时,使用散列函数进行计算,确定元素的桶号H0,按此桶号查看该桶,如果是所 要搜索的元素的关键码,则说明表中已有此元素,不再进行此元素的插入,否则即为冲突,再查看紧 随其后的下一个桶,如果是空桶,则搜索失败,新元素插入即可。
在闭散列的情形下不能随便物理删除表中已有的元素。因为若删除元素会影响其他元素的搜索。
散列表的装填因子
散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小
线性探查方法容易产生“堆积”问题,即不同探查序列的关键码占据了可利用的空桶,使得为寻 找某一关键码需要经历不同的探查序列的元素,导致搜索时间增加。因此,当散列表经常变动 时,好不要用闭散列法处理冲突,可以利用二次探查法可以改善上述的“堆积”问题,减少为完成 搜索所需的平均探查次数。
2、二次探查
使用二次探查法,在表中寻找“下一个”空桶的公式为: Hi = (H0 + i^2)%m, Hi = (H0 - i^2)%m, i = 1,2,3…,(m-1)/2 H0 = Hash(x)是通过散列函数Hash()对元素的关键码x进行计算得到的桶号,它是一个非负整数。 m是表的大小,它应该是一个质数。
假设数组的关键码为37,25,14,36,49,68,57,11,假设取m=19,这样可设定为HT[19],采用散列函 数Hash(x) = x % 19 Hash(37)=18 Hash(25)=6 Hash(14)=14 Hash(36)=17 Hash(49)=11 Hash(68)=11 Hash(57)=0 Hash(11)=11
采用二次探查法处理冲突:
研究表明当表的长度TableSize为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而 且任何一个位置都不会被探查两次。因此只要表中有一半的空的,就不会有表满的问题。在搜索时
可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5;如果超出必须考虑增容。
哈希表的结构体:typedef struct{ //哈希表线性探测再散列数据类型定义
int key; //关键字
int si; //插入成功时的次数
}HashTable1;
typedef struct Ha{ //哈希表链地址法数据类型定义
int elem; //数据项
struct Ha *next2; //指向下一个结点的指针
}HashTable2;
typedef struct{ //哈希表二次探测再散列法数据类型定义
int elem[HashSize]; //表中储存数据元素的数组
int count; //表中储存数据元素的个数
int size; //哈希表的尺寸
}HashTable3;
void CreateHashTable1(HashTable1 *H,int *a,int num) //哈希表线性探测再散列(除留余数法)
{
int i,d,cnt;
for(i=0; i
void SearchHash1(HashTable1 *h,int data)
{
int d,i;
d=data%HashSize; //哈希函数
i=d;
if(h[d].key==data) //一次查找成功
printf("\n数字%d的查找次数为:%d\n",h[d].key,h[d].si);
else
{
while(i
主菜单:
静态查找:
文件读取:
折半查找:
顺序查找:
动态查找:
建立二叉排序树:
查找:
散列法查找:
读数据并输出:
线性再散列法查找:
链地址法查找:
本次课程设计开始调试的时候经常出现很多BUG,经常容易烦躁,但是一步步修改代码,调试程序渐渐解决bug的过程也是很愉快的。在程序设计方面,逐渐感觉到模块化设计的重要性,应该分析出功能模块,然后对其细节中的共性和特性作分析。
这次的设计,让我感觉到,成功的程序设计是要建立在熟悉语言的基础之上的。每一次程序设计能大大地增加对语言的熟悉和感知,能使理论与实际应用相结合,提高了自己组织数据及编写程序的能力。培养了基本的、良好的程序设计技能以及合作能力。在上机操作的过程中,培养了我实际分析问题、编程和动手能力,使我掌握了程序设计的基本技能,提高了我适应实际和实践编程的能力。
6.附录#include
这里是完整的设计报告和和代码