算法·入门篇『课堂随笔』

Zero·前言

本文章需结合 B站教学视频 共同学习与观看。


一、O(n)


1.1 分析模型基础

在分析算法复杂度之前,我们必须知道的概念:

  1. 时间复杂度:算法执行所需的时间与输入规模的关系
    • 白话:大概执行了多少次宏观上、可概括的代码段
  2. 空间复杂度:算法执行所需的额外存储空间与输入规模的关系
    • 白话:开辟了多大的空间,这个容量与输入规模的比例

常用表示法:

  • 大O记号(Big-O):表示算法的最坏情况或渐进上界
  • Ω记号(Omega):表示算法的最好情况或渐进下界
  • Θ记号(Theta):表示算法的紧确界

1.2 冒泡排序分析

void bubble_sort(int arr[], int n) {
    for (int i = 0; i < n-1; i++) {
        bool swapped = false;
        for (int j = 0; j < n-i-1; j++) {
            if (arr[j] > arr[j+1]) {
                swap(arr[j], arr[j+1]);
                swapped = true;
            }
        }
        if (!swapped) break;
    }
}

时间复杂度分析

  1. 最好情况(已有序数组):
    • 第一轮遍历后立即退出(借助swapped标志)
    • 比较次数 = n-1
    • 时间复杂度:O(n)
  2. 最坏情况(逆序数组):
    • 完全执行所有循环
    • 比较次数 = (n-1)+(n-2)+…+1 = n(n-1)/2
    • 时间复杂度:O(n²)
  3. 平均情况
    • 仍需进行大量比较和交换
    • 时间复杂度:O(n²)
  4. 稳定性分析
    • 只有相邻元素比较交换
    • 稳定排序

空间复杂度分析

  • 仅使用常数级别的额外空间(i,j和swapped变量)
  • 空间复杂度:O(1)(原地排序)

二、排序算法(入门级别)

排序是很实用的、常用的(几乎是必需品,只不过现在封装的很好,让你感觉不到)

排序的学习可以让你了解一些 算法的基本思想与操作技巧(暴力、比较、交换、分治、减治、空间换时间等)

  • 排序算法也是 分析 时间/空间复杂度的理想模型
  • 排序也是计算机科学早期研究的核心问题(如1956年希尔排序的提出

2.1 选择排序

排序算法中最简单直观的。

思路:不断缩小范围确定当前范围内的最小/最大值,随后将最小/最大值与目标位置进行交换。那么每一趟筛选的行为就是选出第n个最小/最大值!

举例: 从小到大排序下方数字

  • 18、1、5、27、12(下标范围 0 ~ 4,最小值 1 与 下标 0 的 18 交换)
  • 1、18、5、27、12(下标范围 1 ~ 4,最小值 5 与 下标 1 的 18 交换)
  • 1、5、18、27、12(下标范围 2 ~ 4,最小值 12 与 下标 2 的 18 交换)
  • 1、5、12、27、18(下标范围 3 ~ 4,最小值 18 与 下标 3 的 27 交换)
  • 1、5、12、27、18(下标范围 4 ~ 4,单个元素自然有序)
#include <iostream>

using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];
void select_sort(int *arr,int n) {
    for (int i = 0; i < n - 1; i++) {
        int pos = i; // 假设认为当前范围最小值 pos 就是 当前范围的第一个数
        for (int j = i + 1; j < n; j++ ) {
            if (arr[pos] > arr[j]) { // 发现当前范围更小的数
                pos = j; // 记录这个数的下标
            }
        }
        // 交换这俩数
        int t = arr[i];
        arr[i] = arr[pos];
        arr[pos] = t;
    }
}

int main(void) {
    int n ;
    cin >> n;
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }
    select_sort(arr, n);
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }

    return 0;
}

2.2 冒泡排序

见名知意,冒泡就是说将目前第n个最大/最小的数,冒泡到顶端。如果顶端有了k个泡,那么就要排在k个泡之后成为第k+1个泡。

思路:由于每次冒泡都需要排在之前泡泡的后面,并且之前冒泡的数值肯定是合理的。所以缩小冒泡范围并把当前范围最小/最大的值,通过相邻元素两两交换最后冒泡到范围顶端。

举例: 从小到大排序下方数字

  • 18、1、5、27、12

(下标范围 0 ~ 4, 18 > 1 交换)1、18、5、27、12

(下标范围 0 ~ 4, 18 > 5 交换)1、5、18、27、12

(下标范围 0 ~ 4, 18 < 5 不交换)1、5、18、27、12

(下标范围 0 ~ 4, 27 > 12 交换)1、5、18、12、27(第一个泡泡)

  • 1、5、18、12、27

(下标范围 0 ~ 3, 1 < 5 不交换)1、5、18、12、27

(下标范围 0 ~ 3, 5 < 18 不交换)1、5、18、12、27

(下标范围 0 ~ 3, 18 > 12 交换)1、5、12、18、27

  • 1、5、12、18、27

(下标范围 0 ~ 2, 1 < 5 不交换)1、5、12、18、27

(下标范围 0 ~ 2, 5 < 12 不交换)1、5、12、18、27

  • 1、5、12、27、18

(下标范围 0 ~ 1, 1 < 5 不交换)1、5、12、18、27

  • 1、5、12、18、27(下标范围 0 ~ 0,单个元素自然有序)
#include <iostream>

using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];
void buttle_sort(int *arr,int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }

    }
}

int main(void) {
    int n ;
    cin >> n;
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }
    buttle_sort(arr, n);
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }

    return 0;
}

2.2.1 稳定/不稳定排序的说法

如果一个排序算法在排序后能够保持相等元素的原始相对顺序,那么这个算法就是稳定的

[ ("Alice", 90), ("Bob", 85), ("Chris", 90), ("Dave", 80) ]
// 稳定排序的升序(Alice和Chris都是90分,Alice原本在Chris前面,排序后仍然在前面。)
[ ("Dave", 80), ("Bob", 85), ("Alice", 90), ("Chris", 90) ].
// 不稳定排序的升序(可能 Alice 就在 Chris 的后面)
[ ("Dave", 80), ("Bob", 85), ("Chris", 90), ("Alice", 90) ]

2.3 插入排序

插入排序类似于两堆扑克牌,从无序列扑克牌堆里面选牌插入到有序扑克牌堆。

思路 :将数组分为已排序和未排序两部分,每次从未排序部分取出第一个元素,在已排序部分找到合适的位置插入。

举例 :从小到大排序下方数字

  • 18、1、5、27、12(初始状态,第一个元素18视为已排序)
  • 1、18、5、27、12(将1插入到18前面)
  • 1、5、18、27、12(将5插入到1和18之间)
  • 1、5、18、27、12(27已经在正确位置)
  • 1、5、12、18、27(将12插入到5和18之间)
#include <iostream>

using namespace std;
int arr[50];

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

	for (int i = 1; i < n; ++i) {
		// 要插入的元素
		int t = arr[i]; // 存起来
		int j = i - 1;
		// 18 1 5 27 12
		// 1 5 18 27 12
		for (; j >= 0; --j) {
			if (arr[j] > t)
			{
				arr[j + 1] = arr[j];
			}else
			{
				break;
			}
		}
		arr[j + 1] = t;
	}



	for (int i = 0; i < n; ++i) {
		cout << arr[i] << " ";
	}

	return 0;
}

2.4 希尔排序

希尔排序是插入排序的改进版,通过分组(分治思想)让插入排序提高了效率。

