Contest100000586 - 《算法笔记》4.6小节——算法初步->two pointers

大憨熊 提交于 2019-12-27 00:43:45

常用模板

two pointers:利用问题本身与序列的特性,使用两个下标 i,j 对序列进行扫描(可以同向扫描,也可以反向扫描),以较低的复杂度(一般是O(n)O(n))解决问题。

定和问题

给定一个递增正整数序列和一个正整数M,求序列中的两个不同位置的数a和b,使得它们的和恰好为M,输出所有满足条件的方案。
模板如下:

while (i < j)
{
    if (a[i] + a[j] == m)
    {
        cout << i << ' ' << j;
        ++i;
        --j;
    }
    else if (a[i] + a[j] < m)
        ++i;
    else
        --j;
}

由于 i 的初值为0,j 的初值为n - 1(i,j 分别指向序列的第一个和最后一个元素),而 i 只递增,j 只递减,循环当 i >= j 时停止,因此 i 和 j 的操作最多为n次。即时间复杂度为O(n)O(n)

序列合并问题

假设有两个递增序列A与B,要求将它们合并为一个递增序列C。设置两个下标 i 和 j,初值均为0,分别指向序列A和序列B的第一个元素。

int Merge(int A[], int B[], int C[], int n, int m)
{
    int i = 0, j = 0, index = 0; // i指向A[0],j指向B[0]
    while (i < n && j < m)
    {
        if (A[i] <= B[j]) // 如果A[i] <= B[j],将A[i]加入序列C
            C[index++] = A[i++];
        else // 如果A[i] > B[j],将B[i]加入序列C
            C[index++] = B[J++];
    }
    while (i < n)
        C[index++] = A[i++]; // 将序列A的剩余元素加入序列C
    while (j < m)
        C[index++] = B[j++]; // 将序列B的剩余元素加入序列C
    return index;
}

2-路归并排序

递归实现

反复将当前需要排序的区间[left, right]分为两半,对两个子区间[left, mid]和[mid + 1, right]分别递归进行归并排序,然后将两个已经有序的子区间合并为有序序列即可。
模板如下:

const int maxn = 100;
// 将数组A的[L1, R1]与[L2, R2]区间合并为有序区间(此处L2即为R1 + 1)
void Merge(int A[], int L1, int R1, int L2, int R2)
{
    int i = L1, j = L2; // i指向A[L1],j指向A[L2]
    int temp[maxn], index = 0; // temp临时存放合并后的数组,index为其下标
    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++]; // 将[L1, R1]的剩余元素加入序列temp
    while (j <= R2)
        temp[index++] = A[j++]; // 将[L2, R2]的剩余元素加入序列temp
    for (int i = 0; i < index; ++i)
        A[L1 + i] = temp[i]; // 将合并后的序列赋值回数组A
}
// 将array数组当前区间[left, right]进行归并排序
void MergeSort(int A[], int left, int right)
{
    if (left < right) // 只要left小于right
    {
        int mid = (left + right) / 2; // 取[left, right]的中点
        MergeSort(A, left, mid); // 递归,将左子区间[left, mid]归并排序
        MergeSort(A, mid + 1, right); // 递归,将右子区间[mid + 1, right]归并排序
        Merge(A, left, mid, mid + 1, right); // 将左子区间和右子区间合并
    }
}

Merge函数在该模板中的序列合并问题中是有区别。在序列合并中是while (i < n && j < m),而在该模板中是while (i <= R1 && j <= R2)。问题在于,早序列合并中,n和m是两个序列的长度,而数组下标是从0开始的,数组下标取不到n或m,因此是一个左闭右开的区间。而在该模板中,R1和R2都是可以取到的,因此是一个左闭右闭的区间,循环中要加等号。

非递归实现

