吃透这 6 道 Java 后端高频算法题,面试通过率直接拉满

简介: 本文精讲6道Java后端高频算法题(只出现一次的数字、搜索旋转排序数组等),涵盖位运算、二分查找、贪心、栈、回溯等核心思想,并提供多种Java实现方案(异或/Stream/Deque/数组模拟等),附详细复杂度分析与场景适用说明,助力校招社招与性能优化。

算法能力是Java后端开发者的核心硬实力,无论是校招入门、社招晋升,还是业务代码的性能优化,扎实的算法功底都是拉开差距的关键。


前置环境与依赖

本文所有代码基于JDK 17编写,核心maven依赖如下:

<dependencies>
   <!-- lombok -->
   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.18.32</version>
       <scope>provided</scope>
   </dependency>
   <!-- spring核心工具类 -->
   <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-core</artifactId>
       <version>6.1.6</version>
   </dependency>
   <!-- guava集合工具类 -->
   <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>33.1.0-jre</version>
   </dependency>
   <!-- fastjson2 -->
   <dependency>
       <groupId>com.alibaba.fastjson2</groupId>
       <artifactId>fastjson2</artifactId>
       <version>2.0.49</version>
   </dependency>
   <!-- slf4j-api -->
   <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <version>2.0.13</version>
   </dependency>
</dependencies>


一、只出现一次的数字

题目描述

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 示例1: 输入:nums = [2,2,1] 输出:1 示例2: 输入:nums = [4,1,2,1,2] 输出:4

核心原理拆解

本题的核心考点是位运算特性哈希表统计数学逻辑推导,其中位运算方案是时间与空间复杂度最优的解法。核心基础知识点:

  1. 异或运算的核心特性:
  • 任何数和自身异或,结果为0:a ^ a = 0
  • 任何数和0异或,结果为自身:a ^ 0 = a
  • 异或运算满足交换律和结合律:a ^ b ^ a = b ^ (a ^ a) = b ^ 0 = b
  1. 哈希表统计:通过键值对记录每个数字的出现次数,最终筛选出次数为1的数字,是最直观的通用解法。
  2. 数学公式推导:利用2*(所有不重复数字的和) - 原数组的和 = 只出现一次的数字,适用于其余数字均出现两次的限定场景。

实现方案

方案1:异或运算(最优解)

方案原理

利用异或运算的交换律与结合律,将数组中所有元素依次异或,最终剩余的结果就是只出现一次的数字。该方案无需额外的存储空间,是本题的最优解。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;

/**
* 只出现一次的数字
* @author ken
*/

@Slf4j
public class SingleNumberSolution {

   /**
    * 异或运算解法
    * @param nums 输入的整数数组
    * @return 只出现一次的数字
    */

   public static int singleNumberXOR(int[] nums) {
       if (ObjectUtils.isEmpty(nums)) {
           throw new IllegalArgumentException("数组不能为空");
       }
       return Arrays.stream(nums).reduce(0, (a, b) -> a ^ b);
   }

   public static void main(String[] args) {
       int[] nums1 = {2, 2, 1};
       int[] nums2 = {4, 1, 2, 1, 2};
       log.info("示例1结果:{}", singleNumberXOR(nums1));
       log.info("示例2结果:{}", singleNumberXOR(nums2));
   }
}

复杂度分析
  • 时间复杂度:O(n),仅需遍历数组一次
  • 空间复杂度:O(1),仅使用常数级额外空间

方案2:哈希表统计次数

方案原理

通过Java Stream的分组统计功能,统计每个数字的出现次数,最终过滤出出现次数为1的数字。该方案逻辑直观,可扩展至其余数字出现任意次数的场景。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* 只出现一次的数字
* @author ken
*/

@Slf4j
public class SingleNumberSolution {

   /**
    * 哈希表统计解法
    * @param nums 输入的整数数组
    * @return 只出现一次的数字
    */

   public static int singleNumberHashMap(int[] nums) {
       if (ObjectUtils.isEmpty(nums)) {
           throw new IllegalArgumentException("数组不能为空");
       }
       Map<Integer, Long> countMap = Arrays.stream(nums)
               .boxed()
               .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
       return countMap.entrySet().stream()
               .filter(entry -> entry.getValue() == 1)
               .map(Map.Entry::getKey)
               .findFirst()
               .orElseThrow(() -> new IllegalArgumentException("数组中无符合条件的数字"));
   }

   public static void main(String[] args) {
       int[] nums1 = {2, 2, 1};
       int[] nums2 = {4, 1, 2, 1, 2};
       log.info("示例1结果:{}", singleNumberHashMap(nums1));
       log.info("示例2结果:{}", singleNumberHashMap(nums2));
   }
}

复杂度分析
  • 时间复杂度:O(n),需遍历数组统计次数,再遍历哈希表筛选结果
  • 空间复杂度:O(n),需额外的哈希表存储数字出现次数

方案3:数学公式推导

方案原理

基于题目限定条件(其余元素均出现两次),通过数学公式计算得到结果。核心逻辑为:所有重复元素在2*去重和中被计算两次,减去原数组和后,剩余的就是只出现一次的数字。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;

/**
* 只出现一次的数字
* @author ken
*/

@Slf4j
public class SingleNumberSolution {

   /**
    * 数学公式解法
    * @param nums 输入的整数数组
    * @return 只出现一次的数字
    */

   public static int singleNumberMath(int[] nums) {
       if (ObjectUtils.isEmpty(nums)) {
           throw new IllegalArgumentException("数组不能为空");
       }
       int sumDistinct = Arrays.stream(nums).distinct().sum();
       int sumTotal = Arrays.stream(nums).sum();
       return 2 * sumDistinct - sumTotal;
   }

