【算法】算法基础入门详解:轻松理解和运用基础算法

news2024/11/26 16:44:26

😀大家好,我是白晨,一个不是很能熬夜😫,但是也想日更的人✈。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪

在这里插入图片描述

文章目录

  • 前言
  • 算法基础
    • 一、快速排序
    • 二、归并排序
    • 三、二分算法
    • 四、高精度算法
    • 五、前缀和与差分
    • 六、双指针算法
    • 七、位运算
    • 八、离散化
    • 九、区间合并
  • 后记

前言


大家好呀,我是白晨😜!这段时间身边🐏的人挺多的,白晨就老老实实在家摸鱼,真不是有意拖更的(bushi)。本次白晨想要分享的是新手学习必会的几种基础算法,由于这篇文章是新手向的,所以白晨这次对于算法思想尽量讲解的细致生动,代码实现尽量简洁易懂,同时我会贴上练习算法的题目链接,大家看完算法思路一定要自己去动手敲一遍,争取能把基础算法背下来。

算法的代码风格是偏向于快速实用的,没有像工程向代码一样严谨缜密、缩进和换行严格要求,两种代码风格各有优势,本篇文章大多数算法代码采用算法风格。

本篇文章内容分享的算法有:

  • 快速排序
  • 归并排序
  • 二分算法
  • 高精度算法
  • 前缀和与差分
  • 双指针算法
  • 位运算
  • 离散化
  • 区间合并

算法基础


一、快速排序


快速排序是一种高效且使用广泛的排序算法。它的基本思想是选取一个记录作为枢轴,经过一趟排序,将整段序列分为两个部分,其中一部分的值都小于枢轴,另一部分都大于枢轴。然后再对左右区间重复这个过程,直到各区间只有一个数。

实现快速排序算法的关键在于先在待排序的数组中选择一个数字作为枢轴,然后把数组中的数字分成两部分,比枢轴小的数字移动到数组的左边,比枢轴大的数字移动到数组的右边;之后用递归的思路分别对左右两边进行排序。

下面用两道例题带领大家快速理解掌握快速排序:

image-20221226163301389

🍬题目链接:快速排序

🍎算法思想:

  • 快速排序的通用思路:

    1. 选择一个枢轴元素,通常选择数组的第一个元素或者中间的元素。
    2. 将数组中小于枢轴元素的值移动到它的左边,将大于枢轴元素的值移动到它的右边。
    3. 对枢轴元素左右两边的子数组递归执行步骤1和2,直到子数组只剩下一个元素。
  • 模板快速实现详解:

    1. 定义 quick_sort 函数,接受数组指针、左边界和右边界作为参数。
    2. quick_sort 函数中,首先判断左右边界是否合法,如果不合法则直接返回。
    3. 选择一个枢轴元素 key,这里选择的是数组中间的元素。
    4. 使用双指针 ij 分别从左右两端开始扫描数组,当 i 指向的元素小于枢轴元素时,i 向右移动;当 j 指向的元素大于枢轴元素时,j 向左移动。当 i < j 时交换两个指针所指向的元素。
    5. 最后递归对枢轴元素左右两边的子数组进行快速排序。

🍊具体实现:

#include <iostream>

using namespace std;

const int N = 100010;

int q[N];

void quick_sort(int* q, int left, int right)
{
    if (left >= right) // 如果左右边界不合法,直接返回
        return;

    int i = left - 1, j = right + 1;
    int key = q[left + right >> 1]; // 选择枢轴元素

    while (i < j)
    {
        do i++; while (q[i] < key); // i之前的数全部小于等于key
        do j--; while (q[j] > key); // j之后的数全部大于等于key

        if (i < j) swap(q[i], q[j]); // 如果i<j,交换两个指针所指向的元素
    }
    
    quick_sort(q, left, j); // 对左边子数组进行快速排序
    quick_sort(q, j + 1, right); // 对右边子数组进行快速排序
}

int main()
{
    int n;
    scanf("%d", &n);

    for (int i = 0; i < n; i++) scanf("%d", &q[i]);

    quick_sort(q, 0, n - 1); // 对整个数组进行快速排序

    for (int i = 0; i < n; i++) printf("%d ", q[i]);

    return 0;
}

二、归并排序


image-20221226163947134

🍬题目链接:归并排序

  1. 归并排序是一种分治算法,它将一个大数组分成两个小数组,然后递归地对这两个小数组进行排序,最后将两个排好序的小数组合并成一个有序的大数组。
  2. merge_sort函数中,首先判断待排序区间长度是否小于等于1,如果是,则直接返回。否则计算中点mid,然后对左半部分和右半部分分别进行归并排序。
  3. 在对左右两部分排好序之后,使用双指针技术将两个有序的小数组合并成一个有序的大数组。具体来说,定义三个指针ijk,其中ij分别指向左右两部分的起始位置,而k指向辅助数组tmp的起始位置。当i和j都在各自的区间内时,比较v[i]和v[j]的大小关系,并将较小者放入tmp中,并移动相应的指针。当其中一个区间内所有元素都已经放入tmp中时,则依次将另一个区间内剩余元素放入tmp中。
  4. 最后将排好序的tmp复制回原数组v中。
