leetcode hot100(2)

11.200.岛屿数量

本题是图论中经典的连通分量问题,可以用bfs/dfs解决。

class Solution {
    int[][] directions = new int[][]{{-1,0},{0,-1},{1,0},{0,1}};
    public int numIslands(char[][] grid) {
        boolean visited[][] = new boolean[grid.length][grid[0].length];
        int result = 0 ;
        for(int i = 0 ; i < grid.length ; i++){
            for(int j = 0 ; j < grid[0].length ; j++){
                if(grid[i][j] == '1' && !visited[i][j]){
                    visited[i][j] = true;
                    result++;
                    dfs(i,j,grid,visited);
                }
            }
        }
        return result;


    }

    public void dfs(int x , int y , char[][] grid , boolean[][] visited){
        for(int i = 0 ; i < 4 ; i++){
            int nextX = x + directions[i][0];
            int nextY = y + directions[i][1];
            if(nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) continue;
            if(grid[nextX][nextY] == '1' && !visited[nextX][nextY]){
                visited[nextX][nextY] = true;
                dfs(nextX,nextY,grid,visited);
            }
        }
    }
}

使用bfs的话要注意一点:在加入队列的时候做visited标记,而不是在出队列时,否则会造成重复入队。

int[][] directions = new int[][]{{-1,0},{0,-1},{1,0},{0,1}};
    public int numIslands(char[][] grid) {
        boolean visited[][] = new boolean[grid.length][grid[0].length];
        int result = 0 ;
        for(int i = 0 ; i < grid.length ; i++){
            for(int j = 0 ; j < grid[0].length ; j++){
                if(grid[i][j] == '1' && !visited[i][j]){
                    visited[i][j] = true;
                    result++;
                    bfs(i,j,grid,visited);
                }
            }
        }
        return result;


    }

   

    public void bfs(int x, int y , char[][] grid, boolean[][] visited){
        Deque<int[]> queue = new ArrayDeque<>();
        visited[x][y] = true;
        queue.add(new int[]{x,y});
        while(!queue.isEmpty()){
            int[] node = queue.poll();
            int nodeX = node[0];
            int nodeY = node[1];
            for(int i = 0 ; i < 4 ; i++){
                int nextX = nodeX + directions[i][0];
                int nextY = nodeY + directions[i][1];
                if(nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) continue;
                if(grid[nextX][nextY] == '1' && !visited[nextX][nextY]){
                    visited[nextX][nextY] = true;
                    queue.add(new int[]{nextX,nextY});
                }
            }
        }

    }

12.198.打家劫舍

本题是动态规划问题,回顾动态规划五部曲:

(1)确定dp数组及下标含义

(2)确定递推方程

(3)确定如何初始化

(4)确定遍历顺序

(5)dp模拟

(1)dp[i] : 考虑前i家,所能盗取的最大金额

(2)递推方程:结合递推顺序,从前往后递推,dp[i]应该来源于前面已经计算出来的。又因为不能偷盗相邻的房屋,因此对于当前的第i间房屋,分成两种情况考虑:

        不偷,那么最大金额为dp[i-1]

        偷,那么最大金额为dp[i-2] + nums[i]

二者取最大值,dp[i] = Math.max(dp[i-1] , dp[i-2] + nums[i])

(3)确定如何初始化:根据递推方程,需要初始化dp[0]和dp[1]

根据定义,dp[0] = num[0] , dp[1] = Math.max(dp[0],dp[1])

(4)确定遍历顺序 从前往后遍历

(5)dp模拟

public int rob(int[] nums){
        int[] dp = new int[nums.length];
        if(nums.length == 1) return nums[0];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0],nums[1]);
        for(int i = 2 ; i < nums.length ; i++){
            dp[i] = Math.max(dp[i-1],dp[i-2] + nums[i]);
        }
        return dp[nums.length-1];
    }

13.169.多数元素

(1)简单的统计

public int majorityElement(int[] nums) {
        int border = nums.length/2;
        int result = 0;
        Map<Integer,Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num,map.getOrDefault(num,0)+1);
        }
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            int num = entry.getKey();
            int cnt = entry.getValue();
            if(cnt > border){
                result = num;
            }
        }
        return result;
    }

(2)排序