   public static void main(String[] args) {
       int[] nums1 = {2, 2, 1};
       int[] nums2 = {4, 1, 2, 1, 2};
       log.info("示例1结果:{}", singleNumberMath(nums1));
       log.info("示例2结果:{}", singleNumberMath(nums2));
   }
}

复杂度分析
  • 时间复杂度:O(n),需遍历数组计算去重和与总和
  • 空间复杂度:O(n),需额外空间存储去重后的元素

方案横向对比

实现方案 时间复杂度 空间复杂度 核心优势 适用场景
异或运算 O(n) O(1) 时空复杂度最优,代码极简 本题限定场景,其余元素均出现两次
哈希表统计 O(n) O(n) 逻辑通用,可扩展至任意出现次数 不限定其余元素的出现次数,通用场景
数学公式 O(n) O(n) 思路巧妙,逻辑直观 严格限定其余元素均出现两次的场景

二、搜索旋转排序数组

题目描述

整数数组nums按升序排列,数组中的值互不相同。在传递给函数之前,nums在预先未知的某个下标k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]。给你旋转后的数组nums和一个整数target,如果nums中存在这个目标值target,则返回它的下标,否则返回-1。 示例1: 输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4 示例2: 输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1 示例3: 输入:nums = [1], target = 0 输出:-1

核心原理拆解

本题的核心考点是二分查找算法的灵活应用,旋转后的数组虽然整体无序,但可划分为两个升序的子数组,且其中一个子数组一定是完全有序的。核心逻辑是通过二分查找判断mid左右哪一段是有序的,再判断target是否落在有序区间内,逐步缩小查找范围,将时间复杂度从线性查找的O(n)优化至O(log n)。

二分查找核心边界规则:

  1. mid计算采用left + (right - left) / 2,避免(left + right)直接相加导致的整数溢出
  2. 循环条件为left <= right,确保区间内最后一个元素被检查
  3. 区间收缩时,left = mid + 1right = mid - 1,避免死循环

实现方案

方案1:经典二分查找(最优解)

方案原理

直接利用旋转数组的部分有序特性,通过一次二分查找完成目标值的定位。每次二分后,判断左半段或右半段是否有序,再根据target与有序区间边界的大小关系,收缩查找区间。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

/**
* 搜索旋转排序数组
* @author ken
*/

@Slf4j
public class SearchRotatedArraySolution {

   /**
    * 经典二分查找解法
    * @param nums 旋转后的升序数组
    * @param target 目标查找值
    * @return 目标值的下标,不存在返回-1
    */

   public static int searchBinary(int[] nums, int target) {
       if (ObjectUtils.isEmpty(nums)) {
           return -1;
       }
       int left = 0;
       int right = nums.length - 1;
       while (left <= right) {
           int mid = left + (right - left) / 2;
           if (nums[mid] == target) {
               return mid;
           }
           if (nums[left] <= nums[mid]) {
               if (target >= nums[left] && target < nums[mid]) {
                   right = mid - 1;
               } else {
                   left = mid + 1;
               }
           } else {
               if (target > nums[mid] && target <= nums[right]) {
                   left = mid + 1;
               } else {
                   right = mid - 1;
               }
           }
       }
       return -1;
   }

   public static void main(String[] args) {
       int[] nums = {4, 5, 6, 7, 0, 1, 2};
       log.info("示例1结果:{}", searchBinary(nums, 0));
       log.info("示例2结果:{}", searchBinary(nums, 3));
       log.info("示例3结果:{}", searchBinary(new int[]{1}, 0));
   }
}

复杂度分析
  • 时间复杂度:O(log n),二分查找每次将查找区间缩小一半
  • 空间复杂度:O(1),仅使用常数级额外空间

方案2:旋转点定位+两次二分查找

方案原理

先通过二分查找找到数组的旋转点(即数组中最小元素的下标),将旋转数组拆分为两个独立的升序子数组,再分别对两个子数组执行标准二分查找,最终返回目标值的下标。该方案逻辑拆分清晰,更易于理解。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

/**
* 搜索旋转排序数组
* @author ken
*/

@Slf4j
public class SearchRotatedArraySolution {

   /**
    * 旋转点定位+两次二分查找解法
    * @param nums 旋转后的升序数组
    * @param target 目标查找值
    * @return 目标值的下标,不存在返回-1
    */

   public static int searchRotatePoint(int[] nums, int target) {
       if (ObjectUtils.isEmpty(nums)) {
           return -1;
       }
       int n = nums.length;
       int left = 0;
       int right = n - 1;
       while (left < right) {
           int mid = left + (right - left) / 2;
           if (nums[mid] > nums[right]) {
               left = mid + 1;
           } else {
               right = mid;
           }
       }
       int rotateIndex = left;
       int leftResult = binarySearch(nums, 0, rotateIndex - 1, target);
       if (leftResult != -1) {
           return leftResult;
       }
       return binarySearch(nums, rotateIndex, n - 1, target);
   }

   /**
    * 标准升序数组二分查找
    * @param nums 升序数组
    * @param left 查找区间左边界
    * @param right 查找区间右边界
    * @param target 目标查找值
    * @return 目标值下标,不存在返回-1
    */

   private static int binarySearch(int[] nums, int left, int right, int target) {
       while (left <= right) {
           int mid = left + (right - left) / 2;
           if (nums[mid] == target) {
               return mid;
           } else if (nums[mid] < target) {
               left = mid + 1;
           } else {
               right = mid - 1;
           }
       }
       return -1;
   }

   public static void main(String[] args) {
       int[] nums = {4, 5, 6, 7, 0, 1, 2};
       log.info("示例1结果:{}", searchRotatePoint(nums, 0));
       log.info("示例2结果:{}", searchRotatePoint(nums, 3));
       log.info("示例3结果:{}", searchRotatePoint(new int[]{1}, 0));
   }
}

