《算法笔记》第4章常用技巧及排序算法

二、 常用技巧

1. 散列

原理是把一个元素key通过散列函数H转换为H(key)。

最常见的散列法:把key作为数组下标。

散列函数:直接定址法,平方取中法,除留余数法。

解决冲突的方法:线性探测法,平方探测法,链地址法。

字符串hash:把26个字母看做26进制(0~25)的数,将26进制转换为10进制,如BCD看做731。


2. 递归

  • 递归边界:返回最简单底层的结果。
  • 递归式:减少数据规模并向下一次递归。

2.1 全排列问题

#include <cstdio>

const int maxn = 11;
//输出1~n的全排列,P为当前排列,hash记录整数x是否已经在P中
int n, P[maxn], hash[maxn] = {false};

void fullP(int index) { //当前处理排列的第index号位
    if (index == n + 1) { //递归边界,已经处理完排列的1~n位
        for (int i = 1; i <= n; i++) {
            printf("%d", P[i]); //输出当前排列
        }
        printf("\n");
        return;
    }
    for (int x = 1; x <= n; x++) {
        if (hash[x] == false) {
            P[index] = x;
            hash[x] = true;
            fullP(index + 1); //递归式
            hash[x] = false;
        }
    }
}

int main() {
    n = 3; //输出1~3的全排列
    fullP(1); //从P[1]开始填
    return 0;
}

2.2 n皇后问题

#include <cstdio>
#include <cmath>

const int maxn = 11;
// n皇后问题,P为当前排列,hash记录整数x是否已经在P中,count记录合法排列个数
int n, P[maxn], hash[maxn] = {false}, count = 0;

void fullP(int index) { //当前处理排列的第index号位
    if (index == n + 1) { //递归边界,已经处理完排列的1~n位
        bool flag = true;
        for (int i = 1; i <= n; i++) { //遍历任意两个皇后
            for (int j = i + 1; j <= n; j++) {
                if (abs(i - j) == abs(P[i] - P[j])) { //在一条对角线上
                    flag = false;
                }
            }
        }
        if (flag) count++;
        return;
    }
    for (int x = 1; x <= n; x++) {
        if (hash[x] == false) {
            P[index] = x;
            hash[x] = true;
            fullP(index + 1); //递归式
            hash[x] = false;
        }
    }
}

int main() {
    n = 8; //八皇后问题
    fullP(1); //从P[1]开始填
    printf("%d", count);
    return 0;
}

2.3 回溯法优化n皇后问题

#include <cstdio>
#include <cmath>

const int maxn = 11;
// n皇后问题,P为当前排列,hash记录整数x是否已经在P中,ans数组记录每种情况,cnt记录合法排列个数
int n, P[maxn], hash[maxn] = {false}, ans[1000] = {0}, cnt = 0;

void fullP(int index) { //当前处理排列的第index号位
    if (index == n + 1) { //递归边界,能到达这里的一定是合法的
        for (int i = 1; i <= n; i++) {
            ans[cnt] = ans[cnt] * 10 + P[i]; //将结果数组转化为整数
        }
        cnt++;
        return;
    }
    for (int x = 1; x <= n; x++) {
        if (hash[x] == false) {
            bool flag = true;
            for (int pre = 1; pre < index; pre++) { //遍历之前的皇后
                //第index列皇后的行号为x,第pre列皇后的行号为P[pre]
                if (abs(index - pre) == abs(x - P[pre])) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                P[index] = x;
                hash[x] = true;
                fullP(index + 1); //递归式
                hash[x] = false;
            }
        }
    }
}

int main() {
    n = 8; //八皇后问题
    fullP(1); //从P[1]开始填
    printf("%d\n", cnt);
    for (int i = 0; i < cnt; i++) {
        printf("%d\n", ans[i]);
    }
    return 0;
}

3. 贪心

局部最优推全局最优。

3.1 简单贪心

3.2 区间贪心

N个开区间,选择尽可能多的开区间,使这些开区间两两没有交集。

  • 若开区间1被开区间2包含,则选开区间2。
  • 去除开区间包含的情况,先选所有开区间左端点最大的区间。
#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 110;
struct Inteval {
    int x, y; //开区间左右段点
} I[maxn];
bool cmp(Inteval a, Inteval b) {
    if (a.x != b.x)
        return a.x > b.x; //先按左端点从大到小排序
    else
        return a.y < b.y; //左端点相同的按右端点从小到大
}

int main() {
    int n;
    while (scanf("%d", &n), n != 0) {
        for (int i = 0; i < n; i++) {
            scanf("%d%d", &I[i].x, &I[i].y);
        }
        sort(I, I + n, cmp);
        // ans记录不相交区间个数,lastX记录上一个被选中区间的左端点
        int ans = 1, lastX = I[0].x;
        for (int i = 1; i < n; i++) {
            if (I[i].y <= lastX) { //如果该区间右端点在lastX左边
                lastX = I[i].x; //以I[i]作为新选中的区间
                ans++;
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

4. 二分

4.1 二分查找

严格单调递增数列中查找元素

int binarySearch(int arr[], int left, int right, int key) {
    while (left <= right) {
        //下列句子可以优化为mid = left + (right - left) / 2,防止溢出
        int mid = (left + right) / 2; //取中点
        if (arr[mid] == key){ //找到
            return mid;
        } else if (arr[mid] > key) { //中间的数大于key
            right = mid - 1;
        } else { //中间的数小于key
            left = mid + 1;
        }
    }
    return -1; //查找失败,返回-1
}

查找第一个满足条件C的元素

int binarySearch(int arr[], int left, int right, int key) {
    while (left < right) { //对[left,right]来说,left=right意味着到唯一位置
        int mid = left + (right - left) / 2;
        //下面if的括号内填入找的条件,如下是找第一个大于等于key的元素
        if (arr[mid] >= key) { //条件成立,第一个满足条件的元素的位置<=mid
            right = mid;
        } else { //条件不成立,则第一个满足条件的元素的位置>mid
            left = mid + 1;
        }
    }
    return left; //返回夹出的位置
}

查找最后一个满足条件C的元素

先求第一个满足**条件"!C"**的元素,然后将该位置减1。

4.2 二分法拓展(以求根号2近似值为例)

#include <cstdio>

const double eps = 1e-5;
double f(double x) {
    return x * x;
}
double calSqrt() {
    double left = 1, right = 2, mid;
    while (right - left > eps) {
        mid = (left + right) / 2;
        if (f(mid) > 2) {
            right = mid;
        } else
            left = mid;
    }
    return mid;
}

int main() {
    printf("%f", calSqrt());
    return 0;
}

4.3 快速幂

给定三个正整数 a 、 b 、 m ( a < 1 0 9 , b < 1 0 6 , 1 < m < 1 0 9 ) a、b、m(a<10^9,b<10^6,1<m<10^9) abm(a<109,b<106,1<m<109),求解 a b % m a^b\%m ab%m

递归写法

  • b b b是奇数,则 a b = a ∗ a b − 1 a^b =a*a^{b-1} ab=aab1
  • b b b是偶数,则 a b = a b / 2 ∗ a b / 2 a^b=a^{b/2}*a^{b/2} ab=ab/2ab/2
typedef long long LL;
//求a^b%m,递归写法
//若初始时a大于m,则在进入函数前先让a对m取模
//若m=1,则直接判定为0
LL binaryPow(LL a, LL b, LL m) {
    if (b == 0) return 1;
    // b为奇数,转换为b-1
    if (b % 2 == 1) { //可用if (b & 1)替代
        return a * binaryPow(a, b - 1, m) % m;
    } else { // b为偶数,转换为b/2
        LL mul = binaryPow(a, b / 2, m);
        return mul * mul % m;
    }
}

迭代写法

可以证明,任意的 a b a^b ab都可以表示为 a 2 k 、 … a 4 、 a 2 、 a 1 a^{2k}、…a^4、a^2、a^1 a2ka4a2a1中若干项的乘积。若 b b b的二进制的 i i i位为1,则 a 2 i a^{2i} a2i就选中,注意到 a 2 k 、 … a 4 、 a 2 、 a 1 a^{2k}、…a^4、a^2、a^1 a2ka4a2a1序列中的前一项总是等于后一项的平方。

typedef long long LL;
//求a^b%m,递归写法
LL binaryPow(LL a, LL b, LL m) {
    LL ans = 1;
    while (b > 0) {
        if (b & 1) { //如果b的二进制末尾为1,相当于b为奇数
            ans = ans * a % m; //令ans累积上a
        }
        a = a * a % m; //令a平方,相当于a^2i序列
        b >>= 1; // b二进制右移一位,相当于b/=2
    }
    return ans;
}

5. two pointers(双指针)

5.1 求递增数列中的A+B=M

void add(int arr[], int n, int m) {
    int i = 0, j = n - 1;
    while (i < j) {
        if (arr[i] + arr[j] == m) { //找到满足的情况
            printf("%d+%d=%d\n", arr[i], arr[j], m);
            i++;
            j--;
        } else if (arr[i] + arr[j] < m) { // i往右移动
            i++;
        } else // j往左移动
            j--;
    }
}

5.2 序列合并问题

将两个递增序列A与B合并为一个递增序列C。

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和B中最小的元素加入序列C
            C[index++] = A[i++];
        } else {
            C[index++] = B[j++];
        }
    }
    while (i < n) C[index++] = A[i++]; //将序列A中剩余元素直接加入序列C
    while (j < m) C[index++] = B[j++]; //将序列B中剩余元素直接加入序列C
    return index; //返回序列C的长度
}

5.3 [归并排序](#4. 归并排序)

5.4 [快速排序](#5. 快速排序)


6. 打表

  • 在程序中一次性计算出所有需要的结果,之后的查询直接取这些结果,如素数表。
  • 在程序B中分一次或多次计算出所有需要的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果,如提前计算出n皇后问题的方案数。
  • 对没有思路的题,先用暴力程序计算小范围内的结果,然后找规律。

7. 活用递推

找到F(n)与F(n-1)的递推关系,简化计算。


8. 随机选择算法

找到一个无序数组中第K大的数(数组中元素各不相同)

int randPartition(int A[], int left, int right) {
    //生成[left,right]内的随机数
    int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));
    swap(A[p], A[left]); //交换A[p]和A[left]
    int temp = A[left];
    while (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; //返回相遇的下标
}

int randSlect(int A[], int left, int right, int K) {
    if (left == right) return A[left]; //递归边界
    int p = randPartition(A, left, right); //划分后主元的最终位置为p
    int M = p - left + 1; // A[p]是A[left,right]中的第M大
    if (K == M) return A[p]; //找到第K大数
    if (K < M) { //第K大数在主元左侧
        return randSlect(A, left, p - 1, K);
    } else //第K大数在主元右侧
        return randSlect(A, p + 1, right, K - M);
}

应用举例

给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原交集、交为空集,同时在两个子集合的元素个数 n 1 n_1 n1 n 2 n_2 n2之差的绝对值 ∣ n 1 − n 2 ∣ \lvert n_1-n_2 \rvert n1n2尽可能小的前提下,要求它们各自的元素之和 S 1 S_1 S1 S 2 S_2 S2之差的绝对值 ∣ S 1 − S 2 ∣ \lvert S_1-S_2 \rvert S1S2尽可能大,并求 ∣ S 1 − S 2 ∣ \lvert S_1-S_2 \rvert S1S2的最大值。

该问题等价于求原集合中第n/2大元素,同时根据这个元素把集合划分为两部分,使得其中一个子集合中的元素都不小于这个数,而另一个子集合中的元素都大于这个数。因此使用随机选择算法较为简便。

#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <cmath>
#include <algorithm>
using namespace std;

const int maxn = 100010;
int A[maxn], n;

int randPartition(int A[], int left, int right) {
    //生成[left,right]内的随机数
    int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));
    swap(A[p], A[left]); //交换A[p]和A[left]
    int temp = A[left];
    while (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; //返回相遇的下标
}

int randSlect(int A[], int left, int right, int K) {
    if (left == right) return A[left]; //递归边界
    int p = randPartition(A, left, right); //划分后主元的最终位置为p
    int M = p - left + 1; // A[p]是A[left,right]中的第M大
    if (K == M) return A[p]; //找到第K大数
    if (K < M) { //第K大数在主元左侧
        return randSlect(A, left, p - 1, K);
    } else //第K大数在主元右侧
        return randSlect(A, p + 1, right, K - M);
}

int main() {
    //初始化随机数种子
    srand((unsigned)time(NULL));
    // sum和sum1记录所有整数之和与切分后前n/2个元素之和
    int sum = 0, sum1 = 0;
    scanf("%d", &n); //整数个数
    for (int i = 0; i < n; i++) {
        scanf("%d", &A[i]);
        sum += A[i];
    }
    randSlect(A, 0, n - 1, n / 2);
    for (int i = 0; i < n / 2; i++) {
        sum1 += A[i];
    }
    printf("%d", (sum - sum1) - sum1); //求两个子集合的元素和之差
    return 0;
}

三、 排序算法

1. 冒泡排序

void bubbleSort(int arr[], int n) {
    int flag = 0; //标记本轮是否发生元素交换
    for (int i = 1; i <= n - 1; i++) {
        for (int j = 0; j < n - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = 1;
            }
        }
        if (!flag) break;
    }
}

2. 选择排序

void selectSort(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        int min = i; //标记最小元素的下标
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[min]) {
                min = j;
            }
        }
        int temp = arr[i];
        arr[i] = arr[min];
        arr[min] = temp;
    }
}

3. 插入排序

void insertSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int temp = arr[i], j = i;
        while (j > 0 && temp < arr[j - 1]) { //元素后移
            arr[j] = arr[j - 1];
            j--;
        }
        arr[j] = temp; //插入到对应的位置
    }
}

4. 归并排序

4.1 二路归并(递归)

const int maxn = 100;
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]中剩余元素直接加入序列C
    while (j <= R2) temp[index++] = A[j++]; //将[L2,R2]中剩余元素直接加入序列C
    for (i = 0; i < index; i++) {
        A[L1 + i] = temp[i]; //将合并后的序列赋值返回序列A
    }
}

void mergeSort(int A[], int left, int right) {
    if (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); //将左子区间和右子区间合并
    }
}

4.2 二路归并(非递归)

const int maxn = 100;
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]中剩余元素直接加入序列C
    while (j <= R2) temp[index++] = A[j++]; //将[L2,R2]中剩余元素直接加入序列C
    for (i = 0; i < index; i++) {
        A[L1 + i] = temp[i]; //将合并后的序列赋值返回序列A
    }
}

