LeetCode 189, 152, 148, 41

168 阅读3分钟

LeetCode 189 Rotate Array

链接:leetcode.com/problems/ro…

方法:三步翻转法

时间复杂度:O(n)

空间复杂度:O(1)

想法:这道题记住这个做法就好了,是个经典做法。我之前在做这道题的时候推导了一下每个数字都应该放到哪个位置上,然后用for循环和最小公倍数的知识做的,但是后来想想也没必要,因为不管怎么样就算用数学的方法做,照样也只能适用于这一道题,而且实际运行起来还没三步翻转来得快,因此,就观察然后记住这种做法就好了,把数组分成两部分,然后前后两部分分别翻转,然后把整个数组再翻转一下。

代码:

class Solution {
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        k %= n;
        if (k == 0) {
            return;
        }
        
        reverse(nums, 0, n - k - 1);
        reverse(nums, n - k, n - 1);
        reverse(nums, 0, n - 1);
    }
    
    private void reverse(int[] nums, int left, int right) {
        while (left < right) {
            int tmp = nums[left];
            nums[left] = nums[right];
            nums[right] = tmp;
            left++;
            right--;
        }
    }
}

LeetCode 152 Maximum Product Subarray

链接:leetcode.com/problems/ma…

方法:DP

时间复杂度:O(n)

空间复杂度:O(n)

想法:数据规模是2 * 10^4,直接暴力算保准会TLE。然后这个题如果求的不是乘积最大的subarray,而是和最大的subarray,那么我们都知道可以用前缀数组,和一个代表前缀数组和最小值的变量,在O(n)时间内解决问题。但是对于乘积的话还没这么简单,一方面是可能数太大会爆,一方面是可能有0,再一方面是乘积没有这种单调性,你不能拿某数组的乘积除以前面前缀数组乘积的最小值。那么对于这道题,使用坐标型DP,用两个数组minDP、maxDP。minDP[i]、maxDP[i]分别代表的是以nums[i - 1]结尾的子数组当中,乘积最小的子数组的乘积,和乘积最大的子数组的乘积。然后对nums数组进行遍历的时候,如果当前的数是负数,那,以它结尾的数当中最大的子数组的乘积可能是minDP[i - 1] * nums[i - 1],也可能就是它自己(子数组只有一个元素,不跟前面的任何元素合并),因为当前元素是负数,所以假设说包含它、并且乘积最大,那么,要么包含它前面那个元素的最小子数组的乘积是个负数,并且绝对值比较大,这样的话算上当前的元素,乘起来就是个正数;要么不要带前面的元素,子数组就包含它自己。对于其他情况的分析也类似。每次更新res,res = Math.max(res, maxDP[i]);

代码:

class Solution {
    public int maxProduct(int[] nums) {
        int n = nums.length, res = Integer.MIN_VALUE;
        int[] minDP = new int[n + 1];
        int[] maxDP = new int[n + 1];
        minDP[0] = maxDP[0] = 1;
        
        for (int i = 1; i <= n; i++) {
            if (nums[i - 1] > 0) {
                minDP[i] = Math.min(nums[i - 1], minDP[i - 1] * nums[i - 1]);
                maxDP[i] = Math.max(nums[i - 1], maxDP[i - 1] * nums[i - 1]);
            }
            else {
                minDP[i] = Math.min(nums[i - 1], maxDP[i - 1] * nums[i - 1]);
                maxDP[i] = Math.max(nums[i - 1], minDP[i - 1] * nums[i - 1]);
            }
            res = Math.max(res, Math.max(minDP[i], maxDP[i]));
        }
        
        
        return res;
    }
}

LeetCode 148 Sort List

链接:leetcode.com/problems/so…

方法1:自上向下的merge sort

时间复杂度:O(nlogn)

空间复杂度:O(logn)

想法:这个做法是最常见的merge sort的做法。其实这个题还真是就在考排序,就是对于各种排序的时间/空间复杂度是不是理解。那么最常见的merge sort写法就是把链表分成两半,然后对两半分别做merge sort,两边排完以后再把两个有序的链表merge起来。在数组种我们可以用下标做分割,分出两个部分,但是在链表当中不行,所以我们采用快慢指针的方法分割。事实上这个做法空间复杂度是O(logn),题目的follow up问Can you sort the linked list in O(n logn) time and O(1) memory (i.e. constant space)?这个做法是O(logn)的memory,因为一开始会分出两个链表,对两个链表分别做操作,这样先扔到栈里一层。对连个链表分别操作的时候也是这样,直到链表为null或者只有一个元素,那这样的话栈空间就是logn级别的。但不管怎么样,这个做法是能满足一般的要求的,只是不符合follow up的要求。

