ADT
- 头节点的 order 设置为-1,尾节点的 order 设置为 n,都是哨兵,所谓 order 可以用以列表模拟 vector 的循秩访问,只是时间复杂度为 ;
- 创建列表时,列表为空,此时只需要连接头尾节点,各自设置好指针域;
无序列表
增
- , 需要查找时间
删
- ,但是需要查找时间
copy and delete
查
自后向前:
去重
遍历
自前向后:
有序向量
唯一化
查找
- 最好 ,最坏
- 按照循位置访问的方式,物理存储地址与其逻辑次序无关,而只能依据元素间的引用顺序访问
- 如何提高查找效率?——引入其它数据结构
- 使用哈希表: 如果需要频繁地查找链表中的特定元素,可以考虑将链表与哈希表结合使用。可以使用元素的某个唯一属性(例如关键字)作为哈希表的键,将元素的指针存储在哈希表中。这样,可以通过哈希表快速定位到链表中的元素,而不需要遍历整个链表。
- 缓存: 如果你的应用程序对链表的访问具有一定的局部性,可以考虑使用缓存来存储最近访问的链表节点。这可以减少对链表的实际访问次数,提高查找效率。
- 分割链表: 如果链表非常长而且你知道要查找的元素位于链表的某个特定部分,你可以考虑将链表分割为多个较短的链表。这样,你可以只查找特定部分的链表,从而减少查找的时间复杂度。—— 跳转表
- 使用平衡二叉搜索树: 如果你的链表需要支持高效的插入和删除操作,考虑使用平衡二叉搜索树来代替链表。平衡二叉搜索树具有快速的查找、插入和删除性能。
排序
选择排序
基本思想
选择未排序区间的最大值,移动到已排序区间的最小位置
选择排序的选择、置放策略:
未排序区间的搜索策略:
稳定性
性能
共迭代 n 次,在第 k 次迭代中
selectMax()
为 //算术级数swap()
为 //或 remove () + insert () 故总体复杂度应为
尽管如此,元素的移动操作远远少于起泡排序 //实际更为费时 也就是说, 主要来自于元素的比较操作 //成本相对更低
可否... 每轮只做 次比较,即找出当前的最大元素?
可以!利用高级数据结构,selectMax()
可改进至 ——大顶堆。
当然,如此立即可以得到 的排序算法——堆排序。
插入排序
基本思想
从未排序的区间中取一个元素,不失一般性就取区间开头,然后在已就序区间选择合适位置插入:
- 不变性:序列总能视作两部分:
S[0, r) + U[r, n)
- 初始时:
|S| = r = 0
- 如何查找?顺序还是二分?(此处是列表,故只能顺序,但若是向量,可以考虑二分,但是向量的插入操作的时间复杂度为 )
实现
- 紧邻于 search ()接口返回的位置之后插入当前节点,总是保持有序
- 验证各种情况下的正确性,体会哨兵节点的作用: Sr 中含有/不含与 p 相等的元素;Sr 中的元素均严格小于/大于 p
性能
- 插入排序是就地算法:空间复杂度 O (1)
- 在线算法:不必等数据传输完全完成,即可进行排序
- 具有输入敏感性:输入数据序列越有序,时间复杂度越低——
- 优化方向:在有序前缀中的查找定位,采用二分查找——但这仍然是常系数意义上的优化,在数据不大时体现不出来,并且列表本身不支持二分查找
若各元素的取值系独立均匀分布,平均要做多少次元素比较?
- 考查
e=[r]
刚插入完成的那一时刻,此时的有序前缀[0, r]
中,谁是e
? - 观察:其中的 r+1 个元素均有可能,且概率均为
- 因此,在刚完成的这次迭代中为引入 S[r]所花费时间的数学期望为
- 于是,总体时间的数学期望为
- 再问:在 n 次迭代中,平均有多少次无需交换呢?—— 习题 3-10:插入排序性能分析
归并排序
思想
二路归并
循环节
定义
任何一个元素之间可以定义次序的序列 A[0, n)
,都可以分解为若干个循环节:
- 任何一个序列 A,都对应于一个有序序列
S[0, n)
- 元素
A[k]
在 S 中对应的秩,记作 - 元素
A[k]
所属的循环节是: - 每个循环节,长度均不超过
- 循环节之间,互不相交
实例
- J → C → P → E → A → J
- N → L → B → N
P → E → A → J → C → P(P 是 1 中的一个节点,故自身的循环节就是 1)- M → K → H → O → F → I → D → M
- G → G (自成循环节)
单调性
采用交换法,每迭代一步,M 都会脱离原属的循环节,自成一个循环节:
无效交换
问:M 已经就位,无需交换 —— 这种情况会出现几次? 答:最初有 个循环节,就出现 次 —— 最大值为 ,期望
逆序对
定义
考查序列 A[0, n),设元素之间可比较大小,则
为便于统计,可将逆序对统一记到后者的账上 (例如 5>3,则在 3 的逆序数标记上加一):
A[] = { 5, 3, 1, 4, 2 }
中,共有 0 + 1 + 2 + 1 + 3 = 7 个逆序对A[] = { 1, 2, 3, 4, 5 }
中,共有 0 + 0 + 0 + 0 + 0 = 0 个逆序对A[] = { 5, 4, 3, 2, 1 }
中,共有 0 + 1 + 2 + 3 + 4 = 10 个逆序对
显然,逆序对总数 。
冒泡排序与逆序对
在序列中交换一对逆序元素,逆序对总数必然减少:
- 在序列中交换一对紧邻的逆序元素,逆序对总数恰好减一
- 因此对于 Bubblesort 算法而言,交换操作的次数恰等于输入序列所含逆序对的总数
插入排序与逆序对
针对 e=A[r]
的那一步迭代,恰好需要做 次比较
若共含 个逆序对,则
- 关键码比较次数为
- 运行时间为 —— 习题 3-11:逆序对与插入排序分析
如何计算逆序对数?
蛮力算法需要 时间;而借助归并排序,仅需 时间:
游标(静态链表)
某些语言不支持指针类型,即便支持,频繁的动态空间分配也影响总体效率。此时,又当如何实现列表结构呢?
- 利用线性数组,以游标方式模拟列表
elem[]
:对外可见的数据项link[]
:数据项之间的引用
- 维护逻辑上互补的列表 data 和 free
- 在插入或删除元素时,应如何调整?