参考自:https://www.cnblogs.com/coderJiebao/p/Algorithmofnotes08.html
什么是 two pointers
以一个例子引入:给定一个递增的正整数序列和一个正整数 M,求序列中的两个不同位置的数 a 和 b,使得它们的和恰好为 M,输出所有满足条件的方案。
本题的一个最直观的想法是,使用二重循环枚举序列中的整数 a 和 b,判断它们的和是否为 M。时间复杂度为 O(n^2)。当n的规模足够大时,这显然是不可取的。
two pointers 将利用有序序列的枚举特性来有效降低复杂度。它针对本题的算法如下:
令下标 i 的初值为0,下标 j 的初值为 n-1,即令 i、j 分别指向序列的第一个元素和最后一个元素,接下来根据 a[i]+a[j] 与 M 的大小来进行下面三种选择,使 i 不断向右移动、使 j 不断向左移动,直到 i≥j 成立
若 a[i]+a[j]==M ,说明找到了其中一种方案,令 i=i+1、j=j-1。
若 a[i]+a[j]>M,令 j=j-1。
若 a[i]+a[j]<M,令 i=i+1。
反复执行上面三个判断,直到 i≥j 成立,在递增序列的前提下,循环只需要进行到i>=j时停止,所以理想状态下只需要遍历半个序列,时间复杂度只需要O(n)。
代码如下:
1 | while(i < j) { |
序列合并问题
再来看序列合并问题。假设有两个递增序列 A 与 B,要求将它们合并为一个递增序列 C。
同样的,可以设置两个下标 i 和 j ,初值均为0,表示分别指向序列 A 的第一个元素和序列 B 的第一个元素,然后根据 A[i] 与 B[j] 的大小来决定哪一个放入序列 C。
- 若 A[i]≤B[j],把 A[i] 加入序列 C 中,并让 i 加1
- 若 A[i]>B[j],把 B[j] 加入序列 C 中,并让 j 加1
上面的分支操作直到 i、j 中的一个到达序列末端为止,然后将另一个序列的所有元素依次加入序列 C 中,代码如下:
1 | int merge(int A[], int B[], int C[], int n, int m) { |
广义上的 two pointers 是利用问题本身与序列的特性,使用两个下标 i、j 对序列进行扫描(可以同向扫描,也可以反向扫描),以较低的复杂度(一般为 O(n) )解决问题。
归并排序
归并排序是一种基于“归并”思想的排序方法,本节主要介绍其中最基本的 2-路归并排序。2-路归并排序的原理是,将序列两两分组,将序列归并为$\lceil \frac n 2 \rceil$个组,组内单独排序;然后将这些组再两两归并,生成$\lceil \frac n 4 \rceil$个组,组内再单独排序;以此类推,直到只剩下一个组为止。时间复杂度为 O(nlogn)。
说明:对于偶数个的数组正常对半分就行,对于奇数个的数组,留下最后1个多余的单独1组,其余两两1组。
我们先写一个merge函数将数组的两个区间合并为一个有序区间。
1 | const int maxn = 100; |
1. 递归实现
只需反复将当前区间 [left,right] 分为两半,对两个子区间 [left,mid] 与 [mid+1, right] 分别递归进行归并排序,然后将两个已经有序的子区间合并为有序序列即可。代码如下:
1 | // 归并排序递归实现 |
2.非递归实现
非递归实现主要考虑到这样一点:每次分组时组内元素个数上限都是2的幂次。于是就可以想到这样的思路:令步长 step 的初值为2,然后将数组中每 step 个元素作为一组,将其内部进行排序;再令 step 乘以2,重复上面的操作,直到 step/2 超过元素个数 n 。代码如下:
1 | // 归并排序非递归实现 |
如果题目只要求给出归并排序每一趟结束时的序列,可以用sort函数代替 merge 函数,只要时间允许。
3.算法分析
时间复杂度: O(nlogn)
归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情
况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。
空间复杂度:O(n)
它有一个致命的“弱点”,那就是归并排序不是原地排序算法。
这是因为归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。
在每次进行合并操作时,需要O(n)的数组空间存放数据。