2-路归并排序的非递归实现是递归实现的反过程,递归从整体到局部,而非递归是局部到整体。令步长step的初值为2,然后将数组(数组下标从1开始)中每step个元素作为一组,将其内部进行排序。即把左step / 2个元素与右step / 2个元素合并,而若元素个数不超过step / 2则不操作(这一般发生在序列末尾)。再令step乘以2,重复以上操作,直到step / 2超过元素个数。因为如果step / 2不超过元素个数n,那么还是可以进行一次分组排序的。

// step为组内元素个数,step / 2为左子区间元素个数,等号可以把不取
void MergeSort(int A[])
{
    for (int step = 2; step / 2 <= n; step *= 2)
    { // 每step个元素一组,组内前step / 2和后step / 2个元素进行合并
        for (int i = 1; i <= n; i += step) // 对每一组
        {
            int mid = i + step / 2 - 1; // 左子区间元素个数为step / 2
            if (mid + 1 <= n) // 右子区间存在元素则合并
                // 左子区间为[i, mid],右子区间为[mid + 1, mid(i + step - 1, n)]
                Merge(A, i, mid, mid + 1, min(i + step - 1, n)); // 调用递归模板中的Merge函数
        }
    }
}

当然,如果题目中只要求给出归并排序每一趟结束时的序列,那么完全可以使用sort函数来代替Merge函数(只要时间限制允许),模板如下:

// step为组内元素个数,step / 2为左子区间元素个数,等号可以把不取
void MergeSort(int A[])
{
    for (int step = 2; step / 2 <= n; step *= 2)
    { // 每step个元素一组,组内[i, min(i + step, n + 1)]进行排序
        for (int i = 1; i <= n; i += step) // 对每一组
            sort(A + i, A + min(i + step, n + 1));
    }
    // 此处可以输出归并排序的某一趟结束的序列
}

快速排序

这一块的知识点内容建议阅读算法笔记4.6.3小节——快速排序,这里就不做简单的记录或总结了。

简单快排

模板如下:

// 对区间[left, right]进行划分
int Partition(int A[], int left, int right)
{
    int temp = A[left]; // 将A[left]存放至临时变量temp
    while (left < right) // 只要left与right不相遇
    {
        while (left < right && A[right] > temp) // 反复左移right
            --right;
        A[left] = A[right]; // 将A[right]挪到A[left]
        while (left < right && A[left] <= temp) // 反复右移left
            ++left;
        A[right] = A[left]; // 将A[left]挪到A[right]
    }
    A[left] = temp; // 把temp放到left与right相遇的地方
    return left; // 返回相遇的下标
}
// 快速排序,left和right初值为序列首尾下标(例如1与n)
void QuickSort(int A[], int left, int right)
{
    if (left < right) // 当前区间的长度超过1
    {
        int pos = Partition(A, left, right); // 将[left, right]按A[left]一分为二
        QuickSort(A, left, pos - 1); // 对左子区间递归进行快速排序
        QuickSort(A, pos + 1, right); // 对右子区间递归进行快速排序
    }
}

生成随机数的模板:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main()
{
    srand((unsigned)time(NULL));
    for (int i = 0; i < 10; ++i)
        cout << rand() << endl;

    return 0;
}

生成[a, b]范围内的随机数:

int c = rand() % (b - a + 1) + a;

rand()函数只能生成[0, RAND_MAX]范围内的整数,RAND_MAX是stdlib头文件中的一个常数,随机器不同而不同。在一般的机器中它是32767,用十六进制表示就是0x7FFF,即b只要超过了32767就生成不了。那么怎么生成更大范围(a, b)内的随机数呢?主要有以下几种:

  • 多次生成rand随机数,然后用位运算拼接起来;
  • 多次生成rand随机数,然后将它们相乘;
  • 随机生成每一个数位的值(0~9),然后拼接成一个大整数;
  • 先用rand()生成一个[0, RAND_MAX]范围内的随机数,然后用这个随机数除以RAND_MAX,这样就会得到一个[0, 1]范围内的浮点数。再将浮点数乘以(b - a)然后加上a即可。