思路

  1. 选择一个增量序列(通常为n/2, n/4, …, 1)
  2. 依据每个增量去分组,然后将每组都进行插入排序
  3. 到最后增量为1,也就是要进行一次完整的插入排序

希尔排序会有一种假象:让初学者误认为嵌套循环多就是速度慢的、进而忽略了有序序列堆插入排序具体实现过程的影响。

举例 :从小到大排序下方数字 [18, 1, 5, 27, 12, 9, 6, 15]

  1. 初始增量=4(数组长度8/2):
    • 分组:[18,12], [1,9], [5,6], [27,15]
    • 排序后:[12,18], [1,9], [5,6], [15,27]
    • 数组变为:[12, 1, 5, 15, 18, 9, 6, 27]
  2. 增量=2:
    • 分组:[12,5,18,6], [1,15,9,27]
    • 排序后:[5,6,12,18], [1,9,15,27]
    • 数组变为:[5, 1, 6, 9, 12, 15, 18, 27]
  3. 增量=1(标准插入排序):
    • 最终排序结果:[1, 5, 6, 9, 12, 15, 18, 27]
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];

void shell_sort(int *arr, int n) {
    for (int gap = n / 2; gap > 0; gap /= 2) { // 增量序列
        for (int i = gap; i < n; i++) { // 对每个分组进行插入排序
            int temp = arr[i];
            int j;
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap];
            }
            arr[j] = temp;
        }
    }
}

int main(void) {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }
    shell_sort(arr, n);
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

2.5 计数排序

核心思想 :统计每个元素出现的次数,然后按顺序输出

特点

  • 非比较排序,时间复杂度O(n+k),k为数据范围
  • 适用于整数且范围不大的情况
  • 稳定排序
void countingSort(int arr[], int n) {
    // 假设数据范围在0到100之间
    int count[101] = {0};
    
    // 统计每个元素出现的次数
    for (int i = 0; i < n; i++)
        count[arr[i]]++;
    
    // 按顺序填充原数组
    int index = 0;
    for (int i = 0; i <= 100; i++) {
        while (count[i]--) {
            arr[index++] = i;
        }
    }
}

2.6 归并排序(前置知识:双指针)

归并排序:应用快慢指针与递归去解决排序问题的算法。

#include <iostream>
using namespace std;

// 5 4 3 2 1
void merge_sort(int *arr, int l, int r) {
	if(l >= r) return;
	int mid = (l + r) / 2;
	merge_sort(arr, l, mid);
	merge_sort(arr, mid + 1, r);
	int tmp[10];
	int i = l, j = mid + 1, k = 0;
	while(i <= mid && j <= r) {
		if(arr[i] < arr[j]) tmp[k++] = arr[i++];
		else tmp[k++] = arr[j++];
	}
	while(i <= mid) tmp[k++] = arr[i++];
	while(j <= r) tmp[k++] = arr[j++];
	for(i = l, k = 0; i <= r; ++i,++k) {
		arr[i] = tmp[k];
	}
}

int main(void){
	int n;
	int arr[10];
	cin >> n;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	
	merge_sort(arr, 0, n - 1);
	for(int i = 0; i < n; ++i) {
		cout << arr[i] << " ";
	}
	return 0;
}

2.7 快速排序(前置知识:双指针)

快速排序:应用左右指针与递归去解决排序问题的算法。

#include <iostream>
using namespace std;

// 5 4 3 2 1
void quick_sort(int *arr, int l, int r) {
	if(l >= r) return;
	int x = arr[(l + r)  / 2];
	int i = l - 1, j = r + 1;
	while(i < j) {
		do i++; while(arr[i] < x);
		do j--; while(arr[j] > x);
		if(i < j) {
			int temp = arr[i];
			arr[i] = arr[j];
			arr[j] = temp;
		}
	}
	quick_sort(arr, l, j);
	quick_sort(arr, j + 1, r);
}

int main(void){
	int n;
	int arr[10];
	cin >> n;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	
	quick_sort(arr, 0, n - 1);
	for(int i = 0; i < n; ++i) {
		cout << arr[i] << " ";
	}
	return 0;
}

三、大整数四则运算

接下来,我带着大家继续锻炼模拟思维。

何为大整数:通常指的是非常大的整数,其值远超常规数据类型(如32位或64位整数)能直接表示的范围。

意义

  • 现代加密算法(如 RSA、ECC)依赖大质数或大整数运算,防止暴力破解。例如:RSA-2048 使用两个 1024 位的质数相乘,生成密钥。
  • 精确计算极大数值(如天文数字、阶乘、斐波那契数列等)。

3.1 加法

大整数加法应该算是四个运算法则里,最好理解的了。

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main(void) {
	string a,b;
	cin >> a >> b;
	vector<int> A; 
	vector<int> B;
    // 做倒置存入到 vector 中(倒置是因为比较好处理,可以不倒置)
	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');
	}

	/*
	4321
	765
	*/
	int t = 0; // 维护进位值
	vector<int> C; // 存储最后结果
	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 > 0) C.push_back(t); // 若最后进位值还有,那么就属于进多了一位

	for(int i = C.size() - 1; i >= 0; --i) {
		cout << C[i];
	}

	return 0;
}

3.2 减法

大整数减法会遇到负数问题的处理,还有前置0。关键点在于这里~

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// 必须要判断出 A、B 两个数谁更大
bool isMax(vector<int> A, vector<int> B)
{
	if (A.size() == B.size())
	{
		for (int i = A.size() - 1; i >= 0; --i)
		{
			if (A[i] == B[i])
			{
				continue;
			}
			return A[i] > B[i];

		}
		return true;
	}else
	{
		return A.size() > B.size();
	}
	
}

int main(void) {
	string a, b;
	cin >> a >> b;
	vector<int> A;
	vector<int> 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');
	}

	bool flag = isMax(A, B);
	if (!flag) // 如果 B 比 A 大则将他们交换,因为我们要默认 A - B 进行模拟运算
	{
		vector<int> temp = A;
		A = B;
		B = temp;
	}

	int t = 0; // 借位
	vector<int> C;
	for (int i = 0; i < A.size() || i < B.size(); ++i) {
		int num = -t;
		if (i < A.size()) num += A[i];
		if (i < B.size()) num -= B[i];
		if (num < 0)
		{
			t = 1;
			num += 10;
		}else
		{
			t = 0;
		}
		C.push_back(num % 10);
	}

	while (C.size() > 1 && C[C.size() - 1] == 0)
	{
		C.pop_back();
	}
	if (C.size() == 1 && C[0] == 0)
	{
		cout << 0 << endl;
		return 0;
	}
	if (!flag) cout << "-";
	for (int i = C.size() - 1; i >= 0; --i) {
		cout << C[i];
	}

	return 0;
}

3.3 乘法

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

int main(void) {
	string a, b;
	cin >> a >> b;
	vector<int> A;
	vector<int> 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');
	}

	/*
	4321
	765
	*/

	vector<int> C(1000005);
	if (A.size() < B.size()) swap(A, B);
	for (int i = 0; i < B.size(); ++i) {
		int j = 0;
		int t = 0;
		for (; j < A.size(); ++j) {
			int num = A[j] * B[i] + t;
			int ret = num % 10;
			t = num / 10;
			C[i + j] += ret;
			C[i + j + 1] += C[i + j] / 10;
			C[i + j] = C[i + j] % 10;

		}
		if (t > 0) C[i + j] += t;
	}
	bool flag = true;
	for (int i = A.size() + B.size(); i >= 0; --i) {
		if (i > 0 && flag && C[i] == 0)
		{
			continue;
		}
		if (flag && C[i] != 0)
		{
			flag = false;
		}
		cout << C[i];
	}


	return 0;
}

