Leetcode 191-210刷题笔记(非困难题)

198.打家劫舍

题目要求两个相邻的家不能同时被偷(暗示偷窃需要间隔n个家(n >=1))。

看到这种题目,我们很容易联想到使用动态规划的思想进行求解。

我们声明一个dp数组用于保存偷窃到该户时,能够产生的最大价值。

那么这个动态规划的状态转移方程式是什么?

我们通过需要间隔可以发现,最基本有两种偷窃模式:

  1. 偷窃当前门户,那么获得上上个的最大价值和当前门户的价值。
  2. 不偷窃当前门户,则我们可以获得上一户的最大价值。

就可以得出 dp[i] = Max(dp[i-1],dp[i-2]+nums[i]) 这个方程

    //打家劫舍(隔一个偷)
    public int rob(int[] nums) {
        int[] dp = new int[nums.length + 1];
        for(int i=1; i<dp.length; i++) {
            if(i == 1) {
                dp[i] = nums[i - 1];
            }else {
                dp[i] = Math.max(dp[i - 2] + nums[i - 1],dp[i - 1]);
            }
        }

        return dp[nums.length];
    }

199.二叉树的右视图

该题目也十分的简单,主要的思路就是我们通过保存访问过的树的对应高度,以用来防止重复访问。

但是需要注意的就是:就算该层被访问过了,但是他的子树部分的高度可能没有被访问过,所以不能return退出递归,而是应该继续向下搜索。

    //用于判断对应高度是否有被访问
    private Set<Integer> heights = new HashSet<>();
    //二叉树右视图
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        dfs(list,root,0);

        return list;
    }

    public void dfs(List<Integer> list, TreeNode root, int height) {
        if(root == null) {
            return;
        }
        //说明该树高已被访问过
        if(!heights.contains(height)) {
            list.add(root.val);
            heights.add(height);
        }

        dfs(list,root.right,height + 1);
        dfs(list,root.left,height + 1);
    }

200.岛屿数量

该题目十分简单,就是一个dfs对碰到的1进行搜索,每一次dfs会把同一块岛屿的1全部访问,之后统计dfs执行成功的次数即是岛屿数量。

    private boolean[][] isVisit;
    //岛屿数量
    public int numIslands(char[][] grid) {
        isVisit = new boolean[grid.length][grid[0].length];
        int ans = 0;
        for(int i=0; i<grid.length; i++) {
            for(int j=0; j<grid[0].length; j++) {
                if(!isVisit[i][j] && grid[i][j] == '1') {
                    dfs(grid,i,j);
                    ans++;
                }
            }
        }

        return ans;
    }

    //将一块岛屿全部标记
    public void dfs(char[][] grid, int i, int j) {
        if(i < 0 || i >= grid.length) {
            return;
        }
        if(j < 0 || j >= grid[0].length) {
            return;
        }
        if(isVisit[i][j] || grid[i][j] == '0') {
            return;
        }
        //标记已访问
        isVisit[i][j] = true;
        //向上下左右进行深度搜索
        dfs(grid,i - 1,j);
        dfs(grid,i + 1,j);
        dfs(grid,i,j - 1);
        dfs(grid,i,j + 1);
    }

201.数字范围的按位与

我们通过观察发现,在范围内的按位与的结果,其实就是[min,max]两端点的公共前缀。

所以我们通过向右位移动,当两个值相同的时候则说明找到了公共前缀,此时我们记录移动了多少位,之后再左移回去,就是目标值了。

    //范围内的按位与(就是找二进制的公共前缀)
    public int rangeBitwiseAnd(int m, int n) {
        int bits = 0;
        while(m < n) {
            //找寻公共前缀
            n >>= 1;
            m >>= 1;
            bits++;
        }
        
        return m << bits;
    }

202.快乐数

该题目需要注意的是可能会出现重复导致无限递归 -> 栈溢出。

所以我们需要对可能是快乐数的数进行set保存,如果出现重复则说明不是快乐数。

    private Set<Integer> numbers = new HashSet<>();

    public boolean isHappy(int n) {
        if(n == 1) {
            return true;
        }
        //说明出现死循环
        if(numbers.contains(n)) {
            return false;
        }
        numbers.add(n);
        int ans = 0;
        while(n > 0) {
            ans += Math.pow(n % 10,2);
            n /= 10;
        }
        return isHappy(ans);
    }

203.移出链表元素

我们可以通过定义一个傀儡头结点,等找到了非val值的ListNode结点,我们选择将节点的next保存并断开,直接再与傀儡头结点进行连接。

    public ListNode removeElements(ListNode head, int val) {
        ListNode ans = new ListNode();
        ListNode temp = ans;

        while(head != null) {
            if(head.val != val) {
                temp.next = head;
                temp = temp.next;
                ListNode next = head.next;
                head.next = null;
                head = next;
            }else {
                head = head.next;
            }
        }

        return ans.next;
    }

