力扣每日练习-java版(二)

  • 1219. 黄金矿工
  • 思路
  • 代码
  • 时空复杂度
  • 备注
  • 19. 删除链表的倒数第 N 个结点
  • 思路
  • 代码
  • 时空复杂度
  • 备注
  • 146. LRU 缓存
  • 思路
  • 代码
  • 时空复杂度
  • 备注
  • 460. LFU 缓存
  • 思路
  • 代码
  • 时空复杂度
  • 备注
  • 704. 二分查找
  • 思路
  • 代码
  • 时空复杂度
  • 备注
  • 34. 在排序数组中查找元素的第一个和最后一个位置
  • 思路
  • 代码
  • 时空复杂度
  • 备注
  • 39. 组合总和
  • 思路
  • 代码
  • 时空复杂度
  • 备注


1219. 黄金矿工

1219.黄金矿工

JavaSwing黄金矿工项目_链表

思路

  1. 遍历所有可能的位置作为入口(值不为0)
  2. 记枚举的起点为 (i, j)(i,j),我们就可以从 (i, j)(i,j) 开始进行递归 + 回溯,枚举所有可行的开采路径。我们用递归函数dfs(x,y,gold)进行枚举,其中(x,y)表示当前所在的位置,gold 表示在开采位置(x,y) 之前,已经拥有的黄金数量。四种路线(上下左右)可以用数组做标记。

代码

class Solution {
    static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    int[][] grid;
    int m, n;
    int ans = 0;
    public int getMaximumGold(int[][] grid) {
        /**
          * 思路:       
          * 1.遍历枚举每一个值不为0的位置作为起点。
          * 2. 记枚举的起点为 (i, j)(i,j),我们就可以从 (i, j)(i,j) 开始进行递归 + 回溯,
          * 枚举所有可行的开采路径。我们用递归函数dfs(x,y,gold)进行枚举,其中(x,y)表示当前
          * 所在的位置,gold 表示在开采位置(x,y) 之前,已经拥有的黄金数量。  
         */
        this.grid = grid;
        this.m = grid.length;
        this.n = grid[0].length;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j]>0){
                    dfs(i,j,0);
                }
            }
        }
        return this.ans;
    }
    public void dfs(int x,int y,int gold){
        gold+=this.grid[x][y];
        ans = Math.max(ans,gold);
        int rec = grid[x][y];//记录当前位置的值,为了后续恢复原值
        grid[x][y] = 0; //将该位置设置为0,防止重复遍历
        //枚举四种情况
        for(int i=0;i<4;i++){
            int nx = x+dirs[i][0];
            int ny = y+dirs[i][1];
            if(nx>=0 && nx<m && ny>=0 && ny<n && this.grid[nx][ny]>0){
                dfs(nx,ny,gold);
            }
        }
        grid[x][y] = rec;//恢复原来的值
    }
}

时空复杂度

JavaSwing黄金矿工项目_深度优先_02

备注

  1. 初始化位置情况,用数组实现:
    static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

19. 删除链表的倒数第 N 个结点

19. 删除链表的倒数第 N 个结点

JavaSwing黄金矿工项目_算法_03

思路

遍历 【链表长度】 - 【倒数位置n】 - 【1】次,即走到要删除节点前面的节点,将它指向要删除节点的下一个节点。
要点:如何一次遍历找到倒数位置n的节点。

代码

/**
 * 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 removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode x = findFromEnd(dummy,n+1);
        x.next = x.next.next;
        return dummy.next;
    }
    public ListNode findFromEnd(ListNode head,int k) {
        ListNode p1 = head;
        for(int i = 0;i<k;i++){
            p1 = p1.next;
        }
        ListNode p2 = head;
        while(p1!=null){
            p1 = p1.next;
            p2 = p2.next;
        }
        return p2;
    }
}

时空复杂度

JavaSwing黄金矿工项目_leetcode_04

备注

  1. 找到链表倒数第k个节点
    两个指针,p1从head走k步,p2指向head,然后p1和p2一起走,直到p1走到链表末尾的空指针。
    虚拟节点 dummy 是为了避免删除倒数第一个元素时出现空指针异常,在头部加入 dummy 节点并不影响尾部倒数第 k 个元素是什么。

146. LRU 缓存

146. LRU 缓存

JavaSwing黄金矿工项目_深度优先_05

思路

lru:最近最少使用,一种缓存策略,容量不够时,优先去除最久没有使用的数据。
这个数据结构的特点:

  1. 有顺序,能够记录使用的先后顺序。链表尾部插入,头部淘汰,更新操作可以先删掉,再重新插到尾部,这样最近使用的一定在链表最后。
  2. 查询快,get方法快速查询,可采用哈希表结构
  3. 插入删除快,put方法,采用链表结构
    要满足以上特点,结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap,双向链表和哈希表的结合体。

代码

class LRUCache {
    int cap;
    LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
    public LRUCache(int capacity) {
        this.cap = capacity;
    }
    
    public int get(int key) {
        if(!cache.containsKey(key)){
            return -1;
        }
        makeRecently(key);
        return cache.get(key);
    }
    
    public void put(int key, int value) {
        if(cache.containsKey(key)){
            cache.put(key,value);
            makeRecently(key);
            return;
        }
        if(cache.size()>=this.cap){
            int oldest = cache.keySet().iterator().next();
            cache.remove(oldest);
        }
        cache.put(key,value);
    }

    public void makeRecently(int key) {
        int val = cache.get(key);
        cache.remove(key);
        cache.put(key,val);
    }
}

/**
 * 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);
 */