#include <iostream>

using namespace std;

const int N = 1e5 + 10; // 定义常量N为100010

int v[N], tmp[N]; // 定义整型数组v和tmp,大小均为N

// 归并排序函数,参数为待排序数组v,以及待排序区间[left, right]
void merge_sort(int v[], int left, int right)
{
    if (left >= right) // 如果待排序区间长度小于等于1,则直接返回
        return;
    int mid = left + right >> 1; // 计算中点mid

    merge_sort(v, left, mid); // 对左半部分进行归并排序
    merge_sort(v, mid + 1, right); // 对右半部分进行归并排序

    int i = left, j = mid + 1, k = left; // 定义三个指针i、j、k
    while (i <= mid && j <= right) // 当i和j都在各自的区间内时
    {
        if (v[i] < v[j]) tmp[k++] = v[i++]; // 如果v[i] < v[j],则将v[i]放入tmp中,并移动指针i和k
        else tmp[k++] = v[j++]; // 否则将v[j]放入tmp中,并移动指针j和k
    }

    while (i <= mid) // 如果左半部分还有剩余元素,则依次放入tmp中
        tmp[k++] = v[i++];
    while (j <= right) // 如果右半部分还有剩余元素,则依次放入tmp中
        tmp[k++] = v[j++];

    for (i = left; i <= right; ++i) // 将排好序的tmp复制回原数组v中
        v[i] = tmp[i];
}

int main()
{
    int n;
    cin >> n; // 输入元素个数n
    for (int i = 0; i < n; ++i)
        scanf("%d", &v[i]); // 输入n个元素

    merge_sort(v, 0, n - 1); // 对整个数组进行归并排序

    for (int i = 0; i < n; ++i)
        printf("%d ", v[i]); // 输出排好序的结果
    return 0;
}

三、二分算法


二分法,即二分搜索法,是通过不断缩小解可能存在的范围,从而求得问题最优解的方法。例如,如果一个序列是有序的,那么可以通过二分的方法快速找到所需要查找的元素,相比线性搜索要快不少。此外二分法还能高效的解决一些单调性判定的问题。

// 模板一
// 求满足check条件的最左下标
#include <iostream>

using namespace std;

template<class T>
int binary_search1(T* v, int l, int r)
{
	while (l < r)
	{
        int mid = l + r >> 1;
		if (check(v[mid])) // check中 v[mid] 永远放在前面,eg. v[mid] >= a
			r = mid;
		else
			l = mid + 1;
	}

	return mid;
}

// 模板二
// 求满足check条件的最右下标
#include <iostream>

using namespace std;

template<class T>
int binary_search1(T* v, int l, int r)
{
	while (l < r)
	{
        int mid = l + r + 1 >> 1; // 必须加一,避免死循环
		if (check(v[mid])) // eg.v[mid] <= a
			l = mid;
		else
			r = mid - 1;
	}

	return mid;
}
  • 数的范围

image-20221226164131064

🍬题目链接:数的范围

// 数的范围

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    int n, m;
    cin >> n >> m; // 输入元素个数n和查询次数m
    vector<int> v(n); // 定义整型向量v,大小为n
    for (int i = 0; i < n; ++i)
        cin >> v[i]; // 输入n个元素

    while (m--) // 循环执行m次查询操作
    {
        int num;
        cin >> num; // 输入要查询的元素num

        // 先找等于num的最左下标
        int l = 0, r = n - 1; // 定义双指针l和r
        while (l < r) // 当l<r时循环执行以下操作
        {
            int mid = l + r >> 1; // 计算中点mid

            if (v[mid] >= num) // 如果v[mid] >= num,则在左半部分继续查找
                r = mid;
            else // 否则在右半部分继续查找
                l = mid + 1;
        }

        if (v[l] != num) // 如果没有找到num,则输出"-1 -1"
        {
            cout << "-1 -1" << endl;
            continue;
        }
        else
        {
            cout << l << " "; // 输出最左下标l

            l = 0, r = n - 1; // 重新定义双指针l和r

            // 再找等于num的最右下标
            while (l < r) // 当l<r时循环执行以下操作
            {
                int mid = l + r + 1 >> 1; // 计算中点mid

                if (v[mid] <= num) // 如果v[mid] <= num,则在右半部分继续查找
                    l = mid;
                else 
                    r = mid - 1;
            }

            cout << l << endl;
        }
    }
    return 0;
}

四、高精度算法


高精度算法是用计算机对于超大数据的一种模拟加,减,乘,除,乘方,阶乘等运算的方法,它用于处理大数字的数学计算。

比如,C++的long long类型最多处理 2 64 2^{64} 264这么大的数,再大的数则无法处理。

  • 高精度加法

