Q39- code49- 字母异位词分组
实现思路
1 方法1: Map + Sort
易错点:
- 如果使用 字符编码之和作为key, 可能会有哈希冲突(如 "az" 和 "by")
- 所以需要 精确使用 字符排序后的字符串作为key
参考文档
代码实现
1 方法1: Map + Sort 时间复杂度: O(nmlogm); 空间复杂度(nm)
ts
function groupAnagrams(strs: string[]): string[][] {
const saved = new Map();
for (let str of strs) {
// 易错点1:字符编码之和作为key, 可能会有哈希冲突(如 "az" 和 "by")
// 正确方法:对字符串中的字符进行排序, 排序后的字符串对于所有异位词都是相同的
const strKey = str.split("").sort().join("");
const pre = saved.get(strKey) || [];
saved.set(strKey, [...pre, str]);
}
return [...saved.values()];
}
Q40- code350- 两个数组的交集 II
实现思路
1 Map
- 优化点: 先判断两个数组的长度,将较短的数组作为l1,较长的数组作为l2
- 易错点1: 由于Map的key是Set,所以需要累增计数,而不能每个key都设置为1
参考文档
代码实现
1 方法1: Map 时间复杂度: O(m + n); 空间复杂度(n)
ts
function intersect(nums1: number[], nums2: number[]): number[] {
// 优化点: 先判断两个数组的长度,将较短的数组作为l1,较长的数组作为l2
const [l1, l2] =
nums1.length <= nums2.length ? [nums1, nums2] : [nums2, nums1];
// 易错点1: 由于Map的key是Set,所以需要累增计数,而不能每个key都设置为1
const saved = new Map();
const res = [];
l1.map((val) => saved.set(val, (saved.get(val) ?? 0) + 1));
for (const val of l2) {
const pre = saved.get(val);
if (pre) {
res.push(val);
saved.set(val, pre - 1);
}
}
return res;
}
Q41- code299- 猜数字游戏
实现思路
1 数字次数统计法
1.1 错误思路
- S1 创建secret的 (idx: val) 记录对
- S2 遍历guess 如果(idx, val) 都对,则作为 A长度+1 + 删除该记录 如果val对, idx不对,则作为 B长度+1 + 删除该记录 如果val不对, idx不对,直接略过
错误点: 以上实现会出现 误删除 后一个位置是正确公牛的情况 如 secret = "011" guess = "110"
1.2 正确思路是,
- 创建 0~9数字的出现次数 统计
- 先处理完所有的公牛情况,再单独处理奶牛
- 奶牛的个数,应该是secret 和 guess 对应0~9数字 的出现的最小次数
参考文档
代码实现
1 方法1: 数字次数统计法 时间复杂度: O(n); 空间复杂度 O(1)
ts
function getHint(secret: string, guess: string): string {
let bulls = 0;
// 长度为10的数组,记录secret中每个数字(0-9)的出现次数(排除公牛后)
const secretCount = new Array(10).fill(0);
// 长度为10的数组,记录guess中每个数字(0-9)的出现次数(排除公牛后)
const guessCount = new Array(10).fill(0);
// 题目保证了secret.length == guess.length
for (let i = 0; i < secret.length; i++) {
if (secret[i] === guess[i]) {
// 如果 位置和值 都相等:说明是公牛,bulls++
bulls++;
} else {
// 将这两个数字分别计入统计数组(排除公牛后的统计)
// 等价于 secretCount[+secret[i]] = secretCount[+secret[i]] + 1
secretCount[+secret[i]]++;
guessCount[+guess[i]]++;
}
}
// 计算奶牛数量:遍历secretCount,
// count:当前数字在secret中的出现次数
// i:当前数字(0-9)
// guessCount[i]:当前数字在guess中的出现次数
// 对于每个数字,取该数字在secret和guess中的出现次数的最小值
// 累加得到奶牛数量
const cows = secretCount.reduce(
(sum, count, i) => sum + Math.min(count, guessCount[i]),
0
);
// 返回结果格式:公牛数量 + "A" + 奶牛数量 + "B"
return `${bulls}A${cows}B`;
}
Q42- code348- 设计井字棋
实现思路
1 方法1:增量更新 + 计数追踪
- 我们只关心:每行/列/对角线被同一玩家占据了多少个位置
- 只存储"获胜条件"的状态,而不是整个棋盘状态
- 这种"增量更新"的思想 在很多算法题中都很常见,比如滑动窗口、前缀和等,都是避免重复计算,利用已知信息快速得到新结果
1.2 具体分析过程
S1 理解题目核心
- 首先明确题目要求:每次落子后,判断是否有玩家获胜。
- 获胜条件是:同一行、同一列 或 同一对角线 都被 同一玩家占据
S2 从简单情况开始思考:暴力解法
- 每次落子后,检查所有行、列、对角线
- 时间复杂度:O(n²),因为要遍历整个棋盘
S3.1 寻找优化思路:观察获胜模式
- 获胜需要:某一行全部相同,或某一列全部相同,或某条对角线全部相同
- 关键点:不需要检查整个棋盘,只需要检查当前落子影响的行、列、对角线
S3.2 计数思维
- 对于每一行:记录每个玩家在该行的棋子数量
- 对于每一列:记录每个玩家在该列的棋子数量
- 对于对角线:记录每个玩家在对角线的棋子数量
- 当某个玩家的计数达到n时,就获胜了
S4 边界情况考虑
- 对角线:只有落子在主对角线或副对角线时才需要更新
- 玩家标识:可以用1和2,或者1和-1来区分
- 平局情况:当棋盘满了但没有获胜者
参考文档
代码实现
1 方法1: 增量更新 + 计数追踪 时间复杂度: O(1); 空间复杂度 O(n)
ts
class TicTacToe {
private n: number;
private rows: number[];
private cols: number[];
private diagonals: number[];
constructor(n: number) {
// 创建 n行 * n列 + 2条对角线的 记录
this.n = n;
this.rows = new Array(n).fill(0);
this.cols = new Array(n).fill(0);
// diagonals[0]主对角线,diagonals[1]副对角线
this.diagonals = [0, 0];
}
move(row: number, col: number, player: number): number {
// 使用 1 和 -1 来区分玩家,简化计算
const val = player === 1 ? 1 : -1;
const n = this.n;
// 更新行计数 和 列计数
this.rows[row] += val;
this.cols[col] += val;
// 更新主对角线:其特点是 row === col
// 如:(0,0) (1,1) (2,2)...
if (row === col) this.diagonals[0] += val;
// 更新副对角线:其特点是 row + col === n - 1
// 如:(0,2) (1,1) (2,0)...
if (row + col === n - 1) this.diagonals[1] += val;
// 判断落下了 该行/该列/该对角线后,此时是否有绝对值 达到了n
if (
Math.abs(this.rows[row]) === n ||
Math.abs(this.cols[col]) === n ||
Math.abs(this.diagonals[0]) === n ||
Math.abs(this.diagonals[1]) === n
) {
return player;
}
// 否则说明落完这一子后,当前还没有获胜者
return 0;
}
}