原题链接:力扣热题-HOT100
题解的顺序和题目的顺序一致,那么接下来就开始刷题之旅吧~
1-8题见LeetCode-hot100题解—Day1
9-16题见LeetCode-hot100题解—Day2
17-24题见LeetCode-hot100题解—Day3
注:需要补充的是,如果对于每题的思路不是很理解,可以点击链接查看视频讲解,是我在B站发现的一个宝藏UP主,视频讲解很清晰(UP主用的是C++),可以结合视频参考本文的java代码。
力扣hot100题解 25-32
25.K个一组翻转链表
思路:
本题采用的思路是,先将链表按K
个节点一组分割,然后单独对每一组进行翻转,最后再拼接起来即可,翻转函数逻辑比较简单,就是采用三个指针,每次让中间的指针(刚开始指向头节点)指向其前一个指针并整体后移,重复操作。分割和拼接的操作其实也很简单,如果看代码不太明白,可以画个链表跟着操作走一遍,就会明白每一步的含义,代码里有注释,希望对你有用,详细的视频讲解点击视频讲解-K个一组翻转链表。
时间复杂度:
时间复杂度为O(n)
,其中n
是链表的长度。代码中有一个while
循环,每次循环都会处理k
个节点(反转、拼接),所以循环的次数最多为n/k
。在循环中,反转操作的时间复杂度为O(k)
,拼接操作的时间复杂度为O(1)
。因此,总的时间复杂度可以近似为O(n)
。
代码实现:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;
//定义首尾指针指向dummy
ListNode start = dummy;
ListNode end = dummy;
//按K个一组分割链表
while(true){
for(int i = 0 ;i < k && end != null;i++) end = end.next;
if(end == null) break;
//分割后的链表的头节点
ListNode startNext = start.next;
//分割后的剩余链表的头节点
ListNode endNext = end.next;
//分割前K个节点
end.next = null;
//对前K个节点的链表进行翻转
start.next = reverse(start.next);
//将翻转后的链表的与剩余的节点拼接到一起
startNext.next = endNext;
//更新头尾节点,开始下一轮的翻转
start = end = startNext;
}
return dummy.next;
}
//定义反转函数
private ListNode reverse(ListNode head){
ListNode cur = head;
ListNode pre = null;
while(cur != null){
ListNode next = cur.next;
//每次cur指针指向前一个节点,达到翻转的目的,然后将三个指针后移,重复操作,直到cur为空
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
26.删除有序数组中的重复项
思路:
本题设置一个辅助下标指针idx
指向数组的第一个元素,然后往后遍历数组,如果遇到了相同的元素就跳过,遇到不同的元素则加入到辅助下标的后一个元素,依次类推,直到遍历数组结束,最后返回idx+1
即为所求的长度。详细的视频讲解点击视频讲解-删除有序数组中的重复项。
时间复杂度:
时间复杂度为O(n)
,只进行了一次数组的遍历。
代码实现:
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length < 2) return nums.length;
int idx = 0;
for(int i = 1;i < nums.length;i++){
if(nums[i] != nums[idx]){
idx = idx + 1;
nums[idx] = nums[i];
}
}
return idx + 1;
}
}
27.移除元素
思路:
本题的思路和第26题一样,需要一个辅助下标idx
,遍历整个数组,当遇到数组元素不等于val
时,则将其加入到删除后的数组中,同时辅助下标后移,遍历结束后返回辅助下标idx
的值即可,详细的视频讲解点击视频讲解-移除元素。
时间复杂度:
时间复杂度为O(n)
,n
为数组的长度。
代码实现:
class Solution {
public int removeElement(int[] nums, int val) {
int idx = 0;
for(int i = 0;i < nums.length;i++){
if(nums[i] != val){
nums[idx] = nums[i];
idx = idx + 1;
}
}
return idx;
}
}
28.找出字符串中第一个匹配项的下标
对KMP算法还没有完全掌握,所以这道题我准备后续单独写一篇博文,来讲解KMP算法,并以它作为例题,这里先跳过这道题啦~可以先看视频讲解视频讲解-KMP算法。
29.两数相除
思路:
本题的暴力做法是将除法变为加法,通过将除数累加,计算经过多少次累加会逼近被除数(注意不能大于等于,因为题目要求是截断),但是这种做法的时间复杂度太高了,容易超时。所以我们可以采用幂次方来解决,这样可以大大减少累加的次数,新建一个exp
的动态数组来保存2的幂次方和幂次方和除数的乘积,然后通过遍历exp数组来计算结果(可能这里的解释有点难懂,所以我下面会根据核心代码来做一个示范,跟走一遍就可以了,示范的时候用的c++
代码,不过思路是一样的),有一点需要注意的是,因为负数表示的数字比正数多一个,所以我们可以将被除数和除数全部取成相反数,最后根据sign
来确定结果的正负值,视频讲解点击视频讲解-两数相除。
时间复杂度:
这段代码的时间复杂度是 O(logN)
,其中 N
是被除数 dividend
和除数 divisor
的绝对值的较大值的对数。循环的次数是以 2 的指数增长,直到除数超过被除数的绝对值。
代码实现:
class Solution {
public int divide(int dividend, int divisor) {
boolean sign = (dividend > 0 && divisor < 0) || (dividend < 0 && divisor > 0);
if(dividend > 0) dividend = -dividend;
if(divisor > 0) divisor = -divisor;
List<Pair<Integer, Integer>> exp = new ArrayList<>();
for(int i = divisor, j = -1; i >= dividend ; i += i, j += j) {
exp.add(new Pair<>(i, j));
if(i < Integer.MIN_VALUE >> 1) break;
}
int ans = 0;
for(int i = exp.size() - 1; i >= 0; i--) {
if(exp.get(i).getKey() >= dividend) {
ans += exp.get(i).getValue();
dividend -= exp.get(i).getKey();
}
}
if(sign) return ans;
if(ans == Integer.MIN_VALUE) return Integer.MAX_VALUE;
return -ans;
}
}
30.下一个排列
思路:
因为我们要找到的下一个排列是大于该排列的最小的排列,所以我们可以从后往前扫描第一个正序的元素,然后将该元素与其后面的元素比较大小,找到比它大的元素中最小的元素,然后交换这两个元素,最后将后面的元素逆转即可(逆转就是按照升序排列,因为第一遍扫描的时候会扫描国的元素是按照降序排列的),步骤如下:
1.从数组的末尾开始,寻找第一个正序的数字,通过找到第一个满足nums[k-1] < nums[k]
的索引k来判断。
2.如果找不到这样的数字,说明整个数组是降序排列的,即没有下一个更大的排列。此时将整个数组翻转,得到最小的排列。
3.如果找到了正序的数字,继续从末尾向前扫描,找到第一个大于nums[k-1]
的数字,记为nums[t]
。
4.交换nums[k-1]
和nums[t]
两个元素,将较大的数字放到前面。
5.将索引k
之后的元素进行翻转操作,使得这部分元素成为升序排列,从而得到下一个排列。视频讲解点击视频讲解-下一个排列。
时间复杂度:
时间复杂度为O(n)
,其中n
是nums
数组的长度
代码实现:
class Solution {
public void nextPermutation(int[] nums) {
//从后向前寻找第一个正序的数字
int k = nums.length - 1;
while(k > 0 && nums[k-1] >= nums[k]) k--;
if(k <= 0){
//直接翻转数组
int i = 0;
int j = nums.length - 1;
reserve(nums,i,j);
}else{
//从后向前扫描,找到第一个大于nums[k-1]的值
int t = nums.length - 1;
while( nums[t] <= nums[k-1]) t--;
//交换两个元素
int temp2 = nums[k-1];
nums[k-1] = nums[t];
nums[t] = temp2;
//翻转剩余的后面的元素
int start = k;
int end = nums.length - 1;
reserve(nums,start,end);
}
}
private void reserve(int arr[],int start,int end){
while(start < end){
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
start++;
end--;
}
}
}
31.最长的有效括号
思路:
本题的思路和之前的22-括号生成一样,我们用两个规则来求解,将整个字符串分为几段,分段的标准即为规则一,当右括号数大于左括号数即前面部分划为一段,然后通过遍历每个分段,利用栈将左括号入栈,遇到右括号出栈,确定有效的饿有边界,然后左右边界相减求最大值即可,视频讲解点击视频讲解-最长的有效括号。
时间复杂度:
时间复杂度是O(n)
,其中n
是字符串s
的长度。
代码实现:
class Solution {
public int longestValidParentheses(String s) {
Stack<Integer> stk = new Stack<>();
int ans = 0;
char[] sc = s.toCharArray();
for(int i = 0,start = -1;i < s.length();i++){
if(sc[i] == '(') stk.push(i);
else{
if(!stk.isEmpty()){
stk.pop();
if(!stk.isEmpty()){
ans = Math.max(ans,i - stk.peek());
}else{
ans = Math.max(ans,i - start);
}
}else{
start = i;
}
}
}
return ans;
}
}
33.搜索旋转排序数组
思路:
本题采用两次二分查找,第一次查找找到旋转点,从而将数组分为两个部分(因为数组是升序,所以只要比较中间点元素的值和nums[0]
的值,如果大于第一个元素的值,说明旋转点在中间点的右侧,更新l
值,如果小于,说明旋转点在中间点的左侧,更新r
值),这两个部分都是升序的,然后通过确定target
和第一个元素的值的大小关系可以确定target
在哪一个部分,并更新l
或者r
的值,再通过二分查找来确定target
的下标。视频讲解点击视频讲解-搜索旋转排序数组。
时间复杂度:
时间复杂度为O(logn)
,使用了两次二分查找。
代码实现:
class Solution {
public int search(int[] nums, int target) {
//二分法查找旋转点
int l = 0;
int r = nums.length - 1;
while(l < r){
int mid = l + (r - l) / 2 + 1;
if(nums[mid] >= nums[0]) l = mid;
else r = mid - 1;
}
//确定target的区间
if(target >= nums[0]) l = 0;
else{
l = l + 1;
r = nums.length - 1;
}
//二分法查找target的下标
while(l < r){
int mid = l + (r - l) / 2 + 1;
if(nums[mid] <= target) l = mid;
else r = mid -1;
}
//这里用r是因为前面有l+1,使用l可能会越界
return nums[r] == target ? r : -1;
}
}
34.在排序数组中查找元素的第一个和最后一个位置
思路:
本题的解题思路和33题目一样,通过两次二分查找来确定左右边界,左边界的查找方法是左边界左边的元素全部小于等于左边界,右边的元素大于等于左边界;有边界的查找方法是右边界左边的元素全部小于等于右边界,右边的元素大于等于右边界,也就是两次二分即可,最后将结果放入List
中,视频讲解点击视频讲解-在排序数组中查找元素的第一个和最后一个位置。
时间复杂度:
时间复杂度是 O(logn)
,因为它使用了二分查找的方式来确定左右边界。在最坏的情况下,它需要进行两次二分查找,所以时间复杂度是 O(logn)
。
代码实现:
class Solution {
public int[] searchRange(int[] nums, int target) {
//边界判断
if(nums.length == 0) return new int[] {-1,-1};
List<Integer> ans = new ArrayList<>(2);
//确定左边界
int l1 = 0;
int r1 = nums.length - 1;
while(l1 < r1){
int mid1 = l1 + (r1 - l1) / 2;
if(nums[mid1] >= target) r1 = mid1;
else l1 = mid1 + 1;
}
if(nums[l1] != target) return new int[] {-1,-1};
ans.add(l1);
//确定右边界
int l2 = 0;
int r2 = nums.length - 1;
while(l2 < r2){
int mid2 = l2 + (r2 - l2) / 2 + 1;
if(nums[mid2] <= target) l2 = mid2;
else r2 = mid2 - 1;
}
ans.add(r2);
//将List转为int[]
int[] result = new int[ans.size()];
for (int i = 0; i < ans.size(); i++) {
result[i] = ans.get(i);
}
return result;
}
}
待续…