我们可以模拟正常的加法,从个位开始,逐位相加,模拟过程中要注意的是:

  • 我们取出字符串的每一个元素都是字符,所以不能直接将其相加,必须要减去'0' 才能得到这个数的真实值。
  • 当一个数的每一位都已经遍历完了,如果另一个数还没遍历完,则在这个数的高位补0。
  • 如果两个数字之和大于等于10,要进位。
  • 每次向要返回字符串插入一个本次相加得到的个位数
  • 最后得到的返回字符串是反的,我们要将其反转。

image-20221226164328456

🍬题目链接:高精度加法

// 高精度加法

#include <iostream>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

vector<int> add(vector<int>& a, vector<int>& b)
{
    int t = 0; // 进位
    vector<int> c;

    // 当a,b大数没有遍历完或者进位不为0时,继续循环
    for (int i = 0; i < a.size() || i < b.size() || t != 0; ++i)
    {
        // 数字没有遍历完才加
        if (i < a.size()) t += a[i];
        if (i < b.size()) t += b[i];
        c.push_back(t % 10);
        t /= 10;
    }

    return c;
}

int main()
{
    string a, b;
    cin >> a >> b;
    vector<int> a, b;
    // 字符串接收数据,用数组来保存数据,其实也可以直接使用字符串计算,为了方便理解以及美观好记
    // 暂时使用数组来存储大数的每一位,但是有一点要注意,数组存储大数时最好反着存,也即0下标存最低位,高下标存高位,类似于小端
    // 因为加法可能要进位,如果0下标存最高位,进位必须整体向后移动一位,这样太耗费时间
    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(b[i] - '0');

    vector<int> c = add(a, b);

    for (auto rit = c.rbegin(); rit != c.rend(); ++rit)
        printf("%d", *rit);

    return 0;
}

压位优化版(了解即可)

它首先定义了一个常量 base1e9,表示每个元素存储9位数。然后定义了一个 add 函数,用于计算两个大数的和。在主函数中,首先读入两个字符串 ab,然后将它们转换为大数存储在向量 AB 中。最后调用 add 函数计算它们的和并输出结果。

#include <iostream>
#include <vector>
#include <string>

using namespace std;

const int base = 1e9;

vector<int> add(vector<int>& A, vector<int>& B)
{
    int t = 0; // 进位
    vector<int> C;
    
    // 当A,B大数没有遍历完或者进位不为0时,继续循环
    for (int i = 0; i < A.size() || i < B.size() || t != 0; ++i)
    {
        // 数字没有遍历完才加
        if (i < A.size()) t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % base);
        t /= base;
    }
    
    if (t) C.push_back(t);
    
    return C;
}

int main()
{
    string a, b;
    cin >> a >> b;
    vector<int> A, B;
    // 压位,一次将9个数字存储到一个位置,可以节省空间以及加快运算
    for (int i = a.size() - 1, s = 0, t = 1, j = 0; i >= 0; --i)
    {
        s += (a[i] - '0') * t;
        j++, t *= 10;
        
        if (j == 9 || i == 0)
        {
            A.push_back(s);
            j = 0, s = 0, t = 1;
        }
    }
    
    for (int i = b.size() - 1, s = 0, t = 1, j = 0; i >= 0; --i)
    {
        s += (b[i] - '0') * t;
        j++, t *= 10;
        
        if (j == 9 || i == 0)
        {
            B.push_back(s);
            j = 0, s = 0, t = 1;
        }
    }
    vector<int> C = add(A, B);
    
    cout << C.back();
    // 高位为0的需要用前导0填充
    for (int i = C.size() - 2; i >= 0; --i) printf("%09d", C[i]);
    
    return 0;
}
  • 高精度减法

和加法大体思路相同,不过要先判断被减数和减数的大小,确定结果的正负。

image-20221226164419455

🍬题目链接:高精度减法

// 高精度减法

#include <iostream>
#include <vector>
#include <string>

using namespace std;

// a大于等于b,返回true;反之,返回false
bool compare(string& a, string& b)
{
    if (a.size() != b.size())
        return a.size() > b.size();

    for (int i = 0; i < a.size(); ++i)
        if (a[i] != b[i])
            return a[i] > b[i];

    return true;
}

vector<int> sub(vector<int>& a, vector<int>& b)
{
    vector<int> c;
    int t = 0; // 借位

    for (int i = 0; i < a.size(); ++i)
    {
        t = a[i] - t;
        if (i < b.size())
            t -= b[i];
        // 无论借没借位,先加上10再模10,就是这一位的值
        c.push_back((t + 10) % 10);
        // 如果没有借位a[i] - t >= b[i],那么t最后是大于等于0
        // 反之,小于0
        if (t < 0) t = 1;
        else t = 0;
    }

    while (c.size() > 1)
    {
        if (c.back() != 0)
            break;
        c.pop_back();
    }

    return c;
}

