文章共分为三篇
第一篇:数据结构 -《大话数据结构》读书笔记(1)
一、数据结构绪论
二、算法
三、线性表
第二篇:数据结构 -《大话数据结构》读书笔记(2)
四、栈与队列
五、串
六、树
七、图
第三篇:数据结构 -《大话数据结构》读书笔记(3)
八、查找
九、排序
八、查找
8.1 查找概论
所有需要被查的数据所在的集合,我们给它一个统称叫查找表
。
查找表是由同一类型的数据元素
(或记录)构成的集合。
关键字
是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素,也可以标识一个记录的某个数据项,我们称为关键码
。
若此关键字可以唯一地表示一个记录,则称此关键字为主关键字
。这也意味着,对不同的记录,其主关键字均不相同。主关键字所在的数据项称为主关键码
。
对于那些可以识别多个数据元素(或记录)的关键字,我们称之为次关键字
,次关键字也可以理解为是不以唯一标识一个数据元素(或记录)的关键字,它对应的数据项就是次关键码
。
查找
就是根据给定的某个值,在查找表中确定一个其关键字等于给定值得数据元素(或记录)。
若表中存在这样的一个记录,则称查找是成功的,此时查找的结果给出整个记录的信息,或指示该记录在查找表中的位置。若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个“空”记录或“空”指针。
查找表按照操作方式来分有两大种:静态查找表 和 动态查找表。
-
静态查找表:只查找操作的查找表。它的主要操作有:
- 查询某个“特定的”数据元素是否在查找表中。
- 检索某个“特定的”数据元素和各种属性。
-
动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。显然动态查找表的操作就是两个:
- 查找时插入数据元素。
- 查找时删除数据元素。
为了提高查找的效率,我们需要专门为查找操作设置数据结构,这种面向查找操作的数据结构称为查找结构
。
8.2 顺序表查找
顺序查找 又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
8.3 有序表查找
8.3.1 折半查找
折半查找技术,又称为二分查找
。它的前提是线性表中的记录必须是关键码有序
(通常从小到大有序),线性表必须采用顺序存储
。折半查找的基本思想是:在有序表中,取中间记录为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,知道查找成功,或所有查找区域无记录,查找失败为止。
8.3.2 插值查找
插值查找是根据要查找的关键字 key
与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于差值的计算公式 (key - a[low])/(a[high] - a[low])
。从时间复杂度来看,它也是 O(logn)
,但对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好得多。反之,数组中如果分布类似 {0,1,2,2000,2001,……,999998,999999}
这种极端不均匀的数据,用插值查找未必是很合适的选择。
8.3.3 斐波那契查找
斐波那契查找算法的核心在于:
- 当
key=a[mid]
时,查找就成功; - 当
key<a[mid]
时,新范围是第low
个到第mid-1
个,此时范围个数为F[k-1]-1
个; - 当
key>a[mid]
时,新范围是第m+1
个到第high
个,此时范围个数为F[k-2]-1
个;
8.4 线性索引查找
数据结构的最终目的是提高数据的处理速度,索引是为了加快查找速度而设计的一种数据结构。索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应该包含关键字和其对应的记录在存储器中的位置等信息。
索引按照结构可以分为线性索引
、树形索引
和多级索引
。我们这里只介绍线性索引技术。所谓线性索引就是将索引项集合组织为线性结构
,也称为索引表
。这里介绍三种线性索引:稠密索引
、分块索引
和倒排索引
。
8.4.1 稠密索引
稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项,如下图:
稠密索引对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。
索引项有序也就意味着,我们要查找关键字时,可以用到折半、插值、斐波那契等有序查找算法,大大提高了效率。这是稠密索引的优点,但是如果数据集非常大,比如上亿,那也就意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。
8.4.2 分块索引
稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大,为了减少索引项的个数,我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。
分块有序,是把数据记得记录分成了若干块,并且这些块需要满足两个条件。
- 块内无序,即每一块内的记录不要求有序。当然,你如果能够让块内有序对查找来说更理想,不过这就要付出大量时间和空间的代价,因此通常我们不要求块内有序。
- 块间有序,例如,要求第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块的所有记录的关键字均要大于第二块的所有记录关键字……因为只有块间有序,才有可能在查找时带来效率。
对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。如下图:
分块索引
- 分块索引的索引项结构分为三个数据项:
- 最大关键码,它存储每一块中的最大关键字,这样的好处是可以使得在他之后的下一块中的最小关键字也能比这一块最大的关键字更大;
- 存储了块中的记录个数,以便于循环时使用;
- 用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历。
8.4.3 倒排索引
索引表的通用结构是:次关键码;记录号表。
其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引。倒排索引源于实际应用中需要根据属性和具有该属性值得各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。
倒排索引的优点显然就是查找记录非常快,基本等于生成索引表后,查找时都不用去读取记录,就可以得到结果。但它的缺点是这个记录号不定长。
8.5 二叉排序树
二叉排序树,又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。
- 若它的左子树不空,则左子树上所有的结点的值均小于它的跟结构的值;
- 若它的右子树不空,则右子树上所有的节点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
从二叉排序树的定义也可以知道,它前提是二叉树,然后它采用了递归的定义方法,再者,它的结点间满足一定的次序关系,左子树结点一定比其双亲结点小,右子树结点一定比其双亲结点大。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。
8.6 散列表查找(哈希表)概述
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f
,使得每个关键字 key
对应一个存储位置 f (key)
。查找时,根据这个确定的对应关系找到给定值key
的影射f(key)
,若查找集合中存在这个记录,则必定在f(key)
的位置上。
这里我们把这种对应关系 f 称为散列函数,又称为哈希函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么关键字对应的记录存储位置我们称为散列地址。
- 整个散列过程其实就两步:
- 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
- 当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。
所以说,散列技术既是一种存储方法,也是一种查找方法。
九、排序
9.1 排序的基本概念与分类
假设含有 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)}
,这样的操作就称为排序。
9.1.1 排序的稳定性
假设 k(i)=k(j)(1<=i<=n,1<=j<=n,i!=j)
,且在排序前的序列中 r(n)
领先于 r(j)
(即i<j)。如果排序后 r(i)
仍领先于 r(j)
,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中 r(j)
领先于 r(i)
,则称所用的排序方法是不稳定的。
9.1.2 内排序与外排序
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。
内排序
是在排序整个过程中,待排序
的所有记录全部被放置在内存中。外排序
是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外村之间多粗交换数据才能进行。
- 对于内排序,排序算法的性能主要是受 3 个方面影响;
- 时间性能
排序是数据处理中经常执行的一种操作,往往属于系统的核心部分,因此排序算法的时间开销是衡量其好坏的最重要的标志。在内排序中,主要进行两种操作:比较和移动。比较指关键字之间的比较,这是要做排序最起码的操作。移动指记录从一个位置移动到另一个位置,事实上,移动可以通过改变记录的存储方式来予以避免。总之,高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。 - 辅助空间
评价排序算法的另一个主要标准是执行算法所需要的辅助存储空间。辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。 - 算法的复杂性
注意这里指的是算法本身的复杂度,而不是指算法的时间复杂度。显然算法过于复杂也会影响排序的性能。
- 时间性能
根据排序过程借助的主要操作,我们把内排序分为:插入排序
、交换排序
、选择排序
和归并排序
。
9.2 几种排序算法
9.2.1 冒泡排序
冒泡排序是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
- 冒泡排序复杂度分析
最好的情况,也就是要排序的表本身就是有序的,那么我们比较次数,根据最后改进的代码,可以推断出就是n-1
次的比较,没有数据交换,时间复杂度为O(n)
;当最坏的情况,即待排序表是逆序的情况,此时需要比较n(n-1)
次,并作等数量级的记录移动。因此,总的时间复杂度为O(n*n)
。
9.2.2 简单选择排序
简单选择排序法就是通过 n-i
次关键字间的比较,从 n-i+1
个记录选出关键字最小的记录,并和第 i (1<=i<=n)
个记录交换之。
- 简单选择排序复杂度分析
从简单选择排序的过程来看,它最大的特点就是交换移动数据次数相当少,这样也就节约了相应的时间。分析它的时间复杂度发现,无论最好最差的情况,其比较次数都是一样的多,第i
趟排序需要进行n-i
次关键字的比较,此时需要比较n(n-1)/2
次。而对于交换次数而言,当最好的时候,交换为0
次,最差的时候,也就初始降序时,交换次数为n-1
次,基于最终的排序时间是比较与交换的次数的综合,因此,总的时间复杂度依然为O(n*n)
。
9.2.3 直接插入排序
直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增 1 的有序表。
- 直接插入排序复杂度分析
当最好的情况,也就是要排序的表本身就是有序的,没有移动的记录,时间复杂度为O(n)
;当最坏的情况,即待排序表示逆序的情况,此时需要比较(n+2)(n-1)/2
次,而记录的移动次数也达到最大值(n+4)(n-1)/2
次;如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次数约为n*n/4
次。因此我们得出直接插入排序法的时间复杂度为O(n*n)
。从这里也可以看出,同样的时间复杂度,直接插入排序法比冒泡和简单排序的性能要好一些。
9.2.4 希尔排序
- 算法步骤:
1)选择一个增量序列t1,t2,…,tk
,其中ti>tj
,tk=1
;
2)按增量序列个数k
,对序列进行k
趟排序;
3)每趟排序,根据对应的增量ti
,将待排序列分割成若干长度为m
的子序列,分别对各子表进行直接插入排序。仅增量因子为1
时,整个序列作为一个表来处理,表长度即为整个序列的长度。
9.2.5 堆排序
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的 n-1
个序列重新构造成一个堆,这样就会得到n
个元素中的次小值。如此反复执行,便能得到一个有序序列了。
9.2.6 快速排序
快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
- 几种排序算法对比:
相关文章:算法-几种排序算法 OC 版
代码传送门:排序算法 OC 版
结束语:由于个人能力有限,这三篇读书笔记难免有错误或不足之处,还望各位道友能不吝赐教,谢谢。
最后安利一下这本书:PDF版
网友评论