复杂度分析
  • 时间复杂度:O(log n),查找旋转点为O(log n),两次二分查找均为O(log n),整体仍为对数级别
  • 空间复杂度:O(1),仅使用常数级额外空间

方案3:Stream API线性查找

方案原理

通过Java Stream的IntStream生成数组下标,过滤出数组值等于target的下标,直接返回结果。该方案代码极简,适用于小数据量场景。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.util.stream.IntStream;

/**
* 搜索旋转排序数组
* @author ken
*/

@Slf4j
public class SearchRotatedArraySolution {

   /**
    * Stream API线性查找解法
    * @param nums 旋转后的升序数组
    * @param target 目标查找值
    * @return 目标值的下标,不存在返回-1
    */

   public static int searchStream(int[] nums, int target) {
       if (ObjectUtils.isEmpty(nums)) {
           return -1;
       }
       return IntStream.range(0, nums.length)
               .filter(i -> nums[i] == target)
               .findFirst()
               .orElse(-1);
   }

   public static void main(String[] args) {
       int[] nums = {4, 5, 6, 7, 0, 1, 2};
       log.info("示例1结果:{}", searchStream(nums, 0));
       log.info("示例2结果:{}", searchStream(nums, 3));
       log.info("示例3结果:{}", searchStream(new int[]{1}, 0));
   }
}

复杂度分析
  • 时间复杂度:O(n),需遍历整个数组
  • 空间复杂度:O(1),仅使用常数级额外空间

方案横向对比

实现方案 时间复杂度 空间复杂度 核心优势 适用场景
经典二分查找 O(log n) O(1) 时空效率最优,一次遍历完成 大数据量、高性能要求的生产场景
旋转点+两次二分 O(log n) O(1) 逻辑拆分清晰,易于理解和调试 学习入门、需要分步调试的场景
Stream线性查找 O(n) O(1) 代码极简,可读性强 小数据量、快速实现的场景

三、合并区间

题目描述

以数组intervals表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。 示例1: 输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠,将它们合并为 [1,6] 示例2: 输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间

核心原理拆解

本题的核心考点是贪心算法区间排序,核心逻辑是:

  1. 区间重叠的前提:按区间左边界排序后,若后一个区间的左边界小于等于前一个区间的右边界,则两个区间重叠,可合并为一个新的区间,新区间的右边界为两个区间右边界的最大值。
  2. 贪心策略:按左边界排序后,每次合并都尽可能扩大当前区间的右边界,确保覆盖所有重叠的区间。

实现方案

方案1:排序+列表遍历合并(最优解)

方案原理

先按区间的左边界进行升序排序,再遍历排序后的区间,维护一个结果列表,每次将当前区间与结果列表中最后一个区间对比,若重叠则合并,否则直接加入结果列表。该方案是工业界最常用的实现,逻辑清晰,效率最优。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
* 合并区间
* @author ken
*/

@Slf4j
public class MergeIntervalsSolution {

   /**
    * 排序+列表遍历合并解法
    * @param intervals 输入的区间数组
    * @return 合并后的不重叠区间数组
    */

   public static int[][] mergeClassic(int[][] intervals) {
       if (ObjectUtils.isEmpty(intervals) || intervals.length == 1) {
           return intervals;
       }
       Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
       List<int[]> result = new ArrayList<>();
       result.add(intervals[0]);
       for (int i = 1; i < intervals.length; i++) {
           int[] lastInterval = result.get(result.size() - 1);
           int[] currentInterval = intervals[i];
           if (currentInterval[0] <= lastInterval[1]) {
               lastInterval[1] = Math.max(lastInterval[1], currentInterval[1]);
           } else {
               result.add(currentInterval);
           }
       }
       return result.toArray(new int[result.size()][]);
   }

   public static void main(String[] args) {
       int[][] intervals1 = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
       int[][] intervals2 = {{1, 4}, {4, 5}};
       log.info("示例1结果:{}", Arrays.deepToString(mergeClassic(intervals1)));
       log.info("示例2结果:{}", Arrays.deepToString(mergeClassic(intervals2)));
   }
}

复杂度分析
  • 时间复杂度:O(n log n),排序的时间复杂度为O(n log n),遍历区间为O(n),整体由排序操作主导
  • 空间复杂度:O(log n),排序所需的栈空间,结果存储的空间不计入额外空间复杂度

方案2:Stream API+Reduce累积合并

方案原理

通过Java Stream的sorted方法完成区间排序,再通过reduce方法实现区间的累积合并逻辑,将合并逻辑封装为函数式表达式,符合Java8+的函数式编程风格。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
* 合并区间
* @author ken
*/

@Slf4j
public class MergeIntervalsSolution {

   /**
    * Stream+Reduce累积合并解法
    * @param intervals 输入的区间数组
    * @return 合并后的不重叠区间数组
    */

   public static int[][] mergeStreamReduce(int[][] intervals) {
       if (ObjectUtils.isEmpty(intervals) || intervals.length == 1) {
           return intervals;
       }
       List<int[]> result = Arrays.stream(intervals)
               .sorted(Comparator.comparingInt(a -> a[0]))
               .reduce(
                       new ArrayList<>(),
                       (list, interval) -> {
                           if (list.isEmpty()) {
                               list.add(interval);
                           } else {
                               int[] last = list.get(list.size() - 1);
                               if (interval[0] <= last[1]) {
                                   last[1] = Math.max(last[1], interval[1]);
                               } else {
                                   list.add(interval);
                               }
                           }
                           return list;
                       },
                       (list1, list2) -> list1
               );
       return result.toArray(new int[result.size()][]);
   }

