原题链接:力扣热题-HOT100
题解的顺序和题目的顺序一致,那么接下来就开始刷题之旅吧~
1-8题见LeetCode-hot100题解—Day1
9-16题见LeetCode-hot100题解—Day2
注:需要补充的是,如果对于每题的思路不是很理解,可以点击链接查看视频讲解,是我在B站发现的一个宝藏UP主,视频讲解很清晰(UP主用的是C++),可以结合视频参考本文的java代码。
力扣hot100题解 17-24
17.电话号码的字母组合
思路:
本题采用深度优先搜索来解决,可以参考下图,以23为例,进行深度优先搜索,我们将每个数字代表的字符串定义到数组strs
中,开始深度优先搜索,每次当idx == digits.length()
时,说明已经搜索到了digits
串最后一个数字,即为一个结果,加入ans
中,否则取出digits
中下标为idx
的数字所对应的strs
中的字符串,进行遍历,将对应下标的字符加入combine
中,并递归调用dfs
,实现深度优先搜索,注意搜索后要将combine
中最后一个字符弹出,进行新一轮的搜索(如得到ad
后弹出d
,继续搜索可得到ae
,依次类推…),详细讲解参考视频讲解-电话号码的字母组合。
时间复杂度:
时间复杂度为O(4^n)
,其中n是输入字符串digits
的长度。代码中使用了深度优先搜索算法,通过递归遍历digits
中的每个数字对应的字符集合,然后进行组合生成结果。每个数字对应的字符集合的大小为4,所以总共有4^n
种可能的组合。在遍历每个数字对应的字符集合时,需要进行递归调用dfs
函数,因此总共需要进行n次递归调用。所以总的时间复杂度为O(4^n)
。
代码实现:
class Solution {
public List<String> letterCombinations(String digits) {
List<String> ans = new ArrayList<>();
String[] strs = new String[]{
"","","abc","def",
"ghi","jkl","mno",
"pqrs","tuv","wxyz"
};
//如果传入的串是空的,直接返回结果,即空串
if(digits.length() == 0) return ans;
//由于方便进行组合需要对结果字符串尾部进行插入和删除元素操作,所以使用StringBulider更方便
StringBuilder combine = new StringBuilder("");
//调用函数
dfs(digits,0,combine,strs,ans);
return ans;
}
//定义搜索函数
private void dfs(String digits,int idx,StringBuilder combine,String[] strs,List<String> ans){
if(idx == digits.length()){
ans.add(combine.toString());
return ;
}
String s = strs[digits.charAt(idx)-'0'];
for(int i = 0;i < s.length();i++){
combine.append(s.charAt(i));
dfs(digits,idx+1,combine,strs,ans);
combine.deleteCharAt(combine.length() - 1);
}
}
}
知识拓展:StringBuilder
的使用
在本题中用到了StringBuilder
,如果要对某个字符串进行尾部插入或者删除一个字符的操作,则可以使用StringBuilder
,用法如下:
//插入一个元素
StringBuilder sb = new StringBuilder("Hello");
char characterToAdd = '!';
sb.append(characterToAdd);
//注意,需要将sb使用toString()函数转换为字符串
String newStr = sb.toString();
System.out.println(newStr); // 输出 "Hello!"
//删除一个元素
StringBuilder sb = new StringBuilder("Hello!");
sb.deleteCharAt(sb.length() - 1);
String newStr = sb.toString();
System.out.println(newStr); // 输出 "Hello"
18.四数之和
思路:
这道题的思路和之前第15题三数之和的思路一样,只要在三元组求解的基础上再枚举一下第四个数的值,具体的思路可以参考第15题,视频讲解点击视频讲解-四数之和。
时间复杂度:
时间复杂度为O(n^3)
,其中n
是数组nums
的长度。代码使用了两个嵌套的for
循环,每个循环的时间复杂度都为O(n)
。在内部的while
循环中,l
和r
指针分别从数组的两端向中间移动,最坏情况下需要移动n
次。因此,总体的时间复杂度为O(n^3)
。
代码实现:
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
for(int i=0;i<nums.length;i++){
if(i > 0 && nums[i] ==nums[i-1]) continue;
for(int j=i+1;j<nums.length;j++){
if(j>i+1 && nums[j] == nums[j-1]) continue;
int l = j + 1;
int r = nums.length - 1;
long sum = (long)target - (long)nums[i] - (long)nums[j];
while(l < r){
if((long)nums[l] + (long)nums[r] == sum){
ans.add(Arrays.asList(nums[i],nums[j],nums[l],nums[r]));
while(l < r && nums[l] == nums[l+1]) l++;
while(l < r && nums[r] == nums[r-1]) r--;
l++;
r--;
}else if((long)nums[l] + (long)nums[r] < sum){
l++;
}else{
r--;
}
}
}
}
return ans;
}
}
注意:
long sum = (long)target - (long)nums[i] - (long)nums[j];
这样写是因为没有考虑到目标值 target 可能超过了 Integer.MAX_VALUE
,它的取值范围为 [-2^31, 2^31 - 1]
。当 target
的值超过 Integer.MAX_VALUE
时,计算 sum = target - nums[i] - nums[j]
时可能会溢出。为了解决这个问题,可以将 target
的类型更改为 long
,以便支持更大的值。然后在计算 sum
时,也将 nums[i] 和 nums[j]
转换为 long
类型,避免溢出。否则会报下面的错误,不能通过全部的测试用例。
19.删除链表的倒数第N个结点
思路:
本题采用快慢指针来解决,让快指针比慢指针先走n
步,当快指针到达链表尾部时,慢指针所指向的下一个结点就是需要删除的倒数第n
个结点,在做链表相关的题目时一般需要在头节点的前面设置一个虚拟的结点,方便边界的判断。详细的视频讲解点击视频讲解-删除链表的倒数第N个结点。
时间复杂度:
时间复杂度为O(n)
,其中n
是链表的长度。代码中有一个循环,循环的次数是链表的长度n
,因此时间复杂度为O(n)
。
代码实现:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
ListNode slow = dummy;
ListNode fast = dummy;
dummy.next = head;
//让快指针先向前走n步
for(int i = 0;i < n;i++){
fast=fast.next;
}
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
//删除慢指针的下一个结点,即需要删除的结点
slow.next = slow.next.next;
return dummy.next;
}
}
20.有效的括号
思路:
本题采用栈结构,先将对应的左右括号用哈希表存储,其中key
是左括号,value
是右括号,然后遍历字符串,每次遇到左括号('(','{','[')
则将其对应的右括号压入栈中,当遍历到最近的右括号与栈顶元素匹配则弹出栈顶元素,表示之前的左括号得到了匹配(左括号会和离其最近的右括号配对),最后当栈为空时说明每个左括号都得到匹配。视频讲解点击视频讲解-有效的括号
时间复杂度:
时间复杂度为O(n)
,其中n
是字符串s
的长度。
代码实现:
class Solution {
public boolean isValid(String s) {
HashMap<Character,Character> pairs = new HashMap<>();
pairs.put('(',')');
pairs.put('{','}');
pairs.put('[',']');
Stack<Character> stk = new Stack<>();
for(int i = 0;i < s.length();i++){
//判断是否存在左括号,若存在则将其对应的右括号入栈
if(pairs.containsKey(s.charAt(i))){
stk.push(pairs.get(s.charAt(i)));
}else{
//如果栈为空或者栈顶元素不是匹配的右括号,则不是有效的
if(stk.isEmpty() || stk.peek() != s.charAt(i)){
return false;
}
//否则弹出栈顶元素
stk.pop();
}
}
return stk.isEmpty();
}
}
知识拓展:Java
中Stack
的用法
在Java
中,可以使用Stack<Character>
来声明和创建一个字符栈,下面的示例中总结了Stack
的常见的用法。
public class Main {
public static void main(String[] args) {
// 创建一个字符栈
Stack<Character> stk = new Stack<>();
// 在栈顶添加元素
stk.push('a');
stk.push('b');
stk.push('c');
// 从栈顶弹出元素
char poppedChar = stk.pop();
System.out.println("Popped char: " + poppedChar);
// 获取栈顶元素但不弹出
char topChar = stk.peek();
System.out.println("Top char: " + topChar);
// 检查栈是否为空
boolean isEmpty = stk.isEmpty();
System.out.println("Is stack empty? " + isEmpty);
// 获取栈的大小
int size = stk.size();
System.out.println("Stack size: " + size);
}
}
21.合并两个有序链表
思路:
本题采用双指针来解决,分别遍历两个链表的结点,比较结点的大小,将结点较小的加入到新的链表中并将其指针后移,最后如果两个链表不一样长,遍历结束短的链表后会退出循环,把剩下的链表后面的结点直接加到新链表中即可。详细的讲解点击视频讲解-合并两个有序链表。
时间复杂度:
时间复杂度为O(n + m)
,其中n
和m
分别是list1
和list2
的长度。代码中使用了两个指针list1
和list2
来遍历两个链表,只要两个指针都没有遍历完,就会进行比较并将较小的值添加到新链表中。
在最坏的情况下,需要遍历两个链表的全部节点。因此,时间复杂度为O(n + m)
。这是因为无论链表的长度如何,每个节点只会被访问一次。
代码实现:
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
cur.next = new ListNode(list1.val);
list1 = list1.next;
}else{
cur.next = new ListNode(list2.val);
list2 = list2.next;
}
cur = cur.next;
}
//遍历结束需要将剩下的那个链表元素全部加到新链表
cur.next = list1 != null ? list1 : list2;
return dummy.next;
}
}
22.括号生成
思路:
本题需要用到判断括号串是否有效的充要条件:
规则1:任意前缀"(“数量>=”)"数量(是指在括号串中随便画一条线,前面部分左括号数量一定大于等于右括号数量,等于的情况是画线的前边是整个括号串)
规则2:左右括号数量相等
结合以上两个条件采用深度优先搜索,在不同的位置放左右括号结合两个规则进行有效性的判断得到结果集。视频讲解点击视频讲解-括号生成
""
/ \
"(" ")"
/ \ / \
"((" "()" "()" "))"
/ \ / \
"(((" "(()" "()" "))"
...
时间复杂度:
时间复杂度为O(2^(2n))
,调用了dfs
函数进行深度优先搜索,dfs
函数的时间复杂度取决于递归的次数,每个递归的位置都有2种可能,共有2n
个这样的位置。
代码实现:
class Solution {
public List<String> generateParenthesis(int n) {
List<String> ans = new ArrayList<>();
dfs(0,0,n,"",ans);
return ans;
}
//深度优先函数的实现
//这里需要使用到判断有效括号的充要条件,只要满足以下两点的括号串即为有效的
//规则1:任意前缀"("数量>=")"数量(是指在括号串中随便画一条线,前面部分左括号数量一定大于等于右括号数量,等于的情况是画线的前边是整个括号串)
//规则2:左右括号数量相等
private void dfs(int lc,int rc,int n,String seq,List<String> ans){
//当左右括号的数量等于n时即为结果的一种,加入结果集
if(lc == n && rc == n){
ans.add(seq);
return ;
}
//表示一定还存在左括号
if(lc < n) dfs(lc+1,rc,n,seq+"(",ans);
//表示还剩余右括号并且左括号数量大于右括号数量,参见规则1
if(rc < n && lc > rc) dfs(lc,rc+1,n,seq+")",ans);
}
}
23.合并K个升序链表
思路:
本题采用较为简单的暴力的解法,首先我们将这K
个链表中的元素全部加入到一个动态数组nums
,然后对数组nums
进行排序,再将数组的元素的依次加入新链表中,最后返回新链表的头节点即可。还有一种时间复杂度较低的方法采用小根堆。详细的讲解点击视频讲解-合并K个升序链表。
时间复杂度:
时间复杂度是O(NlogN),其中N是所有链表中节点的总数。以下是时间复杂度的分析:
- 首先,我们遍历所有的链表,将所有节点的值添加到一个列表中。这个过程的时间复杂度是O(N),其中N是所有链表中节点的总数。
- 然后,我们对这个列表进行排序,排序的时间复杂度是O(NlogN)。这取决于使用的排序算法,通常情况下使用的是快速排序或归并排序。
- 最后,我们将排序后的列表重新构建为一个新的链表。这个过程的时间复杂度是O(N),其中N是排序后的列表的长度。
因此,总的时间复杂度是O(N + NlogN + N),即O(NlogN)。
代码实现:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
List<Integer> nums = new ArrayList<>();
for(ListNode list : lists){
while(list != null){
nums.add(list.val);
list = list.next;
}
}
Collections.sort(nums);
ListNode cur = new ListNode(-1);
ListNode head = cur;
for(int num : nums){
cur.next = new ListNode(num);
cur = cur.next;
}
return head.next;
}
}
知识拓展:关于Arrays.sort()
和Collections.sort()
的使用
Arrays.sort()
和Collections.sort()
方法之间的选择基于数据类型的可比较性。
Arrays.sort()
方法适用于对数组进行排序,而且数组的元素类型必须实现了Comparable
接口,即具备可比较性。比如,整数类型int[]
、Integer[]
和字符串类型String[]
都是可以使用Arrays.sort()
进行排序的。
Collections.sort()
方法适用于对集合类进行排序,集合类中的元素类型也必须实现了Comparable
接口。比如,ArrayList<Integer>
、LinkedList<String>
等都是可以使用Collections.sort()
进行排序的。
所以,无论数组的长度是确定的还是动态的,关键是要确定数组元素的类型是否具备可比较性。如果数组元素类型实现了Comparable
接口,可以使用Arrays.sort()
方法进行排序;如果使用的是集合类并且集合元素类型实现了Comparable
接口,可以使用Collections.sort()
方法进行排序。
24.两两交换链表中的节点
思路:
本题采用三指针来解决,首先需要新建一个虚拟头节点,方便操作,然后分别新建三个指针指向虚拟头节点和头节点以及头节点的下一个节点,通过操作三个指针使得节点互换位置,然后将指针整体后移,相同的操作。详细的讲解点击视频讲解-两两交换链表中的节点。
时间复杂度:
时间复杂度为O(n)
,其中n
是链表的长度。
代码实现:
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0);
dummy.next = head;
//定义两个指针分别指向虚拟头节点dummy和其下一个节点
ListNode pre = dummy;
ListNode cur = dummy.next;
while(cur != null && cur.next != null){
//定义第三个指针
ListNode next = cur.next;
pre.next = next;
cur.next = next.next;
next.next = cur;
pre = cur;
cur = cur.next;
}
return dummy.next;
}
}
待续…