LeetCode-hot100题解—Day4

原题链接:力扣热题-HOT100
题解的顺序和题目的顺序一致,那么接下来就开始刷题之旅吧~
1-8题见LeetCode-hot100题解—Day1
9-16题见LeetCode-hot100题解—Day2
17-24题见LeetCode-hot100题解—Day3
注:需要补充的是,如果对于每题的思路不是很理解,可以点击链接查看视频讲解,是我在B站发现的一个宝藏UP主,视频讲解很清晰(UP主用的是C++),可以结合视频参考本文的java代码。

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),其中nnums数组的长度
代码实现

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;
    }
}

待续…

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘子味的小橙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值