题目: 回文子串
原题链接: 回文子串
题解
方法1:暴力(通过了)
-
枚举子串:
- 外层循环
i
作为子串的起点,从0
到s.length() - 1
。 - 内层循环
j
作为子串的终点,从i
到s.length() - 1
,逐步扩展子串。 - 通过
StringBuilder
逐个字符拼接生成子串sb
。
- 外层循环
-
判断回文:
- 复制
sb
并翻转(temp.reverse()
)。 - 判断
sb.toString()
是否等于翻转后的字符串,若相等则res + 1
。
- 复制
时间复杂度: O(n³)
-
O(n²) 遍历所有子串
-
O(n) 检查回文(reverse() 需要 O(n))
public int countSubstrings(String s) {
int res = 0;
for (int i = 0; i < s.length(); i++) {
StringBuilder sb = new StringBuilder();
for (int j = i; j < s.length(); j++) {
sb.append(s.charAt(j));
StringBuilder temp = new StringBuilder(sb); // 因为reverse是原地修改的,所以需要创建一个副本
if (sb.toString().equals(temp.reverse().toString())) {
res += 1;
}
}
}
return res;
}
方法2:中心扩展法
优化效果:
-
时间复杂度降至 O(n²)
-
空间复杂度 O(1),不需要额外存储子串
public int countSubstrings(String s) {
int res = 0;
for (int i = 0; i < s.length(); i++) {
// 以 i 为中心,检查奇数长度的回文
res += expandAroundCenter(s, i, i);
// 以 i 和 i+1 为中心,检查偶数长度的回文
if (i + 1 < s.length()) {
res += expandAroundCenter(s, i, i + 1);
}
}
return res;
}
// 从中心向两边扩展,统计回文子串数量
private int expandAroundCenter(String s, int left, int right) {
int count = 0;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
count++; // 每次扩展成功,找到一个回文子串
left--;
right++;
}
return count;
}
题目: 最长回文子串
原题链接: 最长回文子串
题解
用回文子串
的暴力方法改一下,就会超时。
方法1:中心扩展法
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) return "";
int start = 0;
int maxLength = 1;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > maxLength) {
maxLength = len;
start = i - (len - 1) / 2; // 更新起点
}
}
return s.substring(start, start + maxLength);
}
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
题目: 计数质数
原题链接: 计数质数
题解
public int countPrimes(int n) {
if (n <= 2) return 0;
// boolean 数组,初始时假设所有数都是质数
boolean[] isPrime = new boolean[n];
Arrays.fill(isPrime, true); // 全部初始化为true
// 从 2 开始筛选
for (int i = 2; i * i < n; i++) {
if (isPrime[i]) {
// 将 i 的倍数标记为非质数
for (int j = i * i; j < n; j += i) {
isPrime[j] = false;
}
}
}
// 统计质数的数量
int count = 0;
for (int i = 2; i < n; i++) {
if (isPrime[i]) {
count++;
}
}
return count;
}
题目: 基本计算器 II
原题链接: 基本计算器 II
题解
方法:栈
具体来说,遍历字符串 s,并用变量 preSign 记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据 preSign 来决定计算方式:
- 加号:将数字压入栈;
- 减号:将数字的相反数压入栈;
- 乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。
public static int calculate(String s) {
LinkedList<Integer> stack = new LinkedList<>();
char preSign = '+';// 用于存储前一个操作符,初始为 '+'
int num = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
num = num * 10 + (c - '0');
}
if (!Character.isDigit(c) && s.charAt(i) != ' ' || i == s.length() - 1) {
if (preSign == '+') {
stack.push(num);
} else if (preSign == '-') {
stack.push(-num);
} else if (preSign == '*') {
stack.push(stack.pop() * num);
} else if (preSign == '/') {
stack.push(stack.pop() / num);
}
preSign = c;
num = 0;
}
}
int res = 0;
for (Integer i : stack) {
res += i;
}
return res;
}
题目: 基本计算器
原题链接: 基本计算器
题解
方法:
public static int calculate(String s) {
// ops 存储符号状态 (1 表示正号,-1 表示负号)
Deque<Integer> ops = new LinkedList<Integer>();
ops.push(1); // 初始符号为 1(正号)
int sign = 1; // sign 表示当前计算的符号状态
int ret = 0;
int n = s.length();
int i = 0;
// 循环遍历整个表达式字符串
while (i < n) {
// 跳过空格
if (s.charAt(i) == ' ') {
i++;
} else if (s.charAt(i) == '+') {
sign = ops.peek();
i++;
} else if (s.charAt(i) == '-') {
sign = -ops.peek();
i++;
} else if (s.charAt(i) == '(') {
ops.push(sign);
i++;
} else if (s.charAt(i) == ')') {
ops.pop();
i++;
} else {
// 如果遇到数字,解析整个数字并将其加到结果 ret 中
long num = 0;
while (i < n && Character.isDigit(s.charAt(i))) {
num = num * 10 + s.charAt(i) - '0';
i++;
}
ret += sign * num;
}
}
return ret;
}
- 当遇到左括号 ( 时,把当前的符号状态 sign 压入栈中。这样在括号内的表达式会继承括号外的符号状态。
- 当遇到右括号 ) 时,弹出栈顶符号,恢复括号外的符号状态。
题目-和为 K 的子数组
题解
解法1:两层for循环
public class T560 {
public static int subarraySum(int[] nums, int k) {
int res = 0;
for (int i = 0; i < nums.length; i++) {
int tempSum = 0;
for (int j = i; j < nums.length; j++) {
tempSum += nums[j];
if (tempSum == k) {
res++;
}
}
}
return res;
}
public static void main(String[] args) {
int[] nums = {1, -1, 0};
int k = 0;
System.out.println(subarraySum(nums, k));
}
}
解法2:前缀和+哈希
前缀和
:前缀和数组 sum[i] 表示从数组起点到位置 i 的元素之和。哈希表
:使用哈希表记录前缀和出现的次数,这样在遍历数组时可以快速找到符合条件的子数组。
public class T560 {
public static int subarraySum(int[] nums, int k) {
int res = 0;
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
//
int currentSum = 0;
for (int i = 0; i < nums.length; i++) {
//sum[i...j]=sum[j]-sum[i-1]
currentSum += nums[i];
if (map.containsKey(currentSum - k)) {
res += map.get(currentSum - k);
}
map.put(currentSum, map.getOrDefault(currentSum, 0) + 1);
}
return res;
}
public static void main(String[] args) {
int[] nums = {1, -1, 0};
int k = 0;
System.out.println(subarraySum(nums, k));
}
}
详细解释 currentSum - k
设 currentSum 表示当前的前缀和,也就是从数组起点到当前位置的元素之和。我们希望找到一个子数组,使得这个子数组的和等于 k。假设我们当前遍历到数组位置 j,当前的前缀和为 currentSum。
如果存在某个位置 i 使得从 i 到 j 的子数组和等于 k,根据前缀和的定义,有:sum[j]-sum[i-1]=k
其中sum[j]
就是currentSum
变形一下:sum[j]-sum[i-1]=k
➡️sum[j]-k=sum[i-1]
➡️currentSum-k=sum[i-1]
。(因为已知k,不知道sum[i-1] ,所以把k移到等号左边,看map中是否包含sum[i-1],包含则说明i
到j
的和为k
)
为什么是res += map.get(currentSum - k);
而不是res += 1;
因为可能存在多个前缀和为currentSum - k
。搞懂map存的什么