其中最后一个方法是比较常用的,且得到的数据更为随机。其原理在于得到的浮点数相当于[a, b]范围内的比例位置,后面的乘以(b-a)再加上a的作用等同于前面生成指定范围内随机数,不要想太多了。

(int)(round(1.0 * rand() / RAND_MAX * (b - a) + a));
随机快排

在A[left···right]中随机选取一个主元p,然后以A[p]作为主元来进行划分。
模板如下:

// 选取随机主元,对区间[left, right]进行划分
int RandPartition(int A[], int left, int right)
{
    // 生成[left, right]内的随机数p
    int p = (round(1.0 * rand() / RAND_MAX * (right - left) + left));
    swap(A[p], A[left]); // 交换A[p]和A[left]
    // 以下为原先Partition函数的划分过程,没有任何改变
    int temp = A[left]; // 将A[left]存放至临时变量temp
    while (left < right) // 只要left与right不相遇
    {
        while (left < right && A[right] > temp) // 反复左移right
            --right;
        A[left] = A[right]; // 将A[right]挪到A[left]
        while (left < right && A[left] <= temp) // 反复右移left
            ++left;
        A[right] = A[left]; // 将A[left]挪到A[right]
    }
    A[left] = temp; // 把temp放到left与right相遇的地方
    return left; // 返回相遇的下标
}

问题A:二路归并排序(mergesort)递归法 [2*+]

后台测试数据出错,不管怎么做都是错的,所以别做(反正我也是真讨厌这个OJ平台…)。


问题 B: 基础排序III:归并排序

题目描述

归并排序是一个时间复杂度为O(nlogn)的算法,对于大量数据远远优于冒泡排序与插入排序。

这是一道排序练习题,数据量较大,请使用归并排序完成。

输入

第一行一个数字n,代表输入的组数

其后每组第一行输入一个数字m,代表待排序数字的个数

其后m行每行一个数据,大小在1~100000之间,互不相等,最多有10万个数据。

输出

升序输出排好序的数据,每行一个数字

样例输入

1
10
10
9
8
7
6
5
4
3
2
1

样例输出

1
2
3
4
5
6
7
8
9
10

Note

归并排序简单应用,注意传入参数的时候是左闭右闭的,所以要传入[0, m - 1]。

#include <iostream>
#include <algorithm>
using namespace std;

const int maxn = 100010;

void Merge(int A[], int L1, int R1, int L2, int R2)
{
    int i = L1, j = L2;
    int temp[maxn], index = 0;
    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++];
    for (int i = 0; i < index; ++i)
        A[L1 + i] = temp[i];
}

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);
    }
}

int main()
{
    int n, m;
    while (cin >> n)
    {
        while (n--)
        {
            cin >> m;
            int a[m];
            for (int i = 0; i < m; ++i)
                cin >> a[i];
            MergeSort(a, 0, m - 1);

            for (int i = 0; i < m; ++i)
                cout << a[i] << endl;
        }
    }
}

问题 C: 快速排序 qsort [2*]

题目描述

输入n个整数,用快速排序的方法进行排序

Input

第一行数字n 代表接下来有n个整数
接下来n行,每行一个整数

Output

升序输出排序结果
每行一个数据

Sample Input

5
12
18
14
13
16

Sample Output

12
13
14
16
18

提示

n<=5000

Note

无。

#include <iostream>
#include <algorithm>
using namespace std;

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);
    }
}

int main()
{
    int n, m;
    while (cin >> n)
    {
        int a[n];
        for (int i = 0; i < n; ++i)
            cin >> a[i];
        QuickSort(a, 0, n - 1);

        for (int i = 0; i < n; ++i)
            cout << a[i] << endl;
    }
}

问题 D: 二分递归快排(Qsort) [2*]

快速排序就是基于二分递归的,所以将问题C的代码复制提交即可。


一定要自己写一遍哦~~~

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!