[刷题经验总结]一些算法问题的优雅解法
@水墨不写bug

文章目录
- 一、本栏目开启的缘由
- 二、小试牛刀
-
- [1. 循环左移(类似右移)](#1. 循环左移(类似右移))
- [2. 原地旋转(三次反转法)](#2. 原地旋转(三次反转法))
- [3. 环状替换(原地旋转)](#3. 环状替换(原地旋转))
- [4. 循环队列实现](#4. 循环队列实现)
- [5. 字符串循环移位检测](#5. 字符串循环移位检测)
- [6. 矩阵旋转(90度顺时针)](#6. 矩阵旋转(90度顺时针))
- [7. 约瑟夫环问题(数学解法)](#7. 约瑟夫环问题(数学解法))
- 三、总结
一、本栏目开启的缘由
在做一道简单的题目时,被一个用例卡了时间效率,在参考标准答案的时候,发现了一个优雅的针对数组右旋k位的方法:
cpp
for (int i = 0; i < n; ++i) {
newArr[(i + k) % n] = nums[i];
}
具体来说,对于数组nums,我们创建了一个新数组newArr
,其中每个元素nums[i]被放置到位置(i + k) % n
。这确实是一种巧妙且简洁的方法。
于是这引起我的思考,就总结了许多类似巧妙的算法,它们利用模运算、数学特性或空间换时间的策略实现高效操作。
二、小试牛刀
1. 循环左移(类似右移)
将数组左移 k
位(等价于右移 n-k
位):
cpp
for (int i = 0; i < n; ++i) {
newArr[(i - k + n) % n] = nums[i]; // 左移k位
}
关键 :(i - k + n) % n
确保索引非负。
2. 原地旋转(三次反转法)
不占用额外空间,通过三次反转实现右移:
cpp
// 辅助函数:反转数组区间
void reverse(vector<int>& arr, int start, int end) {
while (start < end) {
swap(arr[start], arr[end]);
start++;
end--;
}
}
// 主逻辑
k %= n; // 处理 k > n 的情况
reverse(nums, 0, n-1); // 反转整个数组
reverse(nums, 0, k-1); // 反转前k部分
reverse(nums, k, n-1); // 反转剩余部分
一般难以想到,但是很有效。
3. 环状替换(原地旋转)
逐个元素移动到目标位置(类似约瑟夫环):
cpp
k %= n;
int count = 0;
for (int start = 0; count < n; start++) {
int current = start;
int prev = nums[start];
do {
int next = (current + k) % n;
swap(prev, nums[next]); // 将prev放到next位置
current = next;
count++;
} while (start != current); // 循环回到起点时结束
}
可以使用,可能会超时。
4. 循环队列实现
利用模运算实现环形缓冲区:
cpp
class CircularQueue {
vector<int> data;
int head = 0, tail = 0, size;
public:
CircularQueue(int k) : size(k+1) { data.resize(k+1); } // 多留一个空位区分满/空
bool enQueue(int val) {
if (isFull()) return false;
data[tail] = val;
tail = (tail + 1) % size; // 关键模运算
return true;
}
bool deQueue() {
if (isEmpty()) return false;
head = (head + 1) % size; // 关键模运算
return true;
}
};
本质是一个环形队列,对于高并发场景下的考虑,就需要建立生产者消费者模型,维护三种关系。可以参考我的之前的文章《生产者消费者模型:环形队列》
5. 字符串循环移位检测
判断字符串 s
是否由 goal
循环移位生成:
cpp
bool isRotated(string s, string goal) {
return s.size() == goal.size() && (s + s).find(goal) != string::npos;
}
// 示例:s="abcde", goal="cdeab" -> (s+s)="abcdeabcde" 包含 "cdeab"
6. 矩阵旋转(90度顺时针)
利用对称性原地旋转 n x n
矩阵:
cpp
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
swap(matrix[i][j], matrix[j][i]); // 转置
}
}
for (int i = 0; i < n; i++) {
reverse(matrix[i].begin(), matrix[i].end()); // 每行反转
}
对于逆时针旋转90度:
方法一:顺时针旋转270°,十分低效;
方法二:先反转每一行,再转置。
cpp
// 1. 反转每一行(行内元素逆序)
for (int i = 0; i < n; i++) {
reverse(matrix[i].begin(), matrix[i].end());
}
// 2. 执行转置(行列互换)
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
顺便解决的问题:
leetcode:48. 旋转图像
7. 约瑟夫环问题(数学解法)
用递推公式高效求解(从0开始编号):
cpp
int josephus(int n, int k) {
int pos = 0;
for (int i = 2; i <= n; i++) {
pos = (pos + k) % i; // 关键递推关系
}
return pos; // 最后剩余的位置
}
三、总结
- 模运算 :处理循环索引的核心(如
(i+k) % n
)。 - 数学特性:利用反转、转置等操作转换问题(如三次反转法)。
- 空间换时间:额外数组简化逻辑(如第一个例子)。
- 递推关系:约瑟夫环问题中的高效递推公式。
这些算法展示了如何通过数学洞察力将复杂问题转化为简洁操作,是编程中"优雅解法"的典型例子。
完
未经作者同意禁止转载
