将一个字符 向后 移位的意思是将这个字符用字母表中 下一个 字母替换(字母表视为环绕的,所以
'z'变成'a')。
当你看到类似上面这种文字描述的时候,你就要想到 模归一化
环形取模(Circular Mod / Positive Mod)技巧
为什么需要?
当数据是环形的时候,例如:
- 字母
'a'~'z' - 星期 1~7
- 时钟 0~23
- 环形数组
- 循环队列
都会遇到:
加法可能超过最大值
减法可能变成负数
所以需要把结果映射回合法区间。
Java 的 % 并不是数学意义上的取模
很多人第一次都会踩坑。
例如
java
-1 % 26 == -1
-2 % 26 == -2
-27 % 26 == -1
Java 的 % 是取余(remainder),不是数学里的 Mod。
因此:
java
(a + b) % m
结果可能是负数。
注意:Python 取模会变成正数
最经典的写法
所以几乎所有涉及环形运算都会写成
java
((x % m) + m) % m
或者
java
(x + m) % m
(当能够保证 x > -m 时)
作用就是:
把负余数转换成正余数。
例如
java
-3 % 26 = -3
(-3 + 26) % 26
=23
于是
-3
↓
23
代表:
前移3位 == 后移23位
为什么要再 % 一次?
很多人疑惑:
java
(x % m + m)
为什么还要 % m?
举个例子。
假设
java
x = 27
那么
27 % 26 = 1
1+26=27
已经又超过了。
所以最后还需要
27%26
=1
因此完整模板始终都是
java
((x % m) + m) % m
它保证结果一定属于
[0,m-1]
字符偏移为什么这样写?
例如
java
'a'~'z'
实际上就是
0~25
所以都会先减去 'a'
java
c-'a'
得到
0~25
然后
当前位置+移动次数
最后
%26
再加回来
+'a'
模板就是
java
(char)((c-'a'+offset)%26+'a')
如果 offset 可能为负数
就变成
java
offset=((offset%26)+26)%26;
(char)((c-'a'+offset)%26+'a')
这几乎是所有字符循环题目的固定模板。
可以总结成一个套路
以后只要看到下面这些关键词:
- 环
- 循环
- 字母
- 星期
- 时钟
- 数组循环
- Rotation
- Shift
脑子里就应该想到
先归一化(Normalize),再取模(Modulo)。
即
java
offset=((offset%m)+m)%m;
然后
java
ans=(cur+offset)%m;
应用例子
参考代码(Java)
java
class Solution {
public String shiftingLetters(String S, int[][] shifts) {
int n = S.length();
int[] diff = new int[n + 1];
for (int[] q : shifts) {
int start = q[0], end = q[1], dir = q[2] * 2 - 1;
diff[start] += dir;
diff[end + 1] -= dir;
}
// 前缀和
for (int i = 1; i < n; i++) {
diff[i] += diff[i - 1];
}
char[] s = S.toCharArray();
for (int i = 0; i < n; i++) {
// 模归一化
s[i] = (char)(((s[i] - 'a' + diff[i]) % 26 + 26) % 26 + 'a');
}
return new String(s);
}
}