(3)随机化算法

(4)分治

(5)Boyer-Moore 投票算法

该算法做到了时间复杂度O(n),空间复杂度O(1)

class Solution {
    public int majorityElement(int[] nums) {
        int count = 0;
        Integer candidate = null;

        for(int num : nums){
            if(count == 0){
                candidate = num;
            }
            count += (num == candidate) ? 1  :  -1;
        }
        return candidate;
    }
}

以上若干解法在本题的leetcode官方题解中有详细介绍

14.238.除自身以外数组的乘积

本题不允许使用除法,同时除法也无法解决0的问题。

(1)前后缀之积

和前面和后面元素有关系的问题,考虑使用前后缀(和接雨水有点神似)

分别用两个数组计算出前缀之积和后缀之积,再相乘,就得到了结果。

这种解法时间上进行了三次顺序遍历,空间上使用了2长度为nums.length的数组,时间复杂度为   O(n),空间复杂度为O(n)

public int[] productExceptSelf(int[] nums) {
        int[] pre = new int[nums.length];
        int[] sur = new int[nums.length];
        pre[0] = 1;
        for(int i = 1 ; i < nums.length ; i++){
            pre[i] = pre[i-1] * nums[i-1];
        }
        sur[nums.length-1] = 1;
        for(int i = nums.length-2 ; i >= 0; i++){
            sur[i] = sur[i+1]*nums[i+1];
        }
        int[] res = new int[nums.length];
        for(int i = 0; i < nums.length ;i++){
            res[i] = pre[i] * sur[i];
        }
        return res;
    }

进阶要求:在常数空间复杂度内完成题目

前后缀实际上用完了就不再需要了,可以即时地计算使用。

时间复杂度O(n),空间复杂度除了返回结果以外,为O(1)

class Solution {
    public int[] productExceptSelf(int[] nums){
        int[] res = new int[nums.length];
        res[0] = 1;
        int pre = 1;
        for(int i = 1 ; i < nums.length ; i++){
            pre *= nums[i-1];
            res[i] = pre;
        }
        int sur = 1;
        for(int i = nums.length-2 ; i >= 0 ; i--){
            sur *= nums[i+1];
            res[i] *= sur;
        }
        return res;
    }
}

或者理解为,在第一轮中直接将pre存储到ans里,第二轮倒着的时候ans直接乘sur。(更进一步的话,pre和sur其实可以用一个变量表示)

15.155.最小栈

(1)使用辅助栈

定义数据栈支持基本的push、pop、top操作

定义辅助栈支持常数时间内确定最小值操作

class MinStack {

    Deque<Integer> dataStack;
    Deque<Integer> minStack;
    public MinStack() {
        dataStack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
    }

    public void push(int val) {
        dataStack.push(val);
        minStack.push((minStack.isEmpty() || minStack.peek()> val) ? val : minStack.peek());

    }

    public void pop() {
        dataStack.pop();
        minStack.pop();
    }