void mergeSort(int A[], int n) {
    // step为组内元素个数,step/2为左子区间元素个数
    for (int step = 2; step / 2 <= n; step *= 2) {
        //每step个元素一组,组内前step/2和后step/2个元素进行合并
        for (int i = 0; i < n; i += step) { //对每一组
            int mid = i + step / 2 - 1;
            if (mid + 1 <= n) { //右子区间存在元素则合并,注意最后一个右子区间
                //左子区间为[i,mid],右子区间为[mid+1,min(i+step-1,n)]
                merge(A, i, mid, mid + 1, min(i + step - 1, n));
            }
        }
    }
}

4.3 用sort函数替代merge实现

void mergeSort(int A[], int n) {
    // step为组内元素个数,step/2为左子区间元素个数
    for (int step = 2; step / 2 <= n; step *= 2) {
        //每step个元素一组,组内前step/2和后step/2个元素进行合并
        for (int i = 0; i < n; i += step) { //对每一组
            sort(A + i, A + min(i + step, n + 1));
        }
    }
}

5. 快速排序

5.1 总是以A[left]为主元

//区间划分
int partition(int A[], int left, int right) {
    int temp = A[left]; //以A[left]为主元
    while (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; //返回相遇的下标
}
//快速排序
void quickSort(int A[], int left, int right) {
    if (left < right) { //当前区间的长度大于1
        int pos = partition(A, left, right); //划分左右子区间
        quickSort(A, left, pos - 1); //对左子区间递归进行快速排序
        quickSort(A, pos + 1, right); //对右子区间递归进行快速排序
    }
}

5.2 随机主元

int randPartition(int A[], int left, int right) {
    //生成[left,right]内的随机数
    int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));
    swap(A[p], A[left]); //交换A[p]和A[left]
    int temp = A[left];
    while (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; //返回相遇的下标
}

void quickSort(int A[], int left, int right) {
    if (left < right) { //当前区间的长度大于1
        int pos = randPartition(A, left, right); //划分左右子区间
        quickSort(A, left, pos - 1); //对左子区间递归进行快速排序
        quickSort(A, pos + 1, right); //对右子区间递归进行快速排序
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值