所有需要被查的数据所在的集合,我们给它一个统称叫查找表
。
查找表是由同一类型的数据元素
(或记录)构成的集合。
关键字
是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素,也可以标识一个记录的某个数据项,我们称为关键码
。
若此关键字可以唯一地表示一个记录,则称此关键字为主关键字
。这也意味着,对不同的记录,其主关键字均不相同。主关键字所在的数据项称为主关键码
。
对于那些可以识别多个数据元素(或记录)的关键字,我们称之为次关键字
,次关键字也可以理解为是不以唯一标识一个数据元素(或记录)的关键字,它对应的数据项就是次关键码
。
查找
就是根据给定的某个值,在查找表中确定一个其关键字等于给定值得数据元素(或记录)。
若表中存在这样的一个记录,则称查找是成功的,此时查找的结果给出整个记录的信息,或指示该记录在查找表中的位置。若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个“空”记录或“空”指针。
查找表按照操作方式来分有两大种:静态查找表 和 动态查找表。
为了提高查找的效率,我们需要专门为查找操作设置数据结构,这种面向查找操作的数据结构称为查找结构
。
顺序查找 又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
折半查找技术,又称为二分查找
。它的前提是线性表中的记录必须是关键码有序
(通常从小到大有序),线性表必须采用顺序存储
。折半查找的基本思想是:在有序表中,取中间记录为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,知道查找成功,或所有查找区域无记录,查找失败为止。
插值查找是根据要查找的关键字 key
与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于差值的计算公式 (key - a[low])/(a[high] - a[low])
。从时间复杂度来看,它也是 O(logn)
,但对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好得多。反之,数组中如果分布类似 {0,1,2,2000,2001,……,999998,999999}
这种极端不均匀的数据,用插值查找未必是很合适的选择。
斐波那契查找算法的核心在于:
key=a[mid]
时,查找就成功;key 时,新范围是第 low
个到第 mid-1
个,此时范围个数为 F[k-1]-1
个;
key>a[mid]
时,新范围是第 m+1
个到第 high
个,此时范围个数为 F[k-2]-1
个;数据结构的最终目的是提高数据的处理速度,索引是为了加快查找速度而设计的一种数据结构。索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应该包含关键字和其对应的记录在存储器中的位置等信息。
索引按照结构可以分为线性索引
、树形索引
和多级索引
。我们这里只介绍线性索引技术。所谓线性索引就是将索引项集合组织为线性结构
,也称为索引表
。这里介绍三种线性索引:稠密索引
、分块索引
和倒排索引
。
稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项,如下图:
对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。
索引项有序也就意味着,我们要查找关键字时,可以用到折半、插值、斐波那契等有序查找算法,大大提高了效率。这是稠密索引的优点,但是如果数据集非常大,比如上亿,那也就意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。
稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大,为了减少索引项的个数,我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。
分块有序,是把数据记得记录分成了若干块,并且这些块需要满足两个条件。
对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。如下图:
索引表的通用结构是:次关键码;记录号表。
其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引。倒排索引源于实际应用中需要根据属性和具有该属性值得各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。
倒排索引的优点显然就是查找记录非常快,基本等于生成索引表后,查找时都不用去读取记录,就可以得到结果。但它的缺点是这个记录号不定长。
二叉排序树,又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。
从二叉排序树的定义也可以知道,它前提是二叉树,然后它采用了递归的定义方法,再者,它的结点间满足一定的次序关系,左子树结点一定比其双亲结点小,右子树结点一定比其双亲结点大。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f
,使得每个关键字 key
对应一个存储位置 f (key)
。查找时,根据这个确定的对应关系找到给定值key
的影射f(key)
,若查找集合中存在这个记录,则必定在f(key)
的位置上。
这里我们把这种对应关系 f 称为散列函数,又称为哈希函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么关键字对应的记录存储位置我们称为散列地址。
假设含有 n
个记录的序列为 {r1, r2, ……, rn}
,其对应的关键字分别为 {k1, k2, ……, kn}
,需确定 1,2,……,n
的一种排列 p1,p2,……,pn
,使其相应的关键字满足 k(p1)<=k(p2)<=……<=k(pn)
(非递减或非递增)关系,即使得序列成为一个按关键字有序的序列 {r(p1),r(p2),……,r(pn)}
,这样的操作就称为排序。
假设 k(i)=k(j)(1<=i<=n,1<=j<=n,i!=j)
,且在排序前的序列中 r(n)
领先于 r(j)
(即ir(j)
,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中 r(j)
领先于 r(i)
,则称所用的排序方法是不稳定的。
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。内排序
是在排序整个过程中,待排序
的所有记录全部被放置在内存中。外排序
是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外村之间多粗交换数据才能进行。
根据排序过程借助的主要操作,我们把内排序分为:插入排序
、交换排序
、选择排序
和归并排序
。
冒泡排序是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
n-1
次的比较,没有数据交换,时间复杂度为 O(n)
;当最坏的情况,即待排序表是逆序的情况,此时需要比较n(n-1)
次,并作等数量级的记录移动。因此,总的时间复杂度为 O(n*n)
。简单选择排序法就是通过 n-i
次关键字间的比较,从 n-i+1
个记录选出关键字最小的记录,并和第 i (1<=i<=n)
个记录交换之。
i
趟排序需要进行n-i
次关键字的比较,此时需要比较 n(n-1)/2
次。而对于交换次数而言,当最好的时候,交换为0
次,最差的时候,也就初始降序时,交换次数为 n-1
次,基于最终的排序时间是比较与交换的次数的综合,因此,总的时间复杂度依然为 O(n*n)
。直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增 1 的有序表。
O(n)
;当最坏的情况,即待排序表示逆序的情况,此时需要比较 (n+2)(n-1)/2
次,而记录的移动次数也达到最大值(n+4)(n-1)/2
次;如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次数约为 n*n/4
次。因此我们得出直接插入排序法的时间复杂度为O(n*n)
。从这里也可以看出,同样的时间复杂度,直接插入排序法比冒泡和简单排序的性能要好一些。t1,t2,…,tk
,其中ti>tj
,tk=1
;k
,对序列进行k
趟排序;ti
,将待排序列分割成若干长度为m
的子序列,分别对各子表进行直接插入排序。仅增量因子为1
时,整个序列作为一个表来处理,表长度即为整个序列的长度。堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的 n-1
个序列重新构造成一个堆,这样就会得到n
个元素中的次小值。如此反复执行,便能得到一个有序序列了。
快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。