    public int top() {
        return dataStack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

压minStack的时候也可以简化判断逻辑,那么需要实现压入一个Integer.MAX_VALUE作为标志,以防minStack为空

class MinStack {

    Deque<Integer> dataStack;
    Deque<Integer> minStack;
    public MinStack() {
        dataStack = new ArrayDeque<>();
        minStack = new ArrayDeque<>();
        minStack.push(Integer.MAX_VALUE);
    }

    public void push(int val) {
        dataStack.push(val);
        minStack.push(( minStack.peek()> val) ? val : minStack.peek());

    }

    public void pop() {
        dataStack.pop();
        minStack.pop();
    }

    public int top() {
        return dataStack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

时间复杂度O(1),空间复杂度O(n)

(2)使用一个栈,但是栈保存元素的数据结构不是基础类型,而是本身的值和对应的最小值。相当于把原来两个栈“垂直”合并了。

只需要保持一个当前的min,同时根据push和pop更新min即可。

或者:

时间复杂度O(1),空间复杂度O(n)

(3)自定义一个栈

时间复杂度O(1),空间复杂度O(n)

16.152. 乘积最大子数组

(1)暴力

时间复杂度O(n^2),会时间超限

class Solution {
    public int maxProduct(int[] nums) {

        int[] product = new int[nums.length];
        int res = Integer.MIN_VALUE ;
        for(int i = 0 ; i < nums.length ;i++){
            product[i] = nums[i];
            res = Math.max(res,product[i]);
            for(int j = i+1 ; j < nums.length ; j++){
                product[j] = product[j-1] * nums[j];
                res = Math.max(res,product[j]);
            }
        }
        
        return res;

    }
}

(2)动态规划

与最大子序和的递推方程不同的是,这里当前位置的最优解未必是由前一个位置转移得来的,原因在于乘法乘子有负数不是越乘越小的,而加法里的负数是越加越小的。

即负数的乘法具有“逆转”的可能性,基于这种逆转的可能性,我们需要维护一个未来可能逆转为最大值的负数,并且我们希望他“越小越好”(因为这样逆转之后会更大)。从这个角度出发,我们在维护max的同时还要维护一个min.

在max和min的dp方程里,他们交叉存在,这是因为基于当前数字的正负,他们随时有交换逆转的可能。

回顾动态规划五部曲:

(1)确定dp数组及下标含义

dpmax[i]:考虑以i结尾的子数组,能够达到的最大乘积

dpmin[i]:考虑以i结尾的子数组,能够达到的最小乘积

(2)递推方程

dpmax[i] = Math.max(dpmax[i-1]*nums[i],nums[i],dpmin[i-1]*nums[i])

dpmin[i] = Math.min(dp[min[i-1]*nums[i[,nums[i],dpmax[i-1]*nums[i])

(3)初始化

根据递推方程和递推顺序,dpmax[0],dpmin[0]需要初始化

根据dp数组及下标含义,dpmax[0] = dpmin[0] = nums[0];

(4)递推顺序

从前往后

(5)dp模拟

class Solution {
    public int maxProduct(int[] nums){
        int[] dpmax = new int[nums.length];
        int[] dpmin = new int[nums.length];
        dpmax[0] = dpmin[0] = nums[0];
        for(int i = 1 ; i < nums.length ; i++){
            dpmax[i] = Math.max(dpmax[i-1]*nums[i],Math.max(dpmin[i-1]*nums[i],nums[i]));
            dpmin[i] = Math.min(dpmin[i-1]*nums[i],Math.min(dpmax[i-1]*nums[i],nums[i]));
        }
        int res = Integer.MIN_VALUE;
        for (int i : dpmax) {
            res = Math.max(res,i);
        }
        return res;
    }
}

考虑空间优化的话,dpmax,dpmin数组只在从前往后推导的时候用到,并且是相邻用到的,实际上用两个变量(但是由于for循环逻辑里dp递推关系是交叉的,还要区分过去的dpmax和现在dpmax,这里容易混淆)就可以替换。

class Solution {
    public static int maxProduct(int[] nums){
        int dpmax = nums[0];
        int dpmin = nums[0];
        int res = nums[0];
        for(int i = 1 ; i < nums.length; i++){
            int dpmaxCur = Math.max(dpmax*nums[i],Math.max(dpmin*nums[i],nums[i]));
            int dpminCur = Math.min(dpmin*nums[i],Math.min(dpmax*nums[i],nums[i]));
            res = Math.max(res,dpmaxCur);
            dpmax = dpmaxCur;
            dpmin = dpminCur;
        }
        return res;
    }
}

17.148.排序链表

(1)纯暴力法

遍历了三次O(n),排序O(nlogn),空间复杂度O(n)

总时间复杂度O(nlogn),空间复杂度O(n)

ublic ListNode sortList(ListNode head) {
        ListNode node = head;
        List<Integer> list = new ArrayList<>();
        int size = 0 ;
        while(node != null){
            size++;
            node = node.next;
        }
        int[] arr = new int[size];
        node = head;
        int i = 0;
        while(node != null){
            arr[i++] = node.val;
            node = node.next;
        }
        Arrays.sort(arr);
        node = head;
        i = 0;
        while(node != null){
            node.val = arr[i++];
            node = node.next;
        }
        
        return head;
    }

(2)进阶要求:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

这样就没办法复制链表到数组里排序了,只能从链表自身排序,才能够保持常数级空间复杂度。

对链表本身使用归并排序。

题目:

147.对链表进行插入排序

lastSorted.next = curr.next
curr.next = prev.next
prev.next = curr

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode insertionSortList(ListNode head) {
        //判断链表是否为空
        if(head == null) return head;
        //创建dummyNode 便于在头节点之前插入节点
        ListNode dummyNode = new ListNode();
        dummyNode.next = head;
        //原来控制循环次数的i,现在有cur != null控制
        //lastSorted记录有序区的位置
        ListNode lastSorted = head;
        ListNode cur = head.next;
        while(cur != null){
            if(cur.val >= lastSorted.val){
                lastSorted = lastSorted.next;
            }else{
                ListNode prev = dummyNode;
                while(prev.next.val < cur.val){
                    prev = prev.next;
                }
                lastSorted.next = cur.next;
                cur.next = prev.next;
                prev.next = cur;
            }
            cur = lastSorted.next;
        }
        return dummyNode.next;
    }
}

时间复杂度O(n^2),空间复杂度O(1).

使用插入排序达到了空间复杂度的要求,但没有达到时间复杂度的要求。

用递归法实现归并排序,空间复杂度是O(nlogn);自下而上实现归并排序,空间复杂度是O(1),满足题目要求。

1.递归实现归并排序

这里使用递归法实现归并排序中,使用快慢指针找到中间节点,需要在节点数是偶数的时候选择左边的,因此需要让fast先走一步。否则找到的中间节点是右边的。

如果找到的节点是右边的,这将导致当链表节点只有两个的时候,分割得到的始终是2和0,2的一侧永远是2,无法满足递归终止条件,会*.

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        if(head == null || head.next == null) return head;
        ListNode slow = head, fast = head.next;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode tmp = slow.next;
        slow.next = null;
        ListNode left = sortList(head);
        ListNode right = sortList(tmp);
        ListNode h = mergeTwoLists(left, right);
        return h;
    }

    public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode newList = new ListNode();
        ListNode head = newList;
        while(list1 != null && list2 != null){
            if(list1.val < list2.val){
                head.next = list1;
                list1 = list1.next;
            }else{
                head.next = list2;
                list2 = list2.next;
            }
            head = head.next;
        }
        head.next = list1 != null ? list1 : list2;
        return newList.next;
    }
}

2.自底向上直接合并 实现归并排序,达到空间复杂度O(1)的要求

将归并排序改造成迭代实现,可以通过一个变量维护需要排序的子链表的长度,这样就知道该在哪里cut、该在哪里merge,当该变量>=链表长度时,排序完成。

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) return head;

        // 统计链表长度
        int length = 0;
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }
        
