【C++】算法笔记_01

一、排序

1.快速排序q[n]

平均时间复杂度:n * log 2 n

(1)确定分界点x:q[l] 或 q[(l + r) / 2] 或 q[r] 或 随机
(2)调整区间:保证q[k]左边的数都小于等于x,q[k]右边的数都大于等于x,q[k]不一定等于x。
(3)在两个区间重复(1)(2)步

不需要开辟额外空间的做法:设两个指针i,j,i指向最左边的数,j指向最右边的数,i向右移,当遇到大于x的数时停下,j向左移,当遇到小于x的数时停下,然后交换i,j所指向的数。i,j以这一规则接着移动,直到i,j相遇为止。然后再分别对i,j分界点的两边进行此算法。

class Solution {
public:
    void quick_sort(int i,int j,vector& nums){       
        int m = j - i;
        if(m < 0 || m == 0){
            return;
        }

        int x = nums[(i + j) >> 1];//参照标准
        int p = i;
        int q = j;
        j++;//因为要先j--再判断,所以先加一
        i--;//因为要先i++再判断,所以先减一
        while(i < j){
            do{
                i++;
            }while(nums[i] < x);
            //nums[i] x);
            if(i < j){
                int a = nums[i];
                nums[i] = nums[j];
                nums[j] = a;
            }
        }

        quick_sort(p,j,nums);
        quick_sort(j + 1,q,nums);
        /*
        这里不能取
        sort(p,i - 1,nums);
        sort(i,q,nums);
        会陷入死循环,如[0,1],
        若想取i,则改变x的值向上取整
        */
        return;
    }

    vector sortArray(vector& nums) {
        int n = nums.size();
        int i = 0;
        int j = n - 1;
        quick_sort(i,j,nums);

        return nums;
    }
};

2.快速选择

题目:给定一个长度为n的整数数列,以及一个整数k,请用快速选择算法求出数列的第k小的数是多少(假设这个数一定在区间内)。

进行一次快速排序时,假设分界点左边元素个数为SL,右边元素个数为SR,此时分为两种情况:
(1)k <= SL,递归左边
(2)k > SL,递归右边,k = k - SL

时间复杂度:n + n / 2 + n / 4 + ... = n(1 + 1/2 + 1/4 + 1/8 + ...) <= 2n,所以时间复杂度为O(n)。

#include 

using namespace std;

const int N = 100010;

int n, k;
int q[N];

int quick_sort(int l, int r, int k){
    if(l == r) return q[l];//因为保证k对应的值始终在区间内,所以区间内至少有一个值,所以可以不写成l >= r

    int x = q[l], i = l - 1, j = r + 1;
    while(i < j){
        while(q[++i] < x);
        while(q[--j] > x);
        if(i < j) swap(q[i], q[j]); 
    }
    
    int sl = j - l + 1;
    if(k <= sl) return quick_sort(l, j, k);
    return quick_sort(j + 1, r, k - sl);
}

int main(){
    cin >> n >> k;
    for(int i = 0; i < n; i++) cin >> q[i];

    cout << quick_sort(0, n - 1, k) << endl;

    return 0;
}

3.归并排序

时间复杂度:n * log 2 n

(1)确定分界点mid:将整个数组平均分为左右两半。
mid = (l + r) / 2,两个区间[l,mid],[mid + 1,r]。
(2)对这两半分别进行递归排序每次分两半,直到每对每组大小为1。
n除以2多少次等于1呢?log 2 n次,所以这一操作时间复杂度为O(log 2 n)。

(3)对于每对的两个有序数组 合并为一个有序数组:设i,j两个指针分别指向两个数组的第一个数,比较这两个指针所指向的数的大小,将小的那个取出,相应指针后移一位,再比较这两个指针所指向的数的大小,以此类推,直到一个指针移到数组的末尾,将另一个指针指向的数及其之后的数放到答案数组的最后面即可。