int main()
{
    string a, b;
    cin >> a >> b;
    vector<int> 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(b[i] - '0');

    vector<int> c;

    if (compare(a, b))
        c = sub(a, b);
    else
    {
        c = sub(b, a);
        cout << "-";
    }

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

    return 0;
}
  • 高精度乘法

先来看乘法用竖式如何计算:

在这里插入图片描述

  • 我们发现,从右到左, num2 每一位都需要乘以 num1 ,并且每乘完一次 num1 所得的数字的权重要乘10。

  • num2 每一位乘 num1 都是个位数* num1 ,所以我们可以先把个位数乘 num1 的结果保存起来,用的时候直接调用。

  • 得到 num2 每一位乘 num1 的字符串后,保存起来,最后和竖式一样,依次相加每一位的结果,得到最后的答案。

image-20221226164508900

🍬题目链接:高精度乘法

题目中 B 的值小于等于 10000,可以直接用 A 的每一位乘 B,具体实现见代码。

// 高精度乘法

#include <iostream>
#include <vector>
#include <string>

using namespace std;

vector<int> mul(vector<int>& A, const int& b)
{
    int t = 0; // 进位
    vector<int> C;

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

    // 删去前导0,防止乘0的特殊情况
    while (C.size() > 1)
    {
        if (C.back() != 0)
            break;
        C.pop_back();
    }
    return C;
}

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

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

  • 高精度除法

思路和上述高精度算法大体相同,具体见代码。

image-20221226164545568

🍬题目链接:高精度除法

// 高精度除法
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

vector<int> div(vector<int>& A, const int& b, int& r)
{
    int t = 0;
    vector<int> C;
    // 除法要从高位开始
    for (int i = A.size() - 1; i >= 0; --i)
    {
        t = t * 10 + A[i];
        C.push_back(t / b);
        t %= b;
    }

    r = t;

    // 由于C是从高位开始记录的,返回时需要反转一下,恢复低位存低位,高位存高位的标准
    reverse(C.begin(), C.end());

    // 除去前导0
    while (C.size() > 1 && C.back() == 0)
        C.pop_back();

    return C;
}

int main()
{
    string a;
    int b;
    cin >> a >> b;
    vector<int> A;
    for (int i = a.size() - 1; i >= 0; --i)
        A.push_back(a[i] - '0');
    int r = 0; // 余数
    vector<int> C = div(A, b, r);

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

五、前缀和与差分


  • 一维前缀和

image-20221226164627343

🍬题目链接:前缀和

首先定义一个函数 get_part_sum,用于计算区间和。在主函数中,首先读入两个整数 nm,然后读入一个长度为 n 的数组 v。接着计算前缀和数组 prefix,最后循环读入查询区间的左右端点 lr,并输出区间和。

// 一维前缀和
#include <iostream>
#include <vector>
using namespace std;

// 计算区间和
int get_part_sum(vector<int>& prefix, int l, int r)
{
    // 区间和 = 前缀和[r] - 前缀和[l - 1]
    return prefix[r] - prefix[l - 1];
}

int main()
{
    int n, m;
    cin >> n >> m;
    vector<int> v(n + 1);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &v[i]);
    
    // 计算前缀和数组
    vector<int> prefix(n + 1);
    for (int i = 1; i <= n; ++i)
        // 前缀和[i] = 前缀和[i - 1] + v[i]
        prefix[i] = prefix[i - 1] + v[i];
    
    int l, r;
    while (cin >> l >> r)
    {
        // 输出区间[l,r]的区间和
        cout << get_part_sum(prefix, l, r) << endl;
    }
    
    return 0;
}
  • 二维前缀和

image-20221226164730481

🍬题目链接:子矩阵的和

// 二维前缀和
#include <iostream>
#include <vector>

using namespace std;

int get_part_sum(vector<vector<int>>& prefix, int x1, int y1, int x2, int y2)
{
    return prefix[x2][y2] - prefix[x1 - 1][y2] - prefix[x2][y1 - 1] + prefix[x1 - 1][y1 - 1];
}

int main()
{
    int n, m, q;
    cin >> n >> m >> q;
    vector<vector<int>> vv(n + 1, vector<int>(m + 1, 0));
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            scanf("%d", &vv[i][j]);
    vector<vector<int>> prefix(n + 1, vector<int>(m + 1, 0));
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            prefix[i][j] = prefix[i - 1][j] + prefix[i][j - 1] - prefix[i - 1][j - 1] + vv[i][j];

    // 二维前缀和计算公式:
    // sum(x1, y1, x2, y2) = prefix(x2, y2) - prefix(x1 - 1, y2) - prefix(x2, y1 - 1) + prefix(x1 - 1, y1 - 1)
    int x1, y1, x2, y2;
    while (q--)
    {
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        cout << get_part_sum(prefix, x1, y1, x2, y2) << endl;
    }
    return 0;
}
  • 一维差分

差分算法是一种利用原数组和差分数组的关系,来快速地对数组中某个区间进行同一操作的方法。例如,如果要对原数组a[]中[l,r]之间的每个数加上c,只需要对差分数组b[]中的b[l]加上c,b[r+1]减去c,然后重新求出a[]即可。

