hot100刷题全解
持续更新…
两数之和
- 初始化一个哈希表,用于存储数组元素和它们的索引。
- 遍历数组,对于每个元素:
- 计算目标值减去当前元素的差值。
- 检查这个差值是否在哈希表中存在。
- 如果存在,说明找到了两个数的和等于目标值,返回它们的索引。
- 如果不存在,将当前元素及其索引存入哈希表中。
- 如果遍历完数组没有找到符合条件的两个数,返回一个特殊的结果,如{-1, -1}。
class Solution {
public int[] twoSum(int[] nums, int target) {
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[]{-1,-1};
}
}
49. 字母异位词分组
-
字符排序法:
- 对于每个字符串,将其转换为字符数组并进行排序。
- 排序后,相同字谜的字符串将变为相同的字符序列。例如,“eat”、“tea"和"ate"排序后都变成了"aet”。
- 使用排序后的字符串作为键,将原始字符串加入对应的值(即一个列表)中。
-
使用哈希表:
- 使用一个哈希表(HashMap)来存储排序后的字符串和原始字符串列表之间的映射关系。
- 如果哈希表中已经存在这个排序后的字符串键,则直接将原始字符串添加到该键对应的列表中。
- 如果哈希表中不存在这个排序后的字符串键,则创建一个新的列表,并将原始字符串添加进去,然后将这个键值对放入哈希表中。
输出结果:
-
最后,将哈希表中的所有值(即所有字符串列表)收集起来作为结果返回。
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 不同的字符顺序 排序之后的结果是一样的
Map<String,List<String>> map = new HashMap<>();
for(String str: strs){
char[] temp = str.toCharArray();// 转换为字符数组
// 对字符数组进行排序
Arrays.sort(temp);
// 将排序之后的字符数组转换为字符串
String ss = String.valueOf(temp);
if(map.containsKey(ss)){
// 如果之前存在这个key 直接将结果添加进去就可以
map.get(ss).add(str);
}else{
// 如果不存在 直接创建一个新的List 塞进去
List<String> newList = new ArrayList<>();
newList.add(str);
map.put(ss,newList);
}
}
List<List<String>> result = new ArrayList<>(map.values());
return result;
}
}
128. 最长连续序列
- 初始化一个 HashSet,将数组中的所有元素添加到集合中去重。
遍历 HashSet 中的每一个元素: - 对于每个元素,检查它的前一个元素是否存在。如果不存在,则说明该元素可能是一个新的连续序列的起点。
- 从该起点开始,依次检查其后续的元素是否存在,直到不再存在为止,并记录这个连续序列的长度。
- 更新记录的最长连续序列长度。
class Solution {
public int longestConsecutive(int[] nums) {
// 使用set
Set<Integer> set = new HashSet<>();
for(int num:nums){
set.add(num);
}
int result = 0;
for(int num:nums){
if(!set.contains(num - 1)){
// 如果不存在 说明num可以作为一个起点
int cur = num;
int now = 1;
// 更新cur 努力向后寻找
while(set.contains(cur + 1)){
cur = cur + 1;
now++;
}
result = Math.max(result,now);// 更新最大长度
}
}
return result;
}
}
283. 移动零
- 双指针 前后指针
- 第一次遍历:将所有非0元素全部向前移动
- 第二次遍历:剩余位置所有元素 全部填充为0
class Solution {
public void moveZeroes(int[] nums) {
// 第一次遍历:将所有非0元素全部向前移动
int j = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] != 0){
nums[j++] = nums[i];
}
}
for(int i = j; i < nums.length; i++){
nums[i] = 0;
}
}
}
11. 盛最多水的容器
- 初始化两个指针 i 和 j,分别指向数组的开始和末尾。
- 初始化一个变量 result 来存储最大容积值。
- 使用一个 while 循环,条件是 i < j。
- 在每次循环中,计算当前容积,并更新 result。
- 比较 height[i] 和 height[j],移动较短线段对应的指针。
- 返回 result,即最大容积值。
class Solution {
public int maxArea(int[] height) {
// 首先设置双指针 分别位于数组的两端
int i = 0;
int j = height.length - 1;
int result = 0;
// 两边的指针轮流进行收缩
while(i < j){
// 找到短板
if(height[i] < height[j]){
// 将短板向内移动 可能边长 所以体积可能会变大
result = Math.max(result,(j - i) * height[i++]);
} else{
result = Math.max(result,(j - i) * height[j--]);
}
}
return result;
}
}
15. 三数之和
- 对数组进行排序。
- 遍历数组:
- 对于每个 nums[i],如果 nums[i] 大于零,直接跳出循环,因为后面的数都比零大,不可能有和为零的三元组。
- 跳过重复的 nums[i]。
- 使用双指针:
- 初始化左指针 l 为 i + 1,右指针 r 为数组末尾。
- 计算三个数的和,根据和的结果调整指针位置,并跳过重复的元素。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length < 3){
return res;
}
Arrays.sort(nums);// 从小到大排序
for(int i = 0; i < nums.length; i++){
if(nums[i] > 0){
break;
}
if(i > 0 && nums[i] == nums[i - 1]){
continue;
}
int l = i + 1;
int r = nums.length - 1;
while(l < r){
int sum = nums[i] + nums[l] + nums[r];
if(sum == 0){
res.add(Arrays.asList(nums[i],nums[l],nums[r]));
while(l < r && nums[l] == nums[l + 1]){
l++;
}
while(l < r && nums[r] == nums[r - 1]){
r--;
}
l++;
r--;
}else if(sum < 0){
l++;
}else if(sum > 0){
r--;
}
}
}
return res;
}
}
42. 接雨水
- 双指针初始化:左右指针分别指向数组的开始和末尾,leftMax 和 rightMax 初始化为左右指针初始位置的高度。
- 遍历数组:
- 更新 leftMax 和 rightMax,确保它们分别表示当前指针位置左侧和右侧的最大高度。
- 通过比较 leftMax 和 rightMax,决定移动哪个指针并计算当前可以存储的水量。
- 由于水量是由较低的挡板决定的,因此当 leftMax 小于 rightMax 时,水量由左侧决定;反之亦然。
- 结果累积:在每次指针移动时,累积当前指针位置的存储水量。
class Solution {
public int trap(int[] height) {
// 双指针 前后指针
// 水桶效应:短的木板决定装多少水
int n = height.length;
int left = 0;
int right = n - 1;
int result = 0;
int leftMax = height[left];
int rightMax = height[right];
left++;
right--;
while(left <= right){
// 更新左右两侧的最大挡板
leftMax = Math.max(leftMax,height[left]);
rightMax = Math.max(rightMax,height[right]);
if(leftMax < rightMax){
result += leftMax - height[left];
left++;
}else{
result += rightMax - height[right];
right--;
}
}
return result;
}
}
3. 无重复字符的最长子串
-
初始化部分:
- left 和 right 指针初始化为0,分别表示当前窗口的左边界和右边界。
- result 变量用于存储最长无重复字符子串的长度。
- set 是一个哈希集合,用于存储当前窗口内的字符。
-
滑动窗口逻辑:
- 在每次循环中,如果 right 指向的字符在 set 中,表示存在重复字符,需要收缩窗口。通过移除 left 指向的字符并右移 left 指针来完成窗口收缩。
- 如果 right 指向的字符不在 set 中,表示没有重复字符,可以扩展窗口。通过将 right 指向的字符添加到 set 中并右移 right 指针来完成窗口扩展。
- 在每次扩展窗口后,更新 result,确保 result 保存的是最大窗口的大小。
-
结果返回:
- 当循环结束时,返回 result 变量,它表示最长无重复字符子串的长度。
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.length() <= 1){
return s.length();
}
int left = 0;
int right = 0;
int result = 0;
Set<Character> set = new HashSet<>();
while(right < s.length() && left < s.length()){
if(set.contains(s.charAt(right))){
set.remove(s.charAt(left));
left++;
}else{
set.add(s.charAt(right));
right++;
}
if(result < set.size()){
result = set.size();
}
}
return result;
}
}
438. 找到字符串中所有字母异位词
-
初始化字典:
-
创建两个哈希表 need 和 window。
need 用于记录字符串 p 中每个字符及其出现的次数。 -
window 用于记录当前滑动窗口中各个字符的数量。
-
填充 need 字典:
-
遍历字符串 p,将每个字符及其出现的次数存入 need。
-
滑动窗口定义:
-
使用两个指针 left 和 right 来表示当前窗口的左右边界,初始时都指向字符串 s 的开始位置。
-
valid 变量用于记录当前窗口中满足 need 条件的字符数量。
-
移动右边界:
-
在一个循环中,不断移动右指针 right,将 s 中的字符加入到窗口中,并更新 window 字典。
-
如果加入的字符在 need 中,更新 window 中该字符的计数,如果该字符的计数满足 need 的要求,valid 加1。
-
窗口收缩条件:
-
当窗口的大小大于等于 p 的长度时,判断当前窗口是否为一个合法的字母异位词:
-
如果 valid 等于 need 中的键数,说明当前窗口中的字符匹配 p 中的字符,记录下 left 位置。
移动左指针 left,缩小窗口,并更新 window 中的字符计数。 -
如果移除的字符在 need 中,且该字符计数减少后不再满足 need 的要求,valid 减1。
-
返回结果:
-
最后返回 result,其中包含所有字母异位词的起始位置。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
Map<Character,Integer> need = new HashMap<>();// 记录字符串p的所有字符
Map<Character,Integer> window = new HashMap<>();// 记录滑动窗口中的字符
for(char ch: p.toCharArray()){
need.put(ch,need.getOrDefault(ch,0) + 1);// 将字符填入进去
}
int left = 0;
int right = 0;
int valid = 0;
List<Integer> result = new ArrayList<>();
// [right,left)
while(right < s.length()){
char c = s.charAt(right);
right++;// right永远指向滑动窗口右边界的下一个字符 左闭右开
// 判断该字符是不是need中的
if(need.containsKey(c)){
window.put(c,window.getOrDefault(c,0) + 1);
if(window.get(c).equals(need.get(c))){
valid++;
}
}
while(right - left >= p.length()){
// 说明滑动窗口需要缩小
if(valid == need.size()){
result.add(left);// 将左边界加入List
}
char t = s.charAt(left);
left++;
if(need.containsKey(t)){
if(window.get(t).equals(need.get(t))){
valid--;
}
// 说明window窗口需要缩小
window.put(t,window.get(t) - 1);
}
}
}
return result;
}
}
560. 和为 K 的子数组
- 使用前缀和数组
- 使用双重for循环 计算一个连续区间的和
class Solution {
public int subarraySum(int[] nums, int k) {
// 子数组是连续的序列 考虑使用前缀和
// 使用前缀和
int[] preSum = new int[nums.length + 1];
// 初始化前缀和数组
preSum[0] = 0;
// 使用前缀和 preSum[i] 记录 前面所有元素的和 所以preSum[i + 1] - preSum[i] = nums[i]
for(int i = 0; i < nums.length; i++){
preSum[i + 1] = preSum[i] + nums[i];
}
int count = 0;
// 双重for循环 计算一个区间内的数据之和
for (int left = 0; left < nums.length; left++){
for (int right = left; right < nums.length; right++){
if(preSum[right + 1] - preSum[left] == k) {
count++;
}
}
}
return count;
}
}
239. 滑动窗口最大值
- 双端队列实现求解最大值窗口的结构,双端队列存储数组中的下标
- 双端队列必须始终维持根据下表对应的数字由大到小的顺序
- 不断地移动窗口,如果进入的数字会打破递减的顺序,队列尾部就会不断地出数字,直到满足递减的要求,再把当前数字插入
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// 滑动窗口每次移动以为位置 就扫描一遍窗口 找出最大值xj
// 窗口对应的数据结构为双端队列 删除首部元素
if (nums == null || nums.length == 0 || k <= 0) {
return new int[0];
}
int n = nums.length;
List<Integer> ans = new ArrayList<>();
Deque<Integer> qmax = new LinkedList<>();// 双端队列存储下标
for (int r = 0; r < n; r++) {
// 针对元素nums[r] pop出队列比他小的所有元素
while(!qmax.isEmpty() && nums[qmax.peekLast()] <= nums[r]) {
qmax.pollLast();// 将所有比新数字小的元素 全部出队
}
// 如果比他小 将当前元素的索引加入队列尾部
qmax.offerLast(r);
// 队列的头部元素是最大值元素的索引 那么判断最大值元素索引是不是在窗口中
//如果最大值元素的索引是r - k 说明已经不是在滑动窗口内部了 新的窗口索引是r - k + 1
// 也就是说最大值的索引是在窗口左侧边界的前一个位置 将该索引从队列头部弹出
if (qmax.peekFirst() == r - k) {
qmax.pollFirst();// 弹出不再窗口内的元素索引
}
// 如果窗口形成
if (r >= k - 1) {
ans.add(nums[qmax.peekFirst()]);// 将当前窗口的最大值加入结果列表中
}
}
// 将结果列表转换为数组
int[] result = new int[ans.size()];
for (int i = 0; i < ans.size(); i++) {
result[i] = ans.get(i);
}
return result;
}
}
76. 最小覆盖子串
-
滑动窗口
-
变量定义:
- need:一个哈希表,记录字符串t中每个字符需要的数量。
- window:一个哈希表,记录当前滑动窗口中每个字符的数量。
- left 和 right:滑动窗口的左右边界指针,初始都设为0。
- valid:记录当前窗口中满足need条件的字符的种类数。
- ans:数组,用于存储最小子串的长度和起止索引位置,初始化为{-1, 0, 0}。
- required:记录t中不同字符的总数
-
扩展右边界:
- 随着right指针的向右移动,每遇到一个字符c,都将其加入到window中。
- 检查加入的这个字符c是否存在于need中,如果存在,并且window中该字符的数量达到了need中对应的数量,那么valid加1。
-
收缩左边界:
- 当valid的值与required相等时,说明当前窗口已经包含了所有t中的字符。
- 此时,尝试通过移动left指针来收缩窗口,优化找到的子串长度。
- 在收缩窗口的过程中,记录可能的最小窗口。如果当前窗口的长度小于之前记录的最小长度(或者是初始值-1),更新ans数组。
- 将left位置的字符从window中数量减1,如果这导致该字符不再满足need的需求,valid减1。
class Solution {
public String minWindow(String s, String t) {
if (s == null || t == null || s.length() == 0 || t.length() == 0) {
return "";
}
// 滑动窗口
Map<Character,Integer> need = new HashMap<>();// 记录字符串t中的所有字符
Map<Character,Integer> window = new HashMap<>();// 滑动窗口
for (char ch:t.toCharArray()) {
need.put(ch ,need.getOrDefault(ch,0) + 1);// 初始化need 记录所有需要的字符
}
int left = 0;
int right = 0;
int valid = 0;// 记录窗口内有效字符个数
int[] ans = {-1,0,0};// 记录窗口长度 左指针 右指针
int required = need.size();
while(right < s.length()) {
char c = s.charAt(right);
// 判断该字符是不是need中的 如果是 塞进窗口 以及更新valid
window.put(c,window.getOrDefault(c,0) + 1);
if(need.containsKey(c)) {
if(window.get(c).intValue() == need.get(c).intValue()) {
valid++;// 说明c字符已经满足条件
}
}
while(left <= right && valid == required) {
// 收缩窗口 + 更新左边界
// 达到要求之后 保存最小窗口
if(ans[0] == -1 || right - left + 1 < ans[0]) {
ans[0] = right - left + 1;// 更新最小窗口长度
ans[1] = left;
ans[2] = right;
}
c = s.charAt(left);// 取出左边界的字符
window.put(c,window.get(c) - 1);
if(need.containsKey(c) && window.get(c).intValue() < need.get(c).intValue()) {
valid--;
}
left++;
}
right++;
}
return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
}
}
53. 最大子数组和
- 动态规划
- dp[i] 代表从nums[0] 到 nums[i] 中最大子数组和
class Solution {
public int maxSubArray(int[] nums) {
// 动态规划
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = nums[0];
for(int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i],nums[i]);// 因为加上之后可能是负数
if(max < dp[i]) {
max = dp[i];
}
}
return max;
}
}
56. 合并区间
-
排序:
- 首先,我们需要将所有的区间按照起始位置进行排序。这样可以方便后续的合并操作,因为相邻的区间更容易检查是否重叠。
-
初始化:
- 如果输入数组为空,直接返回一个空的二维数组。
-
遍历和合并:
- 使用一个循环遍历排序后的区间数组,逐个检查区间是否与当前区间重叠。如果重叠,则合并区间,更新结束位置。如果不重叠,则将当前区间加入结果列表。
-
转换结果:
- 最后,将结果列表转换为二维数组的形式并返回。
class Solution {
public int[][] merge(int[][] intervals) {
// 首先对区间进行排序
if (intervals.length == 0) {
return new int[0][0];
}
Arrays.sort(intervals,Comparator.comparingInt(a -> a[0]));
List<int[]> result = new ArrayList<>();
for (int i = 0; i < intervals.length; i++) {
int start = intervals[i][0];
int end = intervals[i][1];
int j = i + 1;
// 针对后面的区间进行遍历, 如果后面的区间的左边界小于或者等于 当前区间的左边界 那么说明可以合并
while(j < intervals.length && intervals[j][0] <= end) {
// 合并之后 更新区间的右边界
end = Math.max(end,intervals[j][1]);
j++;
}
// 当找到一个区间的左边界和当前区间的右边界脱节的时候 将前面的合并结果添加
result.add(new int[] {start,end});
i = j - 1;// 因为上面的j++
}
// 将List转换为数组
int[][] ans = new int[result.size()][2];
for (int i = 0; i < result.size(); i++) {
ans[i][0] = result.get(i)[0];
ans[i][1] = result.get(i)[1];
}
return ans;
}
}
189. 轮转数组
-
整体翻转:首先,将整个数组进行翻转。这会将所有元素的位置颠倒。
-
找到分割点:根据 k 值,确定分割点。实际上,由于数组是循环的,所以我们只需考虑 k % 数组长度,这样可以忽略掉多余的完全循环。
-
局部翻转:将翻转后的数组分为两部分:前 k 个元素和剩余的元素。分别对这两部分进行再次翻转,就可以得到最终的结果
class Solution {
public void rotate(int[] nums, int k) {
// 首先对整个数组进行翻转
// 找到分割点k
// 对左右子数组进行排序
// 递归
k = k % nums.length;
reverse(nums,0,nums.length - 1);
reverse(nums,0,k - 1);
reverse(nums,k,nums.length - 1);
}
// 首尾元素交换
public void reverse(int[] nums, int start,int end) {
while (start < end) {
int t = nums[start];
nums[start] = nums[end];
nums[end] = t;
start++;
end--;
}
}
}
238. 除自身以外数组的乘积
- 首先初始化前缀积数组pre和后缀积数组suf,分别从左到右和从右到左计算每个位置的积。
- 然后,利用前缀积和后缀积的结果计算最终的结果数组result。
- 最后返回结果数组
class Solution {
public int[] productExceptSelf(int[] nums) {
// 计算前缀积 + 后缀积
int n = nums.length;
int[] pre = new int[n];
// 计算所有元素的前缀积
pre[0] = 1;
// pre[i] = 从0 到 i - 1 的所有元素的乘积
for (int i = 1; i < n; i++) {
pre[i] = pre[i - 1] * nums[i - 1];
}
// 计算后缀积
int[] suf = new int[n];
suf[n - 1] = 1;
// suf[i]等于从i + 1到 n - 1的所有元素的乘积
for (int i = n - 2; i >= 0; i--) {
suf[i] = suf[i + 1] * nums[i + 1];
}
int [] result = new int[n];
for(int i = 0 ; i < n; i++) {
result[i] = pre[i] * suf[i];
}
return result;
}
}
41. 缺失的第一个正数
class Solution {
public int firstMissingPositive(int[] nums) {
// 创建哈希表 记录数组中的数
int n = nums.length;
for (int i = 0; i < n; i++) {
// 通过原地交换使得每一个正整数放到他在数组中的对应位置 比如数字2 应该放在 1索引的位置 nums[1]
// 然后再去遍历数组 找出第一个不在正确位置的正整数 时间复杂度是O(n)
// 加入nums[1]的数字不是2 那么进行交换 强制归位
// 保证每一个数字放在对应的位置上
while(nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
int temp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
// 从头开始遍历 找到第一个缺失的正整数
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return n + 1;// 如果数组中所有的数都在对应位置 那么缺失的数就是n + 1
}
}
73. 矩阵置零
- 首先扫描第一行和第一列 看看是否需要记作0
- 然后在扫描其他行和列的元素的时候 将第一行和第一列的空间作为记录元素为0的标记位置
class Solution {
public void setZeroes(int[][] matrix) {
int row = matrix.length; // 矩阵的行数
int col = matrix[0].length; // 矩阵的列数
boolean row0_flag = false;// 标记第一行是否有0
boolean col0_flag = false;// 标记第一列是否有0
// 本题的思路就是 首先扫描第一行和第一列 看看是否需要记作0
// 然后在扫描其他行和列的元素的时候 将第一行和第一列的空间作为记录元素为0的标记位置
// 检查第一行是否有0
for (int j = 0; j < col; j++) {
if (matrix[0][j] == 0) {
row0_flag = true;
break;
}
}
// 检查第一列是否有0
for (int i = 0; i < row; i++) {
if(matrix[i][0] == 0) {
col0_flag = true;
break;
}
}
// 使用第一行和第一列作为标志位,标记需要重置为0的行和列
for (int i = 1; i < row; i ++) {
for (int j = 1; j < col; j++) {
// 如果发现第i行第j列的元素是0
// 将第i行的第一个元素记作0 第j列的第一个元素记作0
if (matrix[i][j] == 0) {
matrix[i][0] = matrix[0][j] = 0;
}
}
}
// 根据标志位将需要记作0的行和列记录一下
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 如果第一行有0 将第一行全部记作0
if(row0_flag == true) {
for (int j = 0; j < col; j++) {
matrix[0][j] = 0;
}
}
// 如果第一列有0 将第一列全部记作0
if (col0_flag == true) {
for (int i = 0; i < row; i++) {
matrix[i][0] = 0;
}
}
}
}
54. 螺旋矩阵
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
if (matrix.length == 0) {
return new ArrayList<>();
}
// l是左边界 r 是右边界
// t是上边界 b 是下边界
int l = 0, r = matrix[0].length - 1;// 从左到右的范围
int t = 0, b = matrix.length - 1;// 从上到下的范围
int x = 0;
Integer[] res = new Integer[(r + 1) * (b + 1)];// 创建结果数组
while(true) {
// 遍历行 l~r是列的范围 t 是行数
// 从左到右遍历上边界
for (int i = l; i <= r; i++) {
res[x++] = matrix[t][i];// 从左到右遍历上边界
}
// 行数超过下边界 退出循环
if (++t > b) {
break;
}
// 从上到下遍历右边界
for (int i = t; i <= b; i++) {
res[x++] = matrix[i][r];
}
if (l > --r) {
break;
}
// 从右到左遍历下边界
for (int i = r; i >= l; i--) {
res[x++] = matrix[b][i];
}
if(--b < t) {
break;// 移动下边界 超过上边界就退出循环
}
for (int i = b; i >= t; i--) {
res[x++] = matrix[i][l];
}
if(++l > r){
break;// 移动左边界 如果超过右边界 就退出循环
}
}
return Arrays.asList(res);
}
}
48. 旋转图像
- 对角线交换
- 上下交换
class Solution {
public void rotate(int[][] matrix) {
// 首先沿着右上-左下的对角线进行翻转
if (matrix.length == 0 || matrix.length != matrix[0].length) {
return;
}
int length = matrix.length;
// 对角线交换:将矩阵沿着副对角线进行元素交换
// 通过i 和 j遍历矩阵的上半部分 将矩阵的元素 翻转到副对角线的对称位置
for (int i = 0; i < length; i++) {
for (int j = 0; j < length - i; j++) {
// 交换元素
int temp = matrix[i][j];
matrix[i][j] = matrix[length - 1 - j][length - 1 - i];
matrix[length - 1 - j][length - 1 - i] = temp;
}
}
// 上下交换
for (int i = 0; i < (length >> 1); i++) {
for (int j = 0; j < length; j++) {
int t = matrix[i][j];
matrix[i][j] = matrix[length - i - 1][j];
matrix[length - i - 1][j] = t;
}
}
}
}
240. 搜索二维矩阵 II
- 针对每一行进行二分查找
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
if (m == 0 || n == 0){
return false;
}
for (int i = 0; i < m; i++) {
// 遍历每一行
int l = 0;
int r = n - 1;
while(l <= r) {
// 针对每一行进行二分查找
int mid = (l + r + 1) >> 1;
if (matrix[i][mid] < target) {
l = mid + 1;
}else if (matrix[i][mid] > target) {
r = mid - 1;
}else if(matrix[i][mid] == target) {
return true;
}
}
}
return false;
}
}
160. 相交链表
/**
* 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) {
// 不存在环
// 计算链表a和链表b的长度 然后计算差值
ListNode p = headA;
ListNode q = headB;
int lenA = 0;
int lenB = 0;
while(p != null) {
lenA++;
p = p.next;
}
while(q != null) {
lenB++;
q = q.next;
}
p = headA;
q = headB;
if(lenA > lenB) {
// 让第一个指针先走
int t = lenA - lenB;
while(t-- > 0) {
p = p.next;
}
// 然后两个指针开始同时进行移动
while(p!= null && q != null) {
if (p == q) {
return p;
}
p = p.next;
q = q.next;
}
}else {
int t = lenB - lenA;
while(t-- > 0) {
q = q.next;
}
while(q != null && p != null) {
if (p == q) {
return p;
}
p = p.next;
q = q.next;
}
}
return null;
}
}
206. 反转链表
- 前后指针
/**
* 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) {
ListNode p = head;
ListNode q = null;
while(p != null) {
// 前后指针
// 先保存好下一个节点
ListNode next = p.next;
p.next = q;
q = p;
p = next;
}
return q;
}
}
234. 回文链表
- 将链表后半部分进行翻转
- 然后比较链表前后两部分 是否相同
/**
* 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;
}
if (head.next == null) {
return true;
}
// 使用快慢指针找到链表的中点
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 翻转链表的后半部分
ListNode pre = null;
while(slow != null) {
ListNode next = slow.next;
slow.next = pre;
// 移动指针
pre = slow;
slow = next;
}
// 最后pre是后半部分lia
// 比较链表的前后两部分
ListNode left = head;
ListNode right = pre;
while(right != null) {
if (left.val != right.val) {
return false;
}
left = left.next;
right = right.next;
}
return true;
}
}
141. 环形链表
- 快指针走两步 慢指针走一步
- 如果存在环 一定会相遇
/**
* 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) {
// 快慢指针
ListNode fast,slow;
fast = slow = head;
// 快指针走两步 慢指针走一步 判断是否相等
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return true;
}
}
return false;
}
}
142. 环形链表 II
-
检测环
- 使用 while 循环,让 fast 指针每次移动两个节点,slow 指针每次移动一个节点。
- 如果 fast 和 slow 相遇,说明链表中存在环,跳出循环。
- 如果 fast 到达链表末尾 (fast == null 或 fast.next == null),说明链表中没有环,返回 null。
-
找到环的起始节点
- 如果检测到环存在,将 slow 指针重新指向链表头节点 head。
- 然后 slow 和 fast 每次都移动一个节点。
- 当 slow 和 fast 再次相遇时,相遇点即为环的起始节点,返回该节点。
/**
* 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) {
// 首先找到相遇的位置
ListNode fast,slow;
fast = slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
// 说明遇到了环
if (fast == slow) {
break;
}
}
// 到链表末尾 说明没有环
if (fast == null || fast.next == null) {
return null;
}
// 之后将其中一个指针重新指向head
slow = head;
while (slow != fast) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
21. 合并两个有序链表
- 归并排序
/**
* 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 p = list1;
ListNode q = list2;
ListNode result = new ListNode(-1);
ListNode ans = result;
while(p != null && q != null) {
if (p.val < q.val) {
result.next = p;
p = p.next;
}else {
result.next = q;
q = q.next;
}
result = result.next;
}
if (p != null) {
result.next = p;
}
if (q != null) {
result.next = q;
}
return ans.next;
}
}
2. 两数相加
- 高精度加法模版
/**
* 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) {
// 使用高精度加法模版
ListNode l3 = new ListNode(-1);
ListNode node = l3;
ListNode p = l1;
ListNode q = l2;
int t = 0;// 进位
while (p != null || q != null) {
if (p != null) {
t += p.val;
p = p.next;
}
if (q != null) {
t += q.val;
q = q.next;
}
// 将结果插入
ListNode tmp = new ListNode(t % 10);
l3.next = tmp;
l3 = l3.next;
t = t / 10;// 进位结果递进
}
if (t != 0) {
l3.next = new ListNode(1);
}
return node.next;
}
}
19. 删除链表的倒数第 N 个结点
- 找到倒数第n + 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 removeNthFromEnd(ListNode head, int n) {
if (head == null) {
return null;
}
// 找到倒数第N - 1个节点 作为前驱结点 也就是第 length - n + 1的节点
ListNode dummy = new ListNode(0);
dummy.next = head;
// 使用双指针方法
ListNode first = dummy;
ListNode second = dummy;
// 快指针先走 n + 1步
for (int i = 0; i <= n; i++) {
first = first.next;
}
// 快慢指针一起走 快指针走到null
while(first != null) {
first = first.next;
second = second.next;
}
// 此时second指针指向要删除节点的前驱节点
second.next = second.next.next;
return dummy.next;
}
}
24. 两两交换链表中的节点
- 遍历链表 针对链表的节点两两交换
/**
* 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) {
// 两两交换链表的节点
// 制作哑结点
ListNode pre = new ListNode(0);
pre.next = head;// 这个是用来找head
ListNode temp = pre;// 这个用来遍历的
while (temp.next != null && temp.next.next != null) {
// 遍历链表 temp 相当于前驱节点
// 1 2 3
// temp start end 交换 2 3
// 1 3 2
ListNode start = temp.next;
ListNode end = temp.next.next;
// 开始交换 注意顺序
temp.next = end;
start.next = end.next;
end.next = start;
// 交换2 和 3 之后 temp 移动到2 这个位置 作为新的前驱节点
temp = start;
}
return pre.next;
}
}
25. K 个一组翻转链表
- reverseKGroup方法:
- 首先检查链表是否为空,如果为空则直接返回。
- 使用两个指针a和b初始化为链表头。
- 检查链表剩余节点是否至少有k个,如果不足k个则返回当前头节点。
- 调用reverse方法翻转前k个节点。
- 递归调用reverseKGroup处理剩余的节点,并将翻转后的部分连接起来。
- 返回新头节点。
- reverse方法:
- 翻转从a到b(不包含b)之间的节点。
- 使用pre和cur两个指针进行翻转,pre指向前一个节点,cur指向当前节点。
- 遍历并翻转每个节点,直到当前节点等于b。
- 返回翻转后的新头节点。
/**
* 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;
}
ListNode a,b;
a = head;
b = head;
// 检查剩余的节点数目是否足够k个
for (int i = 0; i < k; i++) {
if (b == null) {
return head;// 说明不够k个节点 那就不需要翻转
}
b = b.next;
}
ListNode newHead = reverse(a,b);
a.next = reverseKGroup(b,k);
return newHead;
}
// 根据制定范围 翻转链表
ListNode reverse(ListNode a, ListNode b) {
ListNode pre = null;
ListNode cur = a;
while (cur != b) {
ListNode t = cur.next;
cur.next = pre;
pre = cur;
cur = t;
}
return pre;
}
}
138. 随机链表的复制
- 哈希表
/*
// 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) {
// 利用哈希表一一映射的关系 构建原链表和新链表之间的键值对映射关系
Map<Node,Node> map = new HashMap<>();
// 遍历链表 存储map
Node cur = head;
while(cur != null) {
Node temp= new Node(cur.val);
map.put(cur,temp);
cur = cur.next;
}
// 再次遍历链表 赋值节点
cur = head;
while(cur != null) {
Node newNode = map.get(cur);
newNode.next = map.get(cur.next);
newNode.random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
148. 排序链表
- 归并排序
/**
* 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, ListNode tail) {
if (head == null) {
return head;
}
// 判断链表是否只包含一个节点
if (head.next == tail) {
head.next = null; // 确保单个节点链表 不再指向任何其他节点 然后返回这个节点head
return head;
}
// 快慢指针找到链表的中点
ListNode slow = head;
ListNode fast = head;
// 找到链表的中点
while(fast != tail) {
slow = slow.next;
fast = fast.next;
if (fast != tail) {
fast = fast.next;
}
}
// 慢指针指向链表的中点
ListNode mid = slow;
ListNode list1 = sortList(head,mid);
ListNode list2 = sortList(mid,tail);
ListNode mergeNode = mergeList(list1,list2);
return mergeNode;
}
public ListNode mergeList(ListNode a, ListNode b) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead;// 创建新的节点 连接新的值
ListNode temp1 = a;
ListNode temp2 = b;
// 归并 每一次取出最小的值
while(temp1 != null && temp2 != null) {
if (temp1.val < temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
}else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if(temp1 != null) {
temp.next = temp1;
}
if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
public ListNode sortList(ListNode head) {
// 链表排序 归并排序
return sortList(head,null);
}
}
23. 合并 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 mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
ListNode dummyNode = new ListNode(-1);
ListNode p = dummyNode;
// 创建优先级队列 最小堆 从小到大进行排列
PriorityQueue<ListNode> pd = new PriorityQueue<>(lists.length,(a,b) -> (a.val - b.val));
// 将每一个链表的头结点添加到优先级队列中
for (ListNode node: lists) {
if (node != null) {
pd.add(node);
}
}
// 处理优先级队列
while(!pd.isEmpty()) {
// 获取队列中最小的节点 将其添加到结果链表中
ListNode temp = pd.poll();// 将一个链表的头结点出队
p.next = temp;
// 如果当前节点有下一个节点 将下一个节点挨个添加到队列中 优先级队列会自动进行排序
if (temp.next != null) {
pd.add(temp.next);
}
// 移动指针
p = p.next;
}
return dummyNode.next;
}
}
146. LRU 缓存
最近最少使用
class LRUCache {
int cap;// 容量
LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();// 保持元素的有序性
public LRUCache(int capacity) {
this.cap = capacity;// 初始化容器的容量
}
public int get(int key) {
// 获取指定Key的数据 然后进行更新 表示最近使用过
if(!cache.containsKey(key)) {
return -1;
}
makeRecently(key);// 重新把这个key还有value塞进去
return cache.get(key);// 获取元素
}
public void put(int key, int value) {
// 首先判断这个元素是否 存在
if (cache.containsKey(key)) {
// 直接更新
cache.put(key,value);
makeRecently(key);
return;
}
// 如果不存在 缓存容量超过cao
if (cache.size() >= this.cap) {
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);// 删除长时间不使用的数据
}
cache.put(key, value);
}
private void makeRecently(int key) {
int val = cache.get(key);// 获取元素
// 移除key
cache.remove(key);
cache.put(key,val);// 重新放进去
}
}
/**
* 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);
*/