目录
DAY13
617:合并二叉树
思路:递归。不用新建树,直接在树1上修改,若树1或树2中有空,则直接返回值。否则将树1和树2节点值相加作为树1的值,递归同时遍历子树。
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
return dfs(root1, root2);
}
TreeNode dfs(TreeNode root1, TreeNode root2){
if(root1 == null || root2 == null){
return root1 == null ? root2 : root1;
}
root1.val += root2.val;
root1.left = dfs(root1.left, root2.left);
root1.right = dfs(root1.right, root2.right);
return root1;
}
}
234:回文链表
思路:列表,比较慢。先将链表存入列表中,然后直接索引比较对称的值知否相同即可。
//栈
//列表法
class Solution {
public boolean isPalindrome(ListNode head) {
List<Integer> list = new ArrayList<>();
while(head != null){ //存入列表
list.add(head.val);
head = head.next;
}
int len = list.size();
for(int i = 0; i < len; i++){
if(list.get(i) != list.get(len - 1 - i)) return false;
}
return true;
}
}
DAY14
200:岛屿数量
题目要求:岛屿只能由上下左右相邻的陆地连接而成,以及如果陆地处于二维网格边缘,若它除去边缘以外方向都是水,那它也可以算是岛屿。
思路:DFS。遍历矩阵,如果遇到岛屿 ‘1’ 则展开深度搜索,判断它的上下左右是否还有岛屿范围。遇到越界或水‘0’时停止搜索,计数器+1。
注意为了避免重复搜索,将搜索过的陆地置为‘0’。
class Solution {
public int numIslands(char[][] grid) {
int count = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if(grid[i][j] == '1'){
dfs(grid, i, j);
count++;
}
}
}
return count;
}
void dfs(char[][] grid, int i, int j){
if(i < 0 || j < 0|| i >= grid.length || j >= grid[0].length || grid[i][j] == '0') return;
grid[i][j] = '0'; //将查找过的岛屿置为1,避免重复查找
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
DAY15
146:LRU缓存(最近最少使用)
思路:双向链表+哈希表。这题思路不难,难的是手写链表。
- 哈希表:迅速找到一个节点
- 双向链表:在get操作的时候方便拿到它的前后节点,如果该结点处于比较中间的位置便省去了从头迭代查找的时间。
- 手写双向链表,还要写它的相关方法。比如添加到头部,删除节点。然后用其写出移动到头部和删除尾部的方法
get()主要思路:要获取某个元素,先判断是否存在,若不存在直接返回-1;若存在不仅要返回值,还要将其移动到链表头部表示刚刚使用过了。
put()主要思路:先判断需要放入的key存不存在,若存在则将已存在的key对应的value更换成新的,并将其移动到头部;若不存在,则需要创建一个新的节点存放,并添加到头部,此时需要判断是否超过缓存器的大小,没超过的话直接添加即可;超过的话(先添加)需要删掉链表尾部的元素(最近不常使用)。
class LRUCache {
class DLinkedNode{ //手写双向链表
int key, value;
DLinkedNode pre, next;
public DLinkedNode(){}
public DLinkedNode(int _key, int _value){key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity; //输入的缓存空间大小
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
//构造伪头节点和伪尾结点,省去判断左右节点是否存在
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if(node == null) return -1;
moveToHead(node); //节点若被操作了就移动到头部,没有被访问的放在尾部优先被替换掉
return node.value;
}
public void put(int key, int value) { //作废最近不使用,放入新的
DLinkedNode node =cache.get(key);
if(node == null) {
//如果‘索引’key不存在,那就新创建一个
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{
//如果可以存在,那就修改它的value值,然后再将其移动到头部表示刚使用过
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node){
node.pre = head; //伪头部
node.next = head.next; //挪开
head.next.pre = node; //原来head后面第一个节点的pre指向node
head.next = node;
}
private void removeNode(DLinkedNode node){
node.pre.next = node.next;
node.next.pre = node.pre;
}
private void moveToHead(DLinkedNode node){
removeNode(node); //先移除
addToHead(node); //再添加
}
private DLinkedNode removeTail(){
DLinkedNode res = tail.pre;
removeNode(res);
return res;
}
}
DAY16
56:合并区间
思路:排序。首先两个区间的状态一共有三种情况
- 两个区间有交叉
- 两个区间是包含关系
- 两个区间无交叉无包含
前两种情况都可以进行合并,因为不知道是交叉还是包含,所以在合并之后要选择区间更大的,也就是右边界更大的。
将数组按照起始位置从小到大排序,将结果存放在res数组中,用结果数组的终止位置与当前数组的起始位置作比较,判断是需要合并还是直接另起一个区间放入res中。
(看题解的时候都有点费劲)
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (x, y) -> x[0] - y[0]); //按起始位置从小到大排序
int[][] res = new int[intervals.length][2];
int index = -1;
for(int[] interval : intervals){
//如果当前数组为空或当前数组起始位置>前面数组最后区间的终止位置,不合并
if(index == -1 || res[index][1] < interval[0]){
res[++index] = interval;
}else{ //因为两个数字可能是交叉也可能是包含关系
res[index][1] = Math.max(res[index][1], interval[1]);
}
}
//输出长度为index + 1的结果
return Arrays.copyOf(res, index +1);
}
}
DAY17
55:跳跃游戏
思路:贪心算法。max存放的是该站点能到达的最大距离。如果max距离包含了数组长度-1,那就可以到达终点;如果不能就依次遍历,看下个元素能否满足。
(真的有点翻不过来,好累(ಥ﹏ಥ))
class Solution {
public boolean canJump(int[] nums) {
int len = nums.length;
int max = 0;
for(int i = 0; i < len; i++){
if(i <= max){
max = Math.max(max, nums[i] + i);
if(max >= len - 1) return true;
}
}
return false;
}
}
DAY18
19:删除链表的倒数第n个节点
思路:双指针。快慢指针,首先因为被删除的可能是head,所以要设定一个伪头指针pre.next = head。然后快指针先走,当快指针向前走了n个节点时,慢指针开始一起走,直到快指针走到null的时候慢指针刚好走到了需要删除的倒数第n个节点上。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode(0);
pre.next = head;
ListNode fast = pre, slow = pre;
while(n != 0){
fast = fast.next;
n--;
}
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;//删除节点
return pre.next;
}
}
DAY19
347:前K个高频元素
思路: 堆。这道题真的有意思,首先建立一个哈希表,存放统计每个数出现的次数。然后建立小顶堆,在这里放入对的不是一个数,而是一个数组形式存放的kv键值对,第0位是数字,第1位是数字对应出现的次数。
- 当堆中元素个数小于k时,遍历哈希表直接将数组放入
- 当堆中元素个数大于等于k时,就比较堆顶元素和即将加入的元素谁的出现次数多,堆顶元素是当前堆中出现次数最小的。比它多久换进来,没它多就略过。
最后将堆中每个数组的第0位输出到数组中即可。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer> hash = new HashMap<>();
for(int num : nums){
hash.put(num, hash.getOrDefault(num, 0) + 1);
}
PriorityQueue<int[]> minHeap = new PriorityQueue<int[]>(new Comparator<int[]>(){
public int compare(int[] a, int[] b){
return a[1] - b[1];
}
});
for(Map.Entry<Integer, Integer> entry : hash.entrySet()){ //entrySet返回键值对
int num = entry.getKey(), count = entry.getValue();
if(minHeap.size() == k){ //如果堆里已经装满了k个数
//把kv键值对都放进了堆中,也就是堆里的一个节点就是一个长度为2的数组
if(minHeap.peek()[1] < count){
minHeap.poll();
minHeap.offer(new int[]{num, count});
}
}else{ //没装满就直接加入
minHeap.offer(new int[]{num, count});
}
}
int[] res = new int[k];
for(int i = 0; i < k; ++i){
res[i] = minHeap.poll()[0]; // 只把堆中每个节点的第0位让如res中,也就是key
}
return res;
}
}
141:环形链表
思路一:哈希表。哈希表存放已经访问过的节点,遍历链表时如果某个节点已经在hash表中存在,那么说明链表里存在环。
思路二:双指针(这真的想不到啊...)。设置快指针一次走两个,慢指针一次走一个,如果存在环,快指针就会在环里转圈,那么快慢指针迟早会相遇。
因为快指针一次要走两步,所以要判断fast和fast.next是否为null,不为null就可以直接跳过两个。为null的话就说明没有了。
//双指针
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
ListNode slow = head, fast = head = head.next;
//因为while的判断条件是两个指针是否重合,所以初始时候要一前一后
//如果初始时两个指针都在head,那么while就不会执行了
while(slow != fast){
if(fast == null || fast.next == null) return false;
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
//哈希表
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> hash = new HashSet<>();
while(head != null){
//如果hash.add(head)返回null,说明这个节点之前没有被访问过
//如果访问过那么就已经在hash表中存在过了,此时就不会返回null
if(!hash.add(head)) return true;
head = head.next;
}
return false;
}
}
DAY20
198:打家劫舍Ⅰ
思路:动态规划。
- 当只有一家人时,就偷这家人
- 当有两家人时,只能选其中金额高的一家偷
- 当大于两家人时要判断:偷当前这家,就不能偷它前面的一家,那么总金额就应该是前两家之前偷的金额+本次偷的金额;不偷这家,那么总金额就是前一家之前偷的总金额。
比较谁大就选哪种方案偷。
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len == 0) return 0;
if(len == 1) return nums[0];
int[] dp = new int[len];
dp[0] = nums[0]; //只有一家的时候就偷这家
dp[1] = Math.max(nums[0], nums[1]); //有两家的时候选金额多的偷
for(int i = 2; i < len; i++){
//如果偷i这家,那么i-1就不能偷了,只能算i-2之前最高金额+这次的
//如果不投这家,那么就是前i-1家偷的最大金额
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[len - 1];
}
}
//形式2
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len == 0) return 0;
if(len == 1) return nums[0];
int cur = 0, pre = 0, temp;
for(int num : nums){
temp = cur;
cur = Math.max(pre + num, cur);
pre = temp;
}
return cur;
}
}
补充:array==null和array.length==0的区别
- 给array赋一个null,相当于还是没有给它数组地址,也就是没有开辟一块数组的内存。
- 给array赋了一个数组的地址,数组里没有值,是长度为0的数组,但是在内存里开辟了一块数组的空间。
DAY21
213:打家劫舍Ⅱ
思路:动态规划。这次的房屋是环形的,头尾也算是相邻的,所以:
- 如果偷了第一家,那么数组就应该是nums[ : n - 1 ]
- 如果没偷第一家,应该是nums[ 1 : ]
class Solution {
public int rob(int[] nums) {
if(nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
//如果偷了第一家,那么数组就应该是nums[ : n - 1 ]
//如果没偷第一家,应该是nums[ 1 : ]
return Math.max(robWay(Arrays.copyOfRange(nums, 1, nums.length)), robWay(Arrays.copyOfRange(nums, 0, nums.length - 1)));
}
private int robWay(int[] nums){
//直接用两个变量比数组的复杂度低
int cur = 0, pre = 0, temp;
for(int num : nums){
temp = cur; //记录下前一天最大值
cur = Math.max(pre + num, cur); //判断前两天+今天还是前一天的大
pre = temp; //前一天编程前两天
}
return cur;
}
}
DAY22
337:打家劫舍Ⅲ(优化一下!!!)
思路:动态规划。分情况讨论,若当前节点为o
- o被选中,则它的左右节点都不能被选中,此时节点o最大权值=左孩子没被选中的最大权值 + 右孩子没被选中的最大权值
- o没被选中,左右孩子可以被选也可以不被选,此时节点o最大权值=max(左孩子没被选中的最大权值,左孩子被选中的最大权值) + max(右孩子没被选中的最大权值,右孩子被选中的最大权值)
class Solution {
Map<TreeNode,Integer> f = new HashMap<>(); //该节点被选中的最大权值
Map<TreeNode,Integer> g = new HashMap<>(); //该节点没被选中的最大权值
public int rob(TreeNode root) {
dfs(root);
//找不到key返回0,找到返回对应的value
return Math.max(f.getOrDefault(root, 0), g.getOrDefault(root, 0));
}
public void dfs(TreeNode node){
if(node == null) return;
dfs(node.left);
dfs(node.right);
f.put(node, node.val + g.getOrDefault(node.left, 0) + g.getOrDefault(node.right, 0));
g.put(node, Math.max(f.getOrDefault(node.left, 0), g.getOrDefault(node.left, 0)) +
Math.max(f.getOrDefault(node.right, 0), g.getOrDefault(node.right, 0)));
}
}
DAY23
647:回文子串
思路:中心扩展(遍历)。将字符串中每个字符作为一次中心点向左右扩展,判断回文串的个数。
中心点有两种情况,一个和两个。所以用了在for i里嵌套了for j,j的取值只有两个,0或1。对应的也就是让left和right相等或是相邻然后开始扩展。
class Solution {
public int countSubstrings(String s) {
int count = 0;
char[] sc = s.toCharArray();
for(int i = 0; i < sc.length; i++){
for(int j = 0; j <= 1; j++){ //j为0时中心点是一个,为1时中心点是两个
int left = i;
int right = i + j;
while(left >= 0 && right < sc.length && sc[left--] == sc[right++]){
count++;
}
}
}
return count;
}
}