        ListNode dummy = new ListNode(0, head);

        // 子链表长度,从 1 开始合并,每次翻倍。(这个 for 循环主要是增大两个合并数组的数组长度)
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode prev = dummy;  // prev 用于连接合并后排序好的链表,相当于记录结果
            ListNode cur = dummy.next;  // cur 记录拆分链表的位置

            // 每次遍历整条链表,将链表拆分成若干个长度为 subLength 的子链表,然后合并。(这个 while 循环才是真正的拆分合并)
            while (cur != null) {
                // 1. 拆分subLength长度的链表1
                ListNode head_1 = cur;      // 第一个链表的头节点
                // 找到第一个链表的尾结点,拆分出长度为subLength的链表1,
                // cur.next != null是为了防止下面的head_2 = cur.next(cur=null报错),或者也可以像下面next一样先判断一下cur != null
                for (int i = 1; i < subLength && cur != null && cur.next != null; i++) {
                    cur = cur.next;
                }

                // 2. 拆分subLength长度的链表2
                ListNode head_2 = cur.next;     // 第二个链表的头结点,即链表1尾部的next
                cur.next = null;        // 断开第一个链表和第二个链表的连接
                cur = head_2;       // 第二个链表的头节点,重新赋给cur,继续寻找第二个链表的尾结点
                // 寻找第二个链表的尾结点,再拆分出长度为subLen的链表2
                for (int i = 1; i < subLength && cur != null && cur.next != null; i++) {
                    cur = cur.next;
                }

                // 3. 断开第二个链表和剩下链表的连接
                ListNode next = null;   // next 为剩下链表的头结点(即拆分完两个链表后第二个链表结束位置的next)
                // 第二个链表的尾结点可能为空,不为空时才能更新next,所以不能直接 next = cur.next;
                if (cur != null) {
                    next = cur.next;
                    cur.next = null;    // 断开第二个链表和剩下链表的连接
                }

                // 到这里,已经拆分完毕了,head_1、head_2 都为一条单独的链表,next 为剩下未拆分的链表

                // 4. 合并两个subLength长度的有序链表
                ListNode merged = mergeTwoLists(head_1, head_2);

                prev.next = merged;     // prev.next 指向排序好的链表,连接结果
                // 将prev移动到subLength × 2 的位置,以连接下一次合并的结果
                while (prev.next != null) {
                    prev = prev.next;
                }
                cur = next;     // 将剩余链表next赋给cur,继续下一次的拆分(循环合并剩下的链表)
            }
        }
        return dummy.next;
    }

    // 题21. 合并两个有序链表
    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        cur.next = l1 == null ? l2 : l1;
        return dummy.next;
    }
}