   public static void main(String[] args) {
       int[][] intervals1 = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
       int[][] intervals2 = {{1, 4}, {4, 5}};
       log.info("示例1结果:{}", Arrays.deepToString(mergeStreamReduce(intervals1)));
       log.info("示例2结果:{}", Arrays.deepToString(mergeStreamReduce(intervals2)));
   }
}

复杂度分析
  • 时间复杂度:O(n log n),与经典解法一致,排序操作主导时间复杂度
  • 空间复杂度:O(log n),排序所需的栈空间

方案3:排序+双端队列Deque实现

方案原理

使用Deque双端队列存储合并后的区间,利用Deque的peekLast()方法快速获取队列尾部的最后一个区间,合并时先移除尾部区间,再添加合并后的新区间,逻辑更贴合队列的操作特性,避免对列表元素的直接修改。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedList;

/**
* 合并区间
* @author ken
*/

@Slf4j
public class MergeIntervalsSolution {

   /**
    * 排序+Deque双端队列解法
    * @param intervals 输入的区间数组
    * @return 合并后的不重叠区间数组
    */

   public static int[][] mergeDeque(int[][] intervals) {
       if (ObjectUtils.isEmpty(intervals) || intervals.length == 1) {
           return intervals;
       }
       Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
       Deque<int[]> deque = new LinkedList<>();
       deque.addLast(intervals[0]);
       for (int i = 1; i < intervals.length; i++) {
           int[] last = deque.peekLast();
           int[] current = intervals[i];
           if (current[0] <= last[1]) {
               deque.removeLast();
               deque.addLast(new int[]{last[0], Math.max(last[1], current[1])});
           } else {
               deque.addLast(current);
           }
       }
       return deque.toArray(new int[deque.size()][]);
   }

   public static void main(String[] args) {
       int[][] intervals1 = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
       int[][] intervals2 = {{1, 4}, {4, 5}};
       log.info("示例1结果:{}", Arrays.deepToString(mergeDeque(intervals1)));
       log.info("示例2结果:{}", Arrays.deepToString(mergeDeque(intervals2)));
   }
}

复杂度分析
  • 时间复杂度:O(n log n),与经典解法一致
  • 空间复杂度:O(log n),排序所需的栈空间

方案横向对比

实现方案 时间复杂度 空间复杂度 核心优势 适用场景
排序+列表遍历 O(n log n) O(log n) 逻辑直观,效率最优,工业界标准实现 生产环境、高性能要求场景
Stream+Reduce O(n log n) O(log n) 函数式编程风格,代码简洁优雅 响应式编程、函数式开发场景
排序+Deque O(n log n) O(log n) 操作符合队列特性,无列表元素原地修改 对数据不可变性有要求的场景

四、大数相加(两个字符串相加)

题目描述

给定两个字符串形式的非负整数num1和num2,返回它们的和,同样以字符串形式返回。你不能使用任何内建的用于处理大整数的库,也不能直接将输入的字符串转换为整数形式。 示例1: 输入:num1 = "21543655", num2 = "4332656442" 输出:"4354200097" 示例2: 输入:num1 = "0", num2 = "0" 输出:"0"

核心原理拆解

本题的核心考点是模拟十进制加法的底层逻辑,解决Java中基础数据类型无法存储超长整数的问题。核心逻辑是:

  1. 从两个字符串的末尾(即数字的最低位)开始,逐位相加,同时记录进位。
  2. 若其中一个字符串已遍历完毕,则补0参与计算,直到两个字符串都遍历完毕且无进位剩余。
  3. 由于计算结果是从低位到高位存储的,最终需要反转得到正序的结果字符串。

实现方案

方案1:双指针逐位相加(最优解)

方案原理

用两个指针分别指向两个字符串的末尾,逐位取出数字相加,计算当前位的结果和进位,将当前位结果存入StringBuilder,最终反转StringBuilder得到最终结果。该方案是工业界最常用的实现,时空效率最优。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

/**
* 大数相加
* @author ken
*/

@Slf4j
public class BigNumberAddSolution {

   /**
    * 双指针逐位相加解法
    * @param num1 第一个数字字符串
    * @param num2 第二个数字字符串
    * @return 两个数字的和,字符串形式
    */

   public static String addStringsClassic(String num1, String num2) {
       if (!StringUtils.hasText(num1)) {
           return num2;
       }
       if (!StringUtils.hasText(num2)) {
           return num1;
       }
       int i = num1.length() - 1;
       int j = num2.length() - 1;
       int carry = 0;
       StringBuilder result = new StringBuilder();
       while (i >= 0 || j >= 0 || carry > 0) {
           int n1 = i >= 0 ? num1.charAt(i) - '0' : 0;
           int n2 = j >= 0 ? num2.charAt(j) - '0' : 0;
           int sum = n1 + n2 + carry;
           result.append(sum % 10);
           carry = sum / 10;
           i--;
           j--;
       }
       return result.reverse().toString();
   }

   public static void main(String[] args) {
       log.info("示例1结果:{}", addStringsClassic("21543655", "4332656442"));
       log.info("示例2结果:{}", addStringsClassic("0", "0"));
   }
}

复杂度分析
  • 时间复杂度:O(max(n,m)),n和m分别为两个字符串的长度,需遍历较长的字符串一次
  • 空间复杂度:O(max(n,m)),存储结果所需的StringBuilder空间

方案2:Stream API+AtomicInteger函数式实现

方案原理

通过Java Stream生成索引,从后往前逐位处理两个字符串的数字,使用AtomicInteger维护进位值(解决Stream中无法修改外部变量的问题),最终通过Stream反转结果得到正序的和。该方案符合Java8+的函数式编程风格。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
* 大数相加
* @author ken
*/

@Slf4j
public class BigNumberAddSolution {

   /**
    * Stream+AtomicInteger函数式解法
    * @param num1 第一个数字字符串
    * @param num2 第二个数字字符串
    * @return 两个数字的和,字符串形式
    */

   public static String addStringsStream(String num1, String num2) {
       if (!StringUtils.hasText(num1)) {
           return num2;
       }
       if (!StringUtils.hasText(num2)) {
           return num1;
       }
       int maxLen = Math.max(num1.length(), num2.length());
       AtomicInteger carry = new AtomicInteger(0);
       List<String> digits = IntStream.range(0, maxLen)
               .mapToObj(i -> {
                   char c1 = i < num1.length() ? num1.charAt(num1.length() - 1 - i) : '0';
                   char c2 = i < num2.length() ? num2.charAt(num2.length() - 1 - i) : '0';
                   int n1 = c1 - '0';
                   int n2 = c2 - '0';
                   int sum = n1 + n2 + carry.get();
                   carry.set(sum / 10);
                   return String.valueOf(sum % 10);
               })
               .collect(Collectors.toList());
       if (carry.get() > 0) {
           digits.add(String.valueOf(carry.get()));
       }
       return IntStream.range(0, digits.size())
               .mapToObj(i -> digits.get(digits.size() - 1 - i))
               .collect(Collectors.joining());
   }

   public static void main(String[] args) {
       log.info("示例1结果:{}", addStringsStream("21543655", "4332656442"));
       log.info("示例2结果:{}", addStringsStream("0", "0"));
   }
}

复杂度分析
  • 时间复杂度:O(max(n,m)),与经典解法一致
  • 空间复杂度:O(max(n,m)),存储中间结果和最终结果所需的空间

方案3:Deque双端队列实现

方案原理

使用Deque双端队列存储每一位的计算结果,通过addFirst()方法将当前位结果从队列头部插入,最终直接拼接队列元素即可得到正序的结果,无需额外的反转操作。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.Deque;
import java.util.LinkedList;

/**
* 大数相加
* @author ken
*/

@Slf4j
public class BigNumberAddSolution {

   /**
    * Deque双端队列解法
    * @param num1 第一个数字字符串
    * @param num2 第二个数字字符串
    * @return 两个数字的和,字符串形式
    */

   public static String addStringsDeque(String num1, String num2) {
       if (!StringUtils.hasText(num1)) {
           return num2;
       }
       if (!StringUtils.hasText(num2)) {
           return num1;
       }
       int i = num1.length() - 1;
       int j = num2.length() - 1;
       int carry = 0;
       Deque<String> deque = new LinkedList<>();
       while (i >= 0 || j >= 0 || carry > 0) {
           int n1 = i >= 0 ? num1.charAt(i) - '0' : 0;
           int n2 = j >= 0 ? num2.charAt(j) - '0' : 0;
           int sum = n1 + n2 + carry;
           deque.addFirst(String.valueOf(sum % 10));
           carry = sum / 10;
           i--;
           j--;
       }
       return String.join("", deque);
   }

   public static void main(String[] args) {
       log.info("示例1结果:{}", addStringsDeque("21543655", "4332656442"));
       log.info("示例2结果:{}", addStringsDeque("0", "0"));
   }
}

复杂度分析
  • 时间复杂度:O(max(n,m)),与经典解法一致
  • 空间复杂度:O(max(n,m)),队列存储结果所需的空间

方案横向对比

实现方案 时间复杂度 空间复杂度 核心优势 适用场景
双指针逐位相加 O(max(n,m)) O(max(n,m)) 时空效率最优,逻辑直观,工业界标准实现 生产环境、超长数字处理场景
Stream函数式实现 O(max(n,m)) O(max(n,m)) 函数式编程风格,无显式循环 响应式编程、函数式开发场景
Deque双端队列 O(max(n,m)) O(max(n,m)) 无需反转操作,直接生成正序结果 对字符串反转操作有性能顾虑的场景

五、有效括号匹配

题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串s,判断字符串是否有效。有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。 示例1: 输入:s = "()" 输出:true 示例2: 输入:s = "()[]{}" 输出:true 示例3: 输入:s = "(]" 输出:false

核心原理拆解

本题的核心考点是栈数据结构的应用,括号匹配的核心逻辑是后进先出:最后出现的左括号,必须最先被闭合。核心规则:

  1. 遇到左括号,将其压入栈中。
  2. 遇到右括号,检查栈是否为空,若为空则直接无效;若不为空,弹出栈顶的左括号,检查是否与当前右括号类型匹配,不匹配则直接无效。
  3. 遍历完成后,若栈为空,则所有括号都匹配成功,否则无效。

实现方案

方案1:Deque栈+哈希映射(最优解)

方案原理

使用Deque作为栈的实现(Java官方推荐使用Deque替代Stack类),通过哈希映射存储右括号到对应左括号的映射,遍历字符串时,左括号入栈,右括号则通过映射表快速匹配栈顶元素,最终判断栈是否为空。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* 有效括号匹配
* @author ken
*/

@Slf4j
public class ValidParenthesesSolution {

   private static final Map<Character, Character> BRACKET_MAP = Stream.of(new Object[][]{
           {')', '('},
           {']', '['},
           {'}', '{'}
   }).collect(Collectors.toMap(
           data -> (Character) data[0],
           data -> (Character) data[1]
   ));

   /**
    * Deque栈+哈希映射解法
    * @param s 输入的括号字符串
    * @return 括号是否有效
    */