如何构造差分数组呢?

构造差分数组的方法很简单,只需要用原数组的相邻元素的差来赋值即可。例如,如果原数组a[]为{1,2,4,6},那么差分数组b[]为{1,1,2,2},因为b[1]=a[1]-a[0],b[2]=a[2]-a[1],以此类推。

反之,从差分数组求原数组就为:a[i] = a[i - 1] + b[i](i > 0)。

image-20221226164807160

🍬题目链接:差分

一维差分数组是一种处理区间修改和查询的方法,它可以在O(1)的时间内实现对原数组某个区间内所有元素加上一个常数。具体来说,如果原数组是a[],差分数组是b[],那么有以下关系:

  • a[i] = b[1] + b[2] + … + b[i]
  • b[i] = a[i] - a[i-1]
// 一维差分
#include <iostream>
#include <vector>

using namespace std;

void insert(vector<int>& dif, int l, int r, int c)
{
    dif[l] += c;
    dif[r + 1] -= c;
}

int main()
{
    int n, m;
    cin >> n >> m;
    vector<int> v(n + 2);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &v[i]);
    // 差分公式:mod(l, r, c) : dif(l) + c and dif(r + 1) - c 
    vector<int> dif(n + 2, 0);
    // 差分数组初始化:假设 前缀和 和 差分数组 都是0开始,前缀和数组(i位置)每插入一个数字相当于 在差分数组(i, i)位置插入v[i]
    for (int i = 1; i <= n; ++i)
        insert(dif, i, i, v[i]);
    int l, r, c;
    while (m--)
    {
        cin >> l >> r >> c;
        insert(dif, l, r, c);
    }

    for (int i = 1; i <= n; ++i)
        v[i] = v[i - 1] + dif[i];

    for (int i = 1; i <= n; ++i)
        printf("%d ", v[i]);
    return 0;
}
  • 二维差分

image-20221226164856458

🍬题目链接:差分矩阵

// 二维差分
#include <iostream>
#include <vector>

using namespace std;

const int N = 1010;

int v[N][N];
int dif[N][N];

// 二维差分公式 insert(x1, y1, x2,  y2, c):
// dif[x1][y1] + c && dif[x2 + 1][y1] - c && dif[x1][y2 + 1] - c && dif[x2 + 1][y2 + 1] + c
//  定义一个函数,用于对差分数组进行修改
void insert(int x1, int y1, int x2, int y2, int c)
{
    dif[x1][y1] += c;
    dif[x2 + 1][y1] -= c;
    dif[x1][y2 + 1] -= c;
    dif[x2 + 1][y2 + 1] += c;
}

// 二维差分: dif[n][m] 会直接影响 v[n][m] 及右下方的值
int main()
{
    int n, m, q;
    cin >> n >> m >> q;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            scanf("%d", &v[i][j]);
    // 初始化差分数组
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            insert(i, j, i, j, v[i][j]);
    int x1, y1, x2, y2, c;
    while (q--)
    {
        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)
            v[i][j] = v[i - 1][j] + v[i][j - 1] - v[i - 1][j - 1] + dif[i][j];

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= m; ++j)
            printf("%d ", v[i][j]);
        printf("\n");
    }
    return 0;
}

六、双指针算法


双指针算法是一种在重复遍历对象的过程中,使用两个指针进行访问的方法。它可以利用对象的某种单调性,减少不必要的遍历次数,从而优化时间复杂度。双指针算法有两种常见的类型:快慢指针和对撞指针。

  • 快慢指针是指两个指针以不同的速度或步长在同一个方向上移动,通常用于解决链表中的问题,比如判断链表是否有环,找到链表的中点或倒数第k个结点等。
  • 对撞指针是指两个指针从相反的方向上移动,直到相遇或满足某个条件为止,通常用于解决数组或字符串中的问题,比如寻找两数之和等于目标值的下标,判断一个字符串是否是回文串等。
  • 模板
// 双指针模板
bool check(int i, int j);

void two_pointer()
{
	int n;
	for (int i = 0, j = 0; i < n; ++i)
	{
		while (j <= i && check(i, j))
		{
			// ......
			j++;
			// ......
		}

		// .....
	}
}
  • 最长连续不重复子序列

image-20221226164934011

🍬题目链接:最长连续不重复子序列

#include <iostream>

using namespace std;

const int N = 100010; int v[N]; // 存储输入的数组 
int book[N];//判重数组,用于记录每个数字出现的次数