时空复杂度

1.时间复杂度
对于 put 和 get 都是O(1)。
2.空间复杂度
O(capacity),因为哈希表和双向链表最多存储capacity+1 个元素。

备注

  1. 哈希链表结构
    LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
  2. cache.containsKey(key) 是否有某个key
  3. cache.get(key) 通过key获取value
  4. cache.put(key,value) 像哈希链表中新增键值对
  5. cache.remove(key) 根据key移除某键值对
  6. cache.keySet().iterator().next() 拿到哈希链表中第一个key

460. LFU 缓存

460. LFU 缓存

JavaSwing黄金矿工项目_算法_06

思路

淘汰访问频次最低的数据,如果访问频次最低的数据有多条,需要淘汰最旧的数据。
结构特点:

  1. 只要用 get 或者 put 方法访问一次某个 key,该 key 的 freq 就要加一。==> 使用一个 HashMap 存储 key 到 val 的映射。一个 HashMap 存储 key 到 freq 的映射。
  2. 如果在容量满了的时候进行插入,则需要将 freq 最小的 key 删除,如果最小的 freq 对应多个 key,则删除其中最旧的那一个。==> freq 到 key 的映射,用来找到 freq 最小的 key。freq和key是一对多的关系,key的列表用LinkedHashSet实现。因为LinkedHashSet能保证有序且查找删除快。
    // freq 到 key 列表的映射
    HashMap<Integer, LinkedHashSet> freqToKeys;
    // 记录最小的频次
    int minFreq;

代码

class LFUCache {

    // key 到 val 的映射,我们后文称为 KV 表
    HashMap<Integer, Integer> keyToVal;
    // key 到 freq 的映射,我们后文称为 KF 表
    HashMap<Integer, Integer> keyToFreq;
    // freq 到 key 列表的映射,我们后文称为 FK 表
    HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
    // 记录最小的频次
    int minFreq;
    // 记录 LFU 缓存的最大容量
    int cap;

    public LFUCache(int capacity) {
        keyToVal = new HashMap<>();
        keyToFreq = new HashMap<>();
        freqToKeys = new HashMap<>();
        this.cap = capacity;
        this.minFreq = 0;
    }

    public int get(int key) {
        if (!keyToVal.containsKey(key)) {
            return -1;
        }
        // 增加 key 对应的 freq
        increaseFreq(key);
        return keyToVal.get(key);
    }

    public void put(int key, int val) {
        if (this.cap <= 0) return;

        /* 若 key 已存在,修改对应的 val 即可 */
        if (keyToVal.containsKey(key)) {
            keyToVal.put(key, val);
            // key 对应的 freq 加一
            increaseFreq(key);
            return;
        }

        /* key 不存在,需要插入 */
        /* 容量已满的话需要淘汰一个 freq 最小的 key */
        if (this.cap <= keyToVal.size()) {
            removeMinFreqKey();
        }

        /* 插入 key 和 val,对应的 freq 为 1 */
        // 插入 KV 表
        keyToVal.put(key, val);
        // 插入 KF 表
        keyToFreq.put(key, 1);
        // 插入 FK 表
        freqToKeys.putIfAbsent(1, new LinkedHashSet<>());
        freqToKeys.get(1).add(key);
        // 插入新 key 后最小的 freq 肯定是 1
        this.minFreq = 1;
    }

    private void increaseFreq(int key) {
        int freq = keyToFreq.get(key);
        /* 更新 KF 表 */
        keyToFreq.put(key, freq + 1);
        /* 更新 FK 表 */
        // 将 key 从 freq 对应的列表中删除
        freqToKeys.get(freq).remove(key);
        // 将 key 加入 freq + 1 对应的列表中
        freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<>());
        freqToKeys.get(freq + 1).add(key);
        // 如果 freq 对应的列表空了,移除这个 freq
        if (freqToKeys.get(freq).isEmpty()) {
            freqToKeys.remove(freq);
            // 如果这个 freq 恰好是 minFreq,更新 minFreq
            if (freq == this.minFreq) {
                this.minFreq++;
            }
        }
    }

    private void removeMinFreqKey() {
        // freq 最小的 key 列表
        LinkedHashSet<Integer> keyList = freqToKeys.get(this.minFreq);
        // 其中最先被插入的那个 key 就是该被淘汰的 key
        int deletedKey = keyList.iterator().next();
        /* 更新 FK 表 */
        keyList.remove(deletedKey);
        if (keyList.isEmpty()) {
            freqToKeys.remove(this.minFreq);
            // 问:这里需要更新 minFreq 的值吗?
            // 答:不需要,因为只有在put时,容量满了才会移除,移除后put函数里把minFreq置为1了。因为新添加了key,最小的freq必定是1。
        }
        /* 更新 KV 表 */
        keyToVal.remove(deletedKey);
        /* 更新 KF 表 */
        keyToFreq.remove(deletedKey);
    }
}

