第二章——排序算法
1.选择排序:首先找到数组中最小的元素,将它和数组的第一个元素交换位置,然后在剩下的元素中找到最小的元素,将其和数组的第二个元素交换位置。。。直到将整个数组排序。其运行时间和输入无关,一个有序的数组或主键全部相等的数组和元素随机排列的数组相比,所用的排序时间一样。
2.与选择排序一样,当前索引左边的所有元素都是有序的,但其最终位置还不确定,为了给更小的元素腾出空间,它们可能会移动,但当索引到达数组的最右边时,数组排序就完成了。和选择排序不同,插入排序所用的时间取决于输入元素的初始顺序。(将a[i]插入到a[i-1],a[i-2],a[i-3]…a[0]中)
插入排序对部分有序的数组有效,也适合小规模数组。
3.在排序的过程中,插入排序不会访问当前索引右边的元素,选择排序不会访问当前索引左边的元素。一般来说插入排序比选择排序快一倍(或者是其他常数),两者的运行时间都是平方级别的。
4.希尔排序的思想是使数组中任意间隔为h的元素都是有序的(j-i=h),这样的数组被称为h有序数组。h个有序子数组:[L,M,P,T],[E,H,S,S],[E,L,O,X],[A,E,L,R]。
插入排序是h为1的时特殊的希尔排序。和选择、插入排序相比,希尔排序可以用于大型(任意排序,不一定是随即的)数组排序。希尔排序的运行时间达不到平方级别,在最坏情况下以上代码的比较次数和N^(3/2)成正比。
5.归并排序的优点是其将任意长度的数组排序所需时间和N*logN成正比,缺点是它所需的额外空间和N成正比。
对于长度为N的数组,自顶向下的归并排序需要(1/2)*N*lgN 至N*lgN 次比较。
6.自底向上的归并排序从长度为1的子数组开始,两两归并,结束后对长度为2的子数组进行四四归并。。。直到整个数组归并完成。
对于长度为N的任意数组,自底向上的归并排序需要(1/2)*N*lgN 至N*lgN 次比较,最多访问数组6*N*lgN 次。
当数组的长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和访问数组的次数相同,只是顺序不同。自底向上的归并排序适合用链表组织的数据。
7.快速排序的优点是它是原地排序(只需要很小的辅助栈),将长度为N的数组排序所需的时间和N*lgN 成正比。快速排序还是一种分治的排序算法。归并算法的递归调用发生在处理整个数组之前,而快速排序的递归调用在处理整个数组之后。该算法的重点在方法partition,它确定了j ,a[lo]到a[j-1]的所有元素都不大于a[j],a[j+1]到a[hi]的所有元素都不小于a[j]。实际上递归调用的是方法partition。
数组元素被打乱过,保持随机性是为了预测算法的运行时间,保持随机性的另一种方法是在partition方法中随机选择一个切分元素v。
将长度为N的无重复数组排序,快速排序平均需要~2*N*lnN 次比较和1/6的交换,即使存在重复的元素,平均比较次数也不会大于~N*lgN 。快速排序的缺点在于切分不平衡的时候可能比较低效,所以需要保证数组的随机性,在排序前将数组元素的顺序打乱(P186)。
快速排序最多需要约N^2/2 次比较,但随机打乱数组能预防这种情况。对于大数组,运行时间是平方级别的概率极小,随着N的增大,运行时间会趋向于平均数也就是~2*N*lnN 到~N*lgN 。
总之,对于大小为N的数组,算法2.5的运行时间在1.39*N*lgN 的某个常数因子的范围之内,快速排序比归并排序更快。
8.需要对包含大量重复主键的数组排序时,三向切分的快速排序是最优的:
9.二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级储存(不使用数组的第一个位置,层级越小优先级越高)。在二叉堆的数组中,每个元素都要保证大于等于另两个特定位置的元素。当一棵二叉树的每个结点都大于等于它的两个子结点时(如果有的话),其是堆有序的。
10.注意学习由下至上的堆有序化(上浮)和由上至下的堆有序化(下沉)。对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删除最大元素操作需要不超过2*lgN次比较。
11.将N个元素排序,堆排序只需少于(2*N*lgN+2*N )次比较以及一半次数的交换。2*N来源于堆的构造,2*N*lgN 来源于每次下沉操作最大可能需要2*lgN 次比较。
12.实现Comparable接口只需要定义一个compareTo函数(一个参数),并在其中定义该数据类型中的大小关系。
13.在java中,可以通过用不可变的数据类型(String,Integer,Double,File等)作为键来避免排序后用例还能修改键值。
14.可以通过构造实现Comparator接口的内部类(实现了方法compare,有两个参数)作为比较器,这样就可以为外部类进行多种类型的比较提供更灵活的实现。
15.如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为是稳定的。在java中,一般对原始数据类型使用(三向切分的)快速排序,对引用类型使用归并排序。
16.找到第K小的元素代码,可以用于寻找中位数(不大于一半的元素,又不小于一半的元素),其中partition方法是快速排序中的切分,当k和数组的大小成比例的时候(如中位数)该方法select可以在线性时间内解决问题:
————————
第三章——查找
1.对于无序链表,向一个空表插入N个不同的键需要(~N^2/2)次比较(1+2+3+…+N = N(N+1)/2)。
2.数组的某个元素之后的元素需要集体往后挪动一个位置时,应该按照从后往前的顺序分别向后移动一个位置。
3.关于有序数组(元素个数为N)中的二分查找,递归的二分查找和算法3.2中if(hi<lo) return lo; 意味着数组中不存在对应的键,有三种情况:(1)要查询的键key小于数组中所有元素,因为lo的初始值是0,随着递归或者循环的进行,hi会越来越小,直到小于lo,此时返回的lo值为0;(2)要查询的键key大于数组中所有元素,因为hi的初始值是N-1,随着递归或者循环的进行,lo会越来越大,直到大于hi,此时返回的lo值为N;(3)要查询的键key大于数组中的M个元素,但key不在数组中,可以将这种情况视为第二种情况的特殊形式,最后迭代或递归的时候hi的值为M-1,lo就是M。
4.二分查找所需时间必然在对数范围内。在N个键的有序数组中进行二分查找最多需要(lgN+1)次比较(无论是否成功)。
5.向大小为N的有序数组中插入一个新元素在最坏情况下需要访问~2*N 次数组(新元素小于数组中所有元素时,key数组和value数组中的元素都要往后挪一位),因此向一个空符号表中插入N个元素在最坏情况下需要访问~N^2 次数组(2*(1+2+…+N) = 2N(N+1)/2 = N(N+1) ~N^2)。
6.在二叉搜索树中插入节点时,需要沿着路径往上直到根节点更新每个节点的N值(该节点为根节点的子树的节点个数)或val值,只需要递归调用之后返回被更新的节点给节点对应的父节点即可,不需要另外存储当前节点的父节点,节省存储空间。
7.在由N个随机键组成的二叉查找树中,查找命中、插入操作和查找未命中平均所需的比较次数为~2lnN (约1.39lgN )。
8.在一棵二叉查找树中,所有操作(除了范围查找)在最坏情况下所需的时间都和树的高度成正比。
9.如果要在2-3树中插入新结点,需要和二叉查找树一样先进行一次未命中的查找,然后按照3.3.1节中规定的调整步骤进行调整(重点)。
10.在一棵大小为N的2-3树中,查找和插入操作访问的结点必然不超过lgN 个。
11.对于任意2-3树,只要对结点进行转换,都可以派生出一棵二叉查找树。红黑树既是二叉查找树,也是2-3树(用红色左链接相连的两个2-结点表示3-结点)。
12.如果有一条红色的右链接需要被转化成左链接,这个操作被称为左旋转,其实就是将红色右链接两端的两个键中的较小者作为根结点变为将较大者作为根结点。右旋转和左旋转相反,将较大者作为根结点变为将较小者作为根结点。
在插入新的键时可以用旋转操作保证2-3树和红黑树之间的一一对应关系,因为旋转可以保证红黑树的两个重要性质:有序性和完美平衡性。
13.所有基于红黑树的符号表实现都能保证操作的运行时间为对数级别(范围查找除外)。
14.不同类型的键应该用不同的散列函数进行散列:
(1)将整数散列最常用的方法是除留余数法,需要选择大小为素数(2的幂除外)的数组。
(2)如果键是0至1之间的实数,可以将键表示为二进制数然后再使用除留余数法。
(3)在java中对字符串进行散列时使用和horner方法类似的方法,用N次乘法、加法和取余来计算一个字符串的散列值。
15.对于基于拉链法的、含有M条链表和N个键的散列表中,未命中查找和插入操作所需的比较次数为~N/M 。这种数据结构适用于键的顺序并不重要的应用,无法实现和有序性相关的符号表操作。
16.基于线性探测法的散列表用大小为M的数组存储N个键值对(M>N),基于数组中的空位解决碰撞冲突,按照这种策略构成的散列表被称为开放地址散列表,这种散列表需要调整存储键和值数组的大小,避免散列表被填满时进入无限循环。
17.Java的java.util.TreeMap和java.util.HashMap分别是基于和红黑树和拉链法的散列表的符号表实现。
18.“索引”是一个键和多个值相关联的符号表。
|