int main() 
{ 	
    int n; // 数组的长度 
    cin >> n; for (int i = 0; i < n; ++i) cin >> v[i]; int Max = 0; // 记录最长不重复子数组的长度

	for (int i = 0, j = 0; i < n; ++i) // i和j是两个指针,分别指向子数组的右端点和左端点
	{
    	book[v[i]]++; // 将当前数字的计数加一
    	// 当有重复数字时
    	while (j <= i && book[v[i]] > 1) // 如果当前数字出现了两次以上,说明子数组中有重复元素,需要缩小左边界
    	{
        	// j指向的数字计数--,并且j++,直到数组中没有重复数字
        	book[v[j]]--;
        	j++;
    	}
    	Max = max(Max, i - j + 1); // 更新最长不重复子数组的长度
	}
	cout << Max << endl; // 输出结果
	return 0;
}
  • 数组元素目标和

image-20221226165041031

🍬题目链接:数组元素的目标和

#include <iostream>

using namespace std;

const int N = 100010;

int v1[N]; // 存储第一个有序数组 
int v2[N]; // 存储第二个有序数组

int main() 
{ 
	int n, m, t; // n和m分别是两个数组的长度,t是目标值 
    cin >> n >> m >> t; 
    for (int i = 0; i < n; ++i) cin >> v1[i]; 
    for (int i = 0; i < m; ++i) cin >> v2[i];

	int i = 0, j = m - 1; // i和j是两个指针,分别指向第一个数组的左端点和第二个数组的右端点
	while (i < n && j >= 0) // 当两个指针没有越界时
	{
    	if (v1[i] + v2[j] == t) // 如果两个指针指向的数字之和等于目标值,输出下标对,并将左指针右移
    	{
        	cout << i << " " << j << endl;
        	i++;
    	}
    	else if (v1[i] + v2[j] > t) // 如果两个指针指向的数字之和大于目标值,说明右边的数字太大,需要将右指针左移
        	j--;
   		else // 如果两个指针指向的数字之和小于目标值,说明左边的数字太小,需要将左指针右移
        	i++;
	}
	return 0;
}

七、位运算


  • 获取n的二进制表示中第k位(最低位从0开始)
int n;
int k;
int bitk = n >> k & 1;
  • 获取n的二进制表示中最右边的1
// lowbit:获取n的二进制表示中最右边的1,eg. 输入 10010 返回 00010
int rightbit1 = n & -n; // 等价于 n & (~n + 1)   
  • 二进制中1的个数

image-20221226165156569

🍬题目链接:二进制中1的个数

#include <iostream>

using namespace std;

const int N = 1e5 + 10;
int v[N];


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

    for (int i = 0; i < n; ++i)
    {
        int num = v[i];
        int cnt = 0;
        while (num)
        {
            int x = num & -num; // lowbit
            cnt++;
            num ^= x; // 去除最后一个1
        }
        cout << cnt << " ";
    }
    cout << endl;
    return 0;
}

八、离散化


  • 离散化定义
整数保序离散化
将数据范围很大,但是数字个数很少的数据按照升序对数字坐标进行映射
如,数据范围 10^9 数字个数 10^5 
                      1 200000 88888888 1e9
将上述数据映射到      0    1      2      3
简而言之,就是一种哈希
  • 区间和

image-20221226165255009

🍬题目链接:区间和

// 区间和

// 原版
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 3 * 1e5 + 1;

vector<int> alls; // 存放所有坐标,将其离散化
vector<pair<int, int>> operates; // 所有操作
vector<pair<int, int>> query; // 所有查询

int Hash[N]; // 离散化坐标对应的值
int psum[N]; // 前缀和数组

// 以原坐标值查询映射后的坐标值
int find(int x)
{
    // 二分查找
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x)
            r = mid;
        else
            l = mid + 1;
    }
    return l + 1;
}

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; ++i)
    {
        int x, c;
        cin >> x >> c;

        operates.emplace_back(x, c);
        alls.push_back(x);
    }

    for (int i = 0; i < m; ++i)
    {
        int l, r;
        cin >> l >> r;

        query.emplace_back(l, r);
        alls.push_back(l);
        alls.push_back(r);
    }

    sort(alls.begin(), alls.end()); // 坐标排序
    alls.erase(unique(alls.begin(), alls.end()), alls.end());// 去重,去除重复坐标
    for (int i = 0; i < n; ++i)
    {
        // 由于前缀和数组的下标要从1开始,所有,所有映射坐标+1
        int x = find(operates[i].first), c = operates[i].second;
        Hash[x] += c;
    }

    // 构造前缀和数组
    for (int i = 1; i <= alls.size(); ++i)
        psum[i] = psum[i - 1] + Hash[i];

    for (int i = 0; i < m; ++i)
    {
        int l = find(query[i].first), r = find(query[i].second);
        cout << psum[r] - psum[l - 1] << endl;
    }
    return 0;
}

九、区间合并


  • 区间合并

区间合并算法是一种用于把给定的、可以合并的区间合并的算法。它的基本思想是:

  1. 将所有区间按照左边界值进行从小到大排序。
  2. 遍历所有区间,如果第i+1区间的左边界值比第i区间的右边界值小,即可合并,更新i+1区间的边界值,并且将第i区间打上标记表示该区间被i+1合并过。
  3. 最后输出没有被标记过的区间,即为合并后的结果。

