389. 找不同
问题描述
给定两个字符串 s
和 t
,它们只包含小写字母。
字符串 t
由字符串 s
随机重排,然后在随机位置添加一个字母。
请找出在 t
中被添加的字母。
示例:
输入: s = "abcd", t = "abcde"
输出: "e"
解释: 'e' 是那个被添加的字母
输入: s = "", t = "y"
输出: "y"
解释: s为空,t中'y'是被添加的字母
输入: s = "a", t = "aa"
输出: "a"
解释: 添加了一个'a'
算法思路
方法一:字符频次统计
- 统计字符串
s
中每个字符的出现次数 - 遍历字符串
t
,对每个字符在频次统计中减1 - 当某个字符的频次变为-1时,该字符就是被添加的字符
方法二:ASCII码求和
- 计算字符串
t
中所有字符的ASCII码之和 - 减去字符串
s
中所有字符的ASCII码之和 - 差值对应的字符就是被添加的字符
方法三:异或运算(最优解)
- 利用异或运算的性质:a ^ a = 0, a ^ 0 = a
- 将
s
和t
中所有字符进行异或运算 - 相同字符会相互抵消,最终结果就是被添加的字符
代码实现
方法一:字符频次统计
class Solution {
/**
* 使用字符频次统计找不同字符
*
* @param s 原字符串
* @param t 添加一个字符后的字符串
* @return 被添加的字符
* 时间复杂度: O(n)
* 空间复杂度: O(1) - 固定26大小数组
*/
public char findTheDifference(String s, String t) {
// 创建频次数组
int[] charCount = new int[26];
// 统计s中每个字符的出现次数
for (char c : s.toCharArray()) {
charCount[c - 'a']++;
}
// 遍历t,减少对应字符的计数
for (char c : t.toCharArray()) {
charCount[c - 'a']--;
// 如果计数变为-1,说明该字符在t中比在s中多出现一次
if (charCount[c - 'a'] < 0) {
return c;
}
}
// 理论上不会执行到这里
return ' ';
}
}
方法二:ASCII码求和
class Solution {
/**
* 使用ASCII码求和找不同字符
*
* @param s 原字符串
* @param t 添加一个字符后的字符串
* @return 被添加的字符
* 时间复杂度: O(n)
* 空间复杂度: O(1)
*/
public char findTheDifference(String s, String t) {
int sum = 0;
// 计算t中所有字符的ASCII码之和
for (char c : t.toCharArray()) {
sum += c;
}
// 减去s中所有字符的ASCII码之和
for (char c : s.toCharArray()) {
sum -= c;
}
// 剩余的值就是被添加字符的ASCII码
return (char) sum;
}
}
方法三:异或运算(推荐)
class Solution {
/**
* 使用异或运算找不同字符(最优解)
*
* @param s 原字符串
* @param t 添加一个字符后的字符串
* @return 被添加的字符
* 时间复杂度: O(n)
* 空间复杂度: O(1)
*/
public char findTheDifference(String s, String t) {
char result = 0;
// 对s中所有字符进行异或
for (char c : s.toCharArray()) {
result ^= c;
}
// 对t中所有字符进行异或
for (char c : t.toCharArray()) {
result ^= c;
}
// 相同字符相互抵消,最终结果就是被添加的字符
return result;
}
}
方法四:单循环异或优化
class Solution {
/**
* 单循环异或优化版
*
* @param s 原字符串
* @param t 添加一个字符后的字符串
* @return 被添加的字符
* 时间复杂度: O(n)
* 空间复杂度: O(1)
*/
public char findTheDifference(String s, String t) {
char result = 0;
int i = 0;
// 同时遍历s和t(s比t少一个字符)
for (; i < s.length(); i++) {
result ^= s.charAt(i);
result ^= t.charAt(i);
}
// 处理t中最后一个字符
result ^= t.charAt(i);
return result;
}
}
算法分析
- 时间复杂度:所有方法都是 O(N),N 为字符串 s 的长度
- 空间复杂度:
- 方法一:O(1),固定26大小数组
- 方法二、三、四:O(1),只使用常数额外空间
- 方法对比:
- 方法一:直观易懂,但需要额外数组空间
- 方法二:简洁,但可能有整数溢出风险(实际不会,因为字符ASCII码范围小)
- 方法三:最优解,利用位运算性质,高效且优雅
- 方法四:方法三的优化版,减少了一次循环
算法过程
输入:s = "abcd", t = "abcde"
执行过程(方法一:频次统计):
- 统计s频次:a:1, b:1, c:1, d:1
- 遍历t:
- ‘a’: 频次[0]-- → 0
- ‘b’: 频次[1]-- → 0
- ‘c’: 频次[2]-- → 0
- ‘d’: 频次[3]-- → 0
- ‘e’: 频次[4]-- → -1 < 0 → 返回’e’
执行过程(方法二:ASCII求和):
- t的ASCII和:97+98+99+100+101 = 495
- s的ASCII和:97+98+99+100 = 394
- 差值:495-394 = 101 → 对应字符’e’
执行过程(方法三:异或运算):
- 初始化result = 0
- 异或s:0979899100 = 979899^100
- 异或t:(979899100)979899100101
- 相同字符抵消:97^97=0, 98^98=0, 99^99=0, 100^100=0
- 最终:0^101 = 101 → 对应字符’e’
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
System.out.println("Test 1: " + solution.findTheDifference("abcd", "abcde"));
// 输出: e
// 测试用例2:空字符串
System.out.println("Test 2: " + solution.findTheDifference("", "y"));
// 输出: y
// 测试用例3:添加相同字符
System.out.println("Test 3: " + solution.findTheDifference("a", "aa"));
// 输出: a
// 测试用例4:添加在开头
System.out.println("Test 4: " + solution.findTheDifference("bcd", "abcd"));
// 输出: a
// 测试用例5:添加在中间
System.out.println("Test 5: " + solution.findTheDifference("acd", "abcd"));
// 输出: b
// 测试用例6:长字符串
System.out.println("Test 6: " + solution.findTheDifference("abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyza"));
// 输出: a
// 测试用例7:单字符添加
System.out.println("Test 7: " + solution.findTheDifference("xyz", "xyza"));
// 输出: a
// 测试用例8:重复字符中添加
System.out.println("Test 8: " + solution.findTheDifference("aabbcc", "aabbbcc"));
// 输出: b
// 测试用例9:添加z
System.out.println("Test 9: " + solution.findTheDifference("abcdef", "abcdefz"));
// 输出: z
// 测试用例10:添加x
System.out.println("Test 10: " + solution.findTheDifference("abcdefghijklmnopqrstuvwyz", "abcdefghijklmnopqrstuvwxyz"));
// 输出: x
}
关键点
-
问题本质:
- 找出两个字符串中不同的那个字符
- 由于只有一个字符不同,可以利用各种数学和位运算性质
-
异或运算优势:
- 相同字符异或结果为0
- 0与任何字符异或结果为该字符本身
- 运算顺序不影响结果(异或满足交换律和结合律)
-
边界处理:
- s为空时,t中唯一字符就是答案
- 添加的字符可以是任意位置,包括开头、中间、结尾
-
字符集假设:
- 题目说明"只包含小写字母",所以方法一的26大小数组足够
- 其他方法对任意字符集都适用
常见问题
-
为什么异或运算是最优解?
- 空间复杂度最低(O(1))
- 时间复杂度最优(O(n))
- 代码简洁优雅
- 无溢出风险
- 利用了问题的数学特性
-
ASCII求和法有溢出风险吗?
- 理论上有,但实际不会发生。因为小写字母ASCII码97-122。
-
如果添加了多个字符怎么办?
- 本题明确只添加一个字符
- 如果要找多个不同字符,需要使用频次统计或其他方法
-
异或运算的数学原理是什么?
- 异或运算满足:a ^ a = 0(自反性),a ^ 0 = a(恒等性),a ^ b = b ^ a(交换律),(a ^ b) ^ c = a ^ (b ^ c)(结合律)
- 利用这些性质,相同字符会相互抵消,只剩下不同的字符