LeetCode HOT 100Ⅱ

目录

DAY13

617:合并二叉树

234:回文链表

DAY14

200:岛屿数量

DAY15

146:LRU缓存(最近最少使用)

DAY16

56:合并区间

DAY17

55:跳跃游戏

DAY18

19:删除链表的倒数第n个节点

DAY19

347:前K个高频元素

141:环形链表

DAY20

198:打家劫舍Ⅰ

DAY21

213:打家劫舍Ⅱ

DAY22

337:打家劫舍Ⅲ(优化一下!!!)

DAY23

647:回文子串


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;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值