LC020valid_parentheses
题目:
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
解法:
遇到右括号的时候需要判断有没有对应匹配的左括号,因为必须是类型相同的才能进行闭合,因此需要一个栈来维护遇到的左括号,看栈顶的左括号是否跟当前的右括号匹配。
java
static Map<Character, Character> cache = new HashMap<>();
static {
cache.put(')', '(');
cache.put('}', '{');
cache.put(']', '[');
}
public boolean isValid(String s) {
LinkedList<Character> stack = new LinkedList<>();
for (char c : s.toCharArray()) {
// 遇到右括号就需要一定要有匹配的左括号
if (cache.containsKey(c)) {
if (stack.isEmpty()) {
return false;
}
if (cache.get(c) != stack.pop()) {
return false;
}
} else {
//左括号的情况,直接入栈
stack.push(c);
}
}
return stack.isEmpty();
}
这里有个技巧,维护一个右括号为key 的map,这样方便判断和匹配。
LC021merge_two
题目:
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解法:
遇到链表题,直接创建一个pre 节点,接着轮流比较两边的大小,比较繁琐的是需要判断很多临界情况。
java
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode pre = new ListNode();
ListNode cur = pre;
while (list1 != null || list2 != null) {
ListNode next;
if (list1 == null) {
next = list2;
list2 = list2.next;
} else if (list2 == null) {
next = list1;
list1 = list1.next;
} else {
if (list1.val < list2.val) {
next = list1;
list1 = list1.next;
} else {
next = list2;
list2 = list2.next;
}
}
cur.next = next;
cur = cur.next;
}
return pre.next;
}
LC022generate_parentheses
题目:
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
题解:
一般这种题都是递归的思路,相当于有2n个位置,每个位置都可能至多有2种策略,不是放左括号就是放右括号,放好之后再接着放下一个。这里有个有效括号的限制,所以想清楚这个就相对容易了。
java
List<String> result = new ArrayList<>();
public List<String> generateParenthesis(int n) {
dsf("", n, n);
return result;
}
/**
* 递归的写法
*
* @param path 当前的字符串
* @param left 左括号剩余使用次数
* @param right 右括号剩余使用次数
*/
private void dsf(String path, int left, int right) {
// 错误的情况
if (left > right) {
return;
}
if (left == 0 && right == 0) {
result.add(path);
return;
}
if (left > 0) {
dsf(path + "(", left - 1, right);
}
if (right > 0) {
dsf(path + ")", left, right - 1);
}
}
这里使用剩余使用测试,而不是已经使用次数,会让代码更加清晰。
递归的写法在熟悉递归模板之后,就是对什么时候跳出递归的判断,这一步思考清楚,后面就不难了。
LC023merge_k
题目:
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
题解:
其实就是利用递归的思路,先对半往下递归,最后是两个链表的合并(LC021merge_two )。
java
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists, int left, int right) {
if (left == right) {
return lists[left];
}
int mid = left + (right - left) / 2;
final ListNode l1 = merge(lists, left, mid);
final ListNode l2 = merge(lists, mid + 1, right);
return merge2(l1, l2);
}
private ListNode merge2(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = merge2(l1.next, l2);
return l1;
} else {
l2.next = merge2(l1, l2.next);
return l2;
}
}
递归真是好东西啊,任何复杂的问题都能化繁为简。
LC031next_permutation
题目:
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
给你一个整数数组 nums ,找出 nums 的下一个排列。
题解:
这类题目就是典型的套路题了,当初靠自己想破脑袋估计也想不出来,直接看题解知道套路,掌握思路后加上自己在事例上推演下也就能写出来了。
java
public void nextPermutation(int[] nums) {
final int len = nums.length;
if (len < 2) {
return;
}
int i = len - 2, k = len - 1;
//找到最右边的"小数"
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
//如果不是最后一种情况,注意这里包含=0 的情况
if (i >= 0) {
//从后往前,找到尽可能"小"的大数
while (nums[i] >= nums[k]) {
k--;
}
int tmp = nums[i];
nums[i] = nums[k];
nums[k] = tmp;
}
//需要将i+1 之后的数字排序
//将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。
Arrays.sort(nums, i + 1, len);
}
LC032longest_valid
题目:
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度
题解:
"最*** " 的基本是动态规划的思路。只是这里情况比较复杂,需要考虑很多边界情况
java
public int longestValidParentheses(String s) {
final char[] chars = s.toCharArray();
int[] dp = new int[chars.length];
int max = 0;
for (int i = 1; i < chars.length; i++) {
//只有封闭的右括号才有可能配对
if (chars[i] == ')') {
//跟前一位刚好匹配
if (chars[i - 1] == '(') {
// 数量就是前前位+2,这里>= 还是> 实际是一样的,因为dp[0]=0
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else { // i-1 也是右括号
final int pre = i - 1 - dp[i - 1];
if (pre >= 0 && chars[pre] == '(') {
dp[i] = (pre > 0 ? dp[pre - 1] : 0) + dp[i - 1] + 2;
}
}
max = Math.max(max, dp[i]);
}
}
return max;
}
这个也可以算套路题吧,动态规划还是挺考验功力的,动态方程往往取决动态数组的定义。这类题只能靠多做,多培养感觉了。
LC033search_in
题目:
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
题解:
经典的二分查找啊,之前刚学二分查找的时候觉得这玩意还不简单,没有特意准备,结果是啪啪打脸了。先复习下二分查找模板:
java
left, right = 0, len(array) - 1
/注意这里的=
while left <= right:
mid = left+(right - left) / 2
if array[mid] == target:
# find the target!!
break or return result
elif array[mid] < target:
left = mid + 1
else:
right = mid - 1
回到这道题,关键点是旋转数组始终有一半是有序的,这个时候针对有序的一边进行判断和处理,如果不在这一边就说明在另一边,说明当前这一边可以舍弃,继续下一步迭代。
java
public int search(int[] nums, int target) {
if (nums.length == 0) {
return -1;
}
int left = 0, 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;
}
LC34find_first
题目:
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
题解:
这道题比较有意思,一般我们通过折半查找都是确定具体的位置,这里是要找做边界和右边界,实际跟折半查找非常类似,只是左边界往小的那一边继续找,右边界就往大的那一边继续找。
java
public int[] searchRange(int[] nums, int target) {
final int len = nums.length;
if (len == 0) {
return new int[]{ -1, -1 };
}
int left = 0, right = len - 1;
int begin = -1, end = -1;
//先找左边起始点
while (left <= right) {
int mid = left + (right - left) / 2;
//要找左边界,需要往尽可能小的方向寻找
if (nums[mid] == target) {
begin = mid;
right = mid - 1;
} else if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
left = 0;
right = len - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
//要找右边界,需要往尽可能大的方向寻找
if (nums[mid] == target) {
end = mid;
left = mid + 1;
} else if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return new int[]{ begin, end };
}
可以看到有挺多重复代码,可以提一个方法出来
java
public int[] searchRange(int[] nums, int target) {
int left = 0, right = nums.length - 1;
int begin = getLocation(nums, target, left, right, true);
int end = getLocation(nums, target, left, right, false);
return new int[]{ begin, end };
}
/**
* 找到target 对应的下标
*
* @param nums
* @param target
* @param left
* @param right
* @param isBegin 是否起始点
* @return
*/
private int getLocation(int[] nums, int target, int left, int right, boolean isBegin) {
int location = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (target == nums[mid]) {
location = mid;
//关键点在于找起止点的时候对左右游标的处理
if (isBegin) {
right = mid - 1;
} else {
left = mid + 1;
}
} else if (target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return location;
}
LC39combination_sum
题目:
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
题解:
也是典型的递归思路,关键是怎么保证不走回头路,比如有整数数组是(1,2,3),在递归1 的时候,把所有可以跟1组合的所有数组都穷举了,那么递归到2 的时候,就不能再用1 了,因为这个组合肯定在之前递归1的时候已经存在了。
java
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates.length == 0) {
return result;
}
dfs(candidates, new ArrayList<>(), 0, target);
return result;
}
/**
* 注意这里需要一个start 标识,用来避免走回头路引起的重复数据
*
* @param candidates
* @param path
* @param start
* @param target
*/
private void dfs(int[] candidates, ArrayList<Integer> path, int start, int target) {
if (target == 0) {
result.add(new ArrayList<>(path));
return;
}
if (target < 0) {
return;
}
for (int i = start; i < candidates.length; i++) {
path.add(candidates[i]);
dfs(candidates, path, i, target - candidates[i]);
path.remove(path.size() - 1);
}
}
LC46permutations
题目:
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
题解:
也是用递归解决,难点在于去重。这里通过一个boolean 数组来标识已经使用过的数字。这里可以复习下上一篇的递归模板。
java
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
dfs(nums, new ArrayList<>(), used);
return result;
}
private void dfs(int[] nums, ArrayList<Integer> path, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
path.add(nums[i]);
used[i] = true;
dfs(nums, path, used);
used[i] = false;
path.remove(path.size() - 1);
}
}