3.4 除法

除法这里用到了大整数减法。

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

// 比较两个大整数
bool isGreaterOrEqual(vector<int> A, vector<int> 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;
}

// 减法函数
vector<int> subtractBigInt(vector<int> A, vector<int> B) {
	vector<int> C;
	int t = 0;
	for (int i = 0; i < A.size() || i < B.size(); ++i) {
		int num = -t;
		if (i < A.size()) num += A[i];
		if (i < B.size()) num -= B[i];
		if (num < 0) {
			t = 1;
			num += 10;
		}
		else {
			t = 0;
		}
		C.push_back(num % 10);
	}
	while (C.size() > 1 && C.back() == 0)
		C.pop_back();
	return C;
}

// 除法函数
pair<vector<int>, vector<int>> divideBigInt(vector<int> dividend, vector<int> divisor) {
	vector<int> quotient;
	vector<int> remainder = dividend;
	
	if (!isGreaterOrEqual(remainder, divisor)) {
		return { vector<int>{0}, remainder };
	}
	
	vector<int> current;
	for (int i = remainder.size() - 1; i >= 0; --i) {
		current.insert(current.begin(), remainder[i]);
		while (current.size() > 1 && current.back() == 0)
			current.pop_back();
		
		int q = 0;
		while (isGreaterOrEqual(current, divisor)) {
			current = subtractBigInt(current, divisor);
			q++;
		}
		quotient.push_back(q);
	}
	
	reverse(quotient.begin(), quotient.end());
	while (quotient.size() > 1 && quotient.back() == 0)
		quotient.pop_back();
	
	return { quotient, current };
}

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');
	
	auto [quotient, remainder] = divideBigInt(A, B);
	
	cout << "Quotient: ";
	for (int i = quotient.size() - 1; i >= 0; --i) cout << quotient[i];
	
	cout << "\nRemainder: ";
	for (int i = remainder.size() - 1; i >= 0; --i) cout << remainder[i];
	
	return 0;
}

四、数组基本操作

4.1 增删改查

int arr[50]; // 开辟足够大的数组
int arr[2] = 5; // 比如现在有两个元素了,我们增加一个元素。那就是 为下标2 赋值
// 通过覆盖元素实现删除
int arr[50] = {1,2,3,4,5};

// 起始位置为待删除元素位置 终止位置为最后元素位置
for(int i = 0; i < n; ++i){
    arr[i] = arr[i+1];
}

// 由于删掉一个元素 所以长度 从 n => n - 1
for(int i = 0; i < n - 1; ++i){
    cout << arr[i] << " ";
}
arr[5] = 18; // 改就是重新赋值
cout << arr[5] << endl; // 直接访问即可

4.2 数组元素交换与自身插入

  • 数组元素交换
// 其实就是依赖于下标找到 A 和 B 然后进行 交换操作
int temp = arr[2];
arr[2] = arr[3];
arr[3] = temp;
  • 自身插入:在一个数组中,A元素要插入到B元素的前面或后面
// 这个主要用的是 与删除操作 元素覆盖逆着来的写法。
// 比如我们要把下标3的元素 插入到目前下标为1的前面
int t = arr[3];
for(int i = 3 - 1; i >= 1; --i){
    arr[i + 1] = arr[i]; // 这个元素覆盖就是从左往右覆盖的(由于会覆盖掉待移动的元素,所以我们提前用 变量t 存储起来了)
}
arr[1] = t; // 从下标1 开始都整体往右移动了一位,所以我们只需要将 下标1位置重新赋值为t 即可完成需求

// 比如我们要把下标3的元素 插入到目前下标为1的后面
int t = arr[3];
for(int i = 3 - 1; i > 1; --i){
    arr[i + 1] = arr[i]; // 这个元素覆盖就是从左往右覆盖的(由于会覆盖掉待移动的元素,所以我们提前用 变量t 存储起来了)
}
arr[1 + 1] = t;

4.3 实现动态数组

动态数组是普通数组的增强版,我记忆非常尤新。大一上学期的大作业,我写的就是动态数组的基本实现。

主要它是要实现以下几个扩展功能:

  • 自动扩容
  • 增删改查方法
  • size方法
#include <iostream>
#include <cstring>

using namespace std;

template <typename T>
class DynamicArray {
private:
	T*data;
	size_t size;
	size_t capacity;
	
	// 扩容
	void reserve(size_t new_capacity) {
		if (new_capacity <= capacity) return;
		T* new_data = new T[new_capacity]; // 分配新内存
		for (size_t i = 0; i < size; i++) {
			new_data[i] = data[i];
		}
		delete[] data; // 释放旧内存
		data = new_data;
		capacity = new_capacity;
	}
	
public:
	DynamicArray(size_t init_capacity = 4) 
	: size(0), capacity(init_capacity > 0 ? init_capacity : 4) {
		data = new T[capacity]; // 分配初始内存
	}
	// 析构函数(释放内存)
	~DynamicArray() {
		delete[] data;
	}
	
	void push_back(const T& elem) {
		if(size >= capacity) {
			reserve(capacity * 2);
		}
		data[size++] = elem; // size 就是下标 size++ 就是数量
	}
	
	void remove(size_t index) {
		if(index >= size) {
			throw out_of_range("index out of bounds");
			return;
		}
		for(int i = index; i < size - 1; ++i) {
			data[i] = data[i + 1]; 
		}
		
		data[size - 1].~T(); // 调用析构函数
		size--; // 数量少了一个!
	}
	
	T& operator[](size_t index) {
		if(index >= size) {
			throw out_of_range("index out of bounds");
		}
		return data[index];
	}
	size_t get_size() const {return size;}
	size_t get_capacity() const {return capacity;}
};

int main(void){
	
	DynamicArray<int> arr;
	arr.push_back(12);
	for(int i=0;i<arr.get_size();i++){
		cout  << arr[i] << " ";
	}
	return 0;
}

五、双指针

双指针严格来说不能是算法,它更像是一种技巧或思想。到后面的学习里,我们还会讲到贪心,它也是一种思想。双指针常被用于线性有单调性趋势遍历的优化之中!使其从 O(n^2) => O(n)

5.1 快慢指针(龟兔指针)

概念:通过使用两个指针以不同的速度遍历数据结构(线性结构)。 —— 滑动窗口与快慢指针的思想就很类似

检测链表是否有环

找链表中间节点

删除倒数第N个节点

判断是否为回文链表

寻找数组中重复元素

  • 快指针:先行一步满足前置条件探索者快速遍历并检测符合条件的数据
  • 慢指针:配合快指针解决需求建造者维护已处理数据的正确边界

利用两者之间的速度差,可以在某个时刻根据特殊规则,获得或处理某些数据

大白话:根据需求,当发现只需要用一个变量,先行一步拿到前置条件,再用另一个变量配合该前置条件,就可以完成需求时,那么你应该尝试下快慢指针。

5.1.1 有序数组去重

#include <iostream>
using namespace std;

int arr[50];

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

	if (n == 0) return 0; // 边界检查

	int i = 0; // 慢指针(为了配合快指针的)
	for (int j = 1; j < n; ++j) { // 快指针的目的是找到非重复项
		if (arr[j] != arr[i]) { 
            // 找到之后,用非重复项覆盖掉重复项
			i++;                // 慢指针前进
			arr[i] = arr[j];    // 覆盖重复项
		}
	}

	// 输出去重后的数组(前 i+1 个元素)
	for (int k = 0; k <= i; ++k) {
		cout << arr[k] << " ";
	}
	cout << endl;

	return 0;
}

5.1.2 数组的原地删除

原地删除某元素:比如数组 1,2,3,4,5,6,6,2,1,2,3,2 你就想把 里面的 2 全部删掉。逆向思维去看,其实就是只保留不是2的元素。

#include <iostream>
using namespace std;

void remove_element(arr, n, val) {
    int slow = 0;
    for(int fast = 0; fast < n; ++fast) { // 快指针去遍历
        if(arr[fast] != val){ // 进行筛选
            arr[slow++] = arr[fast]; // 慢指针给数组重新赋值符合条件的 arr[fast]
        }
    }
}

5.1.3 最长连续不重复子序列(经典例题)

在这里插入图片描述

#include <iostream>

using namespace std;
const int maxN = 1e5 + 10;
int arr[maxN];
int main(void) {
	int n;
	cin >> n;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	int maxLen = 0; // 记录最长长度
	// n^3
	// n^2
	// 计数排序
	// n
	int cnt[maxN] = {0};
	int i = 0; // 慢指针
	for(int j = 0; j < n; ++j) {
		cnt[arr[j]]++;
		while(cnt[arr[j]] > 1) { 
			cnt[arr[i]]--;
			i++;
		}
		
		if(maxLen < j - i + 1) maxLen = j - i + 1;
	}
	cout << maxLen << endl;
	return 0;
}

5.2 左右指针

左右指针是一种强大的算法技巧,其归属于双指针。特别适合处理数组字符串问题。与快慢指针不同,左右指针同时从序列的两端向中间移动,形成 对称扫描

在这里插入图片描述
左右指针最直接的应用就是(唐突):二分查找、二分法答案

5.2.1 基础模板

void two_pointers(int* arr, int n) {
    int l = 0, r = n - 1; // r 是最后元素的下标
    while(l < r) { // 只要 l < r 就继续扫描
        if 左侧条件满足 {
            l++;
        }
        if 右侧条件满足 {
            r--;
        }
        // 当然可能同时满足(这个所谓的条件,都是人为定的)
    }
}

5.2.2 两数之和 II - 输入有序数组 (LeetCode 167)

题目:给定一个已按非递减顺序排列的整数数组 numbers,请你从数组中找出两个数,它们的和等于目标数 target。假设每个输入只对应唯一的答案,并且不能使用相同的元素两次。

那么这道题。如果暴力去做,怎么样也得 O(n^2)。但由于题目说提供的数据是有序的,就可以利用左右指针优化到 O(n)

void two_sum(int *arr,int n,int target) {
    // 若升序,肯定遵循左小右大
    int l = 0, r = n - 1;
    while(l < r){
       int total = arr[l] + arr[r];
       if(total == target){
           cout << l << " " << r << endl;
           break;
       }else if(total < target) { // 证明左边的太小了
           l++;
       }else { // 证明右边的太大了
           r--;
       }
    }
    couit << "" << endl;
}

5.2.3 反转字符串 (洛谷 P5705 【深基2.例7】数字反转)

题目:编写一个函数,将输入的字符串反转过来。

#include <iostream>

using namespace std;

int main(void){
	string a;
	cin >> a;
	int i = 0, j = a.size() - 1;
	while(i < j) {
		char t = a[i];
		a[i] = a[j];
		a[j] = t;
		i++;
		j--;
	}
	cout << a << endl;
	
	return 0;
}

六、二分法

6.1 二分查找

  • while(l < r)
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];

int main(void){
	int n,x;s
	cin >> n >> x;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	int l = 0, r = n - 1;
	while(l < r) {
		int mid = (l + r) / 2;
		if(arr[mid] > x) {
			r = mid - 1;
		}else if(arr[mid] < x) {
			l = mid + 1;
		}else {
			cout << mid << endl;
			return 0;
		}
	}
	if(arr[l] == x) cout << l << endl;
	else cout << -1 << endl;
	return 0;
}
  • while(l <= r)
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];

int main(void){
	int n,x;
	cin >> n >> x;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	int l = 0, r = n - 1;
	while(l <= r) {
		int mid = (l + r) / 2;
		if(arr[mid] > x) {
			r = mid - 1;
		}else if(arr[mid] < x) {
			l = mid + 1;
		}else {
			cout << mid << endl;
			return 0;
		}
	}
	cout << -1 << endl;
	return 0;
}

6.2 左右边界

  • while(l < r)
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];

int main(void){
	int n,x;
	cin >> n >> x;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	int l = 0, r = n - 1;
	// 1 2 2 2 3 
	while(l < r) {
		int mid = (l + r) / 2;
		if(arr[mid] >= x) {
			r = mid;
		}else {
			l = mid + 1;
		}
	}
	if(arr[l] == x) cout << l << endl;
	else cout << - 1 << endl;
	l = 0, r = n - 1;
	// 1 2 2 2 3 
	while(l < r) {
		int mid = (l + r + 1) / 2;
		if(arr[mid] <= x) {
			l = mid;
		}else {
			r = mid - 1;
		}
	}
	if(arr[r] == x) cout << r << endl;
	else cout << - 1 << endl;

	return 0;
}
  • while(l <= r)
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];

int main(void){
	int n,x;
	cin >> n >> x;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	int l = 0, r = n - 1;
	// 1 2 2 2 3 
	while(l <= r) {
		int mid = (l + r) / 2;
		if(arr[mid] < x) {
			l = mid + 1;
		}else {
			r = mid - 1;
		}
	}
	if(arr[l] == x) cout << l << endl;
	else cout << - 1 << endl;
	l = 0, r = n - 1;
	// 1 2 2 2 3 
	while(l <= r) {
		int mid = (l + r) / 2;
		if(arr[mid] > x) {
			r = mid - 1;
		}else {
			l = mid + 1;
		}
	}
	if(arr[r] == x) cout << r << endl;
	else cout << - 1 << endl;

	return 0;
}

七、筛质数

质数:只能被1 与 自身整除的数。

7.1 朴素法 O(nlogn)

#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];

bool isPrime(int x) {
	if(x <= 1) return false;
	for(int i = 2 ; i < x; ++i) {
		if( x % i == 0 && x != 2) {
			return false;
		}
	}
	return true;
}

int main(void){
	int n;
	cin >> n;
	for(int i = 2; i <= n; ++i) {
		if(isPrime(i)) {
			cout << i << endl;
		}
	}

	return 0;
}

这里还会有个数学证明,优化了遍历条件。提高了效率,使其 i < x 化为 i * i <= x

  • 首先我们要知道,x = a * b,x / a = b。那么此时比如 i = a,则 x / i = b,也就是说如果一旦它有个因子是a,肯定会有个b的,我们的i就是帮他试试有没有这样的因子a。这个是前提!那么问题了,如果 x 可以被平方根,是不是就直接能确定因子 sqrt(x),那么就不难发现,其它因子无非就是比 sqrt(x) 大或者小,如果 a是比 sqrt(x) 大,那么 b 就比 sqrt(x) 小,这是肯定的。此时我们其实就可以给 i 规定最大范围,即 i <= sqrt(x) 这是没毛病的,因为只要我在 小于等于 sqrt(x) 范围内尝试是否有一个因子,那么另外一个因子也就被确定了。合乎数学逻辑。
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int arr[maxN];