时空复杂度

1.时间复杂度
对于 put 和 get 都是O(1)。
2.空间复杂度
O(capacity),其中capacity 为 LFU 的缓存容量。

备注

  1. keyToVal.containsKey(key) hashmap是否存在某个key
  2. keyToVal.size() hashmap大小(键值对的个数)
  3. freqToKeys.putIfAbsent(key, b) hashmap不存在key的话,创建key,value为b
  4. hashmap.getOrDefault(Object key, V defaultValue) 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。
  5. linkedhashset.isEmpty() linkedhashset是否为空
  6. hashmap.remove(key) 根据key移除键值对
  7. minFreqList.iterator().next() LinkedHashSet的第一个元素

704. 二分查找

704. 二分查找

JavaSwing黄金矿工项目_链表_07

思路

  1. 基础二分查找:选中间数比较,等于直接返回,中间数<target 就到右半部分找,否则到左半部分找。
  2. 寻找左侧边界的二分搜索:nums[mid] >=target时收缩右侧边界,最后检查出界情况。
  3. 寻找右侧边界的二分搜索:nums[mid] >=target时收缩左侧边界,最后检查出界情况。

代码

1.基础二分查找

class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length-1;
        while(left<=right){
            int mid = left+((right-left)>>1);
            if(nums[mid]==target){
                return mid;
            } else if(nums[mid]<target){
                left=mid+1;
            } else if(nums[mid]>target){
                right = mid -1;
            }
        }
        return -1;
    }
}
  1. 寻找左侧边界的二分搜索
int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
    // 检查出界情况
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}

3.寻找右侧边界的二分查找

int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 这里改成收缩左侧边界即可
            left = mid + 1;
        }
    }
    // 这里改为检查 right 越界的情况,见下图
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}

时空复杂度

1.时间复杂度
O(logn),其中 n 是数组的长度。
2.空间复杂度
O(1)

备注

34. 在排序数组中查找元素的第一个和最后一个位置

34. 在排序数组中查找元素的第一个和最后一个位置

思路

二分查找(左边界+右边界方法)

代码

class Solution {
    public int[] searchRange(int[] nums, int target) {
        return new int[]{left(nums,target),right(nums,target)};
    }
     public int left(int[] nums, int target) {
        int l = 0,r = nums.length-1;
        while(l<=r){
            int mid = l+((r-l)>>1);
            if(nums[mid]>=target){
                 r = mid -1;
            } else {
                l = mid + 1;
            }
        }
        if(l>=nums.length || nums[l]!=target){
            return -1;
        }
        return l;
     }
     public int right(int[] nums, int target) {
         int l = 0,r = nums.length-1;
        while(l<=r){
            int mid = l+((r-l)>>1);
            if(nums[mid]<=target){
                l = mid + 1;
            } else {
                r = mid -1;
            }
        }
        if(r<0 || nums[r]!=target){
            return -1;
        }
        return r;
     }
}

时空复杂度

  1. 时间复杂度
    O(logn),其中 n 是数组的长度。
  2. 空间复杂度
    O(1)

备注

39. 组合总和

添加链接描述

思路

回溯算法,这道题的关键在于 candidates 中的元素可以复用多次,注意枚举时的处理。

代码

class Solution {
    List<List<Integer>> res = new LinkedList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates.length == 0) {
            return res;
        }
        backtrack(candidates, 0, target, 0);
        return res;
    }

    // 记录回溯的路径
    LinkedList<Integer> track = new LinkedList<>();

    // 回溯算法主函数
    void backtrack(int[] candidates, int start, int target, int sum) {
        if (sum == target) {
            // 找到目标和
            res.add(new LinkedList<>(track));
            return;
        }

        if (sum > target) {
            // 超过目标和,直接结束
            return;
        }

        // 回溯算法框架
        for (int i = start; i < candidates.length; i++) {
            // 选择 candidates[i]
            track.add(candidates[i]);
            sum += candidates[i];
            // 递归遍历下一层回溯树
            backtrack(candidates, i, target, sum);
            // 撤销选择 candidates[i]
            sum -= candidates[i];
            track.removeLast();
        }
    }
}

时空复杂度

1.时间复杂度
O(S),其中 S 为所有可行解的长度之和。
2.空间复杂度
空间复杂度:O(target)。除答案数组外,空间复杂度取决于递归的栈深度,在最差情况下需要递归 O(target) 层(target个1)。

备注

  1. track.removeLast(); LinkedList删除最后一个元素