205.同构字符串

该题目就是比较两个字符串的组成模式是不是一样的。

例如"add","egg"这两个单词属于ABB型。

比较方式:两个字符串逐个字符进行比较。

1.如果一个字符串的字符没有出现过,另一个也不能出现新的字符。
2.如果一个字符串的字符出现过,则另一个字符也许出现旧字符,并且两个字符上次出现的位置必须相同。

    //同构字符串
    public boolean isIsomorphic(String s, String t) {
        Map<Character,Integer> map1 = new HashMap<>();
        Map<Character,Integer> map2 = new HashMap<>();

        if(s.length() != t.length()) {
            return false;
        }

        for(int i=0; i<s.length(); i++) {
            Character a = s.charAt(i);
            Character b = t.charAt(i);

            if(map1.containsKey(a)) {
                Integer index1 = map1.get(a);
                //S包含而T不包含,说明匹配失败。
                if(!map2.containsKey(b)) {
                    return false;
                }
                Integer index2 = map2.get(b);
                //说明出现位置不同,匹配失败。
                if(!index1.equals(index2)) {
                    return false;
                }
                //匹配成功,则继续向下匹配,并更新map
                map1.put(a,i);
                map2.put(b,i);
            }else {
                map1.put(a,i);
                //如果对应位置S是第一次出现,而T不是第一次出现的字符,说明匹配失败。
                if(map2.containsKey(b)) {
                    return false;
                }else {
                    map2.put(b,i);
                }
            }
        }

        return true;
    }

206.链表反转

使用两个指针,一个指向前一位(初值为null,对应将头结点的next = null),一个指向当前,将当前指向next指向前一位,则反转完毕。

    //链表反转
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        
        while(head != null) {
            ListNode next = head.next;
            
            head.next = prev;
            prev = head;
            head = next;
        }
        
        return prev;
    }

另一种使用傀儡头结点的方式

    //链表反转
    public ListNode reverseList(ListNode head) {
        if(head == null) {
            return null;
        }
        ListNode ans = new ListNode();
        ans.next = head;

        while(head.next != null) {
            ListNode next = head.next;
            head.next = next.next;
            //指向当前头结点
            next.next = ans.next;
            //指向新头结点
            ans.next = next;
        }

        return ans.next;
    }

207.课程表

该题目算是难度比较大的一题,并且有点新颖。

使用的是图的入度出度的概念。

就比如说我们学习高数之前需要学习数学,那么高数 -> 数学,则可以发现高数出度 = 1,数学出度 = 0,出度 = 0代表我们可以直接学习,出度 > 0说明在学习该门课程之前我们还有别得前置课程需要学习。

所以我们使用一个队列,队列的初始化就将出度 = 0 的科目全部入队,然后对这些科目进行学习,学完完毕后,对于这些科目关联的后置科目的出度 - 1(代表学习完毕,前置科目 - 1),当出度减到0时,说明可以学习,则入队。

    //保存一个学习完该科目,可以将多少个科目出度 - 1(出度为0说明可以直接学习)
    private List<List<Integer>> lists = new ArrayList<>();
    //出度表
    private int[] indeed;

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        //初始化
        indeed = new int[numCourses];
        for(int i=0; i<numCourses; i++) lists.add(new ArrayList<>());

        for(int i=0; i<prerequisites.length; i++) {
            //第一位为学习科目,第二位为前置科目(类似于学完科目,然后唤醒后置科目)
            lists.get(prerequisites[i][1]).add(prerequisites[i][0]);
            //出度 + 1
            indeed[prerequisites[i][0]]++;
        }

        Queue<Integer> queue = new LinkedList<>();

        for(int i=0; i<indeed.length; i++) {
            //出度为0,说明可以直接学习
            if(indeed[i] == 0) {
                queue.offer(i);
            }
        }
        int learn = 0;

        while(!queue.isEmpty()) {
            //学习科目 + 1
            learn++;
            //将对应指向该科目的科目出度表 - 1
            Integer poll = queue.poll();
            List<Integer> list = lists.get(poll);
            for(Integer i : list) {
                indeed[i]--;
                if(indeed[i] == 0) {
                    queue.offer(i);
                }
            }
        }

        return learn == numCourses;
    }

208.前缀搜索树

我这里使用的比较费时间的方法,使用的Set集合。

