198.打家劫舍
题目要求两个相邻的家不能同时被偷(暗示偷窃需要间隔n个家(n >=1))。
看到这种题目,我们很容易联想到使用动态规划的思想进行求解。
我们声明一个dp数组用于保存偷窃到该户时,能够产生的最大价值。
那么这个动态规划的状态转移方程式是什么?
我们通过需要间隔可以发现,最基本有两种偷窃模式:
- 偷窃当前门户,那么获得上上个的最大价值和当前门户的价值。
- 不偷窃当前门户,则我们可以获得上一户的最大价值。
就可以得出 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[]{};
}
}
}