目录
一、哈希
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[]{i, map.get(target - nums[i])};
}
else{
map.put(nums[i],i);
}
}
return null;
}
}
2. 字母异位词分组
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
if(strs == null){
return null;
}
for(String str : strs){
char[] arr = str.toCharArray();//字符串转char数组
Arrays.sort(arr);//排序数组
String key = new String(arr);//数组转字符串
List<String> list = map.getOrDefault(key, new ArrayList<String>());//查看hash表中是否存在相同的
list.add(str);//相同(或初始)的str添加到同一列表
map.put(key,list);//把key(排序后的str)和做好的列表放到hash表里
}
return new ArrayList<List<String>> (map.values());//返回hash表的值(拷贝到新的ArrayList里)
}
}
/*
时间复杂度:O(nklogk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要 O(klogk) 的时间进行排序以及 O(1) 的时间更新哈希表,因此总时间复杂度是 O(nklogk)。
空间复杂度:O(nk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要用哈希表存储全部字符串。
*/
3. 最长连续序列
要求时间复杂度为 O(n)
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();//去重
for(int num: nums){
num_set.add(num);
}
int longest = 0;
for(int num : num_set){
if(!num_set.contains(num - 1)){//判断是不是开头
//如果是开头:
int curNum = num;
int curStreak = 1;//初始长度为1
while(num_set.contains(curNum + 1)){//看看有没有后续
curStreak = curStreak + 1;//有的话+1
curNum = curNum + 1;//继续看有没有后续
}
longest = Math.max(curStreak,longest);//取最长
}
}
return longest;
}
}
二、双指针
1. 移动0
class Solution {
public void moveZeroes(int[] nums) {
int left = 0;
int right = 0;
while(right < nums.length){
if(nums[right] != 0){//右指针不为0就交换
swap(nums,left,right);
left++;//交换完毕后左指针+1
}
right++;//右指针每轮都+1
}
}
private void swap(int[] nums,int left,int right){
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
}
}
2. 盛最多水的容器
class Solution {
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int max = -1;
while(left < right){
int area = Math.min(height[left],height[right]) * (right - left);
max = Math.max(area,max);
if(height[left] <= height[right]){
left++;
}
else{
right--;
}
}
return max;
}
}
3. 三数之和
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
if(nums == null || nums.length < 3){
return ans;
}
Arrays.sort(nums);//排序
for(int i = 0 ; i < nums.length; i++){
if(nums[i] > 0){
break;//如果基准大于0,直接结束循环,因为后面都大于0
}
if(i >= 1 && nums[i] == nums[i - 1]){
continue;//去重,如果基准值与前一个相同,结束单次循环即可
}
int left = i + 1;
int right = nums.length - 1;
while(left < right){
if(nums[left] + nums[right] + nums[i] < 0){
left++;
}
else if(nums[left] + nums[right] + nums[i] > 0){
right--;
}
else if(nums[left] + nums[right] + nums[i] == 0){
ans.add(Arrays.asList(nums[i],nums[left],nums[right]));
//去重
while(left < right && nums[left] == nums[left + 1]){
left++;//如果下一个与当前相同,则跳过
}
while(left < right && nums[right] == nums[right - 1]){
right--;//同上
}
//跳过相同的之后,再往后走
left++;
right--;
}
}
}
return ans;
}
}
/*
首先对数组进行排序,排序后固定一个数 nums[i],再使用左右指针指向 nums[i]后面的两端,数字分别为 nums[L] 和 nums[R],计算三个数的和 sum 判断是否满足为 0,满足则添加进结果集
如果 nums[i]大于 0,则三数之和必然无法等于 0,结束循环
如果 nums[i] == nums[i−1],则说明该数字重复,会导致结果重复,所以应该跳过
当 sum == 0 时,nums[L] == nums[L+1] 则会导致结果重复,应该跳过,L++
当 sum == 0 时,nums[R] == nums[R−1] 则会导致结果重复,应该跳过,R−−
时间复杂度:O(n^2),n为数组长度
*/
4. 接雨水
class Solution {
/*
---------------核心思想---------------
这道题的关键在于:每个位置能接的雨水量 = min(左边最高柱, 右边最高柱) - 当前柱高度
双指针法的精髓是:我们不需要同时知道左右两边的最大值,只需要知道哪一边的"瓶颈"更小。
*/
public int trap(int[] height) {
int n = height.length;
int res = 0;
int left = 0, right = n - 1; // 左右指针:分别指向左右两边界的列
// 左指针的左边最大高度、右指针的右边最大高度
int leftMax = height[left], rightMax = height[right];
// 最两边的列存不了水
left++;
right--;
// 向中间靠拢
while(left <= right){
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if(leftMax < rightMax){
// 左指针的leftMax比右指针的rightMax矮
// 说明:左指针的右边至少有一个板子 > 左指针左边所有板子
// 根据水桶效应,保证了左指针当前列的水量决定权在左边
// 那么可以计算左指针当前列的水量:左边最大高度-当前列高度
res += leftMax - height[left];
left++;
}else{
// 右边同理
res += rightMax - height[right];
right--;
}
}
return res;
}
}
三、滑动窗口
1. 无重复字符的最长子串
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s == null || s.length() == 0){
return 0;
}
int maxLen = -1;
int left = 0;//left 是左边
Set<Character> set = new HashSet<>();
for(int i = 0 ; i < s.length(); i++){//i 是右边
char c = s.charAt(i);//右边的字符
//尝试缩小窗口
while(set.contains(c)){//如果检测到重复,从左缩小窗口
char charLeft = s.charAt(left);//左边的字符(与最右边重复)
set.remove(charLeft);//移除左边的字符
left++;//一直循环,直到左边没有字符与最右边重复
}
set.add(c);
if(i - left + 1 > maxLen){
maxLen = i - left + 1;
}
}
return maxLen;
}
}
2. 找到字符串中所有字母异位词
/*
在字符串 s 中构造一个长度为与字符串 p 的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;当窗口中每种字母的数量与字符串 p 中每种字母的数量相同时,则说明当前窗口为字符串 p 的异位词
*/
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length(), pLen = p.length();
if (sLen < pLen) {
return new ArrayList<Integer>();
}
List<Integer> ans = new ArrayList<Integer>();
int[] sCount = new int[26];
int[] pCount = new int[26];
for (int i = 0; i < pLen; i++) {
sCount[s.charAt(i) - 'a']++;
pCount[p.charAt(i) - 'a']++;
}
if (Arrays.equals(sCount, pCount)) {
ans.add(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
sCount[s.charAt(i) - 'a']--;
sCount[s.charAt(i + pLen) - 'a']++;
if (Arrays.equals(sCount, pCount)) {
ans.add(i + 1);
}
}
return ans;
}
}
四、子串
1. 和为K的子数组
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();//哈希表,存储前缀和 → 出现次数的映射
map.put(0, 1);//初始化关键:表示"前缀和为0"出现了1次(即空前缀)
int pre = 0;//当前位置的前缀和(从数组开头到当前元素的和)
int count = 0;//记录满足条件的子数组个数
for(int i = 0 ; i < nums.length; i++){
pre += nums[i];// 计算当前前缀和
if(map.containsKey(pre - k)){ //也就是从map中找 之前的 前缀和
count += map.get(pre - k);
//原理:如果存在某个位置 j,使得从 0 到 j 的前缀和为 pre - k,那么从 j+1 到 i 的子数组和就是 k
// 0*******j j+1****** i(当前)
// pre-k + k = pre(当前)
}
map.put(pre, map.getOrDefault(pre, 0) + 1);
//将当前前缀和加入哈希表,次数+1
}
return count;
}
}
2. 滑动窗口最大值
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();// 单调递减队列,存的是下标
int[] res = new int[nums.length - k + 1];// 结果数组,共 n-k+1 个窗口
for(int j = 0, i = 1 - k; j < nums.length; i++, j++) {
/*
j:当前遍历到的下标(0, 1, 2, ... n-1)
i:当前窗口在结果数组中的下标(从 1-k 开始,即 -(k-1))
---------------表格----------------
j i 含义
0 1-k 窗口还没形成,正在填充
k-1 0 第一个完整窗口,res[0]
n-1 n-k 最后一个窗口,res[n-k]
----------------------------------
*/
// 1 保持 deque 递减
//这是核心操作!新元素 nums[j] 入队前,要先移除所有比它小的元素:
while(!deque.isEmpty() && deque.peekLast() < nums[j])
deque.removeLast();
deque.addLast(nums[j]);
// 2 滑动窗口时 删除 deque 中对应的 nums[i-1]
if(i > 0 && deque.peekFirst() == nums[i - 1])
//nums[i-1] 是滑出窗口左边界的元素
deque.removeFirst();//如果它在队首(说明它曾是最大值),则移除
// 3 记录窗口最大值
if(i >= 0)//当 i >= 0 时,说明窗口已形成(至少有 k 个元素)
res[i] = deque.peekFirst();
//队首 deque.peekFirst() 就是当前窗口的最大值
}
return res;
}
}
3. 最小覆盖子串
class Solution {
public String minWindow(String s, String t) {
//哈希表map来存放t的字符及其对应的个数
Map<Character,Integer> map = new HashMap<>();
for(int i = 0; i < t.length(); i++){
char c = t.charAt(i);
map.put(c,map.getOrDefault(c,0) + 1);
}
int count = 0;//count来判断窗口中的字符是否超过/少于map中t的各个字符对应的个数
Map<Character,Integer> windowMap = new HashMap<>();//哈希表windowmap来增加/减少count的个数
//left为窗口的左边界,minLen是窗口最小长度,minLeft是最小窗口对应的左边界
int left = 0;
int minLeft = 0;
int minLen = Integer.MAX_VALUE;
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(map.containsKey(c)){
windowMap.put(c,windowMap.getOrDefault(c,0) + 1);
if(windowMap.get(c) <= map.get(c)){
count++;
}
}
while(count == t.length()){
//先判断当前窗口是否比之前窗口小,小的话更新数据:minLen,minLeft
if(i - left + 1 < minLen){
minLen = i - left + 1;
minLeft = left;
}
//判断完后继续尝试缩小窗口:拿到left对应的字符charLeft,尝试移除后是否影响count
char charLeft = s.charAt(left);
if(map.containsKey(charLeft)){
windowMap.put(charLeft,windowMap.get(charLeft) - 1);
if(windowMap.get(charLeft) < map.get(charLeft)){
count--;
}
}
left++;
//如果不影响,也就是while会继续循环,left会继续右滑,减小窗口长度
}//如果影响,while(count == t.length)循环结束,右边界往右走,继续添加s的字符,一直到count==t.length(),再次进入循环,找新信息
}
if(minLen > s.length()){
return "";
}
return s.substring(minLeft,minLeft + minLen);
}
}
五、普通数组
1. 最大子数组和
/*
动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans
如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字
如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果
时间复杂度:O(n)
*/
class Solution {
public int maxSubArray(int[] nums) {
int ans = nums[0];
int sum = 0;
for(int num: nums) {
if(sum > 0) {
sum += num;
} else {
sum = num;
}
ans = Math.max(ans, sum);
}
return ans;
}
}
2. 合并区间
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (p,q) -> p[0] - q[0]);//按照左端点 从小到大排序
List<int[]> ans = new ArrayList<>();//存放答案的列表
for(int[] p : intervals){
int size = ans.size();
if(size > 0 && p[0] <= ans.get(size - 1)[1]){//比较 - 前者右端点 vs 当前左端点
//比较 - 前者右端点 vs 当前右端点
//取更大的
ans.get(size - 1)[1] = Math.max(ans.get(size - 1)[1], p[1]);
}else{
ans.add(p);
}
}
return ans.toArray(new int[ans.size()][]);//列表转数组
}
}
3. 轮转数组
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n; // 轮转 k 次等于轮转 k % n 次
reverse(nums, 0, n - 1);//先整体反转
reverse(nums, 0, k - 1);//反转前k个
reverse(nums, k, n - 1);//反转后 n - k 个
}
private void reverse(int[] nums, int i, int j) {
while (i < j) {
int temp = nums[i];
nums[i++] = nums[j];
nums[j--] = temp;
}
}
}
4. 除了自身以外数组的乘积
class Solution {
public int[] productExceptSelf(int[] nums) {
int len = nums.length;
if (len == 0) return new int[0];
int[] ans = new int[len];
//第一次遍历(从左到右):计算左侧乘积
ans[0] = 1;// 第一个元素左边没有元素,乘积为1
for (int i = 1; i < len; i++) {
ans[i] = ans[i - 1] * nums[i - 1];// 当前位置的左侧乘积
//ans[i] 存储的是 nums[0] * nums[1] * ... * nums[i-1]
//也就是 i 位置左侧所有元素的乘积
}
//第二次遍历(从右到左): 计算右侧乘积并合并
int tmp = 1; // 用于累积右侧乘积
for (int i = len - 2; i >= 0; i--) {
//tmp 动态维护 i 位置右侧所有元素的乘积
tmp *= nums[i + 1];// tmp 现在等于 i 位置右侧所有元素的乘积
ans[i] *= tmp; // 左侧乘积 * 右侧乘积 = 最终结果
}
return ans;
}
}
5. 缺失的第一个正数
/*
对于一个长度为 N 的数组,其中没有出现的最小正整数只能在 [1,N+1] 中。
这是因为如果 [1,N] 都出现了,那么答案是 N+1,否则答案是 [1,N] 中没有出现的最小正整数。
思路:
对数组进行遍历,对于遍历到的数 x,如果它在 [1,N] 的范围内,那么就将数组中的第 x−1 个位置(注意:数组下标从 0 开始)打上「标记」。在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 N+1,否则答案是最小的没有打上标记的位置加 1
如何设计这个「标记」呢?由于数组中的数没有任何限制,因此这并不是一件容易的事情。但我们可以继续利用上面的提到的性质:由于我们只在意 [1,N] 中的数,因此我们可以先对数组进行遍历,把不在 [1,N] 范围内的数修改成任意一个大于 N 的数(例如 N+1)。这样一来,数组中的所有数就都是正数了,因此我们就可以将「标记」表示为「负号」
算法的流程如下:
1. 将数组中所有小于等于 0 的数修改为 N+1;
2. 遍历数组中的每一个数 x,它可能已经被打了标记,因此原本对应的数为∣x∣。如果∣x∣∈[1,N],那么我们给数组中的第∣x∣−1 个位置的数添加一个负号。注意如果它已经有负号,不需要重复添加;
3. 在遍历完成之后,如果数组中的每一个数都是负数,那么答案是 N+1,否则答案是第一个正数的位置加 1。
*/
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; ++i) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
for (int i = 0; i < n; ++i) {
int num = Math.abs(nums[i]);
if (num <= n) {
nums[num - 1] = -Math.abs(nums[num - 1]);
}
}
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
return i + 1;
}
}
return n + 1;
}
}
六、矩阵
1. 矩阵置零
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
boolean[] r = new boolean[m];//辅助数组
boolean[] c = new boolean[n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n;j++){
if(matrix[i][j] == 0){
r[i] = true;
c[j] = true;
}
}
}
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(r[i] || c[j]){
matrix[i][j] = 0;
}
}
}
}
}
2. 螺旋矩阵
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
//四条边界
int left = 0;
int right = matrix[0].length - 1;
int top = 0;
int below = matrix.length - 1;
Integer[] res = new Integer[matrix.length * matrix[0].length];
int x = 0;
while(true){
for(int i = left; i <= right; i++){
res[x++] = matrix[top][i];
}
if(++top > below){
break;
}
for(int i = top; i <= below; i++){
res[x++] = matrix[i][right];
}
if(--right < left){
break;
}for(int i = right; i >= left; i--){
res[x++] = matrix[below][i];
}
if(--below < top){
break;
}
for(int i = below; i >= top; i--){
res[x++] = matrix[i][left];
}
if(++left > right){
break;
}
}
return Arrays.asList(res);
}
}
3. 旋转图像
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 第一步:创建一个新矩阵,计算旋转后的位置
int[][] matrix_new = new int[n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
// 核心公式:原矩阵的 [i][j] 旋转后到新矩阵的 [j][n-i-1]
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
// 第二步:将新矩阵复制回原矩阵(因为题目要求原地修改)
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix[i][j] = matrix_new[i][j];
}
}
}
}
4. 搜索二维矩阵Ⅱ
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
// 从左下角开始
// i 是行索引(从下往上),j 是列索引(从左往右)
int i = matrix.length - 1, j = 0;
// 边界条件:i 不能越过上边界,j 不能越过右边界
while(i >= 0 && j < matrix[0].length)
{
if(matrix[i][j] > target){
// 当前值太大,往上移动(当前列的所有下方元素都更大,可以排除)
i--;
}
else if(matrix[i][j] < target){
// 当前值太小,往右移动(当前行的所有左侧元素都更小,可以排除)
j++;
}
else{
// 找到目标
return true;
}
}
// 走出边界,说明不存在
return false;
}
}
七、链表
1. 相交链表
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
ListNode tmp = headA;
while(headA != null){
set.add(headA);
headA = headA.next;
}
while(headB != null){
if(set.contains(headB)){
return headB;
}
headB = headB.next;
}
return null;
}
}
2. 反转链表
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode pre = head;
ListNode cur = head.next;
ListNode tail = head;
while(tail.next != null){
tail = tail.next;
}
while(cur != null){
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
head.next = null;
return tail;
}
}
3. 环形链表Ⅱ
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
ListNode tmp = head;
while(tmp != slow){
tmp = tmp.next;
slow = slow.next;
}
return tmp;
}
}
return null;
}
}
4. 合并两个有序链表
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null){
return list2;
}
else if(list2 == null){
return list1;
}
else if(list1.val < list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}
else{
list2.next = mergeTwoLists(list2.next,list1);
return list2;
}
}
}
5. 两数相加
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode pre = new ListNode(0);//预先指针
ListNode cur = pre;
int carry = 0;
while(l1 != null || l2 != null){
int x = l1 == null? 0 : l1.val;
int y = l2 == null? 0 : l2.val;
int sum = x + y + carry;
carry = sum / 10;
sum = sum % 10;
cur.next = new ListNode(sum);
cur = cur.next;
if(l1 != null){
l1 = l1.next;
}
if(l2 != null){
l2 = l2.next;
}
}
if(carry != 0){
cur.next = new ListNode(carry);
}
return pre.next;
}
}
6. 删除链表的倒数第n个结点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode(0);//pre 预先指针
pre.next = head;
ListNode start = pre;
ListNode end = pre;
for(int i = 0; i < n;i++){
end = end.next;
}
while(end.next != null){
start = start.next;
end = end.next;
}
start.next = start.next.next;
return pre.next;
}
}
7. 两两交换链表中的结点
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null){//递归终止条件:为空/不足两个
return head;
}
ListNode newHead = head.next;
head.next = swapPairs(newHead.next);
newHead.next = head;
return newHead;
}
}
8. K个一组翻转链表
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode hair = new ListNode(0);
hair.next = head;
ListNode pre = hair;
ListNode tail = hair;
while(head != null){
for(int i = 0; i < k; i++){//判断剩余是否够k个
tail = tail.next;
if(tail == null){
return hair.next;
}
}
ListNode nex = tail.next;
//反转单个区间
ListNode[] reverse = myReverse(head,tail);
head = reverse[0];
tail = reverse[1];
//连接反转后的区间
pre.next = head;
tail.next = nex;
//移动指针解决下一个区间
pre = tail;
head = nex;
}
return hair.next;
}
private ListNode[] myReverse(ListNode head,ListNode tail){
ListNode prev = tail.next;
ListNode p = head;
while(tail != prev){
ListNode nex = p.next;
p.next = prev;
prev = p;
p = nex;
}
return new ListNode[]{tail,head};
}
}
9. 随机链表的复制
class Solution {
public Node copyRandomList(Node head) {
if(head == null){
return null;
}
//1.复制
Node cur = head;
while(cur != null){
Node copy = new Node(cur.val);
copy.next = cur.next;
cur.next = copy;
cur = cur.next.next;
}
//2.拷贝
cur = head;
while(cur != null){
if(cur.random != null){
cur.next.random = cur.random.next;
}
cur = cur.next.next;
}
//3.拆解
cur = head;
Node res = head.next;
Node tmp = head.next;
while(tmp.next != null){
cur.next = cur.next.next;
tmp.next = tmp.next.next;
cur = cur.next;
tmp = tmp.next;
}
//4.单独处理原链表尾结点
cur.next = null;
return res;
}
}
10. 排序链表
class Solution {
public ListNode sortList(ListNode head) {
//分割 递归
if(head == null || head.next == null){
return head;
}
//确定中点
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
//分割
ListNode tmp = slow.next;
slow.next = null;
//递归
ListNode left = sortList(head);
ListNode right = sortList(tmp);
//排序 合并
ListNode res = new ListNode(0);//预先指针
ListNode cur = res;//移动指针
while(left != null && right != null){//左右都不为空时
if(left.val < right.val){//左边小
cur.next = left;//cur指向小的
left = left.next;//左边next
}
else{
cur.next = right;//否则指向右边
right = right.next;//右边next
}
cur = cur.next;//指完左右其中一个后,自己再next
//循环,直到左右其中一边结束
}
cur.next = left != null? left : right;//左右哪边有剩余,cur再指向剩余的
return res.next;//返回预先指针next即可
}
}
11. 合并K个升序链表
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists == null || lists.length == 0){
return null;
}
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists,int left,int right){
if(left == right){
return lists[left];
}
int mid = left + (right - left) / 2;
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid+1, right);
return mergeTwoLists(l1, l2);
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2){
if(l1 == null){
return l2;
}
if(l2 == null){
return l1;
}
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2);//注意
return l1;
}else{
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
12. LRU缓存
class LRUCache {
class Node{//双链表结点结构
int key;
int value;
Node next;
Node pre;
Node(int key,int value){
this.key = key;
this.value = value;
}
}
private Map<Integer,Node> map = new HashMap<>();//哈希表
private Node dummyHead = new Node(0,0);
private Node dummyTail = new Node(0,0);
private int capacity;
public LRUCache(int capacity) {//构造器 初始化
this.capacity = capacity;
dummyHead.next = dummyTail;
dummyTail.pre = dummyHead;
}
public int get(int key) {
if(!map.containsKey(key)){
return -1;
}
else{
Node node = map.get(key);
remove(node);
update(node);
return node.value;
}
}
public void put(int key, int value) {
if(map.containsKey(key)){
Node node = map.get(key);
node.value = value;
remove(node);
update(node);
}
else{
Node node = new Node(key,value);
map.put(key,node);//放到map里
if(map.size() > capacity){//判断是否超过容量
map.remove(dummyTail.pre.key);
remove(dummyTail.pre);
}
update(node);
}
}
public void remove(Node node){//维持双链表
node.pre.next = node.next;
node.next.pre = node.pre;
}
public void update(Node node){//维持双链表
node.next = dummyHead.next;
dummyHead.next.pre = node;
node.pre = dummyHead;
dummyHead.next = node;
}
}
八、二叉树
1. 二叉树的中序遍历
//迭代 -- 一个栈stack(使用双端队列Deque创建)
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null){
return result;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()){
if(cur != null){//注意此处判断条件
stack.offerLast(cur);
cur = cur.left;
}
else{
cur = stack.pollLast();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
}
//递归
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(result, root);
return result;
}
private void inorder(List<Integer> result,TreeNode root){
if(root == null){
return;
}
inorder(result,root.left);
result.add(root.val);
inorder(result,root.right);
}
}
2. 二叉树的最大深度
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
else{
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;//返回左右两个子树较高的那个 加上当前层(1)
}
}
}
3. 翻转二叉树
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null){
return null;
}
TreeNode left = invertTree(root.right);
TreeNode right = invertTree(root.left);
root.left = left;
root.right = right;
return root;
}
}
4. 对称二叉树
/*
递归结束条件:
都为空指针则返回 true
只有一个为空则返回 false
递归过程:
判断两个指针当前节点值是否相等
判断 A 的右子树与 B 的左子树是否对称
判断 A 的左子树与 B 的右子树是否对称
短路:
在递归判断过程中存在短路现象,也就是做 与 操作时,如果前面的值返回 false 则后面的不再进行计算
时间复杂度:O(n)
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return isMirror(root, root);
}
private boolean isMirror(TreeNode t1, TreeNode t2){
if(t1 == null && t2 == null){
return true;
}
if(t1 == null || t2 == null){
return false;
}
return (t1.val == t2.val) && isMirror(t1.left, t2.right) && isMirror(t1.right, t2.left);
}
}
5. 二叉树的直径(任意两个节点之间 最长路径的 长度)
/*
1. 求直径(即求路径长度的最大值)等效于求路径经过节点数的最大值减1
2. 任意一条路径均可以被看作由某个节点为起点,从其左孩子和右孩子向下遍历的路径拼接得到
*/
class Solution {
int ans;//全局变量 -- 记录每个结点 路径经过节点数的最大值
public int diameterOfBinaryTree(TreeNode root) {
ans = 1;
depth(root);
return ans - 1;//返回结点数-1
}
private int depth(TreeNode node){
if(node == null){
return 0;// 访问到空节点了,返回0
}
int left = depth(node.left);// 左孩子为根的子树的深度
int right = depth(node.right);// 右孩子为根的子树的深度
ans = Math.max(ans, left + right + 1);// 计算当前结点的最大值(L+R+1) 并更新ans
return Math.max(left, right) + 1; // 返回该节点为根的子树的深度
}
}
6. 二叉树的层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if(root == null){
return result;
}
Queue<TreeNode> queue = new LinkedList<>();//用到队列Queue
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
List<Integer> layerResult = new ArrayList<>();
while(size-- > 0){
TreeNode cur = queue.poll();
layerResult.add(cur.val);
if(cur.left != null){
queue.offer(cur.left);
}
if(cur.right != null){
queue.offer(cur.right);
}
}
result.add(layerResult);
}
return result;
}
}
7. 将有序数组转换为二叉搜素树
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return helper(nums, 0, nums.length - 1);
}
private TreeNode helper(int[] nums,int left, int right){
if(left > right){
return null;
}
int mid = left + (right - left) / 2;//中序遍历,总是选择中间位置左边的数字作为根节点
TreeNode root = new TreeNode(nums[mid]);
root.left = helper(nums, left, mid - 1);
root.right = helper(nums, mid + 1, right);
return root;
}
}
8. 验证二叉搜索树
class Solution {
public boolean isValidBST(TreeNode root) {
return helper(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean helper(TreeNode root, long min, long max){
if(root == null){//终止条件
return true;
}
if(root.val <= min || root.val >= max){//如果当前结点的值不满足:大于左边&&小于右边
return false;//判false
}
return helper(root.left, min, root.val) && helper(root.right, root.val, max);
//左结点: 处于min ~ 当前结点值
//右结点: 处于当前结点值 ~ max
//二者都满足,返回true
}
}
9. 二叉搜索树中第K小的元素
class Solution {
public int kthSmallest(TreeNode root, int k) {//实际上考察:中序遍历 迭代法
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || !stack.isEmpty()){
while(root != null){
stack.push(root);
root = root.left;
}
root = stack.pop();//注意
k--;
if(k == 0){
break;
}
root = root.right;
}
return root.val;
}
}
10. 二叉树的右视图
class Solution {
public List<Integer> rightSideView(TreeNode root) {//解题方法:层序遍历--取每层最右边的即可
List<Integer> result = new ArrayList<>();
if(root == null){
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
while(size > 0){
TreeNode cur = queue.poll();
if(size == 1){//当本层只剩下一个结点的时候,把这个结点的值加到result里即可
result.add(cur.val);
}
if(cur.left != null){
queue.offer(cur.left);
}
if(cur.right != null){
queue.offer(cur.right);
}
size--;
}
}
return result;
}
}
11. 二叉树展开成链表
class Solution {//考察前序遍历
public void flatten(TreeNode root) {
List<TreeNode> list = new ArrayList<>();
preOrder(root, list);
for(int i = 0; i < list.size() - 1; i++){
TreeNode prev = list.get(i);
TreeNode tmp = list.get(i+1);
prev.right = tmp;
prev.left = null;
}
}
private void preOrder(TreeNode root, List<TreeNode> list){//递归 -- 前序遍历
if(root == null){
return;
}
list.add(root);
preOrder(root.left, list);
preOrder(root.right, list);
}
private void preOrder2(TreeNode root, List<TreeNode> list){//迭代 -- 前序遍历
Deque<TreeNode> stack = new LinkedList<>();
if(root == null){
return;
}
stack.offerLast(root);
while(!stack.isEmpty()){
TreeNode cur = stack.pollLast();
list.add(cur);
if(cur.right != null){
stack.offerLast(cur.right);
}
if(cur.left != null){
stack.offerLast(cur.left);
}
}
}
}
12. 从前序与中序遍历序列构造二叉树
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
Map<Integer,Integer> map = new HashMap<>();//哈希表 存储 结点值 对应的中序遍历【位置】
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i],i);
}
return helper(preorder,inorder,0,preorder.length - 1,0,inorder.length - 1,map);
}
private TreeNode helper(int[] preorder,int[] inorder,int preStart,int preEnd,int inStart,int inEnd,Map<Integer,Integer> map){
if(preStart > preEnd || inStart > inEnd){//递归终止条件
return null;
}
TreeNode root = new TreeNode(preorder[preStart]);
int i = map.get(preorder[preStart]);//当前结点在中序遍历中的位置
//中序遍历中,左子树范围:[inStart, i - 1] 左子树结点个数:i - inStart
//中序遍历中,右子树范围:[i + 1, inEnd]
//前序遍历中,左子树范围:[preStart + 1, preStart + i - inStart]
//前序遍历中,右子树范围:[preStart + i - inStart + 1, preEnd]
//preStart + i - inStart 是 前序遍历中 左子树的结束位置
//preStart + i - inStart + 1 则是 前序遍历中 右子树的开始位置
root.left = helper(preorder,inorder,preStart + 1,preStart + i - inStart,inStart,i - 1,map);
root.right = helper(preorder,inorder,preStart + i - inStart + 1,preEnd,i + 1,inEnd,map);
return root;
}
}
13. 路径总和Ⅲ
class Solution {
public int pathSum(TreeNode root, long targetSum) {
if (root == null) {
return 0;
}
//ret是一共有多少个 满足条件的 路径
int ret = rootSum(root, targetSum);//自定义函数来计算当前结点满足条件的个数
ret += pathSum(root.left, targetSum);//递归 左孩子 得出的结果计入ret
ret += pathSum(root.right, targetSum);//递归 有孩子 得出的结果计入ret
return ret;
}
public int rootSum(TreeNode root, long targetSum) {
int ret = 0;//ret是一共有多少个 满足条件的 路径
if (root == null) {
return 0;
}
int val = root.val;//取出当前结点的值
if (val == targetSum) {//如果值 == 目标值
ret++;//路径+1
}
ret += rootSum(root.left, targetSum - val);//递归左孩子 结果计入总和
//(注意:目标值要改为【原目标结点值targetSum - 当前结点值val】)
ret += rootSum(root.right, targetSum - val);//递归右孩子 同上
return ret;
}
}
14. 二叉树的最近公共祖先
/*
递归解析:
1.终止条件:
当越过叶节点,则直接返回 null ;
当 root 等于 p,q ,则直接返回 root ;
2.递推工作:
开启递归左子节点,返回值记为 left ;
开启递归右子节点,返回值记为 right ;
3. 返回值: 根据 left 和 right ,可展开为四种情况;
a.当 left 和 right 同时为空 :说明 root 的左 / 右子树中都不包含 p,q ,返回 null ;
b.当 left 和 right 同时不为空 :说明 p,q 分列在 root 的 异侧 (分别在 左 / 右子树),因此 root 为最近公共祖 先,返回 root ;
c.当 left 为空 ,right 不为空 :p,q 都不在 root 的左子树中,直接返回 right 。具体可分为两种情况:
- p,q 其中一个在 root 的 右子树 中,此时 right 指向 p(假设为 p );
- p,q 两节点都在 root 的 右子树 中,此时的 right 指向 最近公共祖先节点 ;
d.当 left 不为空 , right 为空 :与情况 c. 同理;
观察发现, 情况 a. 可合并至 c. 和 d. 内
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q){
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null && right == null){
return null;// a
}
if(left == null){
return right; // c
}
if(right == null){
return left; // d
}
return root; // b
}
}
15. 二叉树中的最大路径和
class Solution {
int maxSum = Integer.MIN_VALUE;//全局变量,初始设置为最小
public int maxPathSum(TreeNode root) {
gainMax(root);//自定义函数,在里面求maxSum即可
return maxSum;
}
private int gainMax(TreeNode root){
if(root == null){
return 0;
}
int left = Math.max(gainMax(root.left), 0);//左孩子的贡献
//如果比0小,就不用贡献了。。
int right = Math.max(gainMax(root.right), 0);//右孩子的贡献
//如果左+右+当前的值 大于当前的maxSum :更新maxSum即可
maxSum = Math.max(maxSum, left + right + root.val);
//返回给上一层的时候,要取 值最大 的一边返回,也就是:当前结点 + 左右两边更大的一个
return Math.max(left, right) + root.val;
}
}
九、图论
1. 岛屿数量
//本质上还是DFS 和 BFS
//DFS -- 递归
class Solution {
public static final int[][] directions = {{1,0}, {0,1}, {0,-1}, {-1,0}};
public int numIslands(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
boolean[][] isVisited = new boolean[m][n];//记录是否被访问过
int count = 0;//记录岛屿个数
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(!isVisited[i][j] && grid[i][j] == '1'){
count++;//如果没被访问过 并且 是陆地,则count+1
dfs(i,j,m,n,isVisited,grid);//然后把这片陆地遍历完
}
}
}
return count;
}
private void dfs(int i,int j,int m,int n,boolean[][] isVisited,char[][] grid){
if(i < 0 || i >= m || j < 0 || j >= n || isVisited[i][j] || grid[i][j] == '0'){
return;
}
isVisited[i][j] = true;
for(int[] direction : directions){
int x = direction[0] + i;
int y = direction[1] + j;
dfs(x,y,m,n,isVisited,grid);
}
}
}
//BFS -- 队列 + 迭代
class Solution {
public static final int[][] directions = {{1,0}, {0,1}, {0,-1}, {-1,0}};
public int numIslands(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
boolean[][] isVisited = new boolean[m][n];//记录是否被访问过
Queue<int[]> queue = new LinkedList<>();
int count = 0;//记录岛屿个数
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(!isVisited[i][j] && grid[i][j] == '1'){
count++;//如果没被访问过 并且 是陆地,则count+1
queue.offer(new int[]{i, j});
isVisited[i][j] = true;
while(!queue.isEmpty()){
int[] pos = queue.poll();
for(int[] direction : directions){
int x = pos[0] + direction[0];
int y = pos[1] + direction[1];
if(x < 0 || x >= m || y < 0 || y >= n || isVisited[x][y] || grid[x][y] == '0'){
continue;
}
queue.offer(new int[]{x ,y});
isVisited[x][y] = true;
}
}
}
}
}
return count;
}
}
2. 腐烂的橘子
class Solution {
public int orangesRotting(int[][] grid) {//必须使用BFS 结合队列 还有层序遍历的思想
int m = grid.length;
int n = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int freshCount = 0;
// 1. 初始化:统计新鲜橘子,腐烂橘子入队
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) {
queue.offer(new int[]{i, j});
} else if (grid[i][j] == 1) {
freshCount++;
}
}
}
// 如果没有新鲜橘子,直接返回0
if (freshCount == 0) return 0;
int minutes = 0;
int[][] directions = {{1,0}, {-1,0}, {0,1}, {0,-1}};
// 2. 多源BFS
while (!queue.isEmpty() && freshCount > 0) {
//注意判断条件:腐烂队列不为空 并且 freshCount要大于0(不然没得腐烂。。)
int size = queue.size();
// 按层处理:当前层的所有腐烂橘子同时感染
for (int i = 0; i < size; i++) {
int[] pos = queue.poll();
for (int[] dir : directions) {
int x = pos[0] + dir[0];
int y = pos[1] + dir[1];
// 感染新鲜橘子
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1) {
grid[x][y] = 2; // 变腐烂
freshCount--; // 新鲜橘子数减1
queue.offer(new int[]{x, y});
}
}
}
minutes++; // 处理完一层,时间+1
}
// 3. 检查是否还有新鲜橘子
return freshCount == 0 ? minutes : -1;
}
}
十、回溯
通用解法:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择 : 本层集合中的元素) {
处理节点;
backtracking(路径, 选择列表); // 递归
撤销处理; // 回溯
}
}
1. 全排列
class Solution{
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>(); // 存储所有结果
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num); // 将数组转为List,方便交换操作
}
int n = nums.length;
backtrack(n, output, res, 0); // 从第0个位置开始填数
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 终止条件:所有位置都填完了
if (first == n) {
res.add(new ArrayList<Integer>(output)); // 复制当前排列到结果
return;
}
for (int i = first; i < n; i++) {
// 1. 做选择:将第i个元素放到第first个位置
Collections.swap(output, first, i);
// 2. 进入下一层决策树
backtrack(n, output, res, first + 1);
// 3. 撤销选择:恢复原状(回溯)
Collections.swap(output, first, i);
}
}
}
2. 子集
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if(nums == null || nums.length == 0){
return result;
}
find(nums,0,false,new ArrayList<>(),result);
return result;
}
private void find(int[] nums,int index,boolean preSelected,List<Integer> cur,List<List<Integer>> result){
// 1. 终止条件:所有元素处理完毕
if(index == nums.length){
result.add(new ArrayList<>(cur));// 复制当前子集,加入结果
return;
}
// 2. 【不选】当前元素,继续递归
find(nums,index + 1,false,cur,result);
// 3. 去重判断(针对有重复元素的情况)
if(index > 0 && nums[index - 1] == nums[index] && !preSelected){
return;// 跳过,避免重复子集
}
// 4. 【选】当前元素
cur.add(nums[index]);// 加入当前元素
find(nums,index + 1,true,cur,result);// 标记"已选",继续递归
cur.remove(cur.size() - 1);// 回溯,撤销选择
}
}
3. 电话号码的字母组合
class Solution {
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if(digits == null || digits.length() == 0){
return res;
}
// 建立数字到字母的映射表
Map<Character, String> map = new HashMap<>();
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
// cur: 当前构建的字母组合(路径)
StringBuffer cur = new StringBuffer();
// 从第0个数字开始回溯
find(0, digits, cur, res, map);
return res;
}
/**
* 回溯函数
* @param index 当前处理到第几个数字(递归深度/层级)⭐关键:决定cur中删除的位置
* @param digits 输入的数字字符串
* @param cur 当前已构建的字母组合(StringBuffer长度始终等于index)
* @param res 存储所有结果的列表
* @param map 数字-字母映射表
*/
private void find(int index, String digits, StringBuffer cur, List<String> res, Map<Character, String> map){
// 终止条件:处理完所有数字,将当前组合加入结果
if(index == digits.length()){
res.add(cur.toString());
return;
}
// 取出当前要处理的数字
char c = digits.charAt(index);
// 获取该数字对应的所有可选字母
String s = map.get(c);
// 遍历当前数字的每个可选字母
// i: 在当前字母表中的索引(0,1,2...)⭐注意:i只用于取字母,不用于删除!
for(int i = 0; i < s.length(); i++){
// 做选择:将当前字母加入路径末尾
// 此时cur长度变为 index+1
cur.append(s.charAt(i));
// 递归:处理下一个数字(深度+1)
find(index + 1, digits, cur, res, map);
// 撤销选择(回溯):删除当前层的字母
// ⭐⭐⭐ 必须用index(当前深度),不能用i(字母表索引)⭐⭐⭐
// 原因:cur中第index位置存放的就是当前数字选的字母
cur.deleteCharAt(index);
}
}
}
4. 组合总和
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
// 判空处理,防止 NPE
if(candidates == null || candidates.length == 0){
return res;
}
// 排序是剪枝的前提!不排序无法提前终止循环
Arrays.sort(candidates);
find(0, candidates, target, new ArrayList<>(), res);
return res;
}
/**
* 【核心方法】回溯 + 深度优先搜索
*
* @param index 当前从哪个位置开始选(避免重复组合)
* @param candidates 候选数组(已排序)
* @param target 剩余需要凑的目标值
* @param cur 当前已选的路径(引用,会修改)
* @param res 最终结果集
*/
private void find(int index, int[] candidates, int target,
List<Integer> cur, List<List<Integer>> res){
// 终止条件:正好凑齐 target
// 注意:这里只判断 == 0,因为 < 0 的情况被剪枝了
if(target == 0){
// 必须 new ArrayList<>(cur)!
// 如果直接 add(cur),后续 cur 修改会影响已加入的结果
res.add(new ArrayList<>(cur));
return;
}
// 从 index 开始,不是从 0 开始!
// 这样可以保证组合不重复([2,3] 和 [3,2] 视为同一个)
for(int i = index; i < candidates.length; i++){
// 剪枝!因为数组已排序,当前太大则后面的都大
if(target - candidates[i] < 0){
break; // 用 break 不是 continue,后面的都不用看了
}
// 【回溯步骤1】做选择:将当前数加入路径
cur.add(candidates[i]);
// target 要减去 candidates[i](不是传原来的 target!)
find(i, candidates, target - candidates[i], cur, res);
// 【回溯步骤2】撤销选择:移除最后一个,恢复状态
cur.remove(cur.size() - 1);
}
}
}
5. 括号生成
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>(); // ① 结果列表,存储所有有效组合
find("", n, n, res); // ② 启动递归:空字符串,n个左括号,n个右括号
return res; // ⑥ 返回最终结果
}
/**
* 递归生成括号
* @param cur 当前已构建的字符串
* @param left 剩余可用的左括号数量 '('
* @param right 剩余可用的右括号数量 ')'
* @param res 结果列表(引用传递,所有递归共享)
*/
private void find(String cur, int left, int right, List<String> res){
// ③ 递归终止条件:左右括号都用完了,得到一个有效组合
if(left == 0 && right == 0){
res.add(new String(cur)); // new String() 创建副本,避免引用问题(虽然这里cur是局部变量)
return;
}
// ④ 情况一:剩余左右括号数量相等
// 此时必须用左括号,用右括号会导致非法(右括号会多于左括号)
if(left == right){
find(cur + '(', left - 1, right, res); // 消耗一个左括号
// ⑤ 情况二:剩余左括号少于右括号(已用的左括号更多)
// 此时左右括号都可以用(只要还有剩余)
}else if(left < right){
// ⑤-1:还有左括号可用,可以尝试用左括号
if(left > 0){
find(cur + '(', left - 1, right, res);
}
// ⑤-2:用右括号(肯定合法,因为left < right保证了已用左括号更多)
find(cur + ')', left, right - 1, res);
}
}
}
6. 单词搜索
class Solution {
// 定义四个搜索方向:下、上、右、左(对应二维数组的行和列变化)
public static final int[][] directions = {{1,0}, {-1,0}, {0,1}, {0,-1}};
/**
* 主入口方法:判断单词是否存在于二维字符网格中
* @param board 二维字符网格
* @param word 要搜索的单词
* @return 如果存在返回true,否则返回false
*/
public boolean exist(char[][] board, String word) {
int m = board.length; // 网格的行数
int n = board[0].length; // 网格的列数
// 创建访问标记数组,防止重复使用同一个单元格
boolean[][] isVisited = new boolean[m][n];
// 遍历网格中的每个单元格作为起点
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
// 从当前位置(i,j)开始深度优先搜索
boolean flag = dfs(i, j, m, n, board, isVisited, word, 0);
if(flag){
return true; // 找到匹配路径,直接返回true
}
}
}
return false; // 所有起点都尝试完毕,未找到匹配路径
}
/**
* 深度优先搜索(DFS)辅助方法
* @param i 当前行坐标
* @param j 当前列坐标
* @param m 网格总行数
* @param n 网格总列数
* @param board 二维字符网格
* @param isVisited 访问标记数组
* @param word 要搜索的单词
* @param k 当前需要匹配的字符在word中的索引
* @return 是否找到从当前位置开始的完整匹配路径
*/
private boolean dfs(int i, int j, int m, int n, char[][] board,
boolean[][] isVisited, String word, int k){
// 剪枝:当前字符不匹配,直接返回false
if(board[i][j] != word.charAt(k)){
return false;
}
// 终止条件:已匹配到最后一个字符,说明找到了完整单词
if(k == word.length() - 1){
return true;
}
// 标记当前单元格为已访问,防止后续搜索重复使用
isVisited[i][j] = true;
boolean res = false; // 记录搜索结果
// 向四个方向扩展搜索
for(int[] direction : directions){
int x = direction[0] + i; // 计算新行坐标
int y = direction[1] + j; // 计算新列坐标
// 边界检查:越界或已访问的单元格跳过
if(x < 0 || x >= m || y < 0 || y >= n || isVisited[x][y]){
continue;
}
// 递归搜索下一个字符
boolean flag = dfs(x, y, m, n, board, isVisited, word, k + 1);
if(flag){
res = true; // 找到匹配路径
break; // 提前退出循环
}
}
// 回溯:撤销当前单元格的访问标记,允许其他路径使用
// 这是DFS回溯的关键,保证不同起点/路径可以重复使用单元格
isVisited[i][j] = false;
return res;
}
}
7. 分割回文串
class Solution {
public List<List<String>> partition(String s) {
// 结果集:存储所有满足条件的分割方案
List<List<String>> res = new ArrayList<>();
// 边界情况处理:空字符串或null直接返回空结果
if(s == null || s.length() == 0){
return res;
}
// 将字符串转为字符数组,方便后续索引访问
char[] arr = s.toCharArray();
// ============================================
// 第一步:动态规划预处理 - 构建回文判断表
// dp[left][right] = true 表示 s[left..right] 是回文串
// ============================================
boolean[][] dp = new boolean[s.length()][s.length()];
// 枚举所有子串的右端点
for(int right = 0; right < s.length(); right++){
// 枚举所有可能的左端点(left <= right,保证子串有效)
for(int left = 0; left <= right; left++){
// 判断 s[left..right] 是否为回文:
// 条件1: arr[left] == arr[right] - 首尾字符相同
// 条件2: (right - left <= 2) - 子串长度<=3,中间最多1个字符,必是回文
// 或者 dp[left+1][right-1] - 去掉首尾后的子串也是回文
if(arr[left] == arr[right] && (right - left <= 2 || dp[left + 1][right - 1])){
dp[left][right] = true;
}
}
}
// 至此,dp表构建完成,可以在 O(1) 时间内判断任意子串是否为回文
// ============================================
// 第二步:深度优先搜索(DFS) - 枚举所有分割方案
// ============================================
// cur: 当前正在构建的分割方案(路径)
List<String> cur = new ArrayList<>();
// 从索引0开始搜索
dfs(0, s, dp, cur, res);
return res;
}
/**
* 深度优先搜索:尝试从 index 位置开始的所有可能分割
* @param index 当前待处理的起始位置
* @param s 原字符串
* @param dp 回文判断表
* @param cur 当前路径(已选择的回文子串列表)
* @param res 最终结果集
*/
private void dfs(int index, String s, boolean[][] dp, List<String> cur, List<List<String>> res){
// 递归终止条件:已经处理完所有字符
// 说明找到了一个完整的分割方案,将其加入结果集
if(index == s.length()){
// 必须新建ArrayList,避免后续回溯修改影响已保存的结果
res.add(new ArrayList<>(cur));
return;
}
// 枚举所有可能的结束位置 i
// 尝试将 s[index..i] 作为一个回文子串加入当前方案
for(int i = index; i < s.length(); i++){
// 只有当 s[index..i] 是回文串时才考虑这种分割方式
if(dp[index][i]){
// 做选择:将回文子串加入当前路径
cur.add(s.substring(index, i + 1));
// 递归:从 i+1 位置继续分割剩余字符串
dfs(i + 1, s, dp, cur, res);
// 撤销选择:回溯,移除最后加入的子串,尝试其他分割方式
cur.remove(cur.size() - 1);
}
}
// 循环结束,所有以 index 开头的回文子串都已尝试完毕
}
}
十一、二分查找
1. 搜索插入位置
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == target){
return mid;
}else if(nums[mid] > target){
right = mid - 1;
}else{
left = mid + 1;
}
}
return left;//查找结束如果没有相等值则返回 left,该值为插入位置
}
}
2. 搜索二维矩阵
class Solution {
/**
* 主入口方法:在二维矩阵中搜索目标值
* 策略:先通过行搜索确定target可能在哪一行,再在该行中进行列搜索
*/
public boolean searchMatrix(int[][] matrix, int target) {
// 第一步:在第一列中进行二分查找,找到target可能所在的行索引
int rowIndex = rs(matrix, target);
// 如果返回 -1,说明target比第一行的第一个元素还小,肯定不存在
if(rowIndex < 0){
return false;
}
// 第二步:在确定的行中进行二分查找,判断target是否存在
return cs(matrix, rowIndex, target);
}
/**
* rs = Row Search(行搜索)
* 在第一列中进行二分查找,找到最后一个满足 matrix[row][0] <= target 的行索引
*
* 为什么找"最后一个 <= target"?
* 因为每行的第一个元素大于前一行的最后一个元素,所以target只可能出现在
* 某个行首 <= target 且下一行行首 > target 的那一行中
*/
private int rs(int[][] matrix, int target){
// low初始化为-1:处理target小于matrix[0][0]的情况
// 如果最终low保持-1,说明target比第一行第一个元素还小
int low = -1;
// high指向最后一行的索引
int high = matrix.length - 1;
// 循环条件:low < high
// 当low == high时,找到答案
while(low < high){
// 计算中点,使用 (high - low + 1) / 2 实现上取整
// 上取整是为了避免死循环(当low和high相邻时)
// 例如:low=0, high=1,如果下取整mid=0,可能陷入死循环
int mid = low + (high - low + 1) / 2;
// 比较当前行的第一个元素与target
if(matrix[mid][0] <= target){
// mid行可能是答案,或者答案在更下面
// 所以将low移动到mid(保留mid作为候选)
low = mid;
}else{
// mid行的第一个元素已经大于target,答案只可能在上面
// 所以将high移动到mid-1(排除mid)
high = mid - 1;
}
}
// 返回最终确定的行索引
// 可能是-1(不存在),也可能是0到matrix.length-1之间的值
return low;
}
/**
* cs = Column Search(列搜索)
* 在指定行的所有列中进行二分查找,判断target是否存在
*/
private boolean cs(int[][] matrix, int rowIndex, int target){
// 左指针指向该行第一个元素
int left = 0;
// 右指针指向该行最后一个元素
int right = matrix[0].length - 1;
// 标准二分查找:当左右指针不交叉时继续
while(left <= right){
// 计算中点,使用 (right - left) / 2 实现下取整
// 标准写法,避免整数溢出
int mid = left + (right - left) / 2;
// 获取当前中点位置的值
int midVal = matrix[rowIndex][mid];
if(midVal < target){
// 中点值太小,target在右半边
left = mid + 1;
}else if(midVal > target){
// 中点值太大,target在左半边
right = mid - 1;
}else{
// 找到目标值!
return true;
}
}
// 循环结束仍未找到,target不存在于该行
return false;
}
}
3. 在排序数组中查找第一个和最后一个位置
class Solution {
public int[] searchRange(int[] nums, int target) {
// 初始化左右指针,用于二分查找的范围
int left = 0;
int right = nums.length - 1;
// first 用于记录目标值的第一个出现位置,初始值为-1表示未找到
int first = -1;
// last 用于记录目标值的最后一个出现位置,初始值为-1表示未找到
int last = -1;
// ========== 第一轮二分查找:寻找目标值的第一个位置 ==========
// 当左指针不超过右指针时,继续查找
while(left <= right){
// 计算中间位置,使用 left + (right - left) / 2 防止整数溢出
int mid = left + (right - left) / 2;
if(nums[mid] == target){
// 找到目标值,记录当前位置
first = mid;
// 继续在左半部分查找,看是否有更早出现的目标值
// 因为要找第一个位置,所以将右边界移到 mid - 1
right = mid - 1;
}else if(nums[mid] > target){
// 中间值大于目标值,目标值在左半部分
right = mid - 1;
}else{
// 中间值小于目标值,目标值在右半部分
left = mid + 1;
}
}
// ========== 重置指针,进行第二轮二分查找 ==========
left = 0;
right = nums.length - 1;
// ========== 第二轮二分查找:寻找目标值的最后一个位置 ==========
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == target){
// 找到目标值,记录当前位置
last = mid;
// 继续在右半部分查找,看是否有更晚出现的目标值
// 因为要找最后一个位置,所以将左边界移到 mid + 1
left = mid + 1;
}else if(nums[mid] > target){
// 中间值大于目标值,目标值在左半部分
right = mid - 1;
}else{
// 中间值小于目标值,目标值在右半部分
left = mid + 1;
}
}
// 返回结果数组,包含第一个和最后一个位置
// 如果目标值不存在,first 和 last 保持为 -1
return new int[]{first, last};
}
}
4. 搜索旋转排序数组
class Solution {
public int search(int[] nums, int target) {
// 初始化左右指针,分别指向数组的首尾元素
int left = 0;
int right = nums.length - 1;
// 标准二分查找框架:当左指针不超过右指针时继续循环
while(left <= right){
// 计算中间位置,使用 left + (right - left) / 2 防止整数溢出
int mid = left + (right - left) / 2;
// 找到目标值,直接返回索引
if(nums[mid] == target){
return mid;
}
// 关键判断:确定 mid 位于哪个有序区间
// 如果 nums[0] <= nums[mid],说明 [left, mid] 是有序的(未旋转的部分)
if(nums[0] <= nums[mid]){
// 在左半部分有序区间内,判断 target 是否落在 [nums[0], nums[mid]) 范围内
// 注意这里是开区间,因为 nums[mid] != target 已经在上面判断过了
if(nums[0] <= target && nums[mid] > target){
// target 在左半部分有序区间内,收缩右边界
right = mid - 1;
} else {
// target 不在左半部分,去右半部分查找
left = mid + 1;
}
}
// 否则,[mid, right] 是有序的(右半部分是有序区间)
else {
// 在右半部分有序区间内,判断 target 是否落在 (nums[mid], nums[nums.length-1]] 范围内
if(nums[mid] < target && nums[nums.length - 1] >= target){
// target 在右半部分有序区间内,收缩左边界
left = mid + 1;
} else {
// target 不在右半部分,去左半部分查找
right = mid - 1;
}
}
}
// 未找到目标值,返回 -1
return -1;
}
}
5. 搜索旋转排序数组中的最小值
class Solution {//数组不包含重复元素
public int findMin(int[] nums) {
// 初始化左右指针,left指向数组开头,right指向数组末尾
int left = 0;
int right = nums.length - 1;
// 二分查找:当左指针小于右指针时继续循环
while(left < right){
// 计算中间位置,防止整数溢出
int mid = left + (right - left) / 2;
// 关键判断:比较中间元素与最右端元素
if(nums[mid] < nums[right]){
// 情况1:mid元素小于right元素
// 说明[mid, right]这段是有序的(递增)
// 最小值不可能在mid右侧,只能在[left, mid]中
right = mid;
}else{
// 情况2:mid元素大于right元素(由于mid = (left + right) / 2,只要left != right,mid就不可能等于right)
// 说明在mid右侧存在"断崖"
// 即[mid, right]不是完全递增的,最小值在右侧
left = mid + 1;
}
}
return nums[left];
}
}
十二、栈
1. 有效的括号
class Solution {
public boolean isValid(String s) {
Map<Character, Character> map = new HashMap<>();
map.put('(',')');
map.put('[',']');
map.put('{','}');
Deque<Character> stack = new LinkedList<>();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(map.containsKey(c)){
stack.offerLast(c);//如果是左括号,放入栈
}else if(!stack.isEmpty() && c == map.get(stack.peekLast())){
//如果是右括号、而且栈里有东西:如果这个 右括号 与 从栈里最上面的左括号 相匹配
//就把左括号出栈 -- 即完成一次匹配
stack.pollLast();
}else{
return false;//其他情况就说明:括号不有效,返回false即可
}
}
return stack.isEmpty();//如果s已经遍历完了,栈里不为空(有未匹配的左括号),则返回false;栈为空说明全部匹配上了,括号有效,返回true
}
}
2. 最小栈
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() || val <= minStack.peek()){//如果最小栈为空,或者 要存的值小于当前栈中的最小值
minStack.push(val);
}
}
public void pop() {
int val = stack.pop();
if(minStack.peek() == val){//如果推出的值是最小值
minStack.pop();//也在最小栈里推出这个最小值
}
}
public int top() {
return stack.peek();
}
public int getMin() {//O(1)常数级时间复杂度
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();
*/
3. 字符串解码
class Solution {
public String decodeString(String s) {
// 使用双栈法解码字符串
// stack_res: 存储字符串结果的栈,用于保存 '[' 之前的字符串
// stack_num: 存储重复次数的栈,用于保存 '[' 之前的数字
Deque<String> stack_res = new LinkedList<>();
Deque<Integer> stack_num = new LinkedList<>();
// res: 当前正在构建的字符串结果
StringBuilder res = new StringBuilder();
// num: 当前正在解析的数字(用于处理多位数,如 "100[a]")
int num = 0;
// 遍历输入字符串的每个字符
for(char c : s.toCharArray()){
// 情况1: 当前字符是数字
if(c >= '0' && c <= '9'){
// 处理多位数字符串,如 "100"
// 之前的数字左移一位(乘以10),再加上当前数字
num = num * 10 + (c - '0');
}
// 情况2: 当前字符是字母
else if(c >= 'a' && c <= 'z'){
// 直接将字母追加到当前结果字符串中
res.append(c);
}
// 情况3: 当前字符是 '['
else if(c == '['){
// 遇到左括号,表示一个新的编码段开始
// 将当前累积的数字(重复次数)压入数字栈
stack_num.offerLast(num);
// 将当前累积的字符串压入字符串栈(保存 '[' 之前的内容)
stack_res.offerLast(res.toString());
// 重置数字和结果字符串,准备解析括号内的内容
num = 0;
res = new StringBuilder();
}
// 情况4: 当前字符是 ']'
else{
// 遇到右括号,表示一个编码段结束,需要解码
// 从数字栈弹出重复次数
int curNum = stack_num.pollLast();
// 从字符串栈弹出 '[' 之前的字符串
String lastRes = stack_res.pollLast();
// 构建重复字符串:将当前结果重复 curNum 次
StringBuilder tmp = new StringBuilder();
for (int i = 0; i < curNum; i++)
tmp.append(res);
// 将 '[' 之前的字符串 + 重复后的字符串,作为新的当前结果
// 这实现了字符串的拼接,处理嵌套情况
res = new StringBuilder(lastRes + tmp);
}
}
// 遍历结束后,res 中存储的就是解码后的完整字符串
return res.toString();
}
}
4. 每日温度
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
// 使用双端队列作为栈,存储还没找到"下一个更大温度"的那些天的下标
// 栈底到栈顶(或从队首到队尾)保持温度单调递减
Deque<Integer> stack = new LinkedList<>();
// 结果数组,默认值为0
// 如果某天后没有更高的温度,就保持为0,正好符合题意
int[] res = new int[temperatures.length];
// 遍历每一天的温度
for(int i = 0; i < temperatures.length; i++){
// 关键逻辑:当前温度比栈顶那天的温度高时
// 说明栈顶那天等到了"下一个更高温度",就是今天!
while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peekLast()]){
// 弹出栈顶的下标(即之前没找到答案的那天)
int preIndex = stack.pollLast();
// 计算等待天数:今天下标 - 那天的下标
res[preIndex] = i - preIndex;
}
// 当前天的下标入栈,等待后面找它的"下一个更高温度"
// 使用 offerLast 相当于 push(加到栈顶/队尾)
stack.offerLast(i);
}
// 栈中剩余的下标,都是后面没有更高温度的天
// res 中对应位置保持默认的 0,正好符合题意
return res;
}
}
十三、堆
1. 数组中的第k个最大元素
class Solution {
/**
* 查找数组中第K大的元素
* 核心思想:构建大顶堆,然后执行k-1次"弹出最大值"操作,最后堆顶即为第K大元素
*
* @param nums 输入数组
* @param k 要找第K大的元素(1 <= k <= nums.length)
* @return 第K大的元素
*/
public int findKthLargest(int[] nums, int k) {
int heapSize = nums.length; // 初始化堆的大小为数组长度
// 第一步:构建大顶堆,使数组满足堆的性质(父节点 >= 子节点)
bulidMaxHeap(nums, heapSize);
// 第二步:执行k-1次"弹出最大值"操作
// 每次将堆顶(当前最大值)与数组末尾元素交换,然后堆大小减1,再调整堆
// 循环结束后,堆顶就是第K大的元素
for(int i = nums.length - 1; i >= nums.length - k + 1; i--){
swap(nums, 0, i); // 将堆顶(最大值)交换到数组末尾(已排序区域)
heapSize--; // 堆大小减1,排除已排序的最大值
maxHeapify(nums, 0, heapSize); // 从新的堆顶开始,重新调整堆结构
}
return nums[0]; // 返回堆顶元素,即第K大的元素
}
/**
* 构建大顶堆
* 从最后一个非叶子节点开始,自底向上对每个非叶子节点执行下沉调整
*
* @param nums 待构建堆的数组
* @param heapSize 堆的大小
*/
private void bulidMaxHeap(int[] nums, int heapSize){
// 最后一个非叶子节点的下标 = 堆大小/2 - 1
// 从这个节点开始向前遍历到根节点,依次调整
for(int i = nums.length / 2 - 1; i >= 0; i--){
maxHeapify(nums, i, heapSize); // 对每个非叶子节点执行堆调整
}
}
/**
* 堆调整(下沉操作)
* 假设当前节点的左右子树都是堆,但当前节点可能不满足堆性质,通过下沉恢复
*
* @param nums 堆数组
* @param i 当前需要调整的下标
* @param heapSize 当前堆的大小(有效元素范围)
*/
private void maxHeapify(int[] nums, int i, int heapSize){
int left = i * 2 + 1; // 左子节点下标:父节点下标*2+1
int right = i * 2 + 2; // 右子节点下标:父节点下标*2+2
int largest = i; // 假设当前节点是最大值
// 比较左子节点:如果左子节点存在且大于当前最大值,更新最大值下标
if(left < heapSize && nums[left] > nums[largest]){
largest = left;
}
// 比较右子节点:如果右子节点存在且大于当前最大值,更新最大值下标
if(right < heapSize && nums[right] > nums[largest]){
largest = right;
}
// 如果最大值不是当前节点,需要交换并递归调整
if(largest != i){
swap(nums, largest, i); // 将最大值交换到当前节点位置
// 递归调整:被交换到子节点位置的元素可能破坏子树的堆性质
maxHeapify(nums, largest, heapSize);
}
}
private void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
十四、贪心算法
1. 买卖股票的最佳时机
class Solution {
public int maxProfit(int[] prices) {
int maxpf = 0;
int minpr = Integer.MAX_VALUE;
for(int price : prices){
minpr = Math.min(price, minpr);
maxpf = Math.max(maxpf, price - minpr);
}
return maxpf;
}
}
2. 跳跃游戏
class Solution {
public boolean canJump(int[] nums) {
// rightMost 表示当前能够到达的最远位置
int rightMost = 0;
// 遍历数组中的每个位置
for(int i = 0; i < nums.length; i++){
// 关键判断:如果当前位置 i 超出了能到达的最远位置 rightMost
// 说明这个位置无法到达,直接跳过(或返回 false,但这里用 continue 逻辑)
// 实际上如果 i > rightMost,后面的位置也都到不了,可以直接返回 false
if(i <= rightMost){
// 在当前可达位置范围内,更新最远能到达的位置
// i + nums[i] 表示从位置 i 出发能跳到的最远位置
rightMost = Math.max(rightMost, i + nums[i]);
// 提前终止条件:如果最远能到达的位置已经超过或等于数组最后一个下标
// 说明可以到达终点,直接返回 true
if(rightMost >= nums.length - 1){
return true;
}
}
// 如果 i > rightMost,说明当前位置不可达,继续循环(实际上可以直接 break)
}
// 遍历结束后,检查最终是否能到达最后一个位置
// 实际上如果循环正常结束,说明 rightMost < nums.length - 1,返回 false
return false;
}
}
3. 跳跃游戏Ⅱ
class Solution {
/**
* 跳跃游戏 II:计算到达数组最后一个位置的最少跳跃次数
* 使用贪心算法,每次选择能跳得最远的下一步位置
*
* @param nums 每个元素表示在该位置可以跳跃的最大长度
* @return 到达最后一个位置的最少跳跃次数
*/
public int jump(int[] nums) {
// 记录跳跃次数
int steps = 0;
// 当前所在位置的索引
int index = 0;
// 当还没有到达最后一个位置时继续循环
while(index < nums.length - 1){
// 准备进行一次跳跃
steps++;
// 贪心策略:如果当前位置能直接跳到或越过终点,直接返回结果
// nums[index] 表示当前能跳的最大距离,加上当前索引判断是否可达终点
if(nums[index] + index >= nums.length - 1){
return steps;
}
// 当前能到达的最远边界(从当前位置出发,一步能到达的范围)
int border = nums[index] + index;
// 在可达范围内,寻找"下一步位置 + 该位置能跳的距离"最大的那个点
// 这是贪心策略的核心:选择能让下一步跳得最远的落点
int max = 0; // 记录最大可达距离(i + nums[i])
int chooseIndex = index + 1; // 记录最优的下一步位置
// 遍历当前能到达的所有位置,选择最优的下一步
for(int i = index + 1; i <= border; i++){
// 计算从位置i出发能到达的最远位置(i + nums[i])
if(nums[i] + i > max){
max = nums[i] + i; // 更新最大可达距离
chooseIndex = i; // 更新最优落点
}
}
// 跳转到选择的最优位置
index = chooseIndex;
}
// 如果数组长度为1,已经在终点,返回0
return 0;
}
}
4. 划分字母区间
class Solution {
public List<Integer> partitionLabels(String s) {
// 1. 预处理:记录每个字母最后出现的位置
// last[0] 对应 'a',last[1] 对应 'b',以此类推
int[] last = new int[26];
for(int i = 0; i < s.length(); i++){
// s.charAt(i) - 'a' 将字符转换为 0-25 的索引
// 不断更新,最终得到每个字母的最远出现位置
last[s.charAt(i) - 'a'] = i;
}
// 2. 贪心划分区间
int start = 0; // 当前片段的起始位置
int end = 0; // 当前片段的结束位置(动态扩展)
List<Integer> partition = new ArrayList<>();
for(int i = 0; i < s.length(); i++){
// 关键:当前片段必须包含 s[i] 这个字母的最后出现位置
// 所以 end 要取最大值,不断向右扩展边界
end = Math.max(end, last[s.charAt(i) - 'a']);
// 当遍历位置 i 到达当前片段的边界 end 时
// 说明 [start, end] 范围内的所有字母都不会出现在后面
// 可以作为一个完整的片段切分出来
if(i == end){
// 记录当前片段的长度
partition.add(end - start + 1);
// 下一个片段从当前 end 的下一个位置开始
start = end + 1;
}
}
return partition;
}
}
十五、动态规划
1. 爬楼梯
class Solution {
/**
* 爬楼梯问题:计算爬到第 n 阶楼梯的不同方法数
* 规则:每次可以爬 1 阶或 2 阶
* 动态规划:f(n) = f(n-1) + f(n-2)(斐波那契数列)
*
* @param n 楼梯总阶数
* @return 爬到第 n 阶的不同方法总数
*/
public int climbStairs(int n) {
// 三个滚动变量,用于存储相邻的三个状态
// 空间复杂度优化为 O(1),无需数组存储所有状态
int p = 0; // 代表 f(i-2):上上阶的方法数,初始为 0(第0阶之前)
int q = 0; // 代表 f(i-1):上一阶的方法数,初始为 0(还没开始爬)
int r = 1; // 代表 f(i):当前阶的方法数,初始为 1(地面有一种"虚拟"方法)
// 循环 n 次,i 从 0 到 n-1,每次迭代计算一阶的方法数
for(int i = 0; i < n; i++){
// 滚动更新:三个变量依次向后推移
p = q; // p 继承 q 的值:f(i-2) ← f(i-1) 的旧值
// 即:上上阶 ← 上一阶
q = r; // q 继承 r 的值:f(i-1) ← f(i) 的旧值
// 即:上一阶 ← 当前阶
r = p + q; // r 计算新的当前阶:f(i) = f(i-2) + f(i-1)
// 即:当前阶方法数 = 上上阶方法数 + 上一阶方法数
}
// 循环结束后,r 存储的就是第 n 阶的方法数 f(n)
return r;
}
}
2. 杨辉三角
class Solution {
/**
* 生成杨辉三角(帕斯卡三角形)
* @param numRows 需要生成的行数
* @return 包含杨辉三角前 numRows 行的二维列表
*/
public List<List<Integer>> generate(int numRows) {
// 存储最终结果的二维列表,每个元素是一行
List<List<Integer>> res = new ArrayList<>();
// 外层循环:逐行构建,i 表示当前行索引(从0开始)
for(int i = 0; i < numRows; i++){
// 创建当前行的列表,用于存储第 i 行的 i+1 个元素
List<Integer> row = new ArrayList<>();
// 内层循环:填充当前行的每个元素,j 表示当前列索引
// 第 i 行共有 i+1 个元素,所以 j 的范围是 [0, i]
for(int j = 0; j <= i; j++){
// 边界判断:每行的第一个元素(j==0)和最后一个元素(j==i)都是1
if(j == 0 || j == i){
row.add(1);
}else{
// 中间元素:等于上一行中左上方和正上方两个元素之和
// res.get(i - 1) 获取上一行(第 i-1 行)
// .get(j - 1) 获取上一行中左上方元素(第 j-1 列)
// .get(j) 获取上一行中正上方元素(第 j 列)
row.add(res.get(i - 1).get(j - 1) + res.get(i - 1).get(j));
}
}
// 将构建完成的当前行加入结果集
res.add(row);
}
// 返回包含所有行的二维列表
return res;
}
}
3. 打家劫舍
class Solution {
public int rob(int[] nums) {
// 定义dp数组,dp[i] 表示抢劫前i个房屋能获得的最大金额
// 数组长度为 nums.length + 1,因为dp[0]表示没有房屋的情况
int[] dp = new int[nums.length + 1];
// dp[0] = 0:没有房屋可抢,金额为0
dp[0] = 0;
// dp[1] = nums[0]:只有一间房屋,只能抢这一间
dp[1] = nums[0];
// 从第2间房屋开始遍历(对应dp数组的下标i)
for(int i = 2; i <= nums.length; i++){
// 状态转移方程:
// dp[i - 1]:不抢当前第i间房屋,金额等于前i-1间的最大金额
// dp[i - 2] + nums[i - 1]:抢当前第i间房屋,金额等于前i-2间的最大金额 + 当前房屋金额
// 取两者中的较大值
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
}
// 返回抢劫所有房屋能获得的最大金额
return dp[nums.length];
}
}
4.完全平方数
class Solution {
public int numSquares(int n) {
// dp[i] 表示和为 i 的完全平方数的最少个数
int[] dp = new int[n + 1];
// 初始化:dp[0] = 0,和为0不需要任何完全平方数
dp[0] = 0;
// 遍历每个目标值 i,从 1 到 n
for(int i = 1; i < n + 1; i++){
// 最坏情况:i 由 i 个 1 组成(1是完全平方数)
// 所以初始化为 i,后续会被更优解更新
dp[i] = i;
// 尝试每个可能的完全平方数 j*j
// j 从 1 开始,只要 j*j <= i 就继续尝试
for(int j = 1; i - j * j >= 0; j++){
// 状态转移:dp[i] = min(dp[i], dp[i - j*j] + 1)
// 含义:使用一个 j*j,再加上凑成 i-j*j 的最少个数
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
// 返回凑成 n 的最少完全平方数个数
return dp[n];
}
}
5. 零钱兑换
class Solution {
public int coinChange(int[] coins, int amount) {
// ========== 第一步:边界条件检查 ==========
// 如果硬币数组为空、长度为0,或者目标金额小于等于0
// 直接返回0,表示不需要任何硬币
if(coins == null || coins.length == 0 || amount <= 0){
return 0;
}
// ========== 第二步:创建DP数组 ==========
// dp[i] 表示:组成金额 i 所需的最少硬币个数
// 数组长度为 amount + 1,因为要包含 0 到 amount 的所有金额
int[] dp = new int[amount + 1];
// ========== 第三步:初始化DP数组 ==========
// 用循环将所有位置初始化为 Integer.MAX_VALUE(表示"无穷大")
// 意思是:初始状态下,所有金额都认为暂时无法组成
for(int i = 0; i < amount + 1; i++){
dp[i] = Integer.MAX_VALUE;
}
// 基础情况:组成金额 0 需要 0 个硬币
dp[0] = 0;
// ========== 第四步:动态规划核心逻辑 ==========
// 外层循环:遍历从 1 到 amount 的每个目标金额 i
// 我们要逐个计算出组成每个金额的最少硬币数
for(int i = 1; i < amount + 1; i++){
// 内层循环:遍历每一种硬币面值
// 对于当前金额 i,尝试用每一种硬币来凑
for(int coin : coins){
// 关键判断:能否使用当前这枚硬币?
// 条件1:i >= coin → 当前金额必须大于等于硬币面值,才能用
// 条件2:dp[i - coin] != Integer.MAX_VALUE → 剩下的金额(i-coin)必须是可达的
// (如果 i-coin 都凑不出来,那 i 肯定也凑不出来)
if(i >= coin && dp[i - coin] != Integer.MAX_VALUE){
// 状态转移方程:
// dp[i] = min(原来凑i的方案数, 凑i-coin的方案数 + 当前这枚硬币)
// 解释:如果用这枚硬币,那么只需要凑出 i-coin,再加上这一枚
// 我们在所有硬币选择中,选硬币数最少的那个方案
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
// ========== 第五步:返回结果 ==========
// 检查 dp[amount] 是否还是初始的"无穷大"
// 如果是,说明没有任何硬币组合能凑出 amount,返回 -1
// 否则返回计算出的最少硬币数
return dp[amount] == Integer.MAX_VALUE? -1 : dp[amount];
}
}
6. 单词拆分
class Solution {
/**
* 动态规划求解:字符串 s 是否能被 wordDict 中的单词拼接而成
* 思路:dp[i] 表示字符串 s 的前 i 个字符(即 s[0..i-1])能否被拆分
*/
public boolean wordBreak(String s, List<String> wordDict) {
// dp[i] 表示 s 的前 i 个字符能否被 wordDict 中的单词拼接而成
// 数组长度 +1 是为了方便处理前 0 个字符的情况
boolean[] dp = new boolean[s.length() + 1];
// 初始化:空字符串可以被"拆分"(不需要任何单词)
dp[0] = true;
// 将 wordDict 转为 HashSet,实现 O(1) 的查找效率
Set<String> set = new HashSet<>(wordDict);
// 外层循环:遍历字符串的每个位置 i(表示前 i 个字符)
// i 从 1 开始,因为 dp[0] 已经初始化
for(int i = 1; i < s.length() + 1; i++){
// 内层循环:枚举最后一个单词的起始位置 j
// j 从 0 到 i-1,表示将前 i 个字符拆分为 [0..j-1] 和 [j..i-1] 两部分
for(int j = 0; j < i; j++){
// 状态转移条件:
// 1. dp[j] 为 true:前 j 个字符可以被拆分
// 2. s[j..i-1] 在 wordDict 中:中间这段是一个合法单词
// 如果两个条件都满足,则前 i 个字符可以被拆分
if(dp[j] && set.contains(s.substring(j, i))){
dp[i] = true;
break; // 已经找到可行方案,提前跳出内层循环
}
}
}
// 返回整个字符串 s 是否能被拆分
// 即前 s.length() 个字符能否被拼接而成
return dp[s.length()];
}
}
7. 最长递增子序列
class Solution {
/**
* 动态规划求解:最长递增子序列(Longest Increasing Subsequence, LIS)
* 思路:dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
*/
public int lengthOfLIS(int[] nums) {
// dp[i] 表示以 nums[i] 作为结尾元素的最长递增子序列的长度
int[] dp = new int[nums.length];
// 初始化:每个元素自身可以构成长度为 1 的递增子序列
Arrays.fill(dp, 1);
// 记录全局最长递增子序列的长度
int res = 0;
// 外层循环:遍历每个元素,计算以 nums[i] 结尾的最长递增子序列
for(int i = 0; i < nums.length; i++){
// 内层循环:遍历 i 之前的所有元素 nums[j]
for(int j = 0; j < i; j++){
// 只有当 nums[j] < nums[i] 时,才能形成递增序列
// 此时考虑将 nums[i] 接在以 nums[j] 结尾的子序列后面
if(nums[j] < nums[i]){
// 状态转移:选择更长的那个方案
// 要么保持现状(dp[i]),要么接在 nums[j] 后面(dp[j] + 1)
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 更新全局最大值
res = Math.max(res, dp[i]);
}
// 返回整个数组的最长递增子序列长度
return res;
}
}
8. 乘积最大子数组
class Solution {
public int maxProduct(int[] nums) {
// imax: 以当前元素结尾的子数组的最大乘积
int imax = 1;
// imin: 以当前元素结尾的子数组的最小乘积
// 需要维护最小值是因为:由于存在负数,那么会导致最大的变最小的,最小的变最大的
int imin = 1;
// max: 全局最大乘积结果
int max = Integer.MIN_VALUE;
for(int i = 0; i < nums.length; i++){
// 如果当前数是负数,最大值和最小值会互换
// 因为 负数 × 最大正数 = 最小负数
// 负数 × 最小负数 = 最大正数
if(nums[i] < 0){
int tmp = imax;
imax = imin;
imin = tmp;
}
// 更新以当前元素结尾的最大乘积
// 选择:继续乘下去 vs 从当前元素重新开始
imax = Math.max(imax * nums[i], nums[i]);
// 更新以当前元素结尾的最小乘积
// 选择:继续乘下去 vs 从当前元素重新开始
imin = Math.min(imin * nums[i], nums[i]);
// 更新全局最大乘积
max = Math.max(max, imax);
}
return max;
}
}
9. 分割等和子集
class Solution {
public boolean canPartition(int[] nums) {
// 计算数组所有元素的总和
int sum = 0;
for(int i = 0; i < nums.length; i++){
sum += nums[i];
}
// 如果总和为奇数,无法分成两个相等的整数和,直接返回false
if(sum % 2 == 1){
return false;
}
// 目标:从数组中选出一些数,使它们的和等于总和的一半
sum = sum / 2;
// dp[j] 表示能否用数组中的元素凑出和为 j
// 数组大小为 sum+1,因为需要包含 0 到 sum 的所有情况
boolean[] dp = new boolean[sum + 1];
// 初始化:和为 0 总是可以达到的(什么都不选)
dp[0] = true;
// 0/1 背包问题:遍历每个数字,决定是否将其放入"背包"
for(int i = 0; i < nums.length; i++){
// 倒序遍历背包容量(从大到小)
// 原因:防止同一个数字被重复使用(0/1背包每个数只能用一次)
// 如果正序遍历,dp[j-nums[i]] 可能在本轮已经被更新,导致重复计算
for(int j = sum; j >= nums[i]; j--){
// 状态转移:凑出 j 的方法有两种
// 1. 不选当前数 nums[i]:保持 dp[j] 不变
// 2. 选当前数 nums[i]:需要之前能凑出 j-nums[i],即 dp[j-nums[i]] 为 true
dp[j] = dp[j] || dp[j - nums[i]];
}
}
// 返回能否凑出目标和 sum
return dp[sum];
}
}
10. 最长有效括号
class Solution {
public int longestValidParentheses(String s) {
int maxans = 0;
int[] dp = new int[s.length()];
for (int i = 1; i < s.length(); i++) { // 从1开始:需要看前一个字符,且单个字符不可能是有效括号
if (s.charAt(i) == ')') { // 有效括号必须以')'结尾,'('结尾直接跳过
if (s.charAt(i - 1) == '(') { // 情况1:前一个是'(',形成"()"紧邻匹配
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; // i>=2:防止i-2越界,加上前面连续的有效长度
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
// i - dp[i-1] > 0:确保往前跳过中间有效段后,前面还有字符
// s.charAt(i - dp[i-1] - 1) == '(':那个字符必须是'(',才能与当前')'匹配
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
// (i - dp[i-1]) >= 2:匹配后,更前面是否还有字符,防止越界
}
maxans = Math.max(maxans, dp[i]);
}
}
return maxans;
}
}
十六、多维动态规划
1. 不同路径
class Solution {
public int uniquePaths(int m, int n) {
// 创建二维DP数组,dp[i][j] 表示从起点(0,0)到达位置(i,j)的不同路径数量
int[][] dp = new int[m][n];
// 初始化第一列:只能从上往下走,所以每格都只有1种路径
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
// 初始化第一行:只能从左往右走,所以每格都只有1种路径
for(int j = 0; j < n; j++){
dp[0][j] = 1;
}
// 填充其余格子:每个格子的路径数 = 上方格子的路径数 + 左方格子的路径数
// 因为机器人只能向下或向右移动,到达(i,j)只能从(i-1,j)下来或从(i,j-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];
}
}
// 返回到达右下角(m-1, n-1)的总路径数
return dp[m - 1][n - 1];
}
}
2. 最小路径和
class Solution {
/**
* 计算从左上角到右下角的最小路径和
* 每次只能向下或向右移动
* @param grid 二维数组,表示每个格子的数值
* @return 最小路径和
*/
public int minPathSum(int[][] grid) {
// 获取网格的行数
int m = grid.length;
// 获取网格的列数
int n = grid[0].length;
// 创建dp数组,dp[i][j]表示从左上角(0,0)到达(i,j)的最小路径和
int[][] dp = new int[m][n];
// 遍历整个网格,填充dp数组
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
// 情况1:当前格子不在第一行,也不在第一列
// 可以从上方(i-1,j)或左方(i,j-1)到达当前格子
// 选择路径和较小的那个,加上当前格子的值
if(i != 0 && j != 0){
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
// 情况2:当前格子在第一行,但不在第一列(i==0 && j!=0)
// 只能从左边(i,j-1)到达,因为上方没有格子
else if(i == 0 && j != 0){
dp[i][j] = dp[i][j - 1] + grid[i][j];
}
// 情况3:当前格子在第一列,但不在第一行(j==0 && i!=0)
// 只能从上方(i-1,j)到达,因为左边没有格子
else if(j == 0 && i != 0){
dp[i][j] = dp[i - 1][j] + grid[i][j];
}
// 情况4:当前格子是左上角(0,0)
// 这就是起点,路径和就是该格子的值本身
else{
dp[i][j] = grid[i][j];
}
}
}
// 返回右下角格子的dp值,即为从左上角到右下角的最小路径和
return dp[m - 1][n - 1];
}
}
3. 最长回文子串
class Solution {
/**
* 寻找最长回文子串
* 核心思想:中心扩展法
* 回文串的中心可能是一个字符(奇数长度)或两个字符之间(偶数长度)
*/
public String longestPalindrome(String s) {
// 边界情况处理:空字符串或null直接返回
if(s == null || s.length() == 0){
return s;
}
// result[0] 存储最长回文子串的起始索引
// result[1] 存储最长回文子串的结束索引
int[] result = new int[2];
// 遍历每个字符,将其作为回文中心进行扩展
for(int i = 0; i < s.length(); i++){
// 情况1:奇数长度回文,中心为单个字符 s[i]
// 例如 "aba",中心是 'b'
find(s, i - 1, i + 1, result);
// 情况2:偶数长度回文,中心为 s[i] 和 s[i+1] 之间
// 例如 "abba",中心是两个 'b' 之间
find(s, i, i + 1, result);
}
// 根据记录的索引截取最长回文子串
// substring(start, end) 是左闭右开区间,所以 end 需要 +1
return s.substring(result[0], result[1] + 1);
}
/**
* 从给定中心向两边扩展,寻找最长回文
* @param s 原字符串
* @param left 左指针初始位置
* @param right 右指针初始位置
* @param result 记录当前最长回文的起止索引
*/
private void find(String s, int left, int right, int[] result){
// 向两边扩展:要求不越界且左右字符相等
while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
left--; // 左指针左移
right++; // 右指针右移
}
// 循环结束时,left 和 right 指向的是不满足回文条件的位置
// 实际回文范围是 [left+1, right-1]
// 当前找到的回文长度 = (right-1) - (left+1) + 1 = right - left - 1
// 但这里用 right - left - 2 比较的是长度差(因为 result[1]-result[0] 也没加1)
// 如果当前找到的更长,更新结果
// right - left - 2 等价于 (right-1) - (left+1)
if(right - left - 2 > result[1] - result[0]){
result[1] = right - 1; // 更新结束索引
result[0] = left + 1; // 更新起始索引
}
}
}
4. 最长公共子序列
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
// 获取两个字符串的长度
int m = text1.length();
int n = text2.length();
// 创建 (m+1) × (n+1) 的DP数组
// dp[i][j] 表示 text1[0..i-1] 与 text2[0..j-1] 的最长公共子序列长度
// 多留一行一列用于处理空字符串的边界情况
int[][] dp = new int[m + 1][n + 1];
// 遍历 text1 的每个字符,i 从 1 开始(对应 text1 的第 i-1 个字符) i,j都从1开始是为了防止dp数组越界
for(int i = 1; i <= m; i++){
// 获取 text1 当前位置的字符
char c1 = text1.charAt(i - 1);
// 遍历 text2 的每个字符,j 从 1 开始(对应 text2 的第 j-1 个字符)
for(int j = 1; j <= n; j++){
// 获取 text2 当前位置的字符
char c2 = text2.charAt(j - 1);
// 状态转移:判断两个当前字符是否相等
if(c1 == c2){
// 情况1:字符相等,当前字符可加入LCS
// 长度 = 两个字符串都去掉当前字符后的LCS长度 + 1
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
// 情况2:字符不相等,取两种选择的最大值:
// - dp[i][j-1]:text1不变,text2去掉当前字符
// - dp[i-1][j]:text1去掉当前字符,text2不变
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
// 返回最终答案:两个完整字符串的LCS长度
return dp[m][n];
}
}
十七、技巧
1. 只出现一次的数字
/*
位运算。对于这道题,可使用异或运算 ⊕。异或运算有以下三个性质。
1.任何数和 0 做异或运算,结果仍然是原来的数,即 a⊕0=a。
2.任何数和其自身做异或运算,结果是 0,即 a⊕a=0。
3.异或运算满足交换律和结合律,即 a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b。
数组中的全部元素的异或运算结果即为数组中只出现一次的数字。
*/
class Solution {
public int singleNumber(int[] nums) {
int single = 0;
for(int num : nums){
single ^= num;
}
return single;
}
}
2. 多数元素
class Solution {//如果将数组 nums 中的所有元素按照单调递增或单调递减的顺序排序,那么下标为 n/2 的元素(下标从 0 开始)一定是众数。
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
}
/*
时间复杂度:O(nlogn)。将数组排序的时间复杂度为 O(nlogn)。
空间复杂度:O(logn)。如果使用语言自带的排序算法,需要使用 O(logn) 的栈空间。如果自己编写堆排序,则只需要使用 O(1) 的额外空间。
*/
3. 颜色分类
class Solution {
public void sortColors(int[] nums) {
// 三指针解法(荷兰国旗问题)
// redIndex: 红色区域(0)的右边界,初始为-1表示红色区域为空
int redIndex = -1;
// blueIndex: 蓝色区域(2)的左边界,初始为nums.length表示蓝色区域为空
int blueIndex = nums.length;
// index: 当前遍历位置,从数组开头开始扫描
int index = 0;
// 循环条件:当前位置还未进入蓝色区域
// 说明还有待处理的元素
while(index < blueIndex){
if(nums[index] == 0){
// 遇到0(红色):需要放到红色区域
redIndex++; // 扩展红色区域右边界
swap(nums, index, redIndex); // 将当前0与红色区域后一个位置交换
index++; // 交换过来的元素已确定,前进
}else if(nums[index] == 2){
// 遇到2(蓝色):需要放到蓝色区域
blueIndex--; // 扩展蓝色区域左边界
swap(nums, index, blueIndex); // 将当前2与蓝色区域前一个位置交换
// 注意:index不前进!
// 因为从blueIndex-1位置换过来的元素是未知的,需要重新判断
}else{
// 遇到1(白色):已经在中间区域,无需交换
index++; // 直接前进
}
}
// 循环结束时:
// [0, redIndex] → 全是0
// [redIndex+1, blueIndex-1] → 全是1
// [blueIndex, n-1] → 全是2
}
// 交换数组中两个位置的元素
private void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
4. 下一个排列
class Solution {
/**
* 下一个排列算法:将数组重新排列成字典序中下一个更大的排列
* 如果已经是最大的排列,则变为最小的排列(升序)
*/
public void nextPermutation(int[] nums) {
// 第一步:从右向左找到第一个升序对 nums[i] < nums[i+1]
// i 初始化为倒数第二个元素的位置
int i = nums.length - 2;
// 循环条件:i >= 0 且当前元素 >= 下一个元素(即不是升序)
// 目的是找到最右边的一个"拐点",使得 nums[i] < nums[i+1]
while(i >= 0 && nums[i] >= nums[i+1]){
i--;
}
// 如果找到了这样的 i(说明不是完全降序,还有下一个排列)
if(i >= 0){
// 第二步:从右向左找到第一个大于 nums[i] 的元素 nums[j]
int j = nums.length - 1;
// 由于 i 右边的部分是降序的,所以从右找第一个大于 nums[i] 的即可
while(j >= 0 && nums[i] >= nums[j]){
j--;
}
// 第三步:交换 nums[i] 和 nums[j]
// 这样保证了在 i 位置上的数变大了,但增幅最小
swap(nums, i, j);
}
// 第四步:反转 i 位置之后的所有元素
// 无论是否找到 i,都需要反转 i 之后的部分
// 如果找到了 i:交换后,i 之后仍是降序,反转为升序得到最小排列
// 如果没找到 i(i = -1):说明原数组是降序(最大排列),反转变为升序(最小排列)
reverse(nums, i + 1);//把 reverse 放进 if 里面,会导致完全降序的数组无法变成升序,违反了"如果已经是最大排列,则变为最小排列"的要求。
}
/**
* 交换数组中两个位置的元素
*/
private void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/**
* 反转数组中从 left 开始到末尾的部分
* 双指针法,左右交换向中间靠拢
*/
private void reverse(int[] nums, int left){
int right = nums.length - 1;
while(left < right){
swap(nums, left, right);
left++;
right--;
}
}
}
5. 寻找重复数
class Solution {//利用循环链表Ⅱ的思想
public int findDuplicate(int[] nums) {
int slow = 0;
int fast = 0;
slow = nums[slow];
fast = nums[nums[fast]];
while(slow != fast){
slow = nums[slow];
fast = nums[nums[fast]];
}
int tmp = 0;
while(tmp != slow){
slow = nums[slow];
tmp = nums[tmp];
}
return slow;
}
}

1705

被折叠的 条评论
为什么被折叠?



