目录
第一题
题目来源
题目内容
解决方法
方法一:滑动窗口
方法二:双指针加哈希表
第二题
题目来源
题目内容
解决方法
方法一:二分查找
方法二:归并排序
方法三:分治法
第三题
题目来源
题目内容
解决方法
方法一:动态规划
方法二:中心扩展法
方法三:Manacher 算法
3. 无重复字符的最长子串 - 力扣(LeetCode)
该问题可以使用滑动窗口算法来解决。滑动窗口是一种通过移动窗口的起始和结束位置来解决字符串/数组子串问题的常用技巧。
具体算法步骤如下:
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set set = new HashSet<>();
int maxLen = 0, left = 0, right = 0;
while (right < n) {
if (!set.contains(s.charAt(right))) {
set.add(s.charAt(right));
maxLen = Math.max(maxLen, right - left + 1);
right++;
} else {
set.remove(s.charAt(left));
left++;
}
}
return maxLen;
}
}
该算法的时间复杂度为O(n),其中n是字符串s的长度。在最坏情况下,每个字符都需要遍历一次。空间复杂度为O(min(n, m)),其中m是字符集的大小。在最坏情况下,窗口中可能包含所有的字符。
LeetCode运行结果:
除了滑动窗口算法之外,还可以使用双指针加哈希表来解决该问题。
具体算法步骤如下:
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Map map = new HashMap<>();
int maxLen = 0, left = 0, right = 0;
while (right < n) {
char c = s.charAt(right);
if (map.containsKey(c)) {
left = Math.max(left, map.get(c) + 1);
}
map.put(c, right);
maxLen = Math.max(maxLen, right - left + 1);
right++;
}
return maxLen;
}
}
该算法的时间复杂度为O(n),其中n是字符串s的长度。在最坏情况下,每个字符都需要遍历一次。空间复杂度为O(min(n, m)),其中m是字符集的大小。在最坏情况下,哈希表中可能包含所有的字符。
LeetCode运行结果:
4. 寻找两个正序数组的中位数 - 力扣(LeetCode)
本题可以使用二分查找求解,时间复杂度为O(log(min(m, n)))。
由于两个数组都是有序的,所以可以先将问题转化为寻找第k小的数,其中k等于两个数组的长度之和除以2。如果两个数组长度之和是奇数,则中位数就是第k小的数;如果长度之和是偶数,则中位数是第k小和第k+1小数的平均值。
具体算法如下:
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
if (m > n) {
return findMedianSortedArrays(nums2, nums1);
}
int k = (m + n + 1) / 2;
int left = 0, right = m;
while (left < right) {
int i = left + (right - left) / 2;
int j = k - i;
if (nums1[i] < nums2[j - 1]) {
left = i + 1;
} else {
right = i;
}
}
int i = left, j = k - i;
int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
int nums1RightMin = i == m ? Integer.MAX_VALUE : nums1[i];
int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
int nums2RightMin = j == n ? Integer.MAX_VALUE : nums2[j];
if ((m + n) % 2 == 0) {
return (Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin)) / 2.0;
} else {
return Math.max(nums1LeftMax, nums2LeftMax);
}
}
}
该算法的时间复杂度为O(log(min(m, n))),空间复杂度为O(1)。
LeetCode运行结果:
还有另一种方法可以解决这个问题,即使用归并排序的思想。具体步骤如下:
该方法的时间复杂度为O(m + n),其中m和n分别是两个数组的长度。空间复杂度为O(m + n),主要用于存储合并后的数组。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int[] merged = new int[m + n];
int i = 0, j = 0, k = 0;
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) {
merged[k++] = nums1[i++];
} else {
merged[k++] = nums2[j++];
}
}
while (i < m) {
merged[k++] = nums1[i++];
}
while (j < n) {
merged[k++] = nums2[j++];
}
if ((m + n) % 2 == 0) {
int mid = (m + n) / 2;
return (merged[mid - 1] + merged[mid]) / 2.0;
} else {
int mid = (m + n) / 2;
return merged[mid];
}
}
}
LeetCode运行结果:
除了上述两种方法,还可以使用分治法来解决这个问题。该方法的思路是将问题分解为两个子问题,然后对子问题进行递归求解。
具体步骤如下:
这种方法的时间复杂度也为O(log(min(m, n))),空间复杂度为O(1)。与二分查找类似,它通过逐渐缩小问题规模来快速找到中位数。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int total = m + n;
if (total % 2 == 1) { // 奇数长度,中位数是第 total/2 + 1 个元素
return getKthElement(nums1, nums2, total / 2 + 1);
} else { // 偶数长度,中位数是第 total/2 个元素和第 total/2 + 1 个元素的平均值
double left = getKthElement(nums1, nums2, total / 2);
double right = getKthElement(nums1, nums2, total / 2 + 1);
return (left + right) / 2.0;
}
}
private int getKthElement(int[] nums1, int[] nums2, int k) {
int m = nums1.length, n = nums2.length;
int index1 = 0, index2 = 0;
while (true) {
// 边界情况:一个数组的所有元素都被剔除
if (index1 == m) {
return nums2[index2 + k - 1];
}
if (index2 == n) {
return nums1[index1 + k - 1];
}
// 边界情况:k=1,即找到了最小的一个数
if (k == 1) {
return Math.min(nums1[index1], nums2[index2]);
}
// 正常情况
int newIndex1 = Math.min(index1 + k / 2, m) - 1;
int newIndex2 = Math.min(index2 + k / 2, n) - 1;
int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
} else {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
}
}
}
LeetCode运行结果:
5. 最长回文子串 - 力扣(LeetCode)
这道题可以使用动态规划来解决,具体步骤如下:
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
int maxLen = 0;
int start = 0;
for (int j = 0; j < n; j++) {
for (int i = j; i >= 0; i--) {
if (s.charAt(i) == s.charAt(j) && (j - i <= 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
if (j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
}
return s.substring(start, start + maxLen);
}
}
该算法的时间复杂度为O(n^2),空间复杂度为O(n^2),其中n为字符串的长度。通过动态规划,我们可以高效地找到最长回文子串。
LeetCode运行结果:
除了动态规划方法外,还可以使用中心扩展法来解决这个问题。
中心扩展法的思路是,对于每个字符或每对相邻字符,以它们为中心向两边扩展,判断是否是回文串。具体步骤如下:
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
int start = 0, maxLen = 0;
for (int i = 0; i < n; i++) {
// 中心为一个字符的情况
int len1 = expandAroundCenter(s, i, i);
// 中心为相邻字符的情况
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > maxLen) {
maxLen = len;
// 根据中心和回文串长度计算起始索引
start = i - (len - 1) / 2;
}
}
return s.substring(start, start + maxLen);
}
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
// 扩展的长度为 right-left-1,减1是因为不满足条件时left和right多移动了一步
return right - left - 1;
}
}
该算法的时间复杂度为O(n^2),空间复杂度为O(1),其中n为字符串的长度。中心扩展法利用了回文串的特点,可以高效地找到最长回文子串。
除了动态规划和中心扩展法之外,还有一种称为Manacher算法的线性时间算法可以用于查找最长回文子串。Manacher算法的核心思想是利用回文串的对称性,在遍历过程中尽量复用已经计算过的回文子串信息。
具体步骤如下:
#
),以确保新字符串中的回文串长度都是奇数。$
和%
),以便处理边界情况。P
,其中P[i]
表示以新字符串中索引i
为中心的回文串的半径长度(包括中心字符在内)。
center
和right
来维护当前已经找到的最右边界的回文串的中心和右边界。P[i]
的值。
i
在当前最右边界right
的左侧时,使用对称性快速计算出初始猜测值,即P[i] = P[2 * center - i]
。但如果该猜测值超出了最右边界,则需要修正为最右边界到边界之间的距离,即P[i] = right - i
。P[i]
的值并更新最右边界。maxLen = max(P) - 1
,起始索引为start = (maxP - 1) / 2
。class Solution {
public String longestPalindrome(String s) {
int n = s.length();
StringBuilder sb = new StringBuilder("$#");
// 预处理字符串
for (int i = 0; i < n; i++) {
sb.append(s.charAt(i));
sb.append("#");
}
sb.append("%");
String str = sb.toString();
int len = str.length();
int[] P = new int[len];
int center = 0, right = 0;
int maxP = 0, maxLen = 0;
for (int i = 1; i < len - 1; i++) {
if (i < right) {
int mirror = 2 * center - i;
P[i] = Math.min(right - i, P[mirror]);
}
// 中心扩展
while (str.charAt(i + P[i] + 1) == str.charAt(i - P[i] - 1)) {
P[i]++;
}
// 更新最右边界
if (i + P[i] > right) {
center = i;
right = i + P[i];
}
// 记录最长回文子串的起始索引和长度
if (P[i] > maxLen) {
maxLen = P[i];
maxP = i;
}
}
int start = (maxP - maxLen) / 2;
return s.substring(start, start + maxLen);
}
}
Manacher算法的时间复杂度为O(n),空间复杂度为O(n),其中n为字符串的长度。相比于动态规划和中心扩展法,Manacher算法在效率上具有优势,特别适用于处理大规模字符串。