   public static boolean isValidClassic(String s) {
       if (!StringUtils.hasText(s)) {
           return true;
       }
       if (s.length() % 2 != 0) {
           return false;
       }
       Deque<Character> stack = new LinkedList<>();
       for (char c : s.toCharArray()) {
           if (!BRACKET_MAP.containsKey(c)) {
               stack.push(c);
           } else {
               if (stack.isEmpty() || stack.pop() != BRACKET_MAP.get(c)) {
                   return false;
               }
           }
       }
       return stack.isEmpty();
   }

   public static void main(String[] args) {
       log.info("示例1结果:{}", isValidClassic("()"));
       log.info("示例2结果:{}", isValidClassic("()[]{}"));
       log.info("示例3结果:{}", isValidClassic("(]"));
   }
}

复杂度分析
  • 时间复杂度:O(n),仅需遍历字符串一次
  • 空间复杂度:O(n),最坏情况下(全为左括号),栈需要存储所有字符

方案2:数组模拟栈(性能最优)

方案原理

使用数组模拟栈结构,通过栈顶指针维护栈的状态,避免Deque包装类的性能开销,直接通过字符的ASCII码进行匹配判断,是性能最优的实现方案。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

/**
* 有效括号匹配
* @author ken
*/

@Slf4j
public class ValidParenthesesSolution {

   /**
    * 数组模拟栈解法
    * @param s 输入的括号字符串
    * @return 括号是否有效
    */

   public static boolean isValidArray(String s) {
       if (!StringUtils.hasText(s)) {
           return true;
       }
       int length = s.length();
       if (length % 2 != 0) {
           return false;
       }
       char[] stack = new char[length];
       int top = -1;
       for (char c : s.toCharArray()) {
           if (c == '(' || c == '[' || c == '{') {
               stack[++top] = c;
           } else {
               if (top == -1) {
                   return false;
               }
               char topChar = stack[top--];
               if ((c == ')' && topChar != '(')
                       || (c == ']' && topChar != '[')
                       || (c == '}' && topChar != '{')) {
                   return false;
               }
           }
       }
       return top == -1;
   }

   public static void main(String[] args) {
       log.info("示例1结果:{}", isValidArray("()"));
       log.info("示例2结果:{}", isValidArray("()[]{}"));
       log.info("示例3结果:{}", isValidArray("(]"));
   }
}

复杂度分析
  • 时间复杂度:O(n),仅需遍历字符串一次
  • 空间复杂度:O(n),数组存储所需的空间,无额外包装类开销

方案3:Stream API+Reduce函数式实现

方案原理

通过Java Stream将字符串转换为字符流,使用reduce方法累积维护栈的状态与匹配结果,通过自定义状态类封装栈和有效标志,全程采用函数式编程风格,无显式循环。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.Deque;
import java.util.LinkedList;

/**
* 有效括号匹配
* @author ken
*/

@Slf4j
public class ValidParenthesesSolution {

   /**
    * 括号匹配状态封装类
    */

   static class BracketState {
       Deque<Character> stack;
       boolean valid;

       public BracketState(Deque<Character> stack, boolean valid) {
           this.stack = stack;
           this.valid = valid;
       }
   }

   /**
    * Stream+Reduce函数式解法
    * @param s 输入的括号字符串
    * @return 括号是否有效
    */

   public static boolean isValidStream(String s) {
       if (!StringUtils.hasText(s)) {
           return true;
       }
       if (s.length() % 2 != 0) {
           return false;
       }
       BracketState finalState = s.chars()
               .mapToObj(c -> (char) c)
               .reduce(
                       new BracketState(new LinkedList<>(), true),
                       (state, c) -> {
                           if (!state.valid) {
                               return state;
                           }
                           Deque<Character> stack = state.stack;
                           if (c == '(' || c == '[' || c == '{') {
                               stack.push(c);
                               return new BracketState(stack, true);
                           } else {
                               if (stack.isEmpty()) {
                                   return new BracketState(stack, false);
                               }
                               char top = stack.pop();
                               boolean match = (c == ')' && top == '(')
                                       || (c == ']' && top == '[')
                                       || (c == '}' && top == '{');
                               return new BracketState(stack, match);
                           }
                       },
                       (state1, state2) -> state1
               );
       return finalState.valid && finalState.stack.isEmpty();
   }

   public static void main(String[] args) {
       log.info("示例1结果:{}", isValidStream("()"));
       log.info("示例2结果:{}", isValidStream("()[]{}"));
       log.info("示例3结果:{}", isValidStream("(]"));
   }
}

复杂度分析
  • 时间复杂度:O(n),仅需遍历字符串一次
  • 空间复杂度:O(n),栈存储所需的空间

方案横向对比

实现方案 时间复杂度 空间复杂度 核心优势 适用场景
Deque栈+哈希映射 O(n) O(n) 逻辑直观,易于扩展,Java官方推荐实现 生产环境、通用业务场景
数组模拟栈 O(n) O(n) 性能最优,无包装类开销 高性能要求、高频调用场景
Stream函数式实现 O(n) O(n) 函数式编程风格,无显式循环 响应式编程、函数式开发场景

六、电话号码的字母组合

题目描述

给定一个仅包含数字2-9的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。数字到字母的映射与电话按键相同,1不对应任何字母。 电话按键映射: 2:abc,3:def,4:ghi,5:jkl,6:mno,7:pqrs,8:tuv,9:wxyz 示例1: 输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"] 示例2: 输入:digits = "" 输出:[] 示例3: 输入:digits = "2" 输出:["a","b","c"]

核心原理拆解

本题的核心考点是回溯算法(深度优先搜索DFS)广度优先搜索BFS,本质是求解多组集合的笛卡尔积。核心逻辑:

  1. 回溯算法:通过递归逐位处理数字,遍历当前数字对应的所有字母,拼接成临时组合后递归处理下一位数字,递归终止时将完整组合加入结果集,再回溯删除当前字母,尝试其他可能。
  2. 广度优先搜索:通过队列逐层构建组合,每处理一个数字,将队列中已有的所有组合与当前数字的所有字母拼接,生成新的组合存入队列,最终队列中的元素即为所有完整组合。

实现方案

方案1:回溯算法(DFS,最优解)

方案原理

通过回溯算法递归构建所有可能的字母组合,使用StringBuilder维护当前拼接的组合,递归终止时将组合加入结果集,再通过删除最后一个字符完成回溯,尝试其他字母。该方案是本题的经典实现,时空效率最优。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;

/**
* 电话号码的字母组合
* @author ken
*/

@Slf4j
public class LetterCombinationsSolution {

   private static final String[] DIGIT_MAP = {
           "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
   };

   /**
    * 回溯算法解法
    * @param digits 输入的数字字符串
    * @return 所有可能的字母组合
    */

   public List<String> letterCombinationsBacktrack(String digits) {
       List<String> result = new ArrayList<>();
       if (!StringUtils.hasText(digits)) {
           return result;
       }
       backtrack(digits, 0, new StringBuilder(), result);
       return result;
   }

   /**
    * 回溯递归函数
    * @param digits 输入的数字字符串
    * @param index 当前处理的数字索引
    * @param current 当前拼接的字母组合
    * @param result 结果集
    */

   private void backtrack(String digits, int index, StringBuilder current, List<String> result) {
       if (index == digits.length()) {
           result.add(current.toString());
           return;
       }
       int digit = digits.charAt(index) - '0';
       String letters = DIGIT_MAP[digit];
       for (char c : letters.toCharArray()) {
           current.append(c);
           backtrack(digits, index + 1, current, result);
           current.deleteCharAt(current.length() - 1);
       }
   }

   public static void main(String[] args) {
       LetterCombinationsSolution solution = new LetterCombinationsSolution();
       log.info("示例1结果:{}", solution.letterCombinationsBacktrack("23"));
       log.info("示例2结果:{}", solution.letterCombinationsBacktrack(""));
       log.info("示例3结果:{}", solution.letterCombinationsBacktrack("2"));
   }
}

复杂度分析
  • 时间复杂度:O(3^m * 4^n),m为对应3个字母的数字个数,n为对应4个字母的数字个数,需遍历所有可能的组合
  • 空间复杂度:O(m+n),递归调用栈的深度为数字字符串的长度

方案2:BFS队列+Stream实现

方案原理

使用Deque队列实现广度优先搜索,初始队列存入空字符串,每处理一个数字,通过Java Stream将队列中的所有组合与当前数字的字母拼接,生成新的队列,最终队列中的元素即为所有完整的字母组合。该方案无递归,避免了递归栈溢出的风险。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

/**
* 电话号码的字母组合
* @author ken
*/

@Slf4j
public class LetterCombinationsSolution {

   private static final String[] DIGIT_MAP = {
           "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
   };

   /**
    * BFS队列+Stream解法
    * @param digits 输入的数字字符串
    * @return 所有可能的字母组合
    */

   public List<String> letterCombinationsBFS(String digits) {
       if (!StringUtils.hasText(digits)) {
           return new ArrayList<>();
       }
       Deque<String> deque = new LinkedList<>();
       deque.add("");
       for (char c : digits.toCharArray()) {
           int digit = c - '0';
           String letters = DIGIT_MAP[digit];
           deque = deque.stream()
                   .flatMap(s -> letters.chars().mapToObj(ch -> s + (char) ch))
                   .collect(Collectors.toCollection(LinkedList::new));
       }
       return new ArrayList<>(deque);
   }

   public static void main(String[] args) {
       LetterCombinationsSolution solution = new LetterCombinationsSolution();
       log.info("示例1结果:{}", solution.letterCombinationsBFS("23"));
       log.info("示例2结果:{}", solution.letterCombinationsBFS(""));
       log.info("示例3结果:{}", solution.letterCombinationsBFS("2"));
   }
}

复杂度分析
  • 时间复杂度:O(3^m * 4^n),需遍历所有可能的组合
  • 空间复杂度:O(3^m * 4^n),队列需要存储所有可能的组合

方案3:Stream API+Reduce函数式实现

方案原理

通过Java Stream的reduce方法,以包含空字符串的列表为初始值,逐位处理数字,将当前列表中的所有组合与当前数字的字母拼接,生成新的列表,最终得到所有可能的字母组合。该方案全程采用函数式编程风格,代码极简。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
* 电话号码的字母组合
* @author ken
*/

@Slf4j
public class LetterCombinationsSolution {

   private static final String[] DIGIT_MAP = {
           "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
   };

   /**
    * Stream+Reduce函数式解法
    * @param digits 输入的数字字符串
    * @return 所有可能的字母组合
    */

   public List<String> letterCombinationsStreamReduce(String digits) {
       if (!StringUtils.hasText(digits)) {
           return Collections.emptyList();
       }
       return digits.chars()
               .mapToObj(c -> c - '0')
               .reduce(
                       Collections.singletonList(""),
                       (list, digit) -> list.stream()
                               .flatMap(s -> DIGIT_MAP[digit].chars().mapToObj(c -> s + (char) c))
                               .collect(Collectors.toList()),
                       (list1, list2) -> list1
               );
   }

   public static void main(String[] args) {
       LetterCombinationsSolution solution = new LetterCombinationsSolution();
       log.info("示例1结果:{}", solution.letterCombinationsStreamReduce("23"));
       log.info("示例2结果:{}", solution.letterCombinationsStreamReduce(""));
       log.info("示例3结果:{}", solution.letterCombinationsStreamReduce("2"));
   }
}

