数据结构中的查找方法
1.顺序查找
- 从列表中的第一项开始,我们按照初始顺序从一项移到下一项,直到我们发现正在寻找的数据项或者遍历所有数据项。
- 顺序查找的算法复杂度是O(n)
2.二分查找
- 二分搜索将从中间项开始检测,而不是按顺序搜索列表。如果查找项与我们刚搜索到的项匹配,则搜索结束。如果不匹配,我们可以利用列表的有序性来排除掉一半的剩余项。如果查找项比中间项大,我们可以把列表中较小的那一半全部和中间项可以从接下来的考察中排除了。因为如果查找项在列表中,那它一定在较大的那一半。
- 二分法查找的算法复杂度是O(log n)
3.散列
- 散列表是一种数据的集合,其中的每个数据都通过某种特定的方式进行存储以方面日后的查找。散列表的每一个位置叫做槽,能够存放一个数据项,并以从0开始递增的整数命名。
3.1散列函数
- 折叠法创建散列函数的基本步骤是:首先将数据分成相同长度的片段(最后一个片段长度可能不等)。接着将这些片段相加,再求余得到其散列值。例如,如果我们有一串电话号码436-555-4601,我们可以两个一组将这个号码分成5段( 43,64,55,46,01)。然后相加得到。如果我们假设散列表有11个槽,我们就需要将和进行求余。因为,所以电话号码436-555-4601就存放在槽1中。有的折叠法还包含一个每跳过一个数就将下一个数反转再相加的过程。在上面的例子中,我们得到,求余得到 % = 。
- 另一个创建散列函数的数值方法叫做平方取中法。我们首先将数据取平方,然后取平方数的某一部分。例如数据项是44,我们首先计算。接着取出中间的两位数93,然后再进行求余运算。
3.2冲突解决方案
- 一种解决冲突的方法就是搜索散列表并寻找另一个空的槽来存放这个有冲突的数据。一种简单的方法就是从发生冲突的位置开始顺序向下开始寻找,直到我们遇到第一个空的槽。注意到我们可能需要回到第一个槽(循环)来实现覆盖整个散列表。这种冲突解决方法叫做开放地址,它试图在散列表中去寻找下一个空的槽。通过系统地向后搜索每一个槽,我们将这种实现开放地址的技术叫做线性探测。
- 另一种线性探测方法叫做二次探测法。我们不是每次在冲突中选择跳过固定个数的槽,而是使用一个再散列函数使每次跳过槽的数量会依次增加1,3,5,7,9,以此类推。这意味着如果原槽为第h个,那么再散列时访问的槽为第h+1, h+4, h+9, h+16个,以此类推。换言之,二次探测法使用一个连续的完全平方数数列作为它的跳跃值。图11显示了我们的例子在运用二次探测法时的填充结果。
- 另一个解决冲突的替代方法是允许每一个槽都能填充一串而不是一个数据(称作链)。 链能允许多个数据填在散列表中的同一个位置上。当冲突发生时,数据还是填在本应该位于的槽中。随着一个槽中填入的数据的增多,搜索的难度也就随之增加。图12显示了数据在用数据链方法填入散列表的结果。
3.3实现映射的抽象数据类型
数据结构中的排序方法
1.冒泡排序
-
冒泡排序要对一个列表多次重复遍历。它要比较相邻的两项,并且交换顺序排错的项。每对列表实行一次遍历,就有一个最大项排在了正确的位置。大体上讲,列表的每一个数据项都会在其相应的位置“冒泡”。
-
它们的顺序是否正确。如果列表有n项,第一次遍历就要比较n-1对数据。需要注意,一旦列表中最大(按照规定的原则定义大小)的数据是所比较的数据对中的一个,它就会沿着列表一直后移,直到这次遍历结束。
-
因为冒泡排序必须要在最终位置找到之前不断交换数据项,所以它经常被认为是最低效的排序方法。这些“浪费式”的交换操作消耗了许多时间。但是,由于冒泡排序要遍历整个未排好的部分,它可以做一些大多数排序方法做不到的事。尤其是如果在整个排序过程中没有交换,我们就可断定列表已经排好。因此可改良冒泡排序,使其在已知列表排好的情况下提前结束。这就是说,如果一个列表只需要几次遍历就可排好,冒泡排序就占有优势:它可以在发现列表已排好时立刻结束。
说人话:每次比较一个数的左右两项,直到将最大值放在最后(或者将最小值放在最前面)。
不管初始列表中的数据如何排列,排一个长度为n的列表都要进行n-1次遍历。表1显示了每次遍历需要比较的次数。总的比较次数是从1到n-1的所有正整数的和,即1/2(n2-n)。比较复杂度为O(n2)。
2.选择排序
- 选择排序提高了冒泡排序的性能,它每遍历一次列表只交换一次数据,即进行一次遍历时找到最大的项,完成遍历后,再把它换到正确的位置。和冒泡排序一样,第一次遍历后,最大的数据项就已归位,第二次遍历使次大项归位。这个过程持续进行,一共需要n-1次遍历来排好n个数据,因为最后一个数据必须在第n-1次遍历之后才能归位。
说人话:将第一个数依次与所有数进行比较,找到最大值,记录位置,将最大值放到最后,再从第二个数重新开始(最小值类似)。
选择排序的时间复杂度比冒泡排序稍优,比对次数不变,还是O(n2),交换次数则减少为O(n)
3.插入排序
- 它总是保持一个位置靠前的已排好的子表,然后每一个新的数据项被“插入”到前边的子表里,排好的子表增加一项。
插入排序的算法复杂度仍然是O(n2)
4.希尔排序
- 希尔排序有时又叫做“缩小间隔排序”,它以插入排序为基础,将原来要排序的列表划分为一些子列表,再对每一个子列表执行插入排序,从而实现对插入排序性能的改进。划分子列的特定方法是希尔排序的关键。我们并不是将原始列表分成含有连续元素的子列,而是确定一个划分列表的增量“i”,这个i更准确地说,是划分的间隔。然后把每间隔为i的所有元素选出来组成子列表。
说人话:通过取数列中的间隔数进行排序。
对谢尔排序的详尽分析比较复杂,大致说是介于O(n)和O(n2)之间。
5.归并排序
- 归并排序是一种递归算法,它持续地将一个列表分成两半。如果列表是空的或者只有一个元素,那么根据定义,它就被排序好了(最基本的情况)。如果列表里的元素超过一个,我们就把列表拆分,然后分别对两个部分调用递归排序。一旦这两个部分被排序好了,那么这种被叫做归并的最基本的操作,就被执行了。归并是这样一个过程:把两个排序好了的列表结合在一起组合成一个单一的,有序的新列表。
说人话:将一个大列表划分为很多个小列表,小列表继续划分,排好小列表之后再排大列表。
分裂的过程, 借鉴二分查找中的分析结果, 是对数复杂度, 时间复杂度为O(log n)。归并的过程, 相对于分裂的每个部分, 所有数据项都会被比较和放置一次, 所以是线性复杂度, 其时间复杂度是O(n)。
综合考虑,每次分裂的部分都进行一次O(n)的数据项归并,总的时间复杂度是O(nlog n)
6.快速排序
- 快速排序快速排序的思路是依据一个“中值”数据项来把数据表分为两半:小于中值的一半和大于中值的一半, 然后每部分分别进行快速排序(递归)。
- 首先选择一个中值。虽然有很多不同的方法来选择这个数值,我们将会简单地选择列表里的第一项。中值的作用在于协助拆分这个列表。中值在最后排序好的列表里的实际位置,我们通常称之为分割点的,是用来把列表变成两个部分来随后分别调用快速排序函数的。
快速排序过程分为两部分: 分裂和移动如果分裂总能把数据表分为相等的两部分,那么
就是O(log n)的复杂度;而移动需要将每项都与中值进行比对,还是O(n)。综合起来就是O(nlog n);
❖而且, 算法运行过程中不需要额外的存储空间。
总结
- 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序
- 线性时间非比较类排序:不能通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序