image-20221226165423106

🍬题目链接:区间合并

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;
vector<PII> v;

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; ++i)
    {
        int l, r;
        cin >> l >> r;
        v.emplace_back(l, r);
    }
    sort(v.begin(), v.end());

    int l = -2e9, r = -2e9; // 初始化变量 l 和 r 的值为 -2e9
    int cnt = 0; // 初始化计数器 cnt 的值为 0
    for (auto& e : v) // 遍历 vector 中的每个元素 e
    {
        int nl = e.first, nr = e.second; 
        if (nl > r) // 如果新区间与当前区间不相交
        {
            cnt++; // 区间计数器加一
            l = nl; 
        }
        r = max(nr, r); // 更新当前区间右端点
    }
    cout << cnt << endl;
    return 0;
}

后记


在这篇文章中,我们介绍了算法基础的一些概念和方法,包括算法的定义、特征、分类、设计和分析等。我们也通过一些例子展示了如何运用算法解决实际问题,并且分析了算法的效率和优化。我们希望这篇文章能够给你提供一个对算法基础的初步认识和兴趣,让你感受到算法的魅力和价值。

img


如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。

如果大家喜欢这个系列,还请大家多多支持啦😋!

如果这篇文章有帮到你,还请给我一个大拇指 👍和小星星 ⭐️支持一下白晨吧!喜欢白晨【算法】系列的话,不如关注👀白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/402980.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

查找Pycharm跑代码下载模型存放位置以及有关模型下载小技巧(model_name_or_path参数)

目录一、前言二、发现问题三、删除这些模型方法一&#xff1a;直接删除注意方法二&#xff1a;代码删除一、前言 当服务器连不上&#xff0c;只能在本地跑代码时需要使用***预训练语言模型进行处理 免不了需要把模型下载到本地 时间一长就会发现C盘容量不够 二、发现问题 正…

c++11 标准模板(STL)(std::unordered_map)(九)

定义于头文件 <unordered_map> template< class Key, class T, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator< std::pair<const Key, T> > > class unordered…

Python进阶-----高阶函数zip() 函数

目录 前言&#xff1a; zip() 函数简介 运作过程&#xff1a; 应用实例 1.有序序列结合 2.无序序列结合 3.长度不统一的情况 前言&#xff1a; 家人们&#xff0c;看到标题应该都不陌生了吧&#xff0c;我们都知道压缩包文件的后缀就是zip的&#xff0c;当然还有r…

Mybatis源码分析系列之第四篇:Mybatis中代理设计模型源码详解

一&#xff1a; 前言 我们尝试在前几篇文章的内容中串联起来&#xff0c;防止各位不知所云。 1&#xff1a;背景 我们基于Mybatis作为后台Orm框架进行编码的时候&#xff0c;有两种方式。 //编码方式1 UserDao userDao sqlSession.getMapper(UserDao.class); userDao.quer…

[入门必看]数据结构1.1:数据结构的基本概念

[入门必看]数据结构1.1&#xff1a;数据结构的基本概念第一章 绪论1.1 数据结构的基本概念知识总览1.1.1 基本概念和术语数据类型、抽象数据类型&#xff1a;1.1.2 数据结构的三要素数据的逻辑结构数据的物理结构&#xff08;存储结构&#xff09;数据的运算知识回顾与重要考点…

【数据库概论】第十一章 数据库并发控制

第十一章 并发控制 在多处理机系统中&#xff0c;每个处理机可以运行一个事务&#xff0c;多个处理机可以同时运行多个事务&#xff0c;实现多个事务并行运行&#xff0c;这就是同时并发方式。当多个用户并发存取数据库时会产生多个事务同时存取同一事务的情况&#xff0c;如果…

ESP32设备驱动-红外寻迹传感器驱动