class Solution {
public:
    void merge_sort(vector& nums,int l,int r){//归并排序      
        //数组中没有数据或只有一个数据时,返回
        if(l >= r){
            return;
        }       
        //先进行对半分
        int middle = (l + r) >> 1;
        merge_sort(nums, l, middle);
        merge_sort(nums, middle + 1, r);      
        //然后对每对数组进行排序
        int temp[r - l + 1];//暂存结果
        int n = 0;
        int i = l,j = middle + 1;
        while(i <= middle && j <= r){
            if(nums[i] < nums[j]){
                temp[n++] = nums[i++];      
            }
            else{
                temp[n++] = nums[j++];
            }
        }
        if(i > middle){
            while(j <= r){
                temp[n++] = nums[j++];
            }
        }
        else if(j > r){
            while(i <= middle){
                temp[n++] = nums[i++];
            }
        }
        //从暂存数组中取出数据
        int p = l,q = 0;
        while(p <= r){
            nums[p++] = temp[q++];
        }
    }

    vector sortArray(vector& nums) {
        int n = nums.size();
        int l = 0;
        int r = n - 1;
        merge_sort(nums,l,r);
        return nums;
    }
};

4.求逆序对的数量

逆序对:序列中的一对数,前面的数比后面的数大。

使用归并排序,共有三种情况:
(1)这一对数都在mid的左边:左半边内部的逆序对数量:merge_sort(L, mid)
(2)这一对数都在mid的右边:右半边内部的逆序对数量:merge_sort(mid + 1, R)
(3)一个数在mid左边,一个数在mid右边:设右边有m个数。对右边第一个点,左边有S1个数比它大,那么有S1个逆序对。以此类推,对右边第m个点,左边有Sm个数比它大,那么有Sm个逆序对。那么这种情况下一共有S1 + S2 + ... + Sm个逆序对。
        在合并过程中,设指针i指向左边,指针j指向右边,若指针i指向的数比指针j指向的数小,则i向后移一位。直到指针i指向的数比指针j指向的数大,则S = mid - i + 1,j向后移一位。

设序列中有n个数,则最多有n(n - 1) / 2个逆序对,数值可能会很大,大于int的最大值,要用long long来存。

class Solution {
public:
    typedef long long LL;
    int merge_sort(int l, int r, vector& nums){
        if(l >= r) return 0;

        int mid = (l + r) >> 1;
        LL res = merge_sort(l, mid, nums) + merge_sort(mid + 1, r, nums);

        int temp[r - l + 1];
        int i = l, j = mid + 1, k = 0;
        while(i <= mid && j <= r){
            if(nums[i] <= nums[j]) temp[k++] = nums[i++];
            else{
                temp[k++] = nums[j++];
                res = res + mid - i + 1;
            }
        }
        //扫尾       
        while(i <= mid) temp[k++] = nums[i++];
        while(j <= r) temp[k++] = nums[j++];
        //物归原主
        for(int p = l, q = 0; p <= r; p++, q++){
            nums[p] = temp[q];
        }

        return res;
    }

    int reversePairs(vector& nums) {
        int n = nums.size();
        int result = merge_sort(0, n - 1, nums);
        return result;
    }
};

二、二分查找

1.二分查找

(1)首先确定区间的中点位置mid
(2)将待查的K值与R[mid]比较:
若相等,则查找成功并返回此位置,
否则,①若R[mid].key>K,将查找区间变为[left,mid-1]
           ②若R[mid].key

当l > r时无解

class Solution {
public:
    //假设所有元素不重复
    int search(vector& nums, int target) {
        int n = nums.size();
        int l = 0,r = n - 1;
        int mid = (r + l) >> 1;
        //cout << mid << endl;
        while(l <= r){
            if(nums[mid] == target){
                return mid;
            }
            else if(nums[mid] > target){
                r = mid - 1;
            }
            else{
                l = mid + 1;
            }
            mid = (r + l) >> 1;
            //cout << r << " " << l << " " << mid << endl;
        }
        return -1;
    }
};

2.数的三次方根