bool isPrime(int x) {
	if(x <= 1) return false;
	for(int i = 2 ; i * i <= x; ++i) {
		if( x % i == 0 && x != 2) {
			return false;
		}
	}
	return true;
}

int main(void){
	int n;
	cin >> n;
	for(int i = 2; i <= n; ++i) {
		if(isPrime(i)) {
			cout << i << endl;
		}
	}

	return 0;
}

7.2 埃氏筛 O(nloglogn)

朴素筛的时候,我们就知道用数学所谓的 平方根,因子相关知识进行优化。那么也不难想到,任何数的倍数其实都不可能是质数的!所以我们只需要筛出不是质数的,剩下的就是质数喽。

#include <iostream>
#include <string.h>
using namespace std;

const int maxN = 1e5 + 10;
bool primeArr[maxN];

int main(void){
	int n;
	cin >> n;
	memset(primeArr, true, sizeof(bool) * maxN);
	primeArr[0] = false;
	primeArr[1] = false;
	for(int i = 2; i * i <= n; ++i) {
		for(int j = 2 * i; j < n; j += i) {
			primeArr[j] = false;
		}
	}
	for(int i = 0; i < n; ++i) {
		if(primeArr[i]) {
			cout << i << endl;
		}
	}
	
	return 0;
}

7.3 欧拉筛 O(n)

其实朴素筛、埃氏筛 速度已经不错了,但有些时候可能需要最好的性能。此时就需要 使用 欧拉筛,它主要是优化了埃氏筛的缺点。

埃氏筛中,有这样一种情况,会导致多筛!比如 6 = 2 * 3,那么 对于2来说会有 * 3的时候,而对于 3 来说又会有*2的时候,但其实只需要筛一次就够,如果能保证筛一次的话,时间复杂度就可以 O(n)

  • 基本观察:每个非质数(合数)都会有唯一的 最小质因子
  • 关键机制:如果我们可以确保每个合数只在它 最小质因子与对应因数相乘时才被标记为 合数,那么就只筛了一次,不会多筛。就比如说 6 只在 最小质因子 2 乘以 3 的时候,才会被标记,其余情况不会被标记。
#include <iostream>
#include <string.h>
using namespace std;

const int maxN = 1e5 + 10;
int primes[maxN]; // 升序
bool vis[maxN];

int main(void){
    int n;
    cin >> n;
    memset(vis, false, sizeof(bool) * maxN); // 假设都没被标记为合数
    int pos = 0;

    for(int i = 2; i <= n; ++i) {
        if(!vis[i]) // 先判断是不是合数
        {
            primes[pos++] = i; // i 是质数
            // 当前这个质数 * 其它已经确认的质数 = 合数
            for (int j = 0;j < pos && i * primes[j] <= n; ++j) {
                vis[i * primes[j]] = true; // 这个是合数
            }
        }else {
            // 当前这个合数 * 已经确认的质数(从最小的开始) = 合数
            // 但当前这个合数很可能是 某个最小质数的倍数,所以如果用它继续筛就筛多了
            // 必须判断出它是哪个最小质数的倍数
            for (int j = 0;j < pos && i * primes[j] <= n; ++j) {
                vis[i * primes[j]] = true; // 这个是合数
                if (i % primes[j] == 0) { // 看下当前这个 i 是不是最小质数
                    break; // 证明不是最小质数了,可以是 primes[j] 的倍数,那么这个 primes[j] 在后续就可以帮他继续筛完
                    // 即 当前的 i 就不用去做没意义的筛了。
                }
            }
        }
    }

    for(int i = 0; i <pos; ++i) {
        cout << primes[i] << endl;
    }

    return 0;
}

八、前缀和与差分

  • 前缀和:快速求某个区间中所有元素的累加和。(所以这些数肯定是要 反复使用进行累加的)

  • 差分:对多个区间进行加数,快速得到操作后的数组集合(需要学习前缀和)

8.1 一维前缀和

  • 朴素写法
#include <iostream>

using namespace std;
const int maxN = 1e5 + 10;
int arr[maxN];
int main() {
	int n;
	cin >> n;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	int p,q;
	cin >> p >> q;
	int sum = 0;
	for(int i = p; i <= q; ++i) {
		sum += arr[i];
	}
	cout << sum << endl;
	return 0;
}
  • 前缀和写法
#include <iostream>

using namespace std;
const int maxN = 1e5 + 10;
int arr[maxN];
int sum[maxN] = {0};
int main() {
	int n;
	cin >> n;
	for(int i = 0; i < n; ++i) {
		cin >> arr[i];
	}
	sum[1] = arr[0];
	for(int i = 2; i <= n; ++i) {
		sum[i] = sum[i - 1] + arr[i - 1];
	}
	// 1 2 3 4 5
	// 1 3 6 10 15
	for(int i = 0; i < n + 1; ++i) {
		cout << sum[i] << " ";
	}
	int p,q;
	cin >> p >> q;

	cout << endl;
	cout << sum[q + 1] - sum[p - 1] << endl;
	return 0;
}

8.2 二维前缀和

核心思想还是 前缀和,只不过是从 一维数组拓展到了二维矩阵。所以我们要探索有哪些变化,总结出通式,解决二维前缀和问题。

#include <iostream>

using namespace std;
const int maxN = 1005;
int arr[maxN][maxN] = {0};
int sum[maxN][maxN] = {0};

int main() {
    int n,m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> arr[i][j];
        }
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + arr[i][j];
            cout << sum[i][j] << " ";
        }
        cout << endl;
    }

    int q;
    cin >> q;
    while (q--) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1;
        cin >> x2 >> y2;
        cout << sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]
        << endl;
    }
    return 0;
}

8.3 一维差分

原理:构建差分数组,将其当作新的原数组。然后进行前缀和操作,意外发现由差分数组还原成了真正的原数组。所以,只要我们在差分数组里进行区域内的加数操作,得到普遍规律可循,最后进行前缀和就可以得到我们的目标数组。

#include <iostream>

using namespace std;
const int maxN = 1e5 + 10;
int arr[maxN] = {0};
int d[maxN] = {0};

int main() {
    int n,q;
    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
    }
    for (int i = 1; i <= n; i++) {
        d[i] = arr[i] - arr[ i - 1];
    }
    while (q--) {
        int l, r, c;
        cin >> l >> r >> c;
        d[l] += c;
        if (r + 1 < n) {
            d[r + 1] -= c;
        }
    }
    for (int i = 1; i <= n; i++) {
        arr[i] = d[i] + arr[i - 1];
        cout << arr[i] << " ";
    }
    return 0;
}

8.4 二维差分

#include <iostream>
using namespace std;

const int maxN = 1005;
int arr[maxN][maxN] = {0};
int d[maxN][maxN] = {0};  // 差分数组
int sum[maxN][maxN] = {0}; // 前缀和数组

int main() {
    int n, m, q;
    cin >> n >> m >> q;

    // 输入原始数组并初始化差分数组
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> arr[i][j];
            // 计算差分数组
            d[i][j] = arr[i][j] - arr[i-1][j] - arr[i][j-1] + arr[i-1][j-1];
        }
    }

    // 处理查询
    while (q--) {
        int x1, y1, x2, y2, c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;

        // 更新差分数组
        d[x1][y1] += c;
        if (y2 + 1 <= m) d[x1][y2+1] -= c;
        if (x2 + 1 <= n) d[x2+1][y1] -= c;
        if (x2 + 1 <= n && y2 + 1 <= m) d[x2+1][y2+1] += c;
    }

    // 通过差分数组计算前缀和数组
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            sum[i][j] = d[i][j] + sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
            cout << sum[i][j] << " ";
        }
        cout << endl;
    }

    return 0;
}

