目录
第一题
题目来源
题目内容
解决方法
方法一:动态规划
方法二:栈
方法三:双指针
第二题
题目来源
题目内容
解决方法
方法一:二分查找
方法二:线性扫描
方法三:递归
第三题
题目来源
题目内容
解决方法
方法一:二分查找
方法二:线性扫描
方法三:双指针
32. 最长有效括号 - 力扣(LeetCode)
n
的数组 dp
,用于保存以当前字符结尾的最长有效括号子串的长度。dp
数组的所有元素为 0。s
的每个字符:
(
,则直接跳过。)
,则判断前一个字符是否是 (
:
(
,则更新 dp[i] = dp[i-2] + 2
,表示以当前字符结尾的最长有效括号子串长度为前一个字符结尾的最长有效括号子串长度加上当前的两个括号。)
,则判断前一个字符结尾的最长有效括号子串之前的字符是否是 (
,即判断 i-dp[i-1]-1
位置的字符是否是 (
:
(
,则更新 dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2
,表示以当前字符结尾的最长有效括号子串长度为前一个字符结尾的最长有效括号子串长度加上前一个字符结尾的最长有效括号子串之前的最长有效括号子串长度加上当前的两个括号。dp
数组中的最大值,即为最长有效括号子串的长度。public class Solution {
public int longestValidParentheses(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int n = s.length();
int[] dp = new int[n];
int maxLen = 0;
for (int i = 1; i < n; i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxLen = Math.max(maxLen, dp[i]);
}
}
return maxLen;
}
}
复杂度分析:
时间复杂度:
s
的每个字符需要 O(n) 的时间。空间复杂度:
n
的数组 dp
来保存以当前字符结尾的最长有效括号子串的长度。综合起来,该解法的时间复杂度为 O(n),空间复杂度为 O(n)。
注意:对于这个特定的问题,在给定的限制条件下(0 <= s.length <= 3 * 10^4),这个解法是高效且可行的。
LeetCode运行结果:
该方法的思路如下:
-1
放入栈中。s
的每个字符:
(
,将其索引位置压入栈中。)
,弹出栈顶元素,表示当前右括号匹配了一个左括号。
import java.util.Stack;
public class Solution {
public int longestValidParentheses(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int maxLen = 0;
Stack stack = new Stack<>();
stack.push(-1);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') {
stack.push(i);
} else {
stack.pop();
if (stack.isEmpty()) {
stack.push(i);
} else {
maxLen = Math.max(maxLen, i - stack.peek());
}
}
}
return maxLen;
}
}
复杂度分析:
时间复杂度:
空间复杂度:
综合起来,该方法的时间复杂度为 O(n),空间复杂度为 O(n)。
LeetCode运行结果:
除了动态规划和栈,还有一种双指针的方法来解决最长有效括号问题。
这种方法的思路如下:
public class Solution {
public int longestValidParentheses(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int left = 0; // 左括号数量
int right = 0; // 右括号数量
int maxLen = 0; // 最大长度
// 从左到右遍历
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') {
left++;
} else {
right++;
}
// 如果左括号数量等于右括号数量,则计算当前有效括号子串的长度
if (left == right) {
maxLen = Math.max(maxLen, right * 2);
} else if (right > left) { // 右括号数量大于左括号数量,重置左右括号数量为0
left = 0;
right = 0;
}
}
left = 0;
right = 0;
// 从右到左遍历
for (int i = s.length() - 1; i >= 0; i--) {
char c = s.charAt(i);
if (c == ')') {
right++;
} else {
left++;
}
// 如果左括号数量等于右括号数量,则计算当前有效括号子串的长度
if (left == right) {
maxLen = Math.max(maxLen, left * 2);
} else if (left > right) { // 左括号数量大于右括号数量,重置左右括号数量为0
left = 0;
right = 0;
}
}
return maxLen;
}
}
复杂度分析:
时间复杂度:
空间复杂度:
综合起来,该方法的时间复杂度为 O(n),空间复杂度为 O(1)。
LeetCode运行结果:
33. 搜索旋转排序数组 - 力扣(LeetCode)
使用二分查找的思想,通过判断左右半边哪一边是有序的来决定向哪边继续查找。具体步骤如下:
left
为数组的第一个元素的索引,右指针 right
为数组最后一个元素的索引。mid
。mid
。nums[left] <= nums[mid]
):
right
移动到 mid - 1
,继续在左半边查找。left
移动到 mid + 1
,继续在右半边查找。left
移动到 mid + 1
,继续在右半边查找。right
移动到 mid - 1
,继续在左半边查找。public class Solution {
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
if (nums[left] <= nums[mid]) { // 左半边有序
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // 右半边有序
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
}
复杂度分析:
总结起来,该算法具有较低的时间复杂度和空间复杂度,能够高效地解决搜索旋转排序数组的问题。
LeetCode运行结果:
除了二分查找的方法,还可以使用线性扫描的方法来搜索旋转排序数组。
该算法从数组的第一个元素开始,依次遍历数组中的每个元素,如果找到目标值,则返回其索引;如果遍历结束仍未找到目标值,则返回-1。
public class Solution {
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) {
return i;
}
}
return -1;
}
}
复杂度分析:
相对于二分查找算法的O(log n)时间复杂度,线性扫描算法的时间复杂度较高。但在某些特定场景或者输入规模较小的情况下,线性扫描算法也可以快速解决问题。
综上所述,线性扫描算法适用于简单的问题或者规模较小的数据集,但在更大规模的数据集上,二分查找算法通常更具优势。
LeetCode运行结果:
除了二分查找和线性扫描的方法,还可以使用递归的方法来搜索旋转排序数组。
该算法与二分查找算法类似,也是通过判断左右半边哪一边是有序的来决定向哪边继续查找。不同之处在于,该算法使用递归的方式实现,将数组的搜索范围不断缩小。
public class Solution {
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
return search(nums, target, 0, nums.length - 1);
}
private int search(int[] nums, int target, int left, int right) {
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
if (nums[left] <= nums[mid]) { // 左半边有序
if (target >= nums[left] && target < nums[mid]) {
return search(nums, target, left, mid - 1);
} else {
return search(nums, target, mid + 1, right);
}
} else { // 右半边有序
if (target > nums[mid] && target <= nums[right]) {
return search(nums, target, mid + 1, right);
} else {
return search(nums, target, left, mid - 1);
}
}
}
}
复杂度分析:
时间复杂度:
空间复杂度:
综合考虑,递归搜索旋转排序数组的算法在最坏情况下的时间复杂度为 O(n),空间复杂度为 O(log n)。但如果数组是近似有序的或者目标值位于有序部分内,时间复杂度可以接近 O(log n),效率较高。
需要注意的是,递归算法和二分查找算法类似,但由于需要额外的栈空间,因此可能会更慢。在处理超大规模数据时,可能会导致栈溢出问题。
LeetCode运行结果:
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = {-1, -1};
if (nums == null || nums.length == 0) {
return result;
}
int leftIndex = findLeft(nums, target);
int rightIndex = findRight(nums, target);
if (leftIndex <= rightIndex) {
result[0] = leftIndex;
result[1] = rightIndex;
}
return result;
}
private int findLeft(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid;
} else {
left = mid + 1;
}
}
return nums[left] == target ? left : -1;
}
private int findRight(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2 + 1;
if (nums[mid] <= target) {
left = mid;
} else {
right = mid - 1;
}
}
return nums[left] == target ? left : -1;
}
}
复杂度分析:
LeetCode运行结果:
除了二分查找,还可以使用线性扫描的方法来搜索排序数组中目标值的开始位置和结束位置。
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = {-1, -1};
if (nums == null || nums.length == 0) {
return result;
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) {
result[0] = i;
break;
}
}
if (result[0] != -1) {
for (int j = nums.length - 1; j >= 0; j--) {
if (nums[j] == target) {
result[1] = j;
break;
}
}
}
return result;
}
}
复杂度分析:
因为二分查找的时间复杂度为 log n,而线性扫描的时间复杂度为 n,所以在数组较大且已排序的情况下,二分查找的性能更好。它减少了搜索空间的大小,使得平均查找次数更低。然而,在数组较小或未排序的情况下,线性扫描是一种简单有效的方法。
LeetCode运行结果:
除了二分查找和线性扫描,还有一种常用的方法是双指针法。这种方法适用于有序数组或部分有序数组,并且可以在 O(n) 的时间复杂度内找到目标值的开始位置和结束位置。
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = {-1, -1};
if (nums == null || nums.length == 0) {
return result;
}
int left = 0;
int right = nums.length - 1;
// 查找目标值的开始位置
while (left <= right) {
if (nums[left] == target && nums[right] == target) {
result[0] = left;
result[1] = right;
break;
}
if (nums[left] < target) {
left++;
}
if (nums[right] > target) {
right--;
}
}
return result;
}
}
在上述代码中,我们使用两个指针 left 和 right 来标记搜索区间。通过不断更新指针的位置,我们可以确定目标值的开始位置和结束位置。
具体步骤如下:
复杂度分析:
需要注意的是,双指针法适用于有序数组或部分有序数组,如果数组无序,则双指针法无法正确找到目标值的开始位置和结束位置。
LeetCode运行结果: