常见leetcode算法

227 阅读9分钟

回溯问题

回溯算法全面总结

  1. 回溯算法核心思想

回溯算法是一种系统性搜索问题的算法,它通过深度优先搜索的方式遍历所有可能的解空间,并在搜索过程中通过剪枝来避免无效的搜索。

核心特点:

试错思想:尝试各种可能的选择,发现不可行时回退

系统性遍历:保证找到所有可行解

剪枝优化:提前排除不可能的解,提高效率

  1. 回溯算法通用框架

''' def backtrack(路径, 选择列表): if 满足结束条件: 结果集.append(路径副本) return

for 选择 in 选择列表:
    if 选择不合法:  # 剪枝操作
        continue
        
    做选择
    backtrack(新路径, 新选择列表)
    撤销选择

'''

  1. 三大类回溯问题及解法

3.1 组合问题(Combinations) 特点:不关心顺序,[1,2]和[2,1]是相同的排列 ''' class CombinationSolution {

// 组合总和:可重复使用元素
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(candidates); // 排序便于剪枝
    backtrack(candidates, target, 0, new ArrayList<>(), result, 0);
    return result;
}

private void backtrack(int[] candidates, int target, int start, 
                      List<Integer> path, List<List<Integer>> result, int currentSum) {
    // 结束条件:当前和等于目标值
    if (currentSum == target) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    // 结束条件:当前和超过目标值
    if (currentSum > target) {
        return;
    }
    
    for (int i = start; i < candidates.length; i++) {
        // 剪枝:如果当前和加上候选数已经大于目标,跳过
        if (currentSum + candidates[i] > target) {
            continue;
        }
        
        // 做出选择
        path.add(candidates[i]);
        currentSum += candidates[i];
        
        // 递归:注意这里传入 i 而不是 i+1,允许重复使用元素
        backtrack(candidates, target, i, path, result, currentSum);
        
        // 撤销选择
        path.remove(path.size() - 1);
        currentSum -= candidates[i];
    }
}

// 组合总和 II:元素不可重复使用
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(candidates); // 排序便于去重
    backtrack2(candidates, target, 0, new ArrayList<>(), result, 0);
    return result;
}

private void backtrack2(int[] candidates, int target, int start,
                       List<Integer> path, List<List<Integer>> result, int currentSum) {
    if (currentSum == target) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    for (int i = start; i < candidates.length; i++) {
        // 去重:跳过同一层相同的元素
        if (i > start && candidates[i] == candidates[i - 1]) {
            continue;
        }
        
        // 剪枝
        if (currentSum + candidates[i] > target) {
            break;
        }
        
        path.add(candidates[i]);
        currentSum += candidates[i];
        
        // 递归:传入 i+1,不允许重复使用元素
        backtrack2(candidates, target, i + 1, path, result, currentSum);
        
        path.remove(path.size() - 1);
        currentSum -= candidates[i];
    }
}

}

''' 3.2 排列问题(Permutations)

特点:关心顺序,[1,2]和[2,1]是不同的排列

解法关键:使用used数组标记已用元素 ''' // 全排列问题 class PermutationSolution {

// 全排列(无重复元素)
public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    boolean[] used = new boolean[nums.length];
    backtrack(nums, new ArrayList<>(), result, used);
    return result;
}

private void backtrack(int[] nums, List<Integer> path, 
                      List<List<Integer>> result, boolean[] used) {
    // 结束条件:路径长度等于数组长度
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        // 跳过已使用的元素
        if (used[i]) {
            continue;
        }
        
        used[i] = true;
        path.add(nums[i]);
        
        backtrack(nums, path, result, used);
        
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

// 全排列 II(包含重复元素)
public List<List<Integer>> permuteUnique(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(nums); // 排序便于去重
    boolean[] used = new boolean[nums.length];
    backtrackUnique(nums, new ArrayList<>(), result, used);
    return result;
}

private void backtrackUnique(int[] nums, List<Integer> path,
                            List<List<Integer>> result, boolean[] used) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        // 跳过已使用的元素
        if (used[i]) {
            continue;
        }
        
        // 去重:当前元素与前一个元素相同,且前一个元素未被使用。顺序性,前一个未被使用,现在这个也不能使用。
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
            continue;
        }
        
        used[i] = true;
        path.add(nums[i]);
        
        backtrackUnique(nums, path, result, used);
        
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

}

'''