九、链表、队列、栈

9.1 单链表

#include <iostream>
using namespace std;

struct Node {
    Node* next;
    int data;
};
int main() {
    Node* A = new Node();
    Node* B = new Node();
    A -> data = 1;
    B -> data = 2;
    A -> next = B;
    B -> next = NULL;
    Node* currentNode = A;
    // 1. 遍历
    // Node* currentNode = A;
    // while (currentNode != NULL) {
    //     cout << currentNode -> data << endl;
    //     currentNode = currentNode -> next;
    // }

    // 2. 通关下标找到目标节点
    // int pos;
    // cin >> pos;
    // for (int i = 0; i < pos; i++) {
    //     if (i == 0) {
    //         currentNode = A;
    //     }else {
    //         currentNode = currentNode -> next;
    //     }
    //     if (currentNode == NULL) {
    //         break;
    //     }
    // }
    // if (currentNode == NULL) cout << A->data << endl;
    // else cout << currentNode->data << endl;

    // 3. 通过下标删除目标节点
    // int pos;
    // cin >> pos;
    // Node* preNode = A;
    // currentNode = preNode -> next;
    // if (pos == 0) {
    //     A = currentNode;
    //     delete preNode;
    // }else if (pos > 0) {
    //     for (int i = 2; i <= pos; i++) {
    //         preNode = currentNode;
    //         currentNode = preNode -> next;
    //         if (currentNode == NULL) {
    //             break;
    //         }
    //     }
    //     if (currentNode == NULL) {cout << "删除失败" << endl;}
    //     else {
    //         preNode -> next = currentNode->next;
    //         delete currentNode;
    //     }
    // }
    //
    // currentNode = A;
    // while (currentNode != NULL) {
    //     cout << currentNode -> data << endl;
    //     currentNode = currentNode -> next;
    // }

    // 4. 在下标后面插入一个节点
    int pos;
    cin >> pos;
    int data;
    cin >> data;
    for (int i = 0; i <= pos; i++) {
        if (i == 0) {
            currentNode = A;
        }else {
            currentNode = currentNode -> next;
        }
        if (currentNode == NULL) {
            break;
        }
    }
    if (currentNode == NULL) cout << -1 << endl;
    else {
        Node* newNode = new Node();
        newNode -> data = data;
        newNode -> next = currentNode ->next;
        currentNode -> next = newNode;
    }
    currentNode = A;
    while (currentNode != NULL) {
        cout << currentNode -> data << endl;
        currentNode = currentNode -> next;
    }


    return 0;
}

9.2 双链表

#include <iostream>
using namespace std;

struct Node {
    Node* pre;
    Node* next;
    int data;
};
int main() {
    Node* head = new Node();
    Node* A = new Node();
    Node* B = new Node();
    head -> next = A;
    A -> data = 1;
    B -> data = 2;
    A -> pre = head;
    A -> next = B;
    B -> pre = A;
    B -> next = NULL;
    Node* cNode = head -> next;
    while (cNode != NULL) {
        cout << cNode -> data << endl;
        cNode = cNode -> next;
    }

    // 删除
    int pos;
    cin >> pos;
    int t = pos + 1; // 第 pos + 1 个
    cNode = head;
    for (int i = 1; i <= t; i++) {
        cNode = cNode -> next;
        if (cNode == NULL) {
            cout << "删除失败" << endl;
            return 0;
        }
    }

    Node* preNode = cNode -> pre;
    preNode -> next = cNode->next;
    delete cNode;

    cNode = head -> next;
    while (cNode != NULL) {
        cout << cNode -> data << endl;
        cNode = cNode -> next;
    }


    return 0;
}

9.3 栈

#include <iostream>
using namespace std;

struct Node {
    Node* pre;
    Node* next;
    int data;
};
Node* tail = NULL;
void push(int data) {
    Node* newNode = new Node();
    newNode->data = data;
    newNode->next = NULL;
    newNode->pre = tail;
    tail->next = newNode;
    tail = newNode;
}

int pop() {
    Node* preNode = tail->pre;
    preNode -> next = NULL;
    int ret = tail -> data;
    delete tail;
    tail = preNode;
    return ret;
}

int main() {
    Node* head = new Node();
    Node* A = new Node();
    Node* B = new Node();
    head -> next = A;
    A -> data = 1;
    B -> data = 2;
    A -> pre = head;
    A -> next = B;
    B -> pre = A;
    B -> next = NULL;
    tail = B;
    Node* cNode = head -> next;
    while (cNode != NULL) {
        cout << cNode -> data << endl;
        cNode = cNode -> next;
    }

    push(3);
    push(4);

    cNode = head->next;
    while (cNode != NULL) {
        cout << cNode->data << endl;
        cNode = cNode -> next;
    }

    pop();


    cNode = head->next;
    while (cNode != NULL) {
        cout << cNode->data << endl;
        cNode = cNode -> next;
    }



    return 0;
}

9.4 队列

#include <iostream>
using namespace std;

struct Node {
    Node* pre;
    Node* next;
    int data;
};
Node* tail = NULL;
void push(int data) {
    Node* newNode = new Node();
    newNode->data = data;
    newNode->next = NULL;
    newNode->pre = tail;
    tail->next = newNode;
    tail = newNode;
}

int pop() {
    Node* preNode = tail->pre;
    preNode -> next = NULL;
    int ret = tail -> data;
    delete tail;
    tail = preNode;
    return ret;
}
Node* head = new Node();
int outQue() {
    Node* firstNode = head->next;
    if (firstNode == NULL) return -1;
    head -> next = firstNode -> next;
    int ret = firstNode -> data;
    delete firstNode;
    return ret;
}

int main() {

    Node* A = new Node();
    Node* B = new Node();
    head -> next = A;
    A -> data = 1;
    B -> data = 2;
    A -> pre = head;
    A -> next = B;
    B -> pre = A;
    B -> next = NULL;
    tail = B;
    Node* cNode = head -> next;
    while (cNode != NULL) {
        cout << cNode -> data << endl;
        cNode = cNode -> next;
    }

    push(3);
    push(4);

    cNode = head->next;
    while (cNode != NULL) {
        cout << cNode->data << endl;
        cNode = cNode -> next;
    }

    outQue();


    cNode = head->next;
    while (cNode != NULL) {
        cout << cNode->data << endl;
        cNode = cNode -> next;
    }


    return 0;
}

9.5 数组模拟链表、栈、队列

链表既然能够实现栈和队列,那么自然 数组就也可以实现 栈和队列,甚至可以 模拟链表的逻辑。我们来看下。

  • 模拟链表
    链表结构的核心就在于 “链”,它可以直接找到对应的节点。那么我们可以通过 数组构建一个映射表,该数组的下标就相当于某个节点,然后存储的数据就是 链接的节点下标。这样我们就构建出了单链表的逻辑结构。
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int nodeArr [maxN];
int linkArr [maxN];

int main() {
    linkArr[0] = 1; // 0号 指向 1号节点
    nodeArr[1] = 1; // 1号 节点
    nodeArr[2] = 2; // 2号 节点
    linkArr[1] = 2; // 1号 指向 2号节点
    linkArr[2] = -1; // 定义 -1 是下个节点无 NULL

    int nextPos = linkArr[0];
    int cNode = nodeArr[nextPos];
    while (nextPos != -1) {
        cout << cNode << endl;
        nextPos = linkArr[cNode];
        cNode = nodeArr[nextPos];
    }

    return 0;
}
  • 模拟栈
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int stack [maxN];
int tail = 0; // 当前要入栈位置