假设范围内有正数也有负数,范围为(l, r)。
设mid = (l + r) / 2。
if(mid^3 == x) result = mid;
if(mid^3 > x) r = mid;
else l = mid;

如果结果保留n位小数,则r - l > 10^(-n-2)

#include 

using namespace std;

int main(){
    double x;
    cin >> x;
    
    double l = -10000, r = 10000;
    while(r - l > 1e-8){//1e-8是10的-8次方
        double mid = (l - r) / 2;
        if(mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    printf("%lf\n",l);//float型输入用%f,double型输入用%lf

    return 0;
}

三、高精度

1.两个大的整数相加A + B(大的整数的位数一般 <= 10^6)

2.两个大的整数相减A - B

3.大的整数乘小的整数A * a(小的整数一般 <= 10^9)

4.大的整数除以小的整数A / a

注:这里假设A,B,a都大于等于0

1.两个大的整数相加A + B 可划分为两个问题
①大整数存储:将整数的每一位存到数组中
例如:A = 123456789,比较好的处理办法是从个位开始存:9存到nums[0],8存到nums[1],以此类推。从个位开始存的原因是比较好处理进位问题。
②大整数相加:注意进位。

#include
#include

using namespace std;

// C = A + B, A >= 0, B >= 0
vector add(vector &A, vector &B)
{
    vector C;
    int t = 0;//进位
    for (int i = 0; i < A.size() || i < B.size(); i ++ )
    {
        if(i < A.size()) t += A[i];
        if(i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(1);//若加完还有一进位
    return C;
}
int main(){
    string a,b;
    vector A,B;
    cin >> a >> b;//a = "123456"
    for(int i = a.size() - 1;i >= 0;i--) A.push_back(a[i] - '0');//A = [6,5,4,3,2,1]
    for(int i = b.size() - 1;i >= 0;i--) B.push_back(b[i] - '0');

    auto C = add(A,B);

    for(int i = C.size() - 1;i >= 0;i--) printf("%d",C[i]);
    return 0;
}

2.两个大的整数相减A - B 可划分为两个问题
①大整数存储:同1
②大整数相减:若A > B,则运算A - B;若A < B,则运算-(B - A)。注意借位。

#include
#include

using namespace std;

//判断是否A >= B
bool cmp(vector &A, vector &B){
    if(A.size() != B.size()){
        return A.size() > B.size();
    }
    for(int i = A.size() - 1; i >= 0; i--){
        if(A[i] != B[i]){
            return A[i] > B[i];
        }
    }
    return true;
}

//C = A - B
vector sub(vector &A, vector &B){
    vector C;
    for(int i = 0, t = 0; i < A.size(); i++){
        t = A[i] - t;
        if(i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);//t > 0或t < 0皆适宜
        if(t < 0) t = 1;
        else t = 0;
    }
    while(C.size() > 1 && C.back() == 0) C.pop_back();//如答案为001,则需去掉前面两个零。若答案为000,则剩余一个零。
    return C; 
}

int main(){
    string a, b;
    vector A, B;
    cin >> a >> b;
    for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
    for(int i = b.size() - 1; i >= 0; i--) B.push_back(a[i] - '0');

    if(cmp(A, B)){
        auto C = sub(A, B);
        for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
    }
    else{
        auto C = sub(B, A);
        printf("-");
        for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
    }
}

3.大的整数乘小的整数A * a 可划分为两个问题
①大整数存储:同1
②大整数相乘:将A的每一位与a相乘,结果加上进位所得值t,t取10的余数为当前位结果,t除以10为进位。

#include 
#include 

using namespace std;

vector mul(vector &A, int b){
    vector C;
    int t = 0;
    for(int i = 0; i < A.size() || t; i++){
        if(i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }
    return C;
}

int main(){
    string a;
    int b;
    cin >> a >> b;
    vector A;
    for(int i = a.size() - 1; i >= 0; i--){
        A.push_back(a[i] - '0');
    }
    auto C = mul(A,b);
    for(int i = C.size() - 1; i >= 0; i--){
        printf("%d",C[i]);
    }
    return 0;
}

4.大的整数除以小的整数A / a 可划分为两个问题
①大整数存储:同1
②大整数相除:将A的每一位 加上 上一位求得的余数*10 再除以a,得到的商为当前位的值。

#include 
#include 
#include 

using namespace std;

// A / b = C ... r, A >= 0, b > 0
vector div(vector &A, int b, int &r)
{
    vector C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i -- )
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());//需要#include 
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

int main(){
    string a;
    int b;
    cin >> a >> b;
    vector A;
    for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');

    int r;
    auto C = div(A, b, r);

    for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
    cout << endl << r << endl;
    
    return 0;
}

四、前缀和与差分

1.一维前缀和

设有一个数组:a1,a2,...,an
前缀和数组:S0 = 0,...,S[i] = a[1] + ... + a[i],...

作用:如求数组a中第l项到第r项的和,则答案等于S[r] - S[l - 1]

题目:输入一个长度为n的整数序列。接下来再输入m个询问,每个询问输入一对l,r。对于每个询问,输出原序列中从第i个数到第r个数的和。

#include 

using namespace std;

const int N = 100010;

int n, m;
int a[N], s[N];
//假设n范围为1~100000,数列中元素的值取值范围为-1000~1000,s[N]最大取值为10^8,int的取值范围为-2^31~2^31-1,即-2147483648~2147483647,所以这里s[N]不用定义为long long类型

int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
    for(int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];//前缀和初始化
    
    while(m--){
        int l,r;
        scanf("%d%d",&l, &r);
        printf("%d\n",s[r] - s[l -1]);
    }
    return 0;
}

2.二维前缀和

S[i,j]表示包含a[i,j]的左上角的子矩阵中数的和:一行一行求:S[i,j] = S[i - 1,j] + S[i,j - 1] - S[i -1,j - 1] + a[i,j]

作用:输出原矩阵中顶点为(x1,y1),(x2,y2)的子矩阵中数的和:S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

题目:输入一个边长为n,m的整数矩阵。接下来再输入q个询问,每个询问输入x1,y1,x2,y2。对于每个询问,输出原矩阵中顶点为(x1,y1),(x2,y2)的子矩阵中数的和。

#include 

const int N = 1010;

int n,m,q;
int a[N][N], S[N][N];

int main(){
    scanf("%d%d%d",&n,&m,&q);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            scanf("%d",&a[i][j]);

    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + a[i][j];//求二维前缀和

    while(q--){
        int x1,y1,x2,y2;
        scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
        printf("%d\n",S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1]);//子矩阵的和
    }
    return 0;
}

3.一维差分

给定原数组a[n],构造数组b[n],使得a[i] = b[1] + b[2] + ... + b[i]。此时,b为a的差分,a为b的前缀和。
即b[1] = a[1];
b[2] = a[2] - a[1];
b[3] = a[3] - a[2];
...
b[n] = a[n] - a[n - 1];

有了b数组,可以在O(n)的时间内得到a数组。

作用:若我们想把a数组内从a[l]到a[r]的项全部加上c,若直接对a数组操作,则需要O(r - l + 1)的时间复杂度,可若有了b数组,可以只对b[l]与b[r + 1]操作:b[l] = b[l] + c,b[r + 1] = b[r + 1] - c,时间复杂度为O(1)。

b初始化:可以将a数组的初值想象成0,然后我们需要给a的每个点加上a[i]。

题目:输入一个长度为n的整数序列。接下来再输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c。请你输出进行完所有操作后的序列。

#include 

using namespace std;

const int N = 100010;

int n, m;
int a[N], b[N];

void insert(int l, int r, int c){
    b[l] += c;
    b[r + 1] -= c;
}

int main(){
    scanf("%d%d",&n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);//a数组初始化
    for(int i = 1; i <= n; i++) insert(i, i, a[i]);//b数组初始化
    //m操作
    while(m--){
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }
    //m操作之后的数组
    for(int i = 1; i <= n; i++) a[i] = a[i - 1] + b[i];
    for(int i = 1; i <= n; i++) printf("%d ", a[i]);

    return 0;
}

4.二维差分

给定原矩阵a[i, j],构造差分矩阵b[i, j],使b[i, j]的二维前缀和为a[i, j]。

作用:假设我们需要给a矩阵的一个子矩阵(x1,y1),(x2,y2)中的所有数加上c。
(1)我们若让b[x1,y1] += c,那么a[x1,y1]这一点右下角的所有点的前缀和都会加c。
(2)我们再让b[x2 + 1,y1] -= c,b[x1,y2 + 1] -= c,b[x2 + 1,y2 + 1] += c。
这么做会让时间复杂度变成O(1)。

b初始化:可以将a数组的初值想象成0,然后我们需要给a矩阵的每个1*1的子矩阵加上a[i,j]。

题目:输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数x1, y1, x2, y2, c,其中(x1, y1)和(x2, y2)表示一个子矩阵的左上角坐标和右下角坐标。每个操作都要将选中的子矩阵中的每个元素的值加上c。请将进行完所有操作后的矩阵输出。

#include 

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];

void insert(int x1, int y1, int x2, int y2, int c){
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}

int main(){
    scanf("%d%d%d", &n, &m, &q);
    //a初始化
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            scanf("%d", &a[i][j]);
    //b初始化
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            insert(i, j, i, j, a[i][j]);
    //q操作
    while(q--){
        int x1, y1, x2, y2, c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        insert(x1, y1, x2, y2, c);
    }
    //操作后的数组
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++) 
            printf("%d ", a[i][j]);
    return 0;
}