代码:

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        
        ListNode fast = head, slow = head, prev = null;
        while (fast != null && fast.next != null) {
            prev = slow;
            slow = slow.next;
            fast = fast.next.next;
        }
        
        prev.next = null;
        return merge(sortList(head), sortList(slow));
    }
    
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode p = dummy;
        
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                p.next = l1;
                l1 = l1.next;
            }
            else {
                p.next = l2;
                l2 = l2.next;
            }
            p = p.next;
        }
        
        if (l1 != null) {
            p.next = l1;
        }
        if (l2 != null) {
            p.next = l2;
        }
        
        return dummy.next;
    }
}

方法2:自下向上的merge sort

时间复杂度:O(n)

空间复杂度:O(1)

想法:参考花花酱zxi.mytechroad.com/blog/divide… 。相当于是手动把链表拆掉,然后先把一个个只有一个元素的链表排序接起来,成为一个个只有两个元素的有序链表,然后继续接成一个个只有四个元素的有序链表......于是就需要手动写这个操作。

具体操作该怎么写呢?就是按照上面所说,每次先做出来两个元素的有序链表。这里用一个变量n,从1开始每次扫描完链表就乘2。当n为1的时候,一开始tail指向dummy,cur指向dummy.next,然后从cur这个地方,先砍出来包含一个元素的链表,然后砍出来在这之后的包含一个元素的链表,然后这两个合并起来,合并结束返回的时候返回合并好的有序链表的头和尾,然后原本的tail接到有序链表的头,有序链表的尾的next指向原本砍完之后的链表的头,就相当于这个地方排完序给它接回去了。然后更新tail,直到cur为null也就是说链表扫到尾了,这样把n乘2,再来一遍。直到链表整个排完。

代码:

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        
        ListNode dummy = new ListNode(0, head);
        int len = getLen(head);
        
        for (int n = 1; n < len; n <<= 1) {
            ListNode tail = dummy;
            ListNode cur = dummy.next;
            
            while (cur != null) {
                ListNode l = cur;
                ListNode r = split(cur, n);
                cur = split(r, n);
                ListNode[] mergeRes = merge(l, r);
                tail.next = mergeRes[0];
                tail = mergeRes[1];
            }
        }
        
        return dummy.next;
    }
    
    private ListNode split(ListNode head, int n) {
        while (--n > 0 && head != null) {
            head = head.next;
        }
        
        ListNode rest = head != null ? head.next : null;
        if (head != null) {
            head.next = null;
        }
        
        return rest;
    }
    
    private ListNode[] merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode p = dummy;
        
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                p.next = l1;
                l1 = l1.next;
            }
            else {
                p.next = l2;
                l2 = l2.next;
            }
            p = p.next;
        }
        
        if (l1 != null) {
            p.next = l1;
        }
        if (l2 != null) {
            p.next = l2;
        }
        
        while (p.next != null) {
            p = p.next;
        }
        
        return new ListNode[] { dummy.next, p };
    }
    
    private int getLen(ListNode head) {
        int res = 0;
        while (head != null) {
            res++;
            head = head.next;
        }
        return res;
    }
}

LeetCode 41 First Missing Positive

链接:leetcode.com/problems/fi…

方法:桶排序

时间复杂度:O(n)

空间复杂度:O(1)

想法:题目要求"You must implement an algorithm that runs in O(n) time and uses constant extra space." 当时做没做出来,方法还是太巧妙了。想法就是我们一定要把1放在数组的下标为0这个位置,一定要把2放在下标为1这个位置,即,一定要把nums[i]放在index=nums[i]-1这个位置。具体操作就是每次遍历到一个nums[i],我们就看看nums[nums[i] - 1],看一下这个值是不是等于nums[i],如果是就拉倒了;如果不是,那就交换元素,把nums[i]扔到那个地方去。每一次会把一个数放在一个正确的位置上,因此虽然说代码是for里面套while,但是时间复杂度还是O(n)。

代码:

class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            while (nums[i] > 0 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) {
                int tmp = nums[nums[i] - 1];
                nums[nums[i] - 1] = nums[i];
                nums[i] = tmp;
            }
        }
        
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }
        
        return n + 1;
    }
}