two pointers 简介
two pointers 是一种解决相应问题所采用的算法思想,其广义含义是利用问题本身与序列的特性,使用两个下标对序列进行扫描,以比较低的复杂度来解决问题的思想
引例:给定一个递增的正整数序列和正整数M,求序列中两个不同位置的数a和b,使他们的和恰好为M。输出所有满足条件的方案
对于引例中的问题,最直观的方法是采用二重嵌套循环(计数器未 i 和 j ),但这个方法复杂度为O(n2),在序列较大时是不可接受的。我们可以分析其高复杂度的原因:
- 对于一个确定的 a[i] 而言,如果当前 a[j] 使得 a[i] + a[j] > M 恰好成立,显然也会有 a[i] + a[j+1] > M,因此实际上不需要对此时的 a[j] 之后的元素进行枚举
- 对一个确定的 a[i] 而言,若找到一个 a[j] 使得 a[i] + a[j] > M 恰好成立,那么对 a[i+1] 来说,一定也会有 a[i+1] + a[j] > M 成立,因此 a[i] 之后的元素也不必再枚举
i 和 j 的相互牵制导致了程序复杂度的极大上升。为此,我们采用 two pointers 方法,利用有序序列的枚举特性来有效降低复杂度:
#include<cstdio>
#define maxn 100005
int a[maxn];
int main() {
int i = 0,j = maxn - 1,m; //令i,j下标分别指向有序序列的首末元素,然后根据情况不断移动i和j寻找合适的方案,直到i>=j
scanf("%d",&m);
while(i < j) {
if (a[i] + a[j] == m) { //找到了一组方案,剩余方案只可能在[i+1,j-1]区间内产生
printf("%d %d\n",i,j);
i++;
j++;
} else if (a[i] + a[j] < m) { //剩余方案只可能在[i+1,j]区间产生
i++
} else if (a[i] + a[j] > m) { //剩余方案只可能在[i,j-1]区间产生
j++;
}
}
}
时间复杂度被降低到O(n)
序列合并问题:假设有两个递增序列A和B,要求将他们合成一个递增序列C
我们同样可以使用 two pointers 的思想来解决问题:
int merge(int A[], int B[], int C[], int n, int m) {
int i = 0, j = 0, index = 0; //i和j分别标记到序列A和B首位
while ( i < n && j < m ) { //根据当前标记元素的大小决定放入C中的元素,直到A或B其中一个序列放置完毕
if ( A[i] <= B[j] ) {
C[index++] = A[i++];
} else {
C[index++] = B[j++];
}
}
//处理A或B中剩余元素
while ( i < n ) C[index++] = A[i++];
while ( j < m ) C[index++] = B[j++];
return index;
}
归并排序
归并排序是一种基于归并思想的排序算法,这里主要介绍其中最基本的最基本的2路归并排序,其原理为:将序列两两分组,将序列归并为 n/2 个组,组内单独排序。然后将这些组再两两归并,以此类推,直到只剩下一个组为止。归并排序的时间复杂度为O(logn),其核心在于将两个有序序列合成一个有序序列
归并排序的递归实现
const int maxn = 100;
//将数组A的[L1,R1]和[L2,R2]两个区间合并为新的有序区间
void merge(int A[], int L1, int R1, int L2, int R2) {
int i = L1, j = L2; //i和j分别标记在两个区间首
int temp[maxn], index = 0; //temp数组用于临时存放合并结果,index为temp数组使用的下标
while (i <= R1 && j <= R2) {
if (A[i] <= A[j]) {
temp[index++] = A[i++];
} else {
temp[index++] = A[j++];
}
}
//加入剩余元素
while (i <= R1) temp[index++] = A[i++];
while (j <= R2) temp[index++] = A[j++];
//将合并后的元素放回A
for (int i = 0; i < index; i++) {
A[L1 + i] = temp[i];
}
}
//对数组当前区间[left, right]进行递归的归并排序
void mergeSort(int A[], int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(A, left, mid);
mergeSort(A, mid + 1, right);
merge(A, left, mid, mid + 1, right);
}
}
归并排序的非递归实现(待补充)
快速排序
快速排序的基本实现
快速排序是排序算法中平均复杂度为O(nlogn)的一种算法,其实现需要先解决一个问题:调整一个序列 A[1]~A[n] 中元素的位置,使得调整之后的序列中,A[1]左侧的元素都小于A[1],右侧的元素都大于A[1]。解决这个问题速度最快的方法就是利用 two pointers 的思想:
- 先将 A[1] 存储到临时变量temp,然后使用两个变量 left、right 分别指向序列首尾
- 只要 right 指向的元素大于 A[1] ,就将 right 不断左移,直到发现一个不大于 A[1] 的元素,则将之移动到 left 当前的位置
- 只要 left 指向的元素小于 A[1] ,就将 left 不断右移,直到发现一个不小于 A[1] 的元素,则将之移动到 right 当前的位置
- 重复上面两个步骤,直到 left 和 right 相遇,在此时的位置放入 temp 中存储的 A[1]
在上面的方法中,用以划分区间的元素 left 被称为主元
而利用上面的方法,我们就可以得到快速排序的基本写法:
- 调整序列中元素的位置,使得主元左边的元素均小于主元,右边的元素均大于主元
- 对划分后的左右两边的区间内的元素也递归使用一样的方法来调整,直到当前调整区间的长度不大于1
由此可以得到快速排序的基本代码:
//根据主元将区间[left, right]划分
int Partition(int A[], int left, int right) {
int temp = A[left];
while (left < right) {
while (left < right) && A[right] > temp) right--;
A[left] = A[right];
while (left < right) && A[left] < temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
//快速排序方法
void QuickSort(int A[], int left, int right) {
if (left < right) {
int pos = Partition(A, left, right);
QuickSort(A, left, pos - 1);
QuickSort(A, pos + 1, right);
}
}
快速排序的改进
快速排序当序列元素的排列比较随机时效率最高,但元素接近有序时会达到最坏的时间复杂度O(n2)。产生这一状况的原因是主元没有把当前区间划分为两个长度一样的子区间。一个可行的解决方案是随机选择主元,为了实现这一目标,我们需要使用生成随机数的方法:
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<cmath>
int main() {
int n;
scanf("%d",&n);
srand((unsigned)time(NULL)); //初始化随机种子
for (int i = 0; i < n; i++) {
printf(",%d"+!i, rand()); //生成并输出[0, RAND_MAX]范围的随机数
}
printf("\n");
printf("----------");
printf("\n");
int a = 0, b = 1000;
for (int i = 0; i < n; i++) {
printf(",%d"+!i, rand() % (b - a + 1) + a); //生成并输出[a,b]范围内的随机数
}
printf("\n");
printf("----------");
printf("\n");
printf("%d",RAND_MAX); //输出RAND_MAX的大小
printf("\n");
printf("----------");
printf("\n");
for (int i = 0; i < n; i++) {
//生成并输出[10000,60000]范围的随机数(超过RAND_MAX的写法)
printf(",%d"+!i, (int)(round(1.0 * rand()/RAND_MAX * 50000 + 10000)));
}
}
利用上述的随机数生成的方法可以改进快速排序,代码如下:
int RandPartition(int A[], int left, int right) {
int p = round(1.0 * rand()/RAND_MAX * (right - left) + left); //生成[left, right]范围的随机数作为随机主元下标
swap(A[p], A[left]); //交换随机主元和left指向的元素
int temp = A[left];
while (left < right) {
while (left < right) && A[right] > temp) right--;
A[left] = A[right];
while (left < right) && A[left] < temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}