void push(int data) {
	if(tail >= maxN) {
		cout << "满了" <<endl;
		return;
	}
	stack[tail++] = data;
}

int pop(){
	if(tail == 0) return -1;
	return stack[--tail];
}

int main() {

	push(1);
	push(2);
	
	for(int i = 0; i < tail; ++i) {
		cout << stack[i] << endl;
	}
	
	pop();
	
	for(int i = 0; i < tail; ++i) {
		cout << stack[i] << endl;
	}
	
	return 0;
}
  • 模拟队列
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int q[maxN];
int top = 0;
int tail = -1;

void push(int x) {
    q[++tail] = x;
}
int pop() {
    return q[top++];
}
int main() {
    push(2);
    if (tail < top) {
        cout << "空的" << endl;
    }
    cout << "数量" << tail + 1 - top << endl;
    cout << pop() << endl;

    return 0;
}
  • 模拟循环队列
#include <iostream>
using namespace std;

const int maxN = 1e5 + 10;
int q[maxN];
int top = 0; // 头元素下标
int tail = 0; // 待插入节点的下标(即队尾的下一个下标)

bool isFull () {
    return (tail + 1) % maxN == top;
}

void push(int data) {
    if (isFull()) {
        cout << "队列满了" << endl;
        return;
    }
    q[tail] = data;
    tail = (tail + 1) % maxN;
}

int outQ() {
    if (top == tail) {
        cout << "队列空的" << endl;
        return -1;
    }
    int ret = q[top];
    top = (top + 1) % maxN;
    return ret;
}

int main() {
    push(1);
    push(2);

    cout << outQ() << endl;

    return 0;
}

十、DFS、BFS

讲DFS与BFS 这就不得不说一个典故 (迷宫)

想象一下,你进入了一个巨大的迷宫,你的目标是从起点 S 找到终点 T。 迷宫的结构如下,其中 # 代表墙,不能走;. 代表路代表路,可以走。

S . . # . . .
. # . . . # .
. # . . . . .
. . # # . . .
# . # T . . .

现在有两个问题被提出:

  • 走到终点的方案有多少种,分别都打印出来(DFS)
  • 走到终点最近的方案是什么路径,打印出来(BFS)

10.1 DFS

DFS:深度优先搜索,被戏称为一路走到黑。随便选一条路,死磕到底,碰壁后再倒回来换条路去走。

  • 你的行动方式

    • 从起点 S 开始,开始,你标记“我已经来过这里了”。
    • 你面前有多个岔路口,你随机选一个(比如总是先向右),然后一直走下去。
    • 每到一个新地方,你都继续沿用这个规则(比如先向右),完全不管其他还没走的岔路。
    • 如果有一天你发现前面是墙或者已经走过的地方(死胡同),你就原路返回,回到上一个有未探索岔路口的地方,换另一条路继续深入。
    • 直到你在某条路的尽头发现了终点 T
  • 辅助容器:栈

    • 为了管理这种“一路到底,然后回溯”的过程,你需要另一个工具——
    • 把起点 S 压入栈。
    • 只要栈不为空,就取出栈顶的那个位置(比如 B)。注意,拿的是最新的那个
    • 检查 B 是不是终点?如果是,胜利!
    • 如果不是,就把 B 周围所有能走且没走过的邻居,按顺序压入栈
    • 重复步骤2。(这样,下一次就会先探索刚压进去的其中一个邻居,从而不断深入)
  • 非递归版本

#include <iostream>

using namespace std;
// 我们是 节点,二维数组,所以最好是 建立 x,y 
// 题目要求我们 用 x,y 下标 去确定一个节点的话,那么我们就要定义 这样的结构
typedef struct {
	int x, y;
}Node;
typedef struct {
	Node point;
	int dir; // 下次探索的方向 0 1 2 3
}SNode;
const int maxN = 105;
Node d[4] = {{-1,0}, {0,1}, {1,0}, {0,-1}};
bool vis[maxN][maxN]; // 为走过的节点打标记
Node path[maxN * maxN];
int pathLen = 0;
char arr[maxN][maxN];
SNode s[maxN * maxN];
int top = -1;
int main(void) {
	int n,m;
	cin >> n >> m;
	for(int i = 0; i < n; ++i) {
		for(int j = 0; j < m; ++j) {
			cin >> arr[i][j];
			vis[i][j] = false;
			if(arr[i][j] == 'S') {
				s[++top] = {i,j, 0};
				vis[i][j] = true;
				pathLen = 1;
				path[0] = {i,j};
			}
		}
	}
	
	while(top >= 0) { // 只有栈为空的时候,才不去探索
		SNode* cNode = &s[top];
		if(cNode->dir > 3) {
			vis[cNode->point.x][cNode->point.y] = false;
			pathLen--;
			top--;
			continue;
		}
		int nx = cNode->point.x + d[cNode->dir].x;
		int ny = cNode->point.y + d[cNode->dir].y;
		cNode->dir++;
		if(nx >= 0 && nx < n && ny >= 0 && ny < m 
		   && !vis[nx][ny] && arr[nx][ny] != '#') {
			vis[nx][ny] = true;
			path[pathLen++] = {nx,ny};
			if(arr[nx][ny] == 'T') {
				for(int i = 0; i < pathLen; ++i) {
					cout << path[i].x << "," << path[i].y << " ";
				}
				cout << endl;
				vis[nx][ny] = false;
				pathLen--;
			}else {
				s[++top] = {nx,ny, 0};
			}
		}
	}
	
	
	
	return 0;
}
  • 递归版本
#include<iostream>

using namespace std;
const int maxN = 1005;
typedef struct {
    int x, y;
} Point;
typedef struct {
    Point p;
    int nextDir; // 下一步尝试的方向 (0~3)
} StackNode;
// 方向:上下左右
Point d[4] = {{-1,0}, {0,1}, {1,0}, {0,-1}};
bool vis[maxN][maxN]; // 是否走过了
Point path[maxN];
int pathLen = 0;
int totalPaths = 0; // 记录多少种路径方案
char arr[maxN][maxN];
int n, m;; // 5,7

void dfs(int x, int y) {
    vis[x][y] = true;
    path[pathLen++] = {x, y};
    if (arr[x][y] == 'T') {
        printf("Path %d:\n", ++totalPaths);
        for (int i = 0; i < pathLen; i++) {
            printf("(%d,%d) ", path[i].x, path[i].y);
        }
        printf("\n\n");
        return;
    }
    for (int i = 0; i < 4; i++) {
        int nx = x + d[i].x;
        int ny = y + d[i].y;
        if (nx >= 0 && nx < n && ny >= 0 && ny < m
           && !vis[nx][ny] && arr[nx][ny] != '#') { // 没有访问过
            dfs(nx, ny);
            vis[nx][ny] = false;
            pathLen--;
        }
    }
}
int main(void) {

    cin >> n >> m;
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            cin >> arr[i][j];
        }
    }
    int startX = -1, startY = -1; // 从 -1 开始
    for(int i = 0; i < n; ++i){
        for(int j = 0; j < m; ++j) {
            if(arr[i][j] == 'S') {
                startX = i;
                startY = j;
                break;
            }
        }
        if (startX != -1) break;
    }
    if (startX == -1) {
        printf("没有起始点\n");
        return 1;
    }

    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            vis[i][j] = false;

    dfs(startX, startY);
    if (totalPaths == 0) {
        printf("No path found!\n");
    } else {
        printf("Total paths: %d\n", totalPaths);
    }

    return 0;
}