复杂度分析
  • 时间复杂度:O(3^m * 4^n),需遍历所有可能的组合
  • 空间复杂度:O(3^m * 4^n),需存储所有可能的组合

方案横向对比

实现方案 时间复杂度 空间复杂度 核心优势 适用场景
回溯算法DFS O(3^m * 4^n) O(m+n) 空间效率最优,逻辑清晰,经典实现 通用场景、面试首选方案
BFS队列+Stream O(3^m * 4^n) O(3^m * 4^n) 无递归,避免栈溢出风险,逻辑直观 超长数字字符串、避免递归的场景
Stream+Reduce O(3^m * 4^n) O(3^m * 4^n) 函数式编程风格,代码极简优雅 响应式编程、函数式开发场景

写在最后

本文覆盖的6道算法题,是Java后端开发面试中最高频的考点,不仅考察算法逻辑,更考验开发者对Java语言特性的掌握程度与代码规范的理解。每一道题的多种实现方案,都对应了不同的业务场景与开发风格,大家可根据实际需求选择合适的实现。算法能力的提升,从来不是靠死记硬背,而是理解底层逻辑,多写多练,将算法思维融入到日常的业务代码开发中,才能真正做到融会贯通。

目录
相关文章
|
3月前
|
SQL 存储 安全
别等漏洞被挖才补救!Java 代码安全审计全链路实战指南
本文系统阐述Java代码安全审计的核心规范与实操体系,涵盖输入输出、数据安全、权限认证等6大规范维度,深度解析SQL注入、XSS、反序列化等高危漏洞的底层逻辑与修复方案,并提供全流程审计方法论及CI/CD自动化集成实践。
438 1
|
2月前
|
Arthas 监控 数据可视化
深度剖析:Java 并发三大量难题 —— 死锁、活锁、饥饿全解
本文深入剖析Java并发中三大顽疾:死锁(线程永久阻塞)、活锁(线程忙等无效运行)、饥饿(低优先级线程长期得不到资源)。厘清其本质区别、触发条件、实战案例及jstack/Arthas等排查方案,并给出统一锁序、定时锁、公平锁等落地解决策略。
292 1
|
3月前
|
Java 关系型数据库 MySQL
DDD 领域驱动设计:从战略到战术,终结微服务拆分的所有混乱
本文深入剖析微服务拆分困境,指出问题根源在于混淆技术边界与业务边界。提出DDD(领域驱动设计)作为破局之道:以战略设计(领域划分、统一语言、事件风暴、上下文映射)确定微服务合理边界;以战术设计(四层架构、聚合根、值对象等)保障领域模型内聚。结合电商订单域完整落地示例,揭示DDD本质是“先懂业务,再写代码”的设计思想。
725 3
|
Java
Mac下安装JDK11(国内镜像)
Mac下安装JDK11(国内镜像)
9223 0
|
移动开发 vr&ar
数据库系统概论——关系代数详解
关系代数是一种抽象的查询语言,是关系数据操纵语言的一种传统表达方式,它是利用对关系的运算来表达查询的。任何运算都是将一定的运算符作用于一定的运算对象上,得到预期的运算结果。关系代数的运算对象是关系,运算结果亦为关系。集合运算符将关系看成元组的集合从关系的“水平”方向即行的角度来进行运算专门的关系运算符不仅涉及行而且涉及列算术比较符辅助专门的关系运算符进行操作逻辑运算符辅助专门的关系运算符进行操作。
2715 1
数据库系统概论——关系代数详解
|
5月前
|
数据采集 缓存 开发框架
RFC规范解释、URL 与 Body 、GET/POST 的核心区别详解
本文深入解析RFC规范下GET与POST的本质区别:GET语义为“只读”,安全且幂等,适用于获取资源;POST为“写操作”,不安全也不幂等,用于提交数据。详解URL与Body用法误区,并揭示安全、幂等属性对开发的影响,助你避开常见坑,写出更规范的接口。
662 3
|
应用服务中间件 nginx Windows
音视频系列六:Windows搭建Nginx+rtmp推流服务器
在前面 阿里云服务器搭建Nginx+rtmp推流服务器 中,我们已经配置把阿里云的rtmp推流服务搭建好了,用的是PC软件OBS来进行推流到阿里云服务器转发然后本地拉流。Windows也是大同小异,现在是用Windows进行推流服务的搭建,本地ffmpeg命令行推流,本地ffplay拉流播放/VLC拉流播放。
1972 0
音视频系列六:Windows搭建Nginx+rtmp推流服务器
|
存储 Java fastjson
Java泛型-4(类型擦除后如何获取泛型参数)
Java泛型-4(类型擦除后如何获取泛型参数)
505 1
|
12月前
|
XML 人工智能 Java
注入Java Bean的方式
本文总结了 Spring Boot 中常见的 Bean 注入方式,包括字段注入(`@Autowired`)、构造器注入(推荐)、Setter 方法注入、`@Resource` 按名称注入、`@Bean` 定义复杂 Bean、`@Value` 注入配置值、XML 配置、`ApplicationContextAware` 手动获取 Bean 以及 JSR-330 的 `@Inject`。同时分析了 Setter 注入逐渐被淡化的原因,强调构造器注入的不可变性和安全性优势,并推荐结合 Lombok 简化代码。文章还提供了按需选择注解的建议和最佳实践,帮助开发者根据场景选择合适的依赖注入方式。
1092 49
|
XML Java 数据库连接
springboot中整合mybatis及简单使用
这篇文章介绍了如何在Spring Boot项目中整合MyBatis,包括依赖引入、配置数据源、创建测试表、编写Mapper接口和XML文件、以及创建Service和Controller层的步骤。
springboot中整合mybatis及简单使用