目录
第一题
题目来源
题目内容
解决方法
方法一:双指针
方法二:递归
方式三:迭代
方法四:优先队列
第二题
题目来源
题目内容
解决方法
方法一:贪心算法
方法二:数学方法
方法三:递归算法
第三题
题目来源
题目内容
解决方法
方法一:回溯法
方法二:动态规划
方法三:栈
方法四:暴力法
21. 合并两个有序链表 - 力扣(LeetCode)
由于题目要求合并两个升序链表,并且新链表也要按照升序排列,因此我们可以使用双指针的方法,依次比较两个链表的结点值大小,将较小的结点接在新链表的最后面。
具体来讲,在遍历两个链表的过程中,我们可以维护一个指针cur,它指向新链表的当前最后一个结点。对于每个结点值比较小的链表,我们就将它的结点接在cur结点的后面,并将cur指针后移,继续比较。当其中一个链表为空时,说明没有可以比较的结点了,我们将另一个链表的剩余结点全部接在新链表的最后面。最后返回新链表的第一个有效结点即可。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1); // 新链表的虚拟头结点
ListNode cur = dummy; // cur指向当前新链表的最后一个结点
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) { // l1结点的值比较小
cur.next = l1; // 将l1的结点接在当前新链表的最后一个结点后面
l1 = l1.next; // 指针后移
} else { // l2结点的值比较小
cur.next = l2; // 将l2的结点接在当前新链表的最后一个结点后面
l2 = l2.next; // 指针后移
}
cur = cur.next; // 指针后移
}
// l1或者l2还有剩余结点,将它们全部接到新链表的最后一个结点后面
if (l1 != null) {
cur.next = l1;
} else if (l2 != null) {
cur.next = l2;
}
return dummy.next; // 返回新链表的第一个有效结点
}
}
复杂度分析:
需要注意的是:这里的时间复杂度是线性的,而不是二次的,因为我们每次都只移动一个链表的指针,而不是将所有结点都拷贝到新链表中。因此,这个解法是非常高效的。
LeetCode运行结果:
除了使用双指针比较的方法外,还可以考虑使用递归实现合并两个升序链表的操作。
递归的思路是,对于两个链表l1和l2,我们比较它们的头结点的值,将较小的头结点作为新链表的头结点,并递归地处理剩余的结点,直到其中一个链表为空。然后,将另一个链表的剩余部分直接接到新链表的末尾。
具体来说,我们定义一个递归函数mergeLists(l1, l2)来处理两个链表的合并操作:
递归终止条件是当两个链表都为空时,返回空链表。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 递归终止条件:其中一个链表为空
if (l1 == null || l2 == null) {
return l1 != null ? l1 : l2;
}
// 比较两个链表的头结点值
else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2); // 递归处理剩余结点
return l1;// 返回新链表的头结点
}
else {
l2.next = mergeTwoLists(l1, l2.next);// 递归处理剩余结点
return l2;// 返回新链表的头结点
}
}
}
复杂度分析:
时间复杂度分析:
空间复杂度分析:
总结:递归方法的时间复杂度是O(m+n),空间复杂度是O(m+n)。
LeetCode运行结果:
除了使用递归和双指针的方法外,还可以使用迭代的方式来合并两个升序链表。
迭代的思路是,我们创建一个新的链表,用来存储合并后的结果。然后我们使用两个指针分别指向两个链表的头节点,比较两个节点的值,将较小的节点添加到新链表中,并将相应的指针后移一位,直到其中一个链表遍历完毕。最后,将剩余部分的链表直接接到新链表的末尾。
具体来说,我们定义三个指针:dummy指向新链表的头节点,curr指向新链表的当前节点,p1和p2分别指向两个链表的当前节点。初始时,将dummy和curr都指向一个虚拟的头节点,p1指向链表l1的头节点,p2指向链表l2的头节点。
然后,我们进行循环比较操作:
每次操作完成后,curr指针和被接入节点都后移一位。
循环终止时,其中一个链表已经遍历完毕。如果另一个链表还有剩余节点,则将剩余部分直接接入新链表的末尾。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0); // 虚拟头节点
ListNode curr = dummy; // 当前节点
ListNode p1 = l1; // 链表l1的当前节点
ListNode p2 = l2; // 链表l2的当前节点
// 循环比较操作
while (p1 != null && p2 != null) {
if (p1.val <= p2.val) {
curr.next = p1;
p1 = p1.next;
} else {
curr.next = p2;
p2 = p2.next;
}
curr = curr.next;
}
// 将剩余部分直接接入新链表的末尾
curr.next = (p1 != null) ? p1 : p2;
return dummy.next; // 返回新链表的头节点
}
}
复杂度分析:
总结:迭代方法的时间复杂度是O(m+n),空间复杂度是O(1)。与递归方法相比,迭代方法具有相同的时间复杂度,但空间复杂度更低。
LeetCode运行结果:
除了递归和迭代方法外,还有一种另类思路,可以使用优先队列(Priority Queue)来实现合并两个升序链表。
具体操作如下:
这种思路在理论上是可行的,因为小根堆可以始终保证堆顶元素是当前所有元素的最小值。而对于每个链表中的节点,只需要依次入堆一次,出堆一次,时间复杂度都是O(log(m+n)),由于共有m+n个节点,因此总时间复杂度是O((m+n)log(m+n))。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
PriorityQueue pq = new PriorityQueue<>((a, b) -> a.val - b.val);
ListNode dummy = new ListNode(0), curr = dummy;
// 将两个链表的所有节点依次加入优先队列中
if (l1 != null) pq.offer(l1);
if (l2 != null) pq.offer(l2);
while (!pq.isEmpty()) {
ListNode node = pq.poll(); // 取出最小值
curr.next = node; // 加入新链表
curr = curr.next; // 当前节点后移
if (node.next != null) {
pq.offer(node.next); // 将该元素所属的链表中的下一个节点入堆
}
}
return dummy.next; // 返回新链表的头节点
}
}
复杂度分析:
总结:使用优先队列的方法可以实现合并两个升序链表,但时间复杂度较高,为O((m+n)log(m+n)),空间复杂度为O(m+n)。这种方法在实践中并不是最优解,相比之下,递归和迭代方法更为简洁、高效。
LeetCode运行结果:
LCP 06. 拿硬币 - 力扣(LeetCode)
我们可以通过贪心算法来解决这个问题。
具体步骤如下:
1、遍历数组 coins,对于每一堆力扣币:
2、将每一堆力扣币所需的次数累加,得到总的最少次数。
class Solution {
public int minCount(int[] coins) {
int count = 0;
for (int i = 0; i < coins.length; i++) {
if (coins[i] % 2 == 0) {
count += coins[i] / 2;
} else {
count += coins[i] / 2 + 1;
}
}
return count;
}
}
复杂度分析:
注意:这种贪心算法仅适用于给定的限制条件,即力扣币数量较小。如果力扣币数量较大,或者限制条件有所改变,可能需要使用其他算法来解决。
LeetCode运行结果:
还可以考虑利用数学方法来解决这个问题。
观察到题目中只有两种操作:拿取一枚力扣币(计数为1)和拿取两枚力扣币(计数为2)。我们可以将每堆力扣币按拿取两枚力扣币的方式进行分组,即将余数为0的力扣币堆直接计数为拿取两枚力扣币的次数。然后,再统计余数为1的力扣币堆,每堆需要额外拿取一枚力扣币,将其计数为拿取两枚力扣币的次数,并将拿取一枚力扣币的次数加1。通过这种方法,可以直接得到最少的拿取次数。
class Solution {
public int minCount(int[] coins) {
int count = 0;
for (int i = 0; i < coins.length; i++) {
count += coins[i] / 2; // 余数为0的力扣币堆直接计数为拿取两枚力扣币的次数
if (coins[i] % 2 != 0) {
count++; // 余数为1的力扣币堆需要额外拿取一枚力扣币
}
}
return count;
}
}
复杂度分析:
因此,使用数学方法解决该问题的算法复杂度较低,是一种高效的解决方案。
LeetCode运行结果:
public class Solution {
public int minCount(int[] coins) {
int count = 0;
for (int coin : coins) {
count += getCoinCount(coin);
}
return count;
}
private int getCoinCount(int coinNum) {
if (coinNum == 0) {
return 0;
}
if (coinNum <= 2) {
return 1; // 如果只有1或2个力扣币,则最少拿取1次
}
if (coinNum % 2 == 0) {
return coinNum / 2; // 如果力扣币数能够被2整除,尽量拿取2枚力扣币
} else {
return (coinNum / 2) + 1; // 如果力扣币数不能被2整除,则需要额外拿取1枚力扣币
}
}
}
这个代码实现使用了递归的思路,每次递归的时候判断剩余的力扣币数量,并根据剩余数量选择拿取1枚力扣币或2枚力扣币。在递归过程中计算拿取次数直到剩余的力扣币数量为0为止。
详细地说,如果剩余的力扣币数量为0,则返回0;如果剩余的力扣币数量只有1或2个,则返回1;如果剩余的力扣币数量可以被2整除,则尽量拿取2枚力扣币;否则,就要额外拿取1枚力扣币。
复杂度分析:
虽然在时间复杂度方面不如其他算法效率高,但是它实现简单易懂,应对一些简单的场景也是完全可以的。
LeetCode运行结果:
22. 括号生成 - 力扣(LeetCode)
要生成有效的括号组合,可以使用回溯法来解决这个问题。在回溯过程中,需要维护左括号和右括号的数量,并根据一定的条件进行剪枝。
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List generateParenthesis(int n) {
List result = new ArrayList<>();
backtrack(result, "", 0, 0, n);
return result;
}
private void backtrack(List result, String current, int leftCount, int rightCount, int n) {
if (current.length() == n * 2) {
result.add(current);
return;
}
if (leftCount < n) {
backtrack(result, current + "(", leftCount + 1, rightCount, n);
}
if (rightCount < leftCount) {
backtrack(result, current + ")", leftCount, rightCount + 1, n);
}
}
}
在这个代码中,generateParenthesis 方法接收一个整数 n,代表括号的对数,返回一个包含所有可能的且有效的括号组合的列表。backtrack 方法用于进行回溯。它维护了一个当前的字符串 current,其中 leftCount 和 rightCount 分别表示当前已使用的左括号和右括号的数量。n 则表示要生成的括号对数。在回溯过程中,如果当前字符串的长度达到了 n*2,就代表已经生成了一个有效的括号组合,将其添加到结果列表中。然后,通过递归调用 backtrack 方法,分别尝试放置左括号和右括号。在放置左括号时,需要判断已使用的左括号数量是否小于 n,如果是,则可以继续放置左括号。在放置右括号时,需要判断已使用的右括号数量是否小于左括号数量,如果是,则可以继续放置右括号。通过不断进行递归调用和回溯,最终可以得到所有可能的且有效的括号组合。
复杂度分析:
时间复杂度:
空间复杂度:
current
存储当前的括号组合,字符串的长度最大为 2n。综上所述,生成有效括号组合的函数的时间复杂度为指数级别的 O(2^2n),空间复杂度为线性级别的 O(2n)。
LeetCode运行结果:
除了回溯法外,还可以使用动态规划来生成有效括号组合。
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List generateParenthesis(int n) {
List> dp = new ArrayList<>();
dp.add(new ArrayList<>());
dp.get(0).add("");
for (int i = 1; i <= n; i++) {
List current = new ArrayList<>();
for (int j = 0; j < i; j++) {
List inside = dp.get(j);
List outside = dp.get(i - j - 1);
for (String in : inside) {
for (String out : outside) {
current.add("(" + in + ")" + out);
}
}
}
dp.add(current);
}
return dp.get(n);
}
}
复杂度分析:
时间复杂度:
空间复杂度:
需要注意的是,具体括号组合的数量取决于具体的 n。在某些情况下,括号组合的数量可能大于 2^n,因此花费的时间和空间可能会更多。
综上所述,使用动态规划生成有效括号组合的函数的时间复杂度为 O(n^2 * Cn),空间复杂度为 O(n * m)。
LeetCode运行结果:
我们使用栈来模拟括号的匹配过程。
1、在回溯函数 backtrack 中,我们维护两个计数器 open 和 close,分别表示当前已经使用的左括号和右括号的个数。
2、如果栈的大小达到了 2n(n 是括号对数),说明已经得到了一个有效的括号组合,将其转换为字符串并添加到结果列表中。
否则,我们有两种选择:
3、每次递归调用结束后,我们需要将栈顶元素弹出,以便进行下一次选择。
4、通过不断选择和回溯,最终得到所有可能的有效括号组合。
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class Solution {
public List generateParenthesis(int n) {
List result = new ArrayList<>();
backtrack(result, new Stack(), 0, 0, n);
return result;
}
private void backtrack(List result, Stack stack, int open, int close, int n) {
if (stack.size() == 2 * n) {
StringBuilder sb = new StringBuilder();
for (char c : stack) {
sb.append(c);
}
result.add(sb.toString());
return;
}
if (open < n) {
stack.push('(');
backtrack(result, stack, open + 1, close, n);
stack.pop();
}
if (close < open) {
stack.push(')');
backtrack(result, stack, open, close + 1, n);
stack.pop();
}
}
}
复杂度分析:
综上所述,使用栈的解决方案的时间复杂度为 O(2^(2n)),空间复杂度为 O(n)。
LeetCode运行结果:
当括号对数较小(例如,n <= 8)时,我们可以使用暴力法生成所有可能的括号组合。暴力法的思路是通过递归生成所有可能的组合,然后检查它们是否有效。
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List generateParenthesis(int n) {
List combinations = new ArrayList<>();
generateAll(new char[2 * n], 0, combinations);
return combinations;
}
private void generateAll(char[] current, int pos, List result) {
if (pos == current.length) {
if (isValid(current)) {
result.add(new String(current));
}
} else {
current[pos] = '(';
generateAll(current, pos + 1, result);
current[pos] = ')';
generateAll(current, pos + 1, result);
}
}
private boolean isValid(char[] current) {
int balance = 0;
for (char c : current) {
if (c == '(') {
balance++;
} else {
balance--;
if (balance < 0) {
return false;
}
}
}
return balance == 0;
}
}
复杂度分析:
时间复杂度分析:
空间复杂度分析:
需要注意的是,当括号对数较大时,暴力法的时间复杂度和空间复杂度非常高,因此在实际应用中,可以考虑使用动态规划或其他优化方法来解决。
LeetCode运行结果: