Leetcode热题100道
👏作者简介:大家好,我是 枫度柚子🍁,Java摆烂选手,很高兴认识大家 👀
📕CSDN/掘金/B站: 枫吹过的柚 🍁
🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
🛰微信群: 加微信 **QaQ-linv **
🐧QQ群: 995832569
哈希
两数之和_1
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
class Solution {
public int[] twoSum(int[] nums, int target) {
if (nums == null || nums.length == 0)
return new int[] {};
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])) {
return new int[] { map.get(target - nums[i]), i };
}
map.put(nums[i], i);
}
return new int[] {};
}
}
字母异位词分组_49
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
- 将每串字符串的字符进行排序后作为key,然后放到对应的value集合里
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
if (strs == null || strs.length == 0)
return new ArrayList<>();
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
char[] chs = str.toCharArray();
Arrays.sort(chs);
String key = new String(chs);
// 根据key获取value集合
List<String> values = map.getOrDefault(key, new ArrayList<>());
values.add(str);
map.put(key, values);
}
return new ArrayList<>(map.values());
}
}
最长连续序列_128
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输出:9
- 将所有nums存入numset去重,遍历numset,用过的num存入另外一个usedset,然后持续遍历set
class Solution {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
Set<Integer> numset = new HashSet<>();
for (int num : nums) {
numset.add(num);
}
Set<Integer> usedset = new HashSet<>();
int longest = 0;
// 注意遍历的是numset
for (int num : numset) {
if (usedset.contains(num))
continue;
// 下一个连续的num
int nextnum = num + 1;
int curlongest = 1;
while (numset.contains(nextnum)) {
curlongest++;
nextnum++;
usedset.add(nextnum);
}
longest = Math.max(longest, curlongest);
}
return longest;
}
}
双指针
移动零_283
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
// 去除多余,再补上0
class Solution {
public void moveZeroes(int[] nums) {
if (nums == null || nums.length == 0)
return;
// 返回去除0的索引位置
int index = remmoveZero(nums);
while (index < nums.length) {
nums[index] = 0;
index++;
}
}
int remmoveZero(int[] nums) {
int slow = 0, fast = 0;
while (fast < nums.length) {
if (nums[fast] != 0) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
盛最多水的容器_11
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
**说明:**你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
- area = min(height[l … r]) * min(r-l)
class Solution {
public int maxArea(int[] height) {
if (height == null || height.length == 0)
return 0;
// 采用双指针的方式
// area = Math.min(height[i]) * Math.max(r-l)
int l = 0, r = height.length - 1;
int max = 0;
while (l < r) {
int area = (r - l) * Math.min(height[l], height[r]);
max = Math.max(max, area);
if (height[l] < height[r])
l++;
else
r--;
}
return max;
}
}
三数之和_15
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
- 保证数组升序
- 少遍历一次
- 先确定一个再确认另外两个
- 不可以包含重复的三元组
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
if (nums == null || nums.length == 0)
return new ArrayList<>();
int len = nums.length;
// 先保证升序,后续使用指针
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
// 先确定好一个,再确定另外两个
for (int i = 0; i < len - 1; i++) {
// 答案中不可以包含重复的三元组
if (i > 0 && nums[i - 1] == nums[i])
continue;
int target = -nums[i];
// 采用双指针
int l = i + 1, r = len - 1;
while (l < r) {
int sum = nums[l] + nums[r];
if (sum > target)
r--;
else if (sum < target)
l++;
else {
// 将三个数字放入结果集合
res.add(Arrays.asList(nums[i], nums[l], nums[r]));
l++;
r--;
// 答案中不可以包含重复的三元组
while (l < r && nums[l - 1] == nums[l])
l++;
while (l < r && nums[r] == nums[r + 1])
r--;
}
}
}
return res;
}
}
接雨水_42
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
- water[i] = min( max(左边最高的柱子, max(右边最高的柱子) ) - height[i]
class Solution {
public int trap(int[] height) {
if (height == null || height.length == 0)
return 0;
int len = height.length;
// 左边的高度
int[] lmax = new int[len];
// 右边的高度
int[] rmax = new int[len];
int res = 0;
lmax[0] = height[0];
rmax[len - 1] = height[len - 1];
// 处理 left , left从1开始
for (int i = 1; i < len; i++) {
// 比较前面一个 和 当前的
lmax[i] = Math.max(lmax[i - 1], height[i]);
}
// 处理right ,left从len - 2
for (int i = len - 2; i > 0; i--) {
rmax[i] = Math.max(rmax[i + 1], height[i]);
}
for (int i = 1; i < len - 1; i++) {
res += Math.min(lmax[i], rmax[i]) - height[i];
}
return res;
}
}
滑动窗口
无重复字符的最长子串_3
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
- 用set存储上一次出现的字符
- 右边先删,左边再存
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0)
return 0;
Set<Character> winset = new HashSet<>();
int len = s.length();
int l = 0, r = 0, max = 0;
while (l < len && r < len) {
// 右边的存在了,说明左边的已经有了
if (winset.contains(s.charAt(r))) {
winset.remove(s.charAt(l));
l++;
} else {
winset.add(s.charAt(r));
;
r++;
max = Math.max(r - l, max);
}
}
return max;
}
}
找到字符串中所有字母异位_438
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
- 找出现的起始位置
- 2个26字母的数组,比较两个窗口里的字符出现次数
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
if (s == null || s.length() == 0)
return res;
if (p == null || p.length() == 0)
return res;
int slen = s.length(), plen = p.length();
// p的长度不能大于s的长度
if (plen > slen)
return res;
// 26个字母
int[] parr = new int[26];
int[] sarr = new int[26];
// 创建窗口 同时填充pArr
// 先初始化好第一次的窗口
for (int i = 0; i < plen; i++) {
sarr[s.charAt(i) - 'a']++;
parr[p.charAt(i) - 'a']++;
}
// 每次窗口往前移动一格
// 窗口起始位置和终止位置
int l = 0, r = plen - 1;
// r不能超过sLen
while (r < slen) {
// 比较窗口内的内容, 如果窗口内容相等,就代表是异位词
if (Arrays.equals(parr, sarr)) {
res.add(l);
}
// 终止位置++,新的字符放入sArray数组
r++;
// 在终止位置不等于sLen时,可能会刚好等于sLen
if (r != slen) {
sarr[s.charAt(r) - 'a']++;
}
sarr[s.charAt(l) - 'a']--;
l++;
}
return res;
}
}
子串
和为K的子数组_560
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
- 采用前缀和,k = pre + num
class Solution {
// 前缀和
// k = pre + num
// 看map中key为pre - k的个数即可
public int subarraySum(int[] nums, int k) {
if (nums == null || nums.length == 0)
return 0;
Map<Integer, Integer> presumMap = new HashMap<>();
// 对于下标为 0 的元素,前缀和为 0,个数为 1
presumMap.put(0, 1);
int presum = 0, count = 0;
for (int num : nums) {
presum += num;
// 先获得前缀和为 presum - k 的个数,加到计数变量里
if (presumMap.containsKey(presum - k)) {
count += presumMap.get(presum - k);
}
presumMap.put(presum, presumMap.getOrDefault(presum, 0) + 1);
}
return count;
}
}
滑动窗口最大值_239
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
- n - k + 1
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length == 0) return new int[]{};
int n = nums.length;
// n - k + 1个窗口
int[] res = new int[n - k + 1];
LinkedList<Integer> queue = new LinkedList<>();
for(int i = 0;i < n; i++){
while(!queue.isEmpty() &&
nums[queue.peekLast()] <= nums[i]
){
queue.pollLast();
}
queue.addLast(i);
if(queue.peek() < i - k + 1){
queue.poll();
}
if(i - k + 1 >= 0){
res[i - k + 1] = nums[queue.peek()];
}
}
return res;
}
}
最小覆盖子串_76
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
class Solution {
public String minWindow(String s, String t) {
// 记录t中每个字符出现的次数
Map<Character, Integer> need = new HashMap<>();
// 作为窗口记录每次字符出现的次数
Map<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = Integer.MAX_VALUE;
while (right < s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right);
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
// 窗口中已出现字符的次数与当前需要的出现一致,说明匹配字符ok
if (window.get(c).equals(need.get(c))) {
valid++;
}
}
// 判断左侧窗口是否要收缩,如果已经找到了一个满足条件的子串
while (valid == need.size()) {
int gap = right - left;
// 在这里更新最小覆盖子串
if (gap < len) {
start = left;
len = gap;
}
// d 是将移出窗口的字符
char d = s.charAt(left);
// 左 移窗口 为了下一次前进 去寻找满足条件的子串
left++;
// 进行窗口内数据的一系列更新
if (need.containsKey(d)) {
// 如果之前出现相同的并且将前一次出现次数减少1
if (window.get(d).equals(need.get(d))) {
valid--;
}
window.put(d, window.get(d) - 1);
}
}
}
// 返回最小覆盖子串
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
普通数组
最大子数组和_53
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
- dp[i] = max(dp[i-1] + nums[i], nums[i])
class Solution {
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int len = nums.length;
int[] dp = new int[len + 1];
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < len; i++) {
dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
max = Math.max(dp[i], max);
}
return max;
}
}
合并区间_56
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
- 按照首个元素排序,再通过尾插法放到链表,
- 比较 当前元素cur[0] <= 链表尾元素last[1] => last[1] = max(cur[1],last[1])
class Solution {
public int[][] merge(int[][] intervals) {
if(intervals == null || intervals.length == 0) return new int[][]{};
int len = intervals.length;
Arrays.sort(intervals,Comparator.comparingInt(x->x[0]));
LinkedList<int[]> res = new LinkedList<>();
res.addLast(intervals[0]);
for(int i = 1; i < len; i++){
int[] cur = intervals[i];
int[] last = res.getLast();
if(cur[0] <= last[1]){
last[1] = Math.max(last[1],cur[1]);
}else{
res.addLast(cur);
}
}
return res.toArray(new int[0][0]);
}
}
轮转数组_189
给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
- 开辟新数组,通过 newArr[(i + k) % len] = nums[i]
class Solution {
public void rotate(int[] nums, int k) {
if (nums == null || nums.length == 0)
return;
int len = nums.length;
int[] newArr = new int[len];
for (int i = 0; i < len; i++) {
newArr[(i + k) % len] = nums[i];
}
System.arraycopy(newArr, 0, nums, 0, len);
}
}
除自身以外数组的乘积_238
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 **不要使用除法,**且在 O(n)
时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
- 正序相乘元素,再倒序相乘
class Solution {
public int[] productExceptSelf(int[] nums) {
if (nums == null || nums.length == 0)
return new int[] {};
int pre = 1, suf = 1;
int len = nums.length;
int[] res = new int[len];
for (int i = 0; i < len; i++) {
res[i] = pre;
pre *= nums[i];
}
for (int i = len - 1; i >= 0; i--) {
res[i] *= suf;
suf *= nums[i];
}
return res;
}
}
缺失的第一个正数_41
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
class Solution {
// 遍历一遍将大于0的数放进一个比原数组大2的新数组中。在遍历新数组,找出第一个是0的下标
// 遍历原数组,将大于零且小于新数组长度的值作为下标存储值为1。然后从1开始遍历新数组,第一个为0的下标即为最小正数
public int firstMissingPositive(int[] nums) {
int[] res = new int[nums.length + 2];
// 遍历原数组,将大于零且小于新数组长度的值作为下标存储值为1
for (int num : nums) {
if (num < res.length && num > 0) {
res[num] = 1;
}
}
// 从1开始遍历新数组,第一个为0的下标即为最小正数
for (int i = 1; i < res.length; i++) {
if (res[i] == 0) {
return i;
}
}
return 1;
}
}
矩阵
矩阵置零_73
给定一个 *m* x *n*
的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。**
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
- 通过两个set记录下横、纵里面为0的下标,在两次循环遍历进行判断在不在,如果在就置为0
- 两遍扫matrix,第一遍用集合记录哪些行,哪些列有0;第二遍置0
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
Set<Integer> rows = new HashSet<>();
Set<Integer> cols = new HashSet<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
rows.add(i);
cols.add(j);
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (rows.contains(i) || cols.contains(j)) {
matrix[i][j] = 0;
}
}
}
}
}
螺旋矩阵_54
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
if (matrix == null || matrix.length == 0)
return new ArrayList<>();
int m = matrix.length;
int n = matrix[0].length;
Integer[] res = new Integer[m * n];
// 左、右、上、下、res的index
int l = 0, r = n - 1, t = 0, b = m - 1, idx = 0;
// 从左->右,从上->下,从右->左,从下->上
while (true) {
// 顶部 从左->右 顶部不变
for (int i = l; i <= r; i++) {
res[idx] = matrix[t][i];
idx++;
}
// 下一次顶部超过底部就结束
if (++t > b)
break;
// 右侧 从上->下 右侧不变
for (int i = t; i <= b; i++) {
res[idx] = matrix[i][r];
idx++;
}
// 右边往里走时不能小于左侧
if (--r < l)
break;
// 底部 从右->左 底部不变
for (int i = r; i >= l; i--) {
res[idx] = matrix[b][i];
idx++;
}
if (--b < t)
break;
// 左侧 从下->上 左侧不变
for (int i = b; i >= t; i--) {
res[idx] = matrix[i][l];
idx++;
}
if (++l > r)
break;
}
return Arrays.asList(res);
}
}
旋转图像_48
给定一个 n × n 的二维矩阵 matrix
表示一个图像。请你将图像顺时针旋转 90 度。
你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
- row,col -> col,n-row-1
class Solution {
public void rotate(int[][] matrix) {
if (matrix == null || matrix.length == 0)
return;
int m = matrix.length;
int n = matrix[0].length;
int[][] new_matrix = new int[m][n];
// row,col => col,len - row - 1
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
new_matrix[j][n - i - 1] = matrix[i][j];
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
matrix[i][j] = new_matrix[i][j];
}
}
}
}
搜索二维矩阵2_240
编写一个高效的算法来搜索 *m* x *n*
矩阵 matrix
中的一个目标值 target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false
- 从最下往上找
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0)
return false;
int m = matrix.length;
int n = matrix[0].length;
// 从左下角开始
int i = m - 1, j = 0;
while (i >= 0 && j < n) {
// 目标值 > 当前值
if (target > matrix[i][j])
j++;
else if (target < matrix[i][j])
i--;
else {
return true;
}
}
return false;
}
}
链表
相交链表_160
图示两个链表在节点 c1
开始相交**:**
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为0
listA
- 第一个链表listB
- 第二个链表skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA
和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例 2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
- p_a = p_a == null ? headB : p_a.next;
- p_b = p_b == null ? headA : p_b.next;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null)
return null;
ListNode p_a = headA, p_b = headB;
while (p_a != p_b) {
p_a = p_a == null ? headB : p_a.next;
p_b = p_b == null ? headA : p_b.next;
}
return p_a;
}
}
反转链表_206
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
/**
* 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 reverseList(ListNode head) {
if(head == null) return null;
// 当前节点cur,前一个节点
ListNode cur = head,pre = null;
while(cur != null){
// 暂存后继节点 cur.next
ListNode next = cur.next;
// 修改 next 引用指向
cur.next = pre;
// pre 暂存 cur
pre = cur;
// cur 访问下一节点
cur = next;
}
return pre;
}
}
回文链表_234
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
- 先找中间节点、将链表分成两部分、然后右边的部分反转,再进行两块比较
/**
* 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 boolean isPalindrome(ListNode head) {
if (head == null)
return false;
// 先找中间节点
ListNode slow = middleNode(head);
ListNode l_node = head;
// 翻转链表
ListNode r_node = reverse(slow);
// 判断右边是否为null
while (r_node != null) {
if (l_node.val != r_node.val)
return false;
l_node = l_node.next;
r_node = r_node.next;
}
return true;
}
ListNode middleNode(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
ListNode reverse(ListNode head) {
ListNode cur = head, pre = null;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
环形链表_141
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
- 快慢指针
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null)
return false;
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow)
return true;
}
return false;
}
}
环形链表II_142
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
- slow 走 k,fast 走 2k,要找到环的位置,只要先找到相遇的时候停下,让fast从头走slow剩下的k
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null)
return null;
// 快慢指针
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow)
break;
}
// 表示slow到了终点
if (fast == null || fast.next == null)
return null;
// 将fast指向head
fast = head;
// 直到fast走到slow的位置就找到环的位置
while (fast != slow) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
合并两个有序链表_21
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
- 用l1 l2分别指向list1 list2,创建dummy节点,p指向
/**
* 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 mergeTwoLists(ListNode list1, ListNode list2) {
ListNode l1 = list1, l2 = list2, dummy = new ListNode(-1), p = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
p.next = l1;
l1 = l1.next;
} else {
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
if (l1 != null)
p.next = l1;
if (l2 != null)
p.next = l2;
return dummy.next;
}
}
两数相加_2
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
- 记得暂存进位carry,sum = n1 + n2 + carry
- 大位 sum % 10
- 进位 sum / 10
/**
* 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 addTwoNumbers(ListNode l1, ListNode l2) {
if (l1 == null)
return l2;
if (l2 == null)
return l1;
ListNode dummy = new ListNode(-1), p = dummy;
// 进位
int carry = 0;
while (l1 != null || l2 != null) {
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 != null ? l2.val : 0;
int sum = n1 + n2 + carry;
p.next = new ListNode(sum % 10);
// 进位暂存
carry = sum / 10;
if (l1 != null)
l1 = l1.next;
if (l2 != null)
l2 = l2.next;
p = p.next;
}
if (carry > 0)
p.next = new ListNode(carry);
return dummy.next;
}
}
删除链表的倒数第N个结点_19
相关标签
相关企业
提示
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
- 要删除倒数第N个节点,就得获取倒数第N+1个节点的引用
- 获取单链表的倒数第k个节点,利用双指针 只需要遍历一次,就算出导出第k个节点
/**
* 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) {
if (head == null)
return null;
ListNode dummy = new ListNode(-1);
// slow在fast前面一个节点
ListNode slow = dummy, fast = head;
slow.next = head;
// fast先走前k
for (int i = 0; i < n; i++) {
fast = fast.next;
}
// 此时fast走剩余的路程,也就是slow走到了倒数第n个
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 删除倒数第K个节点
if (slow.next != null) {
slow.next = slow.next.next;
}
return dummy.next;
}
}
两两交换链表中的节点_24
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
- 先把前两个元素翻转,再翻转后面的,再返回新头
/**
* 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 swapPairs(ListNode head) {
// 终止条件
if (head == null || head.next == null)
return head;
ListNode first = head;
ListNode second = head.next;
ListNode others = head.next.next;
// 先把前两个元素翻转
second.next = first;
// 利用递归定义,将剩下的链表节点两两翻转,接到后面
first.next = swapPairs(others);
// 现在整个链表都成功翻转了,返回新的头结点
return second;
}
}
K个一组翻转链表_25
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
/**
* 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 reverseKGroup(ListNode head, int k) {
if (head == null) return null;
// 区间 [a, b) 包含 k 个待反转元素
ListNode a = head, b = head;
// 不足 k 个,不需要反转,base case
for (int i = 0; i < k; i++) {
if (b == null) return head;
b = b.next;
}
// 反转前 k 个元素
ListNode newHead = reverse(a, b);
// 从第k个b开始继续反复执行
a.next = reverseKGroup(b, k);
return newHead;
}
// 反转区间 [a, b) 的元素,注意是左闭右开
public ListNode reverse(ListNode a, ListNode b) {
ListNode pre = null;
ListNode cur = a;
// while 终止的条件改一下就行了
while (cur != b) {
// 暂存后继节点 先接一下当前节点的下一个节点
ListNode tmp = cur.next;
// 引用指向更换 当前的下一个节点指向pre
cur.next = pre;
// 暂存当前节点 pre指针右移
pre = cur;
// 访问下一个节点 curr指针右移
cur = tmp;
}
// 返回反转后的头结点
return pre;
}
}
随机链表的复制_138
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
用一个由 n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。
你的代码 只 接受原链表的头节点 head
作为传入参数。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
- 第一次遍历专门克隆节点,借助哈希表把原始节点和克隆节点的映射存储起来;
- 第二次专门组装节点,照着原数据结构的样子,把克隆节点的指针组装起来
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
// “原节点 -> 新节点” 的 Map 映射
Map<Node, Node> map = new HashMap<>();
Node p = head;
while(p != null){
if(!map.containsKey(p)){
map.put(p,new Node(p.val));
p = p.next;
}
}
// 第二次专门组装节点,照着原数据结构的样子,把克隆节点的指针组装起来
p = head;
while(p != null){
// (p).next = p.next
map.get(p).next = map.get(p.next);
// (p).random = p.random
map.get(p).random = map.get(p.random);
p = p.next;
}
// 返回新链表的头节点
return map.get(head);
}
}
排序链表_148
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
/**
* 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 sortList(ListNode head) {
if (head == null || head.next == null)
return head;
// 中点
ListNode slow = middleNode(head);
// 分割成两块
ListNode new_head = slow.next;
slow.next = null;
// 递归排序
ListNode l_node = sortList(head);
ListNode r_node = sortList(new_head);
// 新节点串起来
ListNode dummy = new ListNode(-1);
ListNode res = dummy;
while (l_node != null && r_node != null) {
if (l_node.val < r_node.val) {
dummy.next = l_node;
l_node = l_node.next;
} else {
dummy.next = r_node;
r_node = r_node.next;
}
dummy = dummy.next;
}
// 剩下的可能为lnode 或 rnode
dummy.next = l_node != null ? l_node : r_node;
return res.next;
}
ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
合并K个升序链表_23
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
/**
* 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 mergeKLists(ListNode[] lists) {
// 判断链表是否为Null
if (lists == null)
return null;
// 优先级队列 升序处理
Queue<ListNode> queue = new PriorityQueue<>(Comparator.comparingInt(x -> x.val));
// 把node全部添加到最小堆里去了
for (ListNode head : lists) {
while (head != null) {
// 入队
queue.offer(head);
head = head.next;
}
}
ListNode dummy = new ListNode();
ListNode head = dummy;
// 队列不为空,就出队
while (!queue.isEmpty()) {
// 把最小的元素先拿到,然后移除
ListNode minNode = queue.poll();
head.next = minNode;
head = head.next;
if (queue.isEmpty()) {
head.next = null;
}
}
return dummy.next;
}
}
LRU缓存_146
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
class LRUCache {
LinkedHashMap<Integer, Integer> cache;
int cap;
public LRUCache(int capacity) {
this.cache = new LinkedHashMap<>(capacity + 1, 0.75f, true);
this.cap = capacity;
}
public int get(int key) {
if (!cache.containsKey(key))
return -1;
return cache.get(key);
}
public void put(int key, int value) {
cache.put(key, value);
if (cache.size() > cap) {
// 链表头部就是最久未使用的 key
cache.remove(cache.keySet().iterator().next());
}
}
}
/**
* 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);
*/
二叉树
二叉树的中序遍历_94
给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if (root == null)
return res;
traverse(root);
return res;
}
// 左根右
void traverse(TreeNode root) {
if (root.left != null) {
traverse(root.left);
}
res.add(root.val);
if (root.right != null) {
traverse(root.right);
}
}
}
二叉树的最大深度_104
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
- max(l,r) + 1
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// max(l,r) + 1
public int maxDepth(TreeNode root) {
if (root == null)
return 0;
int l_max = maxDepth(root.left);
int r_max = maxDepth(root.right);
return Math.max(l_max, r_max) + 1;
}
}
翻转二叉树_226
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
- 左右交换,null终止
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null)
return null;
TreeNode l = root.left;
root.left = root.right;
root.right = l;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
对称二叉树_101
给你一个二叉树的根节点 root
, 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 判断是否对称
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
// 检查两棵子树是否对称
return check(root.left, root.right);
}
private boolean check(TreeNode left, TreeNode right) {
if (left == null && right == null) return true;
if (left == null || right == null) return false;
// 两个根节点需要相同
if (left.val != right.val) return false;
// 左右子节点需要对称相同
return check(left.left, right.right) &&
check(left.right, right.left);
}
}
二叉树的直径_543
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2]
输出:1
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
Integer max = Integer.MIN_VALUE;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return max;
}
int depth(TreeNode root) {
if (root == null)
return 0;
int l_max = depth(root.left);
int r_max = depth(root.right);
max = Math.max(max, l_max + r_max);
return Math.max(l_max, r_max) + 1;
}
}
二叉树的层序遍历_102
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
- 即逐层地,从左到右访问所有节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 即逐层地,从左到右访问所有节点
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null)
return new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
List<List<Integer>> res = new ArrayList<>();
while (!queue.isEmpty()) {
// 这一层的元素
int size = queue.size();
List<Integer> nodes = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node != null) {
nodes.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
res.add(nodes);
}
return res;
}
}
将有序数组转换为二叉搜索树_108
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
// 数组,左,右
return conversion(nums, 0, nums.length - 1);
}
TreeNode conversion(int[] nums, int left, int right) {
if (left > right)
return null;
int mid = (left + right) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = conversion(nums, left, mid - 1);
root.right = conversion(nums, mid + 1, right);
return root;
}
}
验证二叉搜索树_98
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
- root.val 要比左子树的所有节点都更大,要比右子树的所有节点都小
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root) {
return valid(root, null, null);
}
// left=>min right=>max
boolean valid(TreeNode root, TreeNode min, TreeNode max) {
if (root == null)
return true;
if (min != null && min.val >= root.val || max != null && max.val <= root.val)
return false;
return valid(root.left, min, root) && valid(root.right, root, max);
}
}
二叉搜索树中第K小的元素_230
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
个最小元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
- BST 的中序遍历结果是有序的(升序),所以用一个外部变量记录中序遍历结果第 k 个元素即是第 k 小的元素。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int res = 0;
int rank = 0;
public int kthSmallest(TreeNode root, int k) {
traverse(root, k);
return res;
}
void traverse(TreeNode root, int k) {
if (root == null)
return;
// 左
traverse(root.left, k);
rank++;
// 找到第k个
if (k == rank) {
res = root.val;
return;
}
traverse(root.right, k);
}
}
二叉树的右视图_199
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例 1:
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
示例 2:
输入: [1,null,3]
输出: [1,3]
示例 3:
输入: []
输出: []
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Map<Integer, Integer> rightmostValueAtDepth = new HashMap<Integer, Integer>();
int max_depth = -1;
Deque<TreeNode> nodeStack = new LinkedList<TreeNode>();
Deque<Integer> depthStack = new LinkedList<Integer>();
nodeStack.push(root);
depthStack.push(0);
while (!nodeStack.isEmpty()) {
TreeNode node = nodeStack.pop();
int depth = depthStack.pop();
if (node != null) {
// 维护二叉树的最大深度
max_depth = Math.max(max_depth, depth);
// 如果不存在对应深度的节点我们才插入
if (!rightmostValueAtDepth.containsKey(depth)) {
rightmostValueAtDepth.put(depth, node.val);
}
nodeStack.push(node.left);
nodeStack.push(node.right);
depthStack.push(depth + 1);
depthStack.push(depth + 1);
}
}
List<Integer> rightView = new ArrayList<Integer>();
for (int depth = 0; depth <= max_depth; depth++) {
rightView.add(rightmostValueAtDepth.get(depth));
}
return rightView;
}
}
二叉树展开为链表_114
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 右子树放在左子树后,最后将右子树放在左子树
public void flatten(TreeNode root) {
if (root == null)
return;
flatten(root.left);
flatten(root.right);
TreeNode left = root.left;
TreeNode right = root.right;
root.left = null;
root.right = left;
// 将 root 的右子树接到左子树下方,然后将整个左子树作为右子树
TreeNode p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
}
}
从前序与中序遍历序列构造二叉树_105
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 前序: 根左右
// 中序: 左根右
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null ||
inorder == null ||
preorder.length == 0 ||
inorder.length == 0) return null;
// 前序遍历的长度
int preLen = preorder.length;
// 中序遍历的长度
int inLen = inorder.length;
if (preLen != inLen) return null;
// 谦虚遍历
TreeNode root = new TreeNode(preorder[0]);
for (int i = 0; i < preLen; i++) {
// 找到根节点
if (inorder[i] == root.val) {
// 1 = 左 i = 根 i+1=右
// 【0,i)是左
root.left = buildTree(Arrays.copyOfRange(preorder, 1, i + 1),
Arrays.copyOfRange(inorder, 0, i));
// (i,preLen)是右
root.right = buildTree(Arrays.copyOfRange(preorder, i + 1, preLen),
Arrays.copyOfRange(inorder, i + 1, preLen));
}
}
return root;
}
}
路径总和3_437
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
[外链图片转存中…(img-E49P0CV1-1710179156318)]
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int pathSum(TreeNode root, int sum) {
// key是前缀和, value是大小为key的前缀和出现的次数
Map<Long, Integer> prefixSumCount = new HashMap<>();
// 前缀和为0的一条路径
prefixSumCount.put(0L, 1);
// 前缀和的递归回溯思路
return backtrack(root, prefixSumCount, sum, 0L);
}
/**
* 前缀和的递归回溯思路
* 从当前节点反推到根节点(反推比较好理解,正向其实也只有一条),有且仅有一条路径,因为这是一棵树
* 如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
* 所以前缀和对于当前路径来说是唯一的,当前记录的前缀和,在回溯结束,回到本层时去除,保证其不影响其他分支的结果
*
* @param node 树节点
* @param prefixSumCount 前缀和Map
* @param target 目标值
* @param currSum 当前路径和
*
* @return 满足题意的解
*/
int backtrack(TreeNode node, Map<Long, Integer> prefixSumCount, int target, long currSum) {
// 1.递归终止条件
if (node == null) {
return 0;
}
// 2.本层要做的事情
int res = 0;
// 当前路径上的和
currSum += node.val;
// ---核心代码
// 看看root到当前节点这条路上是否存在节点前缀和加target为currSum的路径
// 当前节点->root节点反推,有且仅有一条路径,如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
// currSum-target相当于找路径的起点,起点的sum+target=currSum,当前点到起点的距离就是target
res += prefixSumCount.getOrDefault(currSum - target, 0);
// 更新路径上当前节点前缀和的个数
prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);
// ---核心代码
// 3.进入下一层
res += backtrack(node.left, prefixSumCount, target, currSum);
res += backtrack(node.right, prefixSumCount, target, currSum);
// 4.回到本层,恢复状态,去除当前节点的前缀和数量
prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);
return res;
}
}
二叉树的最近公共祖先_236
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
// 两个节点p、q,分为两种情况
// p和q在相同子树中
// p和q在不同子树中
// 从根节点遍历,递归向左右子树查询节点信息
// 递归终止条件:如果当前节点为空或等于 p 或 q,则返回当前节点
// 递归遍历左右子树,如果左右子树查到节点都不为空,则表明 p 和 q 分别在左右子树中,因此,当前节点即为最近公共祖先
// 如果左右子树其中一个不为空,则返回非空节点。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode lNode = lowestCommonAncestor(root.left, p, q);
TreeNode rNode = lowestCommonAncestor(root.right, p, q);
if (lNode != null && rNode != null) return root;
return lNode != null ? lNode : rNode;
}
}
二叉树中的最大路径和_124
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int max = Integer.MIN_VALUE;
// 二叉树 abc,a 是根结点(递归中的 root),
// bc 是左右子结点(代表其递归后的最优解)
// 返回经过root的单边分支最大和, 即Math.max(root, root+left, root+right)
public int maxPathSum(TreeNode root) {
if (root == null) return 0;
dfs(root);
return max;
}
private int dfs(TreeNode root) {
if (root == null) return 0;
// 计算左边分支最大值,左边分支如果为负数还不如不选择
int lMax = Math.max(0, dfs(root.left));
//计算右边分支最大值,右边分支如果为负数还不如不选择
int rMax = Math.max(0, dfs(root.right));
// left->root->right 作为路径与已经计算过历史最大值做比较
max = Math.max(max, root.val + lMax + rMax);
// 返回经过root的单边最大分支给当前root的父节点计算使用
return root.val + Math.max(lMax, rMax);
}
}
图论
岛屿数量_200
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
class Solution {
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0) return 0;
int m = grid.length;
int n = grid[0].length;
int res = 0;
for(int i = 0;i < m;i++){
for(int j = 0;j < n;j++){
if(grid[i][j] == '1'){
res += 1;
dfs(grid,i,j,m,n);
}
}
}
return res;
}
void dfs(char[][] grid,int x,int y,int m,int n){
// 边界问题
if(x < 0 || y < 0 || x >= m || y >= n || grid[x][y] == '0') return;
// 将之前的淹没
grid[x][y] = '0';
dfs(grid,x-1,y,m,n);
dfs(grid,x+1,y,m,n);
dfs(grid,x,y-1,m,n);
dfs(grid,x,y+1,m,n);
}
}
腐烂的橘子_994
在给定的 m x n
网格 grid
中,每个单元格可以有以下三个值之一:
- 值
0
代表空单元格; - 值
1
代表新鲜橘子; - 值
2
代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1
。
示例 1:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:grid = [[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。
示例 3:
输入:grid = [[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
class Solution {
public int orangesRotting(int[][] grid) {
int M = grid.length;
int N = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int count = 0; // count 表示新鲜橘子的数量
for (int r = 0; r < M; r++) {
for (int c = 0; c < N; c++) {
if (grid[r][c] == 1) {
count++;
} else if (grid[r][c] == 2) {
queue.add(new int[] { r, c });
}
}
}
int round = 0; // round 表示腐烂的轮数,或者分钟数
while (count > 0 && !queue.isEmpty()) {
round++;
int n = queue.size();
for (int i = 0; i < n; i++) {
int[] orange = queue.poll();
int r = orange[0];
int c = orange[1];
if (r - 1 >= 0 && grid[r - 1][c] == 1) {
grid[r - 1][c] = 2;
count--;
queue.add(new int[] { r - 1, c });
}
if (r + 1 < M && grid[r + 1][c] == 1) {
grid[r + 1][c] = 2;
count--;
queue.add(new int[] { r + 1, c });
}
if (c - 1 >= 0 && grid[r][c - 1] == 1) {
grid[r][c - 1] = 2;
count--;
queue.add(new int[] { r, c - 1 });
}
if (c + 1 < N && grid[r][c + 1] == 1) {
grid[r][c + 1] = 2;
count--;
queue.add(new int[] { r, c + 1 });
}
}
}
if (count > 0) {
return -1;
} else {
return round;
}
}
}
课程表_207
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
class Solution {
// 拓扑排序
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 用来进行记录前者和后者的关系,后者指向前者所以记录前者的入度
List<List<Integer>> edges = new ArrayList<>();
// 用来记录上家的入度
int[] inorder = new int[numCourses];
for(int i = 0; i < numCourses;i++){
edges.add(new ArrayList<>());
}
for(int[] edge: prerequisites){
edges.get(edge[1]).add(edge[0]);
inorder[edge[0]]++;
}
Queue<Integer> queue = new LinkedList<>();
// 将入度为0的加入到queue中
for(int i = 0; i < numCourses;i++){
if(inorder[i] == 0){
queue.add(i);
}
}
int finish = 0;
while(!queue.isEmpty()){
Integer poll = queue.poll();
List<Integer> edge = edges.get(poll);
for(Integer next:edge){
// 入度--
inorder[next]--;
if(inorder[next] == 0){
queue.add(next);
}
}
finish++;
}
return finish == numCourses;
}
}
实现Trie前缀树_208
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
class Trie {
// 指向子节点的指针数组children,数组长度为26,即小写英文字母的数量,此时children[0]对应的是a...children[25]对应的是z
private Trie[] children;
// 表示该节点是否为字符串的结尾
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
// index位置无值,存放
if (node.children[index] == null) {
node.children[index] = new Trie();
}
// 下一个
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
Trie searchPrefix(String prefix) {
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
// 子节点不存在。说明字典树中不包含该前缀,返回空指针
if (node.children[index] == null) {
return null;
}
// 子节点存在。沿着指针移动到子节点,继续搜索下一个字符
node = node.children[index];
}
return node;
}
}
回溯
全排列_46
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
class Solution {
// 结果
List<List<Integer>> pathList = new LinkedList<>();
boolean[] used;
List<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
if (len == 0)
return pathList;
used = new boolean[len];
backTrace(nums);
return pathList;
}
// 路径:记录在 path 中
// 选择列表:nums 中不存在于 path 的那些元素(used[i] 为 false)
// 结束条件:nums 中的元素全都在 path 中出现
private void backTrace(int[] nums) {
if (path.size() == nums.length) {
pathList.add(new LinkedList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i])
continue;
path.add(nums[i]);
used[i] = true;
// 进入下一次
backTrace(nums);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
子集_78
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
class Solution {
List<List<Integer>> pathList = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0, nums.length);
return pathList;
}
private void backtrack(int[] nums, int l, int r) {
// 添加路径数组到结果数组中
pathList.add(new LinkedList<>(path));
while (l < r) {
// 做选择,将选择添加到路径数组中
path.add(nums[l]);
// 回溯法
backtrack(nums, l + 1, r);
// 撤销选择,将选择从路径中删除
path.remove(path.size() - 1);
l++;
}
}
}
电话号码的字母组合_17
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
class Solution {
/**
* 初始化对应所有的数字,为了直接对应2-9,新增了两个无效的字符串
*/
private String[] lettersOnDigit = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
/**
* 结果
*/
private List<String> pathList = new LinkedList<>();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return pathList;
}
// 深度优先遍历
dfs(new StringBuilder(), digits);
return pathList;
}
private void dfs(StringBuilder path, String digits) {
// 判断是否走到了叶子节点->判断path的长度是否和digits的长度一样
// 代表所有情况都达到了
if (path.length() == digits.length()) {
pathList.add(path.toString());
return;
}
// 当前数字
int curDigit = digits.charAt(path.length()) - '0';
// 当前数字 对应的字符串 -> lettersOnDigit[curDigit]
String curLetters = lettersOnDigit[curDigit];
// 遍历字符串
for (char c : curLetters.toCharArray()) {
// 加入到当前path
path.append(c);
// 进行下一次
dfs(path, digits);
// 当从递归中走出来时,删除末尾的继续尝试
path.deleteCharAt(path.length() - 1);
}
}
}
组合总和_39
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
class Solution {
// 记录集
List<List<Integer>> pathList = new ArrayList<>();
// 记录路径
List<Integer> path = new ArrayList<>();
// 记录sum
int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
dfs(candidates, target, 0, len - 1);
return pathList;
}
private void dfs(int[] candidates, int target, int start, int end) {
// sum > target
if (sum > target) {
return;
}
if (sum == target) {
pathList.add(new ArrayList<>(path));
return;
}
while (start <= end) {
int candidate = candidates[start];
path.add(candidate);
sum += candidate;
// 为什么从start开始,而不是start + 1 因为还可以可以继续取用之前的值,可以重复使用
dfs(candidates, target, start, end);
// 减去上一个
sum -= candidate;
// 移除之前的
path.remove(path.size() - 1);
start++;
}
}
}
括号生成_22
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
class Solution {
// 记录结果
private List<String> pathList = new LinkedList<>();
// 括号对数
private int maxCount = 0;
public List<String> generateParenthesis(int n) {
if (n == 0) return pathList;
// 设置括号对数
maxCount = n;
// 回溯法
backtrack("", 0, 0);
return pathList;
}
// lCount: 左括号数量 rCount: 右括号数量
private void backtrack(String path, int lCount, int rCount) {
// 判断括号对数是否达到
if (path.length() == maxCount * 2) {
pathList.add(path);
return;
}
// 如果左括号没有达到
if (lCount < maxCount) {
backtrack(path + "(", lCount + 1, rCount);
}
// 如果右括号的数量小于左括号数量
if (rCount < lCount) {
backtrack(path + ")", lCount, rCount + 1);
}
}
}
单词搜索_79
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false
class Solution {
int m, n, wl;
char[] letters;
char[][] board;
boolean[][] visited;
// 回溯法
public boolean exist(char[][] board, String word) {
// 宽
this.m = board.length;
// 长
this.n = board[0].length;
// 单词长度
this.wl = word.length();
this.letters = word.toCharArray();
this.board = board;
this.visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 0 是word的index
boolean res = backtrack(i, j, 0);
if (res) return true;
}
}
return false;
}
private boolean backtrack(int i, int j, int wi) {
// word的index >= w 说明找到了
if (wi >= wl) {
return true;
}
// 说明没找到|或者已经访问过了
if (i < 0 || j < 0 || i >= m || j >= n || letters[wi] != board[i][j] || visited[i][j]) return false;
// 访问过了就不能再去看了
visited[i][j] = true;
// 单词前进
wi++;
boolean res =
backtrack(i + 1, j, wi) ||
backtrack(i, j + 1, wi ) ||
backtrack(i - 1, j, wi) ||
backtrack(i, j - 1, wi);
visited[i][j] = false;
return res;
}
}
分割回文串_131
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
class Solution {
boolean[][] f;
List<List<String>> ret = new ArrayList<List<String>>();
List<String> ans = new ArrayList<String>();
int n;
public List<List<String>> partition(String s) {
n = s.length();
f = new boolean[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(f[i], true);
}
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
dfs(s, 0);
return ret;
}
public void dfs(String s, int i) {
if (i == n) {
ret.add(new ArrayList<String>(ans));
return;
}
for (int j = i; j < n; ++j) {
if (f[i][j]) {
ans.add(s.substring(i, j + 1));
dfs(s, j + 1);
ans.remove(ans.size() - 1);
}
}
}
}
N皇后_61
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> solutions = new ArrayList<List<String>>();
int[] queens = new int[n];
Arrays.fill(queens, -1);
Set<Integer> columns = new HashSet<Integer>();
Set<Integer> diagonals1 = new HashSet<Integer>();
Set<Integer> diagonals2 = new HashSet<Integer>();
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
return solutions;
}
public void backtrack(List<List<String>> solutions, int[] queens, int n, int row, Set<Integer> columns, Set<Integer> diagonals1, Set<Integer> diagonals2) {
if (row == n) {
List<String> board = generateBoard(queens, n);
solutions.add(board);
} else {
for (int i = 0; i < n; i++) {
if (columns.contains(i)) {
continue;
}
int diagonal1 = row - i;
if (diagonals1.contains(diagonal1)) {
continue;
}
int diagonal2 = row + i;
if (diagonals2.contains(diagonal2)) {
continue;
}
queens[row] = i;
columns.add(i);
diagonals1.add(diagonal1);
diagonals2.add(diagonal2);
backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
queens[row] = -1;
columns.remove(i);
diagonals1.remove(diagonal1);
diagonals2.remove(diagonal2);
}
}
}
public List<String> generateBoard(int[] queens, int n) {
List<String> board = new ArrayList<String>();
for (int i = 0; i < n; i++) {
char[] row = new char[n];
Arrays.fill(row, '.');
row[queens[i]] = 'Q';
board.add(new String(row));
}
return board;
}
}
二分查找
- int[][] matrix = {{1, 3, 5, 7}, {10, 11, 16, 20}, {23, 30, 34, 60}}
- int m = matrix.length; 行数
- int n = matrix[0].length; 列数
搜索插入位置_35
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
class Solution {
public int searchInsert(int[] nums, int target) {
if (nums == null || nums.length == 0)
return -1;
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if(target > nums[mid]) l = mid + 1;
else r = mid - 1;
}
// 没找到就插入 程序退出条件应该是l==n了,所以返回l和n
return l;
}
}
搜索二维矩阵_74
给你一个满足下述两条属性的 m x n
整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target
,如果 target
在矩阵中,返回 true
;否则,返回 false
。
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
// m为行数、n为列数
int m = matrix.length,n = matrix[0].length;
// 把二维数组映射到一维
int l = 0,r = m * n - 1;
while(l <= r){
int mid = l + (r - l) / 2;
if(target == get(matrix,mid)) return true;
else if(target < get(matrix,mid)) r = mid - 1;
else if(target > get(matrix,mid)) l = mid + 1;
}
return false;
}
// 通过一维坐标访问二维数组的坐标的元素
int get(int[][] matrix,int index){
int n = matrix[0].length;
int i = index / n, j = index % n;
return matrix[i][j];
}
}
在排序数组中查找元素的第一个和最后一个位置_34
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
- 分为两个二分查找对应前、后下标,但是需要注意first前面的值可能等于mid 或 second后面的值等于mid
class Solution {
public int[] searchRange(int[] nums, int target) {
if (nums == null || nums.length == 0)
return new int[] { -1, -1 };
// first => 0 -> len - 1 ,没找到返回-1
int first = findFirst(nums, target, 0, nums.length - 1);
// second => first -> len - 1,没找到返回-1
int second = findSecond(nums, target, Math.max(first, 0), nums.length - 1);
return new int[] { first, second };
}
int findFirst(int[] nums, int target, int l, int r) {
while (l <= r) {
int mid = l + (r - l) / 2;
if (target == nums[mid]) {
// 说明mid之前就出现了
if (mid - 1 >= 0 && nums[mid - 1] == nums[mid]) {
r = mid - 1;
} else {
return mid;
}
} else if (target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return -1;
}
int findSecond(int[] nums, int target, int l, int r) {
while (l <= r) {
int mid = l + (r - l) / 2;
if (target == nums[mid]) {
// 说明mid之前就出现了
if (mid + 1 <= r && nums[mid + 1] == nums[mid]) {
l = mid + 1;
} else {
return mid;
}
} else if (target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return -1;
}
}
搜索旋转排序数组_33
整数数组 nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
- 数组经过旋转会保证两块有序,分别进行处理
class Solution {
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0)
return -1;
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (target == nums[mid])
return mid;
// 如果左边有序
else if (nums[mid] >= nums[l]) { // 注意
if (target >= nums[l] && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
}
// 如果右边有序
else {
if (target <= nums[r] && target > nums[mid]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
}
}
寻找旋转排序数组中的最小值_153
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 3 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
- 由于数组不包含重复元素,并且只要当前的区间长度不为 1,mid就不会与r重合
class Solution {
public int findMin(int[] nums) {
if (nums == null || nums.length == 0)
return -1;
int l = 0, r = nums.length - 1;
while (l < r) { // 注意
int mid = l + (r - l) / 2;
if (nums[mid] < nums[r]) {
r = mid;
} else {
l = mid + 1;
}
}
return nums[l];
}
}
寻找两个正序数组的中位数_4
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 用 len 表示合并后数组的长度,
// 如果是奇数,我们需要知道第 (len+1)/2 个数就可以了,如果遍历的话需要遍历 int(len/2 ) + 1 次。
// 如果是偶数,我们需要知道第 len/2和 len/2+1 个数
// 也是需要遍历 len/2+1 次。所以遍历的话,奇数和偶数都是 len/2+1 次
// 返回中位数的话,奇数需要最后一次遍历的结果就可以了,偶数需要最后一次和上一次遍历的结果
// 所以我们用两个变量 left 和 right,right 保存当前循环的结果,在每次循环前将 right 的值赋给 left
// 这样在最后一次循环的时候,left 将得到 right 的值,也就是上一次循环的结果,接下来 right 更新为最后一次的结果
int m = nums1.length, n = nums2.length;
int len = m + n;
int left = -1, right = -1;
// 用 num1_start 和 num2_start 分别表示当前指向 A 数组和 B 数组的位置
int num1_start = 0, num2_start = 0;
for (int i = 0; i < (len / 2) + 1; i++) {
left = right;
// ( 如果left < right 说明在left的数组里 或者越界了)
if (num1_start < m && (num2_start >= n || nums1[num1_start] < nums2[num2_start])) {
right = nums1[num1_start++];
} else {
// 如果left > right 说明在right的数组里
right = nums2[num2_start++];
}
}
// 如果是偶数
if ((len & 1) == 0) {
return (left + right) / 2.0;
} else {
return right;
}
}
}
栈
- Stack
- push 入栈
- pop 出栈并返回元素
有效的括号_20
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
- 遍历每个字符,如果是左括号就入栈,否则就进行左出栈判断当前右括号与它是否匹配,不匹配就false,最终栈为空说明满足
class Solution {
public boolean isValid(String s) {
if (s == null || s.length() == 0)
return false;
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
} else {
if (!stack.isEmpty()) {
Character top = stack.pop();
if (!( (c == ')' && top == '(') ||
(c == ']' && top == '[') ||
(c == '}' && top == '{'))) {
return false;
}
} else {
return false;
}
}
}
return stack.isEmpty();
}
}
最小栈_155
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
示例 1:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
- 通过两个Stack完成
- 一个Stack正常存
- 一个minStack先判断是否为空,为空就先存,不为空时取当前值和栈顶元素小的那个存
class MinStack {
Stack<Integer> stack;
Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
// 如果为空就存当前值
if (minStack.isEmpty()) {
minStack.push(val);
} else {
minStack.push(Math.min(minStack.peek(), val));
}
}
public void pop() {
if (stack.isEmpty())
return;
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
字符串解码_394
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
示例 1:
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:
输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
- 一个Stack存倍数 numStack
- 一个Stack存上次结果 resStack
- 每次出栈用来追加后面的字符,需要注意的是数字可能不是个位数,当为左括号时,先预留一串空的,当为右括号时就要开始乘法得到一串字符串segment,resStack栈顶出栈 + segment 即为当前res
class Solution {
public String decodeString(String s) {
if (s == null || s.length() == 0)
return "";
Stack<Integer> numStack = new Stack<>();
Stack<String> resStack = new Stack<>();
int num = 0;
StringBuilder res = new StringBuilder();
for (char c : s.toCharArray()) {
if (c >= '0' && c <= '9') {
num = num * 10 + c - '0';
} else if (c == '[') {
numStack.push(num);
resStack.push(res.toString());
num = 0;
res = new StringBuilder();
} else if (c == ']') {
StringBuilder segment = new StringBuilder();
Integer curNum = numStack.pop();
for (int i = 0; i < curNum; i++) {
segment.append(res.toString());
}
// 上次的出栈 + 本段字符
res = new StringBuilder(resStack.pop() + segment);
} else {
// 将括号中的字符拼出来
res.append(c);
}
}
return res.toString();
}
}
每日温度_739
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例 2:
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
示例 3:
输入: temperatures = [30,60,90]
输出: [1,1,0]
- 用Stack(存比当前值小的下标)
- 遍历数组,判断当前值是否比Stack栈顶元素对应的值小,如果小就Stack栈顶元素出栈,存当前下标
- 最终结果res通过 当前值下标 - 栈顶元素的值(下标)
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
if (temperatures == null || temperatures.length == 0)
return new int[] {};
Stack<Integer> stack = new Stack<>();
int[] res = new int[temperatures.length];
for (int i = 0; i < temperatures.length; i++) {
// 保证出栈非空,有大的就出栈一个,没有默认都为0
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
Integer curTopIdx = minStack.pop();
res[curTopIdx] = i - curTopIdx;
}
stack.push(i);
}
return res;
}
}
柱状图中最大的矩形_84
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
示例 2:
输入: heights = [2,4]
输出: 4
- 用Stack(存入比当前值大的下标),计算面积 = width(右边下标 - 左边下标 - 1) * 较低高度(下标对应的值)
class Solution {
public int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0)
return 0;
Stack<Integer> stack = new Stack<>();
int max = 0;
// 将元素放入新数组居中
int[] newHeights = new int[heights.length + 2];
newHeights[0] = 0;
newHeights[newHeights.length - 1] = 0;
for (int i = 1; i < heights.length + 1; i++) {
newHeights[i] = heights[i - 1];
}
for (int i = 0; i < newHeights.length; i++) {
// 栈顶元素对应的值 > 当前元素的值
while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
// 栈顶元素,之前最小的高度
int height = newHeights[stack.pop()];
int left = stack.peek();
int width = i - left - 1;
int area = width * height;
max = Math.max(max, area);
}
stack.push(i);
}
return max;
}
}
堆
- PriorityQueue
- offer 新增
- poll 删除头元素并返回
- peek 获取头
数组中的第K个最大元素_215
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
- 采用最小堆,元素入堆,剔除前K大的元素
class Solution {
public int findKthLargest(int[] nums, int k) {
if(nums == null || nums.length == 0) return 0;
// 使用最小堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll();
}
}
return minHeap.peek();
}
}
前K个高频元素_347
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
- 采用Map存放出现频率,再使用最小堆,遍历频率Map的key,剔除前K大的元素
class Solution {
public int[] topKFrequent(int[] nums, int k) {
if (nums == null || nums.length == 0)
return new int[] {};
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 使用最小堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>((a, b) -> freqMap.get(a) - freqMap.get(b));
// 遍历频率Map
for (Integer num : freqMap.keySet()) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll();
}
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = minHeap.poll();
}
return res;
}
}
数据流的中位数_295
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
MedianFinder()
初始化MedianFinder
对象。void addNum(int num)
将数据流中的整数num
添加到数据结构中。double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
示例 1:
输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]
解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
- 借助两个堆,长度不相等
- 最小堆放元素,然后放最小堆的poll
- 最大堆放元素,然后放最大堆的poll
class MedianFinder {
PriorityQueue<Integer> small;
PriorityQueue<Integer> large;
public MedianFinder() {
// 最小堆 保存较大的一半
small = new PriorityQueue<>();
// 最大堆 保存较小的一半
large = new PriorityQueue<>((a, b) -> b - a);
}
public void addNum(int num) {
if (small.size() != large.size()) {
small.offer(num);
// 存小的
large.offer(small.poll());
} else {
large.offer(num);
// 存大的
small.offer(large.poll());
}
}
public double findMedian() {
if (small.size() > large.size()) {
return small.peek();
} else if (small.size() < large.size()) {
return large.peek();
} else {
return (small.peek() + large.peek()) / 2.0;
}
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
贪心算法
买卖股票的最佳时机_121
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) return 0;
// 最大值暂定为0,最小值默认用首元素
int max = 0, min = prices[0];
for (int i = 1; i < prices.length; i++) {
min = Math.min(min, prices[i]);
max = Math.max(max, prices[i] - min);
}
return max;
}
}
跳跃游戏_55
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
class Solution {
public boolean canJump(int[] nums) {
if(nums == null || nums.length == 0) return false;
int len = nums.length;
int max_position = 0;
for(int i = 0; i < len - 1; i++){
max_position = Math.max(max_position,nums[i] + i);
if(max_position <= i){
return false;
}
}
return max_position >= len - 1;
}
}
跳跃游戏II_45
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i
向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
class Solution {
public int jump(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int len = nums.length;
int end = 0;
int max_position = 0;
int steps = 0;
for(int i = 0; i < len - 1; i++){
max_position = Math.max(max_position,nums[i] + i);
if(end == i){
end = max_position;
steps++;
}
}
return steps;
}
}
划分字母区间_763
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
class Solution {
public List<Integer> partitionLabels(String s) {
if(s == null || s.length() == 0) return new ArrayList<>();
int[] last = new int[26];
int len = s.length();
// 记录每个字母出现的最后一个下表
for(int i = 0; i < len; i++){
last[s.charAt(i) - 'a'] = i;
}
int start = 0,end = 0;
List<Integer> res = new ArrayList<>();
for(int i = 0; i < len; i++){
end = Math.max(end, last[s.charAt(i) - 'a']);
if(i == end){
res.add(end - start + 1);
start = end + 1;
}
}
return res;
}
}
动态规划
爬楼梯_70
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
杨辉三角_118
给定一个非负整数 *numRows
,*生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> res = new ArrayList<>();
if(numRows < 1){
return res;
}
// 先把第一层装进去作为base case
List<Integer> firstRow = new ArrayList<>();
firstRow.add(1);
res.add(firstRow);
// 从一开始一层一层生成,载入res
for(int i = 2; i <= numRows; i++){
// 获取前一层row
List<Integer> prevRow = res.get(res.size()-1);
res.add(genNextRow(prevRow));
}
return res;
}
// 输入上一层的元素,生成并返回下一层的元素
List<Integer> genNextRow(List<Integer> prevRow){
List<Integer> curRow = new ArrayList<>();
// 首个元素为1
curRow.add(1);
// 遍历上层
for(int i = 0; i < prevRow.size() - 1; i++){
curRow.add(prevRow.get(i) + prevRow.get(i+1));
}
// 最后一个元素
curRow.add(1);
return curRow;
}
}
打家劫舍_198
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int n = nums.length;
// 奇数
int odd_sum = 0;
// 偶数
int even_sum = 0;
for(int i = 0; i < n; i++){
if(i % 2 == 0) {
even_sum += nums[i];
even_sum = Math.max(even_sum,odd_sum);
}else{
odd_sum += nums[i];
odd_sum = Math.max(even_sum,odd_sum);
}
}
return Math.max(odd_sum,even_sum);
}
}
完全平方数_279
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
class Solution {
public int numSquares(int n) {
int[] dp = new int[n+1];
Arrays.fill(dp,Integer.MAX_VALUE);
dp[0] = 0;
for(int i = 1; i <= n; i++){
// 最坏的情况
dp[i] = i;
int j = 1;
while(i - j * j >= 0){
dp[i] = Math.min(dp[i],dp[i-j*j] + 1);
j++;
}
}
return dp[n];
}
}
零钱兑换_322
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
class Solution {
public int coinChange(int[] coins, int amount) {
if(coins == null || coins.length == 0) return -1;
// 表示的凑成总金额为n所需的最少的硬币个数
int[] dp = new int[amount + 1];
Arrays.fill(dp,Integer.MAX_VALUE);
dp[0] = 0;
for(int cur_amount = 1; cur_amount <= amount; cur_amount++){
for(int coin : coins){
int left_amount = cur_amount - coin;
if(left_amount < 0 || dp[left_amount] == Integer.MAX_VALUE) continue;
dp[cur_amount] = Math.min(dp[left_amount] + 1, dp[cur_amount]);
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
单词拆分_139
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> word_set = new HashSet<>(wordDict);
int n = s.length();
boolean[] dp = new boolean[n+1];
// 空串
dp[0] = true;
for(int i = 1; i <= n; i++){
for(int j = 0; j < i; j++){
if(dp[j] && word_set.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[n];
}
}
最长递增子序列_300
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
int max = 1;
dp[0] = 1;
for(int i = 1; i < nums.length; i++){
dp[i] = 1;
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
max = Math.max(dp[i], max);
}
return max;
}
}
乘积最大子数组_152
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
子数组 是数组的连续子序列。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
class Solution {
public int maxProduct(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int n = nums.length;
int[] min_dp = new int[n];
int[] max_dp = new int[n];
max_dp[0] = nums[0];
for(int i = 1; i < n; i++){
min_dp[i] = Math.min(nums[i],Math.min(max_dp[i-1]*nums[i],min_dp[i-1]*nums[i]));
max_dp[i] = Math.max(nums[i],Math.max(max_dp[i-1]*nums[i],min_dp[i-1]*nums[i]));
}
int res = Integer.MIN_VALUE;
for(int num : max_dp){
res = Math.max(res,num);
}
return res;
}
}
分割等和子集_416
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
class Solution {
public boolean canPartition(int[] nums) {
// 先判断是否是偶数
int sum = 0;
for(int num : nums){
sum+=num;
}
if(sum % 2 != 0) return false;
sum /= 2;
int n = nums.length;
boolean[][] dp = new boolean[n+1][sum+1];
for(int i = 0; i <= n; i++){
dp[i][0] = true;
}
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= sum; j++){
// 放不下
if(j < nums[i-1]){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
}
最长有效括号_32
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
class Solution {
public int longestValidParentheses(String s) {
if(s == null || s.length() == 0) return 0;
Stack<Integer> stack = new Stack<>();
stack.push(-1);
int max = 0;
for(int i = 0;i < s.length(); i++){
if(s.charAt(i) == '('){
stack.push(i);
} else {
// 说明碰到了)
stack.pop();
// 说明这个)没有匹配到
if(stack.isEmpty()){
stack.push(i);
}else{
max = Math.max(max, i - stack.peek());
}
}
}
return max;
}
}
多维动态规划
不同路径_62
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
class Solution {
// M X N
// 动态规划
// dp[i][j] 是到达 i,j最多路径
// dp[i][j] = dp[i-1][j] + dp[i][j-1]
// 对于第一行dp[0][j] 或者 第一列dp[i][0] 由于都是在边界,所以只能为1
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int i = 0; i < n; i ++){
dp[0][i] = 1;
}
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[i][j] = dp[i-1][j]+ dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
最小路径和_64
给定一个包含非负整数的 *m* x *n*
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
class Solution {
// M X N
// 从左上角(0,0)->(i,j)的最小路径和
// 走到当前单元格(i,j)的最小路径和 = 从左方单元格(i-1,j)与从上方单元格(i,j-1)走来的两个最小路径和较小的 + 当前单元格值grid[i][j]
// 当左边和上边都不是矩阵边界时,也就是当i!=0 && j!=0 -> dp[i][j] = min(dp[i-1][j],dp[i][j-1] + grid[i][j])
// 当只有左边是矩阵边界时,也就是当i=0,j!=0时, dp[i][j] = dp[i][j-1] + grid[i][j]
// 当只有上边是矩阵边界时,也就是当i!=0,j=0时, dp[i][j] = dp[i-1][j] + grid[i][j]
// 当左边和上边都是边界时,也就是当i=0,j=0时,dp[i][j] = grid[i][j]
// 其实我们完全不需要建立 dp矩阵浪费额外空间,直接遍历 grid[i][j] 修改即可。
// 这是因为:grid[i][j] = min(grid[i - 1][j], grid[i][j - 1])) + grid[i][j]
public int minPathSum(int[][] grid) {
// x
int m = grid.length;
// y
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
// 初始化X轴
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 初始化y轴
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
}
最长回文子串_5
给你一个字符串 s
,找到 s
中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
class Solution {
public String longestPalindrome(String s) {
String res = "";
if (s == null || s.length() == 0) {
return res;
}
// s = "babad"
// 中心扩散法
for (int i = 0; i < s.length(); i++) {
// 以 s[i] 为中心的最长回文子串 也就是 aba这种 b一个字符为中心的情况
String s1 = sub(s, i, i);
// 以 s[i] 和 s[i+1] 为中心的最长回文子串 也就是cabbak 这种以 bb为中心的情况
String s2 = sub(s, i, i + 1);
// res = longest(res, s1, s2)
res = res.length() > s1.length() ? res : s1;
res = res.length() > s2.length() ? res : s2;
}
return res;
}
private String sub(String s, int l, int r) {
// 防止索引越界
while (l >= 0 &&
r < s.length() &&
s.charAt(l) == s.charAt(r)) {
// 向两边展开
l--;
r++;
}
return s.substring(l + 1, r);
}
}
最长公共子序列_1143
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m+1][n+1];
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
// 两个子字符串的最后一位相等
if(text1.charAt(i-1) == text2.charAt(j - 1)){
dp[i][j] = dp[i-1][j-1] + 1;
}else {
dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
}
}
}
return dp[m][n];
}
}
编辑距离_72
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
class Solution {
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
int[][] dp = new int[n1 + 1][n2 + 1];
for(int i = 1; i <= n1; i++){
dp[i][0] = dp[i-1][0] + 1;
}
for(int j = 1; j <= n2; j++){
dp[0][j] = dp[0][j-1] + 1;
}
for(int i = 1; i <= n1; i++){
for(int j = 1; j <= n2; j++){
if(word1.charAt(i - 1) == word2.charAt(j - 1)){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = Math.min(dp[i-1][j],Math.min(dp[i][j-1],dp[i-1][j-1])) + 1;
}
}
}
return dp[n1][n2];
}
}
技巧
只出现一次的数字_136
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
class Solution {
// 一个数和它本身做异或运算结果为 0,
// 即 a ^ a = 0;一个数和 0 做异或运算的结果为它本身,即 a ^ 0 = a。
public int singleNumber(int[] nums) {
int ret = 0;
for(int num : nums){
ret ^= num;
}
return ret;
}
}
多数元素_169
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
class Solution {
// 选出投票数最多的
public int majorityElement(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int cnt = 0;
int target = 0;
// 遍历循环找出投票次数最多的
for (int num : nums) {
if (cnt == 0) {
target = num;
cnt++;
} else {
cnt += target == num ? 1 : -1;
}
}
return target;
}
}
颜色分类_75
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
class Solution {
public void sortColors(int[] nums) {
if(nums == null || nums.length == 0) return;
int n = nums.length;
int p = 0;
// 第一次遍历的时候,将数组的0全部放到头部
for(int i = 0; i < n; i++){
if(nums[i] == 0){
swap(nums,i,p);
p++;
}
}
// 第二次遍历将1放到之后
for(int i = p;i < n; i++){
if(nums[i] == 1){
swap(nums,i,p);
p++;
}
}
}
void swap(int[] nums, int i,int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
下一个排列_31
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3]
,以下这些都可以视作arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]
的下一个排列是[1,3,2]
。 - 类似地,
arr = [2,3,1]
的下一个排列是[3,1,2]
。 - 而
arr = [3,2,1]
的下一个排列是[1,2,3]
,因为[3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组 nums
,找出 nums
的下一个排列。
必须** 原地 **修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
class Solution {
public void nextPermutation(int[] nums) {
if(nums == null || nums.length == 0) return;
// 要保证至少有两个数字
int i = nums.length - 2;
// 如果左 >= 右,直到遇到 左 < 右的,此时 i+1,n肯定为降序
while(i >= 0 && nums[i] >= nums[i+1]) i--;
if(i >= 0){
int j = nums.length - 1;
// 在 i+1,n找到最大数 j满足 a[i] >= a[j] 这样较大数为a[i]
while(j >= 0 && nums[i] >= nums[j]){
j--;
}
// 然后此时得到a[j..n]中最小的值a[j],将两个值交换位置
swap(nums,i,j);
}
// 上面交换完之后,此时 [i+1,n)必是降序,
// 剩下的就是全部进行翻转 我们可以直接使用双指针反转区间 [i+1,n) 使其变为升序,而无需对该区间进行排序
reverse(nums,i+1,nums.length - 1);
}
void reverse(int[] nums,int start,int end) {
while(start < end){
swap(nums,start,end);
start++;
end--;
}
}
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
寻找重复数_287
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1)
的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
提示:
1 <= n <= 105
nums.length == n + 1
1 <= nums[i] <= n
nums
中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
class Solution {
public int findDuplicate(int[] nums) {
if(nums == null || nums.length == 0) return 0;
while(true){
int index = nums[0];
if(nums[index] == nums[0]) return nums[0];
else {
int tmp = nums[0];
nums[0] = nums[index];
nums[index] = tmp;
}
}
}
}