10.2 BFS

DFS:广度优先搜索,又称为地毯式搜索。先探索所有最近的地方(层次感),再探索更远的地方。

  • 你的行动方式

    • 从起点 S 开始,你标记 “我已经来过这里了”。
    • 你站在 S,向四周(上、下、左、右)可通行的方向看,把所有能直接到达的点都记录下来。这些是你的 “第一层” 邻居。
    • 然后,你不再自己往前走,而是召唤出几个分身。每个分身去一个“第一层”邻居点。
    • 所有这些分身在各自的位置上,再次重复你的动作:记录他们周围还未被探索的、“第二层第二层”的邻居点。
    • 这个过程一层一层地展开,就像,就像在水里扔下一块石头,涟漪一圈一圈地扩散出去,直到某个分身找到了终点 T
  • 辅助容器:队列

    • 为了管理这个“分层探索”的过程,你需要一个强大的工具——队列
    • 队列的特点:先进先出 (FIFO),就像现实生活中的排队。先进去的人先出来。
    • 如何使用
      • 把起点 S 放进队列。
      • 只要队列不为空,就拿出队首的那个位置(比如 A)。
      • 检查 A 是不是终点?如果是,胜利! !
      • 如果不是,就把 A 周围所有能走且没走过的邻居,按顺序(比如上、下、左、右)依次放入队列的末尾
      • 重复步骤2。
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
using namespace std;

const int maxN = 1005;


struct Point {
	int x;
	int y;
};

Point q[maxN];
int top = 0;
int tail = 0;


bool ifFull() {
	return (tail + 1) % maxN == top;
}
void push(Point data) {
	if (ifFull()) {
		return;
	}
	q[tail] = data;
	tail = (tail + 1) % maxN;
}

Point outQ() {
	if (top == tail) {
		cout << "队列空的" << endl;
		return {-1,-1};
	}
	Point ret = q[top];
	top = (top + 1) % maxN;
	return ret;
}

Point d[4] = {{-1, 0}, {0, 1}, {1,0}, {0, -1}};
char arr[maxN][maxN];
bool vis[maxN][maxN];
Point pre[maxN][maxN]; // 记录当前节点的上一个节点

int main() {
	int n,m;
	int startX, startY;
	int endX, endY;
	cin >> n >> m;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			cin >> arr[i][j];
			vis[i][j] = false;
			if (arr[i][j] == 'S') {
				startX = i;
				startY = j;
			}
			if (arr[i][j] == 'T') {
				endX = i;
				endY = j;
			}
		}
	}
	push({startX, startY});
	vis[startX][startY] = true;
	pre[startX][startY] = {-1,-1};
	bool flag = false; // 默认没找到 T
	while (top != tail) {
		Point cNode = outQ();
		if (cNode.x == endX && cNode.y == endY) {
			flag = true;
			break;
		}
		for (int i = 0; i < 4; i++) {
			int nx = cNode.x + d[i].x;
			int ny = cNode.y + d[i].y;
			if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
			if (arr[nx][ny] == '#' || vis[nx][ny]) continue;
			vis[nx][ny] = true;
			pre[nx][ny] = cNode;
			push({nx, ny});
		}
	}
	
	if (!flag) {
		cout <<"根本没终点"<<endl;
	}else {
		vector<Point> path;
		int x = endX, y = endY;
		while (x != -1 || y != -1) {
			path.push_back({x,y});
			Point p = pre[x][y];
			x = p.x;
			y = p.y;
		}
		reverse(path.begin(), path.end());
		cout << endl;
		for (int i = 0; i < path.size(); i++) {
			
			cout << path[i].x << " " << path[i].y;
			if (i != path.size() - 1) cout << " -> ";
		}
	}
	
	
	return 0;
}

十一、贪心思想(完结)

贪心思想的核心是:每一步都做出当前看起来“最好”的选择,希望通过局部最优解最终达到全局最优解。

注意:贪心算法并不总能得到全局最优解,但在某些特定问题上,它是有效的,并且效率很高!

  • 贪心选择性质:每一步都做当前看起来最好的选择,不考虑未来。
  • 最优子结构:问题的最优解包含其子问题的最优解。
  • 无后效性:一旦做出选择,就不会再改变,也不会影响之前的选择。

11.1 找零钱问题

问题:假设你有面值为 1元、5元、10元 的硬币,要找给顾客 36 元,要求用最少数量的硬币。

贪心:每次都尽可能用最大面值的硬币就行了。

#include <stdio.h>

int main() {
    // 定义硬币面值(按从大到小排序)
    int coins[] = {10, 5, 1};
    int numCoins = 3; // sizeof(coins) / sizeof(coins[0]); 这个写法是计算数组元素个数的技巧写法

    int amount;
    printf("请输入要找零的金额(元): ");
    scanf("%d", &amount);

    if (amount < 0) {
        printf("金额不能为负数!\n");
        return 1;
    }

    int totalCoins = 0;
    printf("找零方案:\n");

    for (int i = 0; i < numCoins; i++) {
        int count = amount / coins[i];  // 当前面值能用多少个
        if (count > 0) {
            printf("%d元硬币:%d枚\n", coins[i], count);
            totalCoins += count;
            amount -= coins[i] * count;
        }
    }

    printf("总共需要硬币:%d枚\n", totalCoins);
    return 0;
}

11.2 活动选择问题(区间调度)

问题:有 n 个活动,每个活动有开始时间和结束时间。你一次只能参加一个活动,如何选择最多的活动?

  • 贪心:优先选择结束时间最早的活动。因为结束得早,留给后面的时间就多,能安排更多活动。
#include <stdio.h>
#include <stdlib.h>

// 定义活动结构体
typedef struct {
    int start;
    int finish;
    int id; // 活动编号(可选,用于输出原始顺序)
} Activity;

// 比较函数:用于 qsort,按结束时间升序排序
int compare(const void *a, const void *b) {
    Activity *actA = (Activity *)a;
    Activity *actB = (Activity *)b;
    return actA->finish - actB->finish;
}

int main() {
    int n;
    printf("请输入活动数量: ");
    scanf("%d", &n);

    if (n <= 0) {
        printf("活动数量必须大于0。\n");
        return 1;
    }

    Activity activities[n];

    printf("请输入每个活动的开始时间和结束时间(格式:start finish):\n");
    for (int i = 0; i < n; i++) {
        scanf("%d %d", &activities[i].start, &activities[i].finish);
        activities[i].id = i + 1; // 编号从1开始
        // 简单校验
        if (activities[i].start >= activities[i].finish) {
            printf("警告:活动%d的开始时间应小于结束时间!\n", i + 1);
        }
    }

    // 按结束时间升序排序
    qsort(activities, n, sizeof(Activity), compare);

    // 贪心选择
    printf("\n最多可参加的活动如下(按结束时间排序):\n");
    int count = 1;
    int lastFinish = activities[0].finish;
    printf("活动%d: [%d, %d]\n", activities[0].id, activities[0].start, activities[0].finish);

    for (int i = 1; i < n; i++) {
        if (activities[i].start >= lastFinish) {
            printf("活动%d: [%d, %d]\n", activities[i].id, activities[i].start, activities[i].finish);
            count++;
            lastFinish = activities[i].finish;
        }
    }

    printf("\n最多可参加 %d 个活动。\n", count);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值