红外寻迹传感器驱动 1、红外寻迹传感器介绍 红外寻迹传感器具有一对红外线发射管与接收管,发射管发射出一定频率的红外线,当检测方向遇到障碍物(反射面)时,红外线反射回来被接收管接收,经过比较器电路处理之后,输出接口会输出一个数字信号(低电平或高电平,取决于电路…

Nginx配置实例-反向代理案例二

实现效果&#xff1a;使用nginx反向代理&#xff0c;根据访问的路径跳转到不同端口服务 nginx监听端口为9000&#xff0c; 访问 http://127.0.0.1:9000/edu/ 直接跳转到127.0.0.1:8080 访问 http://127.0.0.1:9000/vod/ 直接跳转到127.0.0.1:8081 一、准备工作 1. 准备两个tom…

TCP相关概念

目录 一.滑动窗口 1.1概念 1.2滑动窗口存在的意义 1.3 滑动窗口的大小变化 1.4丢包问题 二.拥塞控制 三.延迟应答 四.捎带应答 五.面向字节流 六.粘包问题 七.TIME_WAIT状态 八.listen第2个参数 九.TCP总结 一.滑动窗口 1.1概念 概念&#xff1a;双方在进行通信时&a…

2023年软考高级-系统分析师考试学习指导计划!

2023年软考高级-系统分析师考试学习指导计划&#xff01; 一、导学阶段 第一天 考试情况及备考攻略&#xff1a;https://www.educity.cn/xuanke/rk/rjsp/?sywzggw 考试介绍&#xff1a;https://www.educity.cn/wangxiao2/c410653/sp110450.html 考试经验分享&#xff1a;h…

如何在Linux中优雅的使用 head 命令,用来看日志简直溜的不行

当您在 Linux 的命令行上工作时&#xff0c;有时希望快速查看文件的第一行&#xff0c;例如&#xff0c;有个日志文件不断更新&#xff0c;希望每次都查看日志文件的前 10 行。很多朋友使用文本编辑的命令是vim&#xff0c;但还有个命令head也可以让轻松查看文件的第一行。 在…

记录使用chatgpt的复杂经历

背景 由于最近要写论文&#xff0c;c站的gpt也变样了&#xff0c;无奈之下和同学借了一个gpt账号&#xff0c;才想起没有npv&#xff0c;不好意思去要&#xff0c;也不想买&#xff0c;于是我找了很多临时免费的直到我看到有一家&#xff0c;邀请10人即可&#xff0c;而且只用…

江苏专转本 专科生的最好出入

专转本 专科生的最好出入(1) 减少每天刷某音&#xff0c;刷朋友圈的时间无所事事、捧着手机刷的不是某音就是朋友圈&#xff0c;三年下来没去成一次图书馆。碎片化信息很大程度只是在浪费时间&#xff0c;不如每天比同龄人至少多出1小时时间&#xff0c;用来看书、护肤、健身。…

磨金石教育摄影技能干货分享|高邮湖上观花海

江苏高邮&#xff0c;说到这里所有人能想到的&#xff0c;就是那烟波浩渺的高邮湖。高邮在旅游方面并不出名&#xff0c;但是这里的自然人文景观绝对不输于其他地方。高邮不止有浩瀚的湖泊&#xff0c;春天的油菜花海同样壮观。春日的午后&#xff0c;与家人相约游玩&#xff0…

C语言--字符串函数1

目录前言strlenstrlen的模拟实现strcpystrcatstrcat的模拟实现strcmpstrcmp的模拟实现strncpystrncatstrncmpstrstrstrchr和strrchrstrstr的模拟实现前言 本章我们将重点介绍处理字符和字符串的库函数的使用和注意事项。 strlen 我们先来看一个我们最熟悉的求字符串长度的库…

【Echart多场景示例应用】Echarts柱状图、折线图、饼图、雷达图等完整示例。 echarts主标题和副标题的位置、样式等设置(已解决附源码)

**【写在前面】**前端时间做一个echarts的页面调整&#xff0c;临时客户要求加一个参数&#xff08;总容量&#xff09;显示&#xff0c;当时我就想用个默认的副标题吧&#xff0c;哪知客户和我说得紧跟在主标题后面&#xff0c;于是乎我就根据设置做了一个调整&#xff0c;我也…

燕山大学-面向对象程序设计实验 - 实验1 C++基础

CSDN的各位uu们你们好,今天千泽燕山大学-面向对象程序设计实验 - 实验1 C基础 相关内容, 接下来让我们一起进入c的神奇小世界吧,相信看完你也能写出自己的实验报告!实验一 C基础 1.1 实验目的 1&#xff0e;了解并熟悉开发环境&#xff0c;学会调试程序&#xff1b; 2&#xf…

超过10000人学习的Fiddler抓包教程,只需一小时就可以精通!

如果还是有朋友不太明白的话&#xff0c;可以看看这套视频&#xff0c;有实战讲解 零基础玩转Fiddler抓包在测试领域应用实战&#xff01;一、Fiddler与其他抓包工具的区别 1、Firebug虽然可以抓包&#xff0c;但是对于分析http请求的详细信息&#xff0c;不够强大。模拟http请…

【Linux】信号的产生、保存、捕捉处理 (四种信号产生、核心存储、用户态与内核态、信号集及其操作函数)

文章目录1、什么是信号&#xff1f;2、信号的产生2.1 通过键盘产生信号2.2 通过系统调用产生信号2.3 硬件异常产生的信号2.4 由软件条件产生的信号2.5 进程的核心转储3、信号的保存4、信号的捕捉4.1 用户态和内核态4.2 用户态到内核态的切换4.3 信号捕捉过程5、信号集操作函数以…

Spring——Spring整合Mybatis(XML和注解两种方式)

框架整合spring的目的:把该框架常用的工具对象交给spring管理&#xff0c;要用时从IOC容器中取mybatis对象。 在spring中应该管理的对象是sqlsessionfactory对象&#xff0c;工厂只允许被创建一次&#xff0c;所以需要创建一个工具类&#xff0c;把创建工厂的代码放在里面&…