五、双指针算法

核心思想:将算法的时间复杂度从O(n^2)优化到O(n)

for(i = 0, j = 0; i < n; i++){
    while(j < i && check(i, j)) j++;
    
    //每道题目的具体逻辑
}

:输入一个英文句子,输出每个单词,假设句子中每个单词之间有一个空格。

#include 
#include 

using namespace std;

int main(){
    char str[1000];
    gets(str);
    int n = strlen(str);

    for(int i = 0; i < n; i++){
        int j = i;
        while(str[j] != " ") j++;
        //这道题的具体逻辑
        for(int k = i; k < j; k++) cout << str[k];
        cout << endl;
        
        i = j;
    }
    return 0;
}

1.最长不重复连续子序列

//朴素做法:O(n^2)
for(int i = 0; i < n; i++){//i为终点
    for(int j = 0; j <= i; j++){//j为起点
        if(check(j, i)){
            res = max(res, i - j + 1);
            break;
        }
    }
}
//改进版:O(n)
for(int i = 0, j = 0; i < n; i++){
    while(j <= i && !check(j, i)) j++;
    res = max(res, i - j + 1);
}

 题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size();
        unordered_map umap;//创建,前者为key,后者为value
        int res = 0;
        for(int i = 0, j = 0; i < n; i++){
            umap[s[i]]++;
            while(umap[s[i]] > 1){
                //这里憋写反了
                umap[s[j]]--;
                j++; 
            }
            res = max(res, i - j + 1);
        }
        return res;
    }
};

六、位运算

1.n的二进制表示中第k位数字是几

(最右边是第0位)

(1)先把第k位移到最后一位:n>>k
(2)看一下最右边是几:x&1

#include 
#include 

using namespace std;

//输出10的每一位(从右边开始)
int main(){
    int n = 10;
    for(int k = 3; k >= 0; k--) cout << (n >> k & 1) << endl;
    return 0;
}

2.lowbit(x)

x = 1010        lowbit(x) = 10
x = 101000        lowbit(x) = 1000

-x = ~x + 1(补码=反码+1)
lowbit(x) = x & (-x) = x & (~x + 1)

作用1:求x的二进制表示中1的个数

int lowbit(int x){
    return x & -x;
}


//main
int res = 0;
while(x) x -= lowbit(x), res++;//每次减去x的最后一位1
return res;

一(三)01:03:54

七、离散化

八、区间合并

你可能感兴趣的:(算法,算法,c++)