class Trie {
    private Set<String> set = new HashSet<>();
    /** Inserts a word into the trie. */
    public void insert(String word) {
        set.add(word);
    }

    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        return set.contains(word);
    }

    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        for(String s : set) {
            if(s.startsWith(prefix)) {
                return true;
            }
        }
        
        return false;
    }
}

查询速度快一点的方法,则采用树进行存储。

自定义一个TrieNode(前缀匹配结点),我们需要包含基础信息必须包括是否是单词的结尾(用于Search()方法的判断)

class Trie {
    private TrieNode head = new TrieNode();

    public void insert(String word) {
        TrieNode temp = head;
        //将单词存储到字典树中
        for(int i=0; i<word.length(); i++) {
            char a = word.charAt(i);
            temp.set(a);
            temp = temp.get(a);
        }
        //设置单词结尾
        temp.setEnd(true);
    }

    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        TrieNode temp = head;

        for(int i=0; i<word.length(); i++) {
            char a = word.charAt(i);
            if(!temp.containsKey(a)) {
                return false;
            }
            temp = temp.get(a);
        }
        return temp.isEnd();
    }

    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        TrieNode temp = head;
        for(int i=0; i<prefix.length(); i++) {
            char a = prefix.charAt(i);
            if(!temp.containsKey(a)) {
                return false;
            }
            temp = temp.get(a);
        }

        return true;
    }
}

class TrieNode {
    private TrieNode[] trieNodes = new TrieNode[26];
    private boolean isEnd = false;
    public void set(char a) {
        if(trieNodes[a - 'a'] == null) {
            trieNodes[a - 'a'] = new TrieNode();
        }
    }

    public TrieNode get(char a) {
        return trieNodes[a - 'a'];
    }

    public boolean containsKey(char a) {
        return trieNodes[a - 'a'] != null;
    }

    public void setEnd(boolean isEnd) {
        this.isEnd = isEnd;
    }

    public boolean isEnd() {
        return isEnd;
    }
}

209.长度最小的子数组

该题目是使用一个双指针的办法,不过这里的双指针是从同一侧出发。题目要求是得出最短的子数组和 >= s。

我们这里将指针left = 0, right = 0,然后开始的时候就加上右侧的值,如果sum此时 >= s,此时我们的[left,right](闭区间),就是我们刚刚满足的 >= s的状态,那么我们此时应该考虑如果缩短长度,则想到了将左边进行尝试移动,并且有一种情况是我们将左指针右移一位后仍然符合>=s,那么我们可以再次移动,此时再次获取最小长度,以此类推。(左指针有个极限情况,就是移动到了当前右指针的下一位了,此时sum = 0,即需要重新开始计算了)

每次进行外层循环的时候,sum一定是 < s的,可以跟着跑一次程序就明白了。

    //长度连续子数组
    public int minSubArrayLen(int s, int[] nums) {
        int left = 0;
        int right = 0;
        int sum = 0;
        int ans = Integer.MAX_VALUE;

        while(right < nums.length) {
            sum += nums[right];
            //尽可能的缩短长度
            while(sum >= s) {
                ans = Math.min(ans,right - left + 1);
                //尽可能在满足条件的情况下,移动左指针
                sum -= nums[left];
                left++;
            }
            right++;
        }

        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

210.课程表2

和前面的课程表题目一样的,只不过需要记录一下学习的顺序,多一步而已。

public class Solution {
    private List<List<Integer>> lists = new ArrayList<>();
    //出度表
    private int[] indeed;

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        //初始化
        indeed = new int[numCourses];
        for(int i=0; i<numCourses; i++) lists.add(new ArrayList<>());

        for(int i=0; i<prerequisites.length; i++) {
            //第一位为学习科目,第二位为前置科目(类似于学完科目,然后唤醒后置科目)
            lists.get(prerequisites[i][1]).add(prerequisites[i][0]);
            //出度 + 1
            indeed[prerequisites[i][0]]++;
        }

        Queue<Integer> queue = new LinkedList<>();

        for(int i=0; i<indeed.length; i++) {
            //出度为0,说明可以直接学习
            if(indeed[i] == 0) {
                queue.offer(i);
            }
        }
        int learn = 0;
        List<Integer> target = new ArrayList<>();
        while(!queue.isEmpty()) {
            //学习科目 + 1
            learn++;
            //将对应指向该科目的科目出度表 - 1
            Integer poll = queue.poll();
            target.add(poll);
            List<Integer> list = lists.get(poll);
            for(Integer i : list) {
                indeed[i]--;
                if(indeed[i] == 0) {
                    queue.offer(i);
                }
            }
        }

        if(learn == numCourses) {
            int[] ans = new int[target.size()];
            for(int i=0; i<ans.length; i++) {
                ans[i] = target.get(i);
            }
            return ans;
        }else {
            return new int[]{};
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值