力扣每日练习-java版(二)
- 1219. 黄金矿工
- 思路
- 代码
- 时空复杂度
- 备注
- 19. 删除链表的倒数第 N 个结点
- 思路
- 代码
- 时空复杂度
- 备注
- 146. LRU 缓存
- 思路
- 代码
- 时空复杂度
- 备注
- 460. LFU 缓存
- 思路
- 代码
- 时空复杂度
- 备注
- 704. 二分查找
- 思路
- 代码
- 时空复杂度
- 备注
- 34. 在排序数组中查找元素的第一个和最后一个位置
- 思路
- 代码
- 时空复杂度
- 备注
- 39. 组合总和
- 思路
- 代码
- 时空复杂度
- 备注
1219. 黄金矿工
思路
- 遍历所有可能的位置作为入口(值不为0)
- 记枚举的起点为 (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;//恢复原来的值
}
}
时空复杂度
备注
- 初始化位置情况,用数组实现:
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
19. 删除链表的倒数第 N 个结点
思路
遍历 【链表长度】 - 【倒数位置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;
}
}
时空复杂度
备注
- 找到链表倒数第k个节点
两个指针,p1从head走k步,p2指向head,然后p1和p2一起走,直到p1走到链表末尾的空指针。
虚拟节点 dummy 是为了避免删除倒数第一个元素时出现空指针异常,在头部加入 dummy 节点并不影响尾部倒数第 k 个元素是什么。
146. LRU 缓存
思路
lru:最近最少使用,一种缓存策略,容量不够时,优先去除最久没有使用的数据。
这个数据结构的特点:
- 有顺序,能够记录使用的先后顺序。链表尾部插入,头部淘汰,更新操作可以先删掉,再重新插到尾部,这样最近使用的一定在链表最后。
- 查询快,get方法快速查询,可采用哈希表结构
- 插入删除快,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 个元素。
备注
- 哈希链表结构
LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>(); - cache.containsKey(key) 是否有某个key
- cache.get(key) 通过key获取value
- cache.put(key,value) 像哈希链表中新增键值对
- cache.remove(key) 根据key移除某键值对
- cache.keySet().iterator().next() 拿到哈希链表中第一个key
460. LFU 缓存
思路
淘汰访问频次最低的数据,如果访问频次最低的数据有多条,需要淘汰最旧的数据。
结构特点:
- 只要用 get 或者 put 方法访问一次某个 key,该 key 的 freq 就要加一。==> 使用一个 HashMap 存储 key 到 val 的映射。一个 HashMap 存储 key 到 freq 的映射。
- 如果在容量满了的时候进行插入,则需要将 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 的缓存容量。
备注
- keyToVal.containsKey(key) hashmap是否存在某个key
- keyToVal.size() hashmap大小(键值对的个数)
- freqToKeys.putIfAbsent(key, b) hashmap不存在key的话,创建key,value为b
- hashmap.getOrDefault(Object key, V defaultValue) 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。
- linkedhashset.isEmpty() linkedhashset是否为空
- hashmap.remove(key) 根据key移除键值对
- minFreqList.iterator().next() LinkedHashSet的第一个元素
704. 二分查找
思路
- 基础二分查找:选中间数比较,等于直接返回,中间数<target 就到右半部分找,否则到左半部分找。
- 寻找左侧边界的二分搜索:nums[mid] >=target时收缩右侧边界,最后检查出界情况。
- 寻找右侧边界的二分搜索: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;
}
}
- 寻找左侧边界的二分搜索
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. 在排序数组中查找元素的第一个和最后一个位置
思路
二分查找(左边界+右边界方法)
代码
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;
}
}
时空复杂度
- 时间复杂度
O(logn),其中 n 是数组的长度。 - 空间复杂度
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)。
备注
- track.removeLast(); LinkedList删除最后一个元素