21. 合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);
ListNode curr = head;
while(l1 != null && l2 != null) {
if(l1.val <= l2.val) {
curr.next = l1;
curr = curr.next;
l1 = l1.next;
} else {
curr.next = l2;
curr = curr.next;
l2 = l2.next;
}
}
if(l1 != null)
curr.next = l1;
if(l2 != null)
curr.next = l2;
return head.next;
}
}
22. 括号生成
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
方法一:暴力法
思路
我们可以生成所有 2^{2n}22n 个 '('
和 ')'
字符构成的序列。然后,我们将检查每一个是否有效。
算法
为了生成所有序列,我们使用递归。长度为 n
的序列就是 '('
加上所有长度为 n-1
的序列,以及 ')'
加上所有长度为 n-1
的序列。
为了检查序列是否为有效的,我们会跟踪 平衡
,也就是左括号的数量减去右括号的数量的净值。如果这个值始终小于零或者不以零结束,该序列就是无效的,否则它是有效的。
class Solution {
public List generateParenthesis(int n) {
List combinations = new ArrayList();
generateAll(new char[2 * n], 0, combinations);
return combinations;
}
public void generateAll(char[] current, int pos, List result) {
if (pos == current.length) {
if (valid(current))
result.add(new String(current));
} else {
current[pos] = '(';
generateAll(current, pos+1, result);
current[pos] = ')';
generateAll(current, pos+1, result);
}
}
public boolean valid(char[] current) {
int balance = 0;
for (char c: current) {
if (c == '(') balance++;
else balance--;
if (balance < 0) return false;
}
return (balance == 0);
}
}
复杂度分析
时间复杂度:O(2^{2n}n)O(22nn),对于 2^{2n}22n 个序列中的每一个,我们用于建立和验证该序列的复杂度为 O(n)O(n)。
空间复杂度:O(2^{2n}n)O(22nn),简单地,每个序列都视作是有效的。请参见 方法三 以获得更严格的渐近界限。
方法二:回溯法
思路和算法
只有在我们知道序列仍然保持有效时才添加 '('
or ')'
,而不是像 方法一 那样每次添加。我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点,
如果我们还剩一个位置,我们可以开始放一个左括号。 如果它不超过左括号的数量,我们可以放一个右括号。
class Solution {
public List generateParenthesis(int n) {
List ans = new ArrayList();
backtrack(ans, "", 0, 0, n);
return ans;
}
public void backtrack(List ans, String cur, int open, int close, int max){
if (cur.length() == max * 2) {
ans.add(cur);
return;
}
if (open < max)
backtrack(ans, cur+"(", open+1, close, max);
if (close < open)
backtrack(ans, cur+")", open, close+1, max);
}
}
复杂度分析
我们的复杂度分析依赖于理解 generateParenthesis(n)
中有多少个元素。这个分析超出了本文的范畴,但事实证明这是第 n
个卡塔兰数 \dfrac{1}{n+1}\binom{2n}{n}n+11(n2n),这是由 \dfrac{4^n}{n\sqrt{n}}nn4n 渐近界定的。
时间复杂度:O(\dfrac{4^n}{\sqrt{n}})O(n4n),在回溯过程中,每个有效序列最多需要 n
步。
空间复杂度:O(\dfrac{4^n}{\sqrt{n}})O(n4n),如上所述,并使用 O(n)O(n) 的空间来存储序列。
方法三:闭合数
思路
为了枚举某些内容,我们通常希望将其表示为更容易计算的不相交子集的总和。
考虑有效括号序列 S
的 闭包数:至少存在 index >= 0
,使得 S[0], S[1], ..., S[2*index+1]
是有效的。 显然,每个括号序列都有一个唯一的闭包号。 我们可以尝试单独列举它们。
算法
对于每个闭合数 c
,我们知道起始和结束括号必定位于索引 0
和 2*c + 1
。然后两者间的 2*c
个元素一定是有效序列,其余元素一定是有效序列。
class Solution {
public List generateParenthesis(int n) {
List ans = new ArrayList();
if (n == 0) {
ans.add("");
} else {
for (int c = 0; c < n; ++c)
for (String left: generateParenthesis(c))
for (String right: generateParenthesis(n-1-c))
ans.add("(" + left + ")" + right);
}
return ans;
}
}
复杂度分析
31. 下一个排列
实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须原地修改,只允许使用额外常数空间。
以下是一些例子,输入位于左侧列,其相应输出位于右侧列。1,2,3
→ 1,3,2
3,2,1
→ 1,2,3
1,1,5
→ 1,5,1
我们需要找到给定数字列表的下一个字典排列,而不是由给定数组形成的数字。
方法一:暴力法
算法
在这种方法中,我们找出由给定数组的元素形成的列表的每个可能的排列,并找出比给定的排列更大的排列。 但是这个方法是一种非常天真的方法,因为它要求我们找出所有可能的排列 这需要很长时间,实施起来也很复杂。 因此,这种方法根本无法通过。 所以,我们直接采用正确的方法。
复杂度分析
方法二:一遍扫描
算法
首先,我们观察到对于任何给定序列的降序,没有可能的下一个更大的排列。
例如,以下数组不可能有下一个排列:
[9, 5, 4, 3, 1]
我们需要从右边找到第一对两个连续的数字 a[i]a[i] 和 a[i-1]a[i−1],它们满足 a[i]>a[i-1]a[i]>a[i−1]。现在,没有对 a[i-1]a[i−1] 右侧的重新排列可以创建更大的排列,因为该子数组由数字按降序组成。因此,我们需要重新排列 a[i-1]a[i−1] 右边的数字,包括它自己。
现在,什么样子的重新排列将产生下一个更大的数字呢?我们想要创建比当前更大的排列。因此,我们需要将数字 a[i-1]a[i−1] 替换为位于其右侧区域的数字中比它更大的数字,例如 a[j]a[j]。
我们交换数字 a[i-1]a[i−1] 和 a[j]a[j]。我们现在在索引 i-1i−1 处有正确的数字。 但目前的排列仍然不是我们正在寻找的排列。我们需要通过仅使用 a[i-1]a[i−1]右边的数字来形成最小的排列。 因此,我们需要放置那些按升序排列的数字,以获得最小的排列。
但是,请记住,在从右侧扫描数字时,我们只是继续递减索引直到我们找到 a[i]a[i] 和 a[i-1]a[i−1] 这对数。其中,a[i] > a[i-1]a[i]>a[i−1]。因此,a[i-1]a[i−1] 右边的所有数字都已按降序排序。此外,交换 a[i-1]a[i−1] 和 a[j]a[j] 并未改变该顺序。因此,我们只需要反转 a[i-1]a[i−1] 之后的数字,以获得下一个最小的字典排列。
下面的动画将有助于你理解:
public class Solution {
public void nextPermutation(int[] nums) {
int i = nums.length - 2;
while (i >= 0 && nums[i + 1] <= nums[i]) {
i--;
}
if (i >= 0) {
int j = nums.length - 1;
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j);
}
reverse(nums, i + 1);
}
private void reverse(int[] nums, int start) {
int i = start, j = nums.length - 1;
while (i < j) {
swap(nums, i, j);
i++;
j--;
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
复杂度分析
时间复杂度:O(n)O(n),在最坏的情况下,只需要对整个数组进行两次扫描。
空间复杂度:O(1)O(1),没有使用额外的空间,原地替换足以做到。
33. 搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7]
可能变为 [4,5,6,7,0,1,2]
)。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1
。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2]
, target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2]
, target = 3
输出: -1
循环版本:
class Solution {
public int search(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
while(low <= high) {
int mid = (low + high) / 2;
if(nums[mid] == target) {
return mid;
} else if(nums[mid] < nums[high]) {
if(nums[mid] < target && nums[high] >= target) {
low = mid + 1;
} else {
high = mid - 1;
}
} else {
if(nums[low] <= target && nums[mid] > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
}
return -1;
}
}
递归版本:
class Solution {
public int search(int[] nums, int target) {
return search(nums, 0, nums.length - 1, target);
}
private int search(int[] nums, int low, int high, int target) {
if (low > high)
return -1;
int mid = (low + high) / 2;
if (nums[mid] == target)
return mid;
if (nums[mid] < nums[high]) {
if (nums[mid] < target && target <= nums[high])
return search(nums, mid + 1, high, target);
else
return search(nums, low, mid - 1, target);
} else {
if (nums[low] <= target && target < nums[mid])
return search(nums, low, mid - 1, target);
else
return search(nums, mid + 1, high, target);
}
}
}
34. 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums
,和一个目标值 target
。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]
。
示例 1:
输入: nums = [5,7,7,8,8,10]
, target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10]
, target = 6
输出: [-1,-1]
class Solution {
public int[] searchRange(int[] nums, int target) {
int start = -1;
int end = -1;
int low = 0;
int high = nums.length - 1;
while(low <= high) {
int mid = (low + high) / 2;
if(nums[mid] == target ) {
if(mid - 1 >= 0 && nums[mid - 1] == target) {
high = mid - 1;
} else {
start = mid;
break;
}
} else if(nums[mid] > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
low = 0;
high = nums.length - 1;
while(low <= high) {
int mid = (low + high) / 2;
if(nums[mid] == target) {
if(mid + 1 < nums.length && nums[mid + 1] == target) {
low = mid + 1;
} else {
end = mid;
break;
}
} else if(nums[mid] > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return new int[]{start, end};
}
}
39. 组合总和
给定一个无重复元素的数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的数字可以无限制重复被选取。
说明:
target
)都是正整数。示例 1:
输入: candidates =[2,3,6,7],
target =7
, 所求解集为: [ [7], [2,2,3] ]
示例 2:
输入: candidates = [2,3,5],
target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
class Solution {
public List> combinationSum(int[] candidates, int target) {
List> res = new ArrayList<>();
Arrays.sort(candidates);
backtracking(res, new ArrayList<>(), candidates, target, 0);
return res;
}
public void backtracking(List> res, List tmp, int[] candidates, int target, int start){
if (target == 0) {
res.add(new ArrayList<>(tmp));
}
// candidates[i] <= target 剪枝
for (int i = start; i < candidates.length && candidates[i] <= target; i++) {
tmp.add(candidates[i]);
backtracking(res, tmp, candidates, target - candidates[i], i);
tmp.remove(tmp.size() - 1);
}
}
}
56. 合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: [[1,3],[2,6],[8,10],[15,18]] 输出: [[1,6],[8,10],[15,18]] 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: [[1,4],[4,5]] 输出: [[1,5]] 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
class Solution {
public int[][] merge(int[][] intervals) {
int length = intervals.length;
if(length<=1){
return intervals;
}
Arrays.sort(intervals, new Comparator(){
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
//前提:已经按最小值排序
// intervals = sort(intervals);
// int[][] temp = new int[length][2];
int count=0;
for(int i = 0;i=intervals[i+1][0])){
count ++;
intervals[i+1][0] = getMin(intervals[i][0],intervals[i+1][0]);
intervals[i+1][1] = getMax(intervals[i][1],intervals[i+1][1]);
intervals[i][0] = -1;
intervals[i][1] = -1;
}
}
int[][] ret = new int[length-count][2];
int j=0;
for(int i = 0;iintervals[j][0]){
int temp1 =intervals[i][0];
int temp2 =intervals[i][1];
intervals[i][0] = intervals[j][0];
intervals[i][1] = intervals[j][1];
intervals[j][0] = temp1;
intervals[j][1] = temp2;
}
}
}
return intervals;
}
public int getMin(int i,int j){
return i>j?j:i;
}
public int getMax(int i,int j){
return i>j?i:j;
}
}