3.3 子集问题(Subsets)

特点:找出集合的所有子集

解法关键:每个节点都是有效结果

''' // 子集问题 class SubsetSolution {

// 子集(无重复元素)
public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), result);
    return result;
}

private void backtrack(int[] nums, int start, List<Integer> path,
                      List<List<Integer>> result) {
    // 每次递归都记录当前路径
    result.add(new ArrayList<>(path));
    
    for (int i = start; i < nums.length; i++) {
        path.add(nums[i]);
        // 从下一个元素开始,避免重复
        backtrack(nums, i + 1, path, result);
        path.remove(path.size() - 1);
    }
}

// 子集 II(包含重复元素)
public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(nums); // 排序便于去重
    backtrackWithDup(nums, 0, new ArrayList<>(), result);
    return result;
}

private void backtrackWithDup(int[] nums, int start, List<Integer> path,
                             List<List<Integer>> result) {
    result.add(new ArrayList<>(path));
    
    for (int i = start; i < nums.length; i++) {
        // 去重:跳过同一层相同的元素
        if (i > start && nums[i] == nums[i - 1]) {
            continue;
        }
        
        path.add(nums[i]);
        backtrackWithDup(nums, i + 1, path, result);
        path.remove(path.size() - 1);
    }
}

归并排序

public class MergeSortOptimized {
    
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        
        // 1. 只在这里创建一次辅助数组
        int[] temp = new int[arr.length];
        
        sort(arr, 0, arr.length - 1, temp);
    }
    
    private static void sort(int[] arr, int left, int right, int[] temp) {
        if (left >= right) return;
        
        int mid = left + (right - left) / 2;
        sort(arr, left, mid, temp);
        sort(arr, mid + 1, right, temp);
        
        // 2. 合并时传入复用的 temp 数组
        merge(arr, left, mid, right, temp);
    }
    
    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;      // 左子数组指针
        int j = mid + 1;   // 右子数组指针
        int k = left;      // temp 数组写入指针(注意:这里直接从 left 开始,而非 0)
        
        // 3. 归并过程:将结果暂存在 temp 的 [left...right] 区间内
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            } else {
                temp[k++] = arr[j++];
            }
        }
        
        // 处理剩余元素
        while (i <= mid) {
            temp[k++] = arr[i++];
        }
        while (j <= right) {
            temp[k++] = arr[j++];
        }
        
        // 4. 将 temp 中 [left...right] 的有序数据拷贝回原数组 arr
        // 注意:这里只需要拷贝这一段,不需要像旧版那样拷贝整个 temp
        for (int idx = left; idx <= right; idx++) {
            arr[idx] = temp[idx];
        }
    }
}

快速排序

  public class QuickSort {
    public static void quickSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        quickSort(arr, 0, arr.length - 1);
    }
    
    private static void quickSort(int[] arr, int left, int right) {
        if (left >= right) return;
        int pivot = partition(arr, left, right);
        quickSort(arr, left, pivot - 1);
        quickSort(arr, pivot + 1, right);
    }
    
    private static int partition(int[] arr, int left, int right) {
      // 挖坑
        int i = left, j = right;
        int mid=left+(right-left)/2;
        swap(arr, mid, left);
        int pivot = arr[left];
        while (i < j) {
            //一定是先-j,返回i
            while (i < j && arr[j] >= pivot) j--;
            while (i < j && arr[i] <= pivot) i++;
            if (i < j) swap(arr, i, j);
        }
      //会填
        swap(arr, left, i);
        return i;
    }
    
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
} 
    

二分查找方法

搜索目标值,如果不存在返回要插入的位置
如果所有元素都大于目标值,返回0
如果所有元素都小于目标值,返回arr.length

public int binarySearchWithInsertPosition(int[] arr, int target) {
    if (arr == null || arr.length == 0) {
        return 0; // 空数组,插入位置为0
    }
    
    int left = 0;
    int right = arr.length - 1;
    
    // 先尝试查找目标值是否存在
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target) {
            return mid; // 找到目标值,返回其索引
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    // 未找到目标值,返回应该插入的位置
    return left;
}


背包问题

背包问题,n种物品,外面遍历物品,内遍历容量
第i个物品体积为weight[i],价值为values[i]
定义  dp[w]  为背包容量为  w  时能获得的最大价值。所有初始化为0

01背包
for (int i = 0; i < n; i++) {
    for (int w = W; w >= weights[i]; w--) {
        // 决策:不选当前物品 或 选当前物品
        dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
    }
}

完全背包
for (int i = 0; i < n; i++) {
    for (int w = weights[i]; w <= W; w++) {
        // 决策:不选当前物品 或 新增一个当前物品
        dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
    }
}    


#恰好装满问题


    /**
     * 完全背包恰好装满的最大价值
     * @param W 背包容量
     * @param weights 物品重量数组
     * @param values 物品价值数组
     * @return 最大价值(若无解返回-1)
     */
    public static int knapCompleteExactlyFull(int W, int[] weights, int[] values) {
        int[] dp = new int[W + 1];
        
        // 初始化:除了容量0,其他状态都不可达
        Arrays.fill(dp, Integer.MIN_VALUE);
        dp[0] = 0;
        
        for (int i = 0; i < weights.length; i++) {
            for (int j = weights[i]; j <= W; j++) {
                // 仅当减去当前物品重量的状态可达时,才能更新
                if (dp[j - weights[i]] != Integer.MIN_VALUE) {
                    dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
                }
            }
        }
        
        return dp[W] < 0 ? -1 : dp[W]; // -1表示无解
    }


    /**
     * 恰好装满的最小物品数量
     * @param W 背包容量
     * @param weights 物品重量数组
     * @return 最小物品数量(若无解返回-1)
     */
    public static int minItemsExactlyFull(int W, int[] weights) {
        int[] dp = new int[W + 1];
        
        // 初始化:除了容量0,其他状态都不可达
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        
        for (int weight : weights) {
            for (int j = weight; j <= W; j++) {
                if (dp[j - weight] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - weight] + 1);
                }
            }
        }
        
        return dp[W] == Integer.MAX_VALUE ? -1 : dp[W];}      
       
}


组合数,不考虑顺序
物品在外,容量在内
和全排列组合数类似
for (int i = 0; i < n; i++) {
    for (int w = weights[i]; w <= W; w++) {
         // 物品在外层循环 → 组合数
        dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
    }
}    



排列数,考虑顺序
容量在外,物品在内
for (int w = 0; w <= W; w++) {
    for (int i = 0; i < n; i++) {
         if(w<weights[i]){continue}
          // 容量在外层循环 → 排列数
        dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
    }
}    

图表示问题


1. 邻接矩阵(Adjacency Matrix)

  • 实现方式:二维数组 int[][] matrixmatrix[i][j] 表示顶点 i 到 j 的边权重(无权图可用 0/1 表示)。
  • 特点
    • 优点:快速判断两顶点是否相邻(O(1))。
    • 缺点:空间复杂度高(O(V²)),适合稠密图。
  • 示例代码
    int[][] graph = new int[V][V];
    graph[0][1] = 1; // 顶点 0 到 1 有一条边
    

2. 邻接表(Adjacency List)

  • 实现方式:数组或 List 存储每个顶点的邻居列表,如 List<List<Integer>>
  • 特点
    • 优点:空间效率高(O(V+E)),适合稀疏图。
    • 缺点:判断两顶点是否相邻需遍历列表(O(V))。
  • 示例代码
    List<List<Integer>> graph = new ArrayList<>();
    for (int i = 0; i < V; i++) {
        graph.add(new ArrayList<>());
    }
    graph.get(0).add(1); // 顶点 0 到 1 有一条边
    

有向图判断是否有环-染色法

public boolean hasCycle() {
    // 0: 未访问, 1: 访问中, 2: 已访问完成
    int[] color = new int[V];
    
    for (int i = 0; i < V; i++) {
        if (color[i] == 0) {
            if (dfs(i, color)) {
                return true;
            }
        }
    }
    return false;
}

private boolean dfs(int v, int[] color) {
    color[v] = 1; // 标记为访问中
    
    for (int neighbor : adj.get(v)) {
        if (color[neighbor] == 0) {
            if (dfs(neighbor, color)) {
                return true;
            }
        } else if (color[neighbor] == 1) {
            return true; // 发现环
        }
    }
    
    color[v] = 2; // 标记为访问完成
    return false;
}

无向图判断是否有环-dfs

public boolean hasCycle() {
        boolean[] visited = new boolean[V];
        
        for (int i = 0; i < V; i++) {
            if (!visited[i]) {
                if (dfs(i, visited, -1)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private boolean dfs(int v, boolean[] visited, int parent) {
        visited[v] = true;
        
        for (int neighbor : adj.get(v)) {
            if (!visited[neighbor]) {
                if (dfs(neighbor, visited, v)) {
                    return true;
                }
            } else if (neighbor != parent) {
                return true; // 发现环
            }
        }
        return false;
    }

LinkedList 实现了多个重要接口

作为 List 列表

add(index, element) - 在指定位置插入

get(index) - 获取指定位置元素

remove(index) - 删除指定位置元素

set(index, element) - 修改指定位置元素


Deque<Integer> deque = new LinkedList<>();
    
Deque<Integer> stack = new LinkedList<>();
    
Queue<Integer> queue = new LinkedList<>();
    
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();

===========================

作为 Queue 队列 (FIFO)

offer(element) / add(element) - 入队

poll() - 出队(队列空返回null)

remove() - 出队(队列空抛异常)

peek() - 查看队首

     Queue<Integer> queue= new LinkedList<>();
            queue.poll();
            queue.offer();
    

===============================

作为 Deque 双端队列

 Deque<Integer> deque= new LinkedList<>();
            deque.offerFirst();
            deque.offerLast();
            deque.pollFirst();
            deque.pollLast();

offerFirst() / offerLast() - 两端入队

pollFirst() / pollLast() - 两端出队

peekFirst() / peekLast() - 查看两端

push(element) - 栈式入栈

pop() - 栈式出栈

================================== 作为 Stack 栈 (LIFO)

push(element) - 入栈

pop() - 出栈

Deque<Integer> stack= new LinkedList<>();
            stack.pop();
            stack.push();

==================================

单独的new priorityQueue(),默认是小根 大根堆写lamda表达式

PriorityQueue<Integer> queue= new PriorityQueue<>();
queue.offer();
queue.poll();

==================================

长度判断:

image.png

List 继承 Collection,size(),isEmpty(),contains(), add(), remove(),都可以用

String 继承CharSequence,length().

数组,是关键字,JVM nitive 方法

=======================================================================

StringBuilder 继承,CharSequence,length()。isEmpty(). AbstractStringBuilder。 delete(star,end),deleteCharAt()

======================================================================= string ,subString 左开右闭

=======================================================================

小写字母或者大写字母,new int[26]。 包含小写或者大写new int[128]具体原因如下。

 // 1. 判断字符是否为大写字母
    public static boolean isUpperCase(char c) {
        return c >= 'A' && c <= 'Z';  // 65-90
    }
    
    // 2. 判断字符是否为小写字母
    public static boolean isLowerCase(char c) {
        return c >= 'a' && c <= 'z';  // 97-122
    }
    
    // 3. 大写转小写(手动实现)
    public static char toLowerCase(char c) {
        if (c >= 'A' && c <= 'Z') {
            return (char)(c + 32);  // 65-90 → 97-122
        }
        return c;
    }
    
    // 4. 小写转大写(手动实现)
    public static char toUpperCase(char c) {
        if (c >= 'a' && c <= 'z') {
            return (char)(c - 32);  // 97-122 → 65-90
        }
        return c;
    }