(该代码来自leetcode题解评论区,注释详尽)

18.146.LRU缓存

模式识别:出现键和值,且时间复杂度要求O(1),考虑使用哈希表。

为了实现O(1)时间内插入、删除,需要使用双向链表。

为了实现O(1)时间内查找,需要使用到哈希表,且哈希表的key应当存储链表的位置,便于在O(1)时间内定位。

Java中有类似的数据结构,LinkedHashMap,但面试时一般期望面试者自己实现,而不是使用已有的api

class LRUCache extends LinkedHashMap<Integer,Integer>{

    private final int capacity;
    public LRUCache(int capacity) {
        super(capacity,0.75F,true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key,-1);
    }

    public void put(int key, int value) {
        super.put(key,value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

自定义链表节点实现

class LRUCache  {

    private int size;
    private int capacity;
    Map<Integer,DLinkedNode> cache = new HashMap<>();
    DLinkedNode head;
    DLinkedNode tail;
    
    class DLinkedNode{
        DLinkedNode prev;
        DLinkedNode next;
        int key;
        int value;
        DLinkedNode(){}
        DLinkedNode(int key,int value){
            this.key = key;
            this.value = value;
        }
    }
    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;

    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if(node == null){
            return -1;
        }else{
            removeNode(node);
            addToHead(node);
            return node.value;
        }
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if(node == null){
            DLinkedNode newNode = new DLinkedNode(key,value);
            cache.put(key,newNode);
            addToHead(newNode);
            size++;
            if(size > capacity){
                DLinkedNode tail = removeTail();
                cache.remove(tail.key);
                size--;
            }
        }else{
            node.value = value;
            removeNode(node);
            addToHead(node);
        }

    }
    
    public void removeNode(DLinkedNode node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    public void addToHead(DLinkedNode node){
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
        
    }
    
    public DLinkedNode removeTail(){
        DLinkedNode node = tail.prev;
        removeNode(node);
        return node;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

HashMap的查找、链表的增加、删除时间复杂度均为O(1),总的时间复杂度O(1)

空间复杂度O(n)

19.环形链表II

1、使用HashMap记录所有访问到的节点,当遇到第一个重复的节点时,这个节点就是入环结点。

时间复杂度O(n),空间复杂度O(n)

2、快慢指针

快慢指针同时出发,如果有环,最终在环内相遇

假设在紫色处相遇,那么慢指针走了a+b步,快指针走了a+b+n*(b+c)步。

又由于 2*(a+b) = a+b+n*(b+c)

得到 a  = (n-1) * (b+c) + c

从相遇点到入环点的距离 再加上若干倍环长,恰好等于从出发点到入环点的距离。那么从相遇开始,派出一个慢指针2从出发点出发,最终两个慢指针会在入环点相遇。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if(fast == slow) {
                ListNode slow2 = head;
                while(slow != slow2){
                    slow = slow.next;
                    slow2 = slow2.next;
                }
                return slow;
            }
        }
       return null;

    }
}

20.141.环形链表

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                return true;
            }
        }
        return false;
    }


}

上一篇:flutter 用PUT的方式传输文件不带分隔符


下一篇:spark集群模式-standalone的配置和使用