From 6cbdf89fe69fe40191611bcb1776d81be735176c Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 28 Nov 2025 09:57:04 +0800 Subject: [PATCH] ArrangementTest --- .../cn/hutool/v7/core/math/Arrangement.java | 175 +++++++++++++----- .../hutool/v7/core/math/ArrangementTest.java | 48 +++++ .../v7/poi/excel/sax/Excel03SaxReader.java | 18 +- .../v7/poi/excel/sax/ExcelSaxReader.java | 82 +++++--- 4 files changed, 239 insertions(+), 84 deletions(-) diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/math/Arrangement.java b/hutool-core/src/main/java/cn/hutool/v7/core/math/Arrangement.java index 918c22b900..2925d18e48 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/math/Arrangement.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/math/Arrangement.java @@ -16,8 +16,6 @@ package cn.hutool.v7.core.math; -import cn.hutool.v7.core.array.ArrayUtil; - import java.io.Serial; import java.io.Serializable; import java.util.*; @@ -74,7 +72,7 @@ public class Arrangement implements Serializable { long result = 1; // 从 n 到 n-m+1 逐个乘 for (int i = 0; i < m; i++) { - long next = result * (n - i); + final long next = result * (n - i); // 溢出检测 if (next < result) { throw new ArithmeticException("Overflow computing A(" + n + "," + m + ")"); @@ -149,11 +147,11 @@ public class Arrangement implements Serializable { return Collections.singletonList(new String[0]); } - long estimated = count(datas.length, m); - int capacity = estimated > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) estimated; + final long estimated = count(datas.length, m); + final int capacity = estimated > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) estimated; - List result = new ArrayList<>(capacity); - boolean[] visited = new boolean[datas.length]; + final List result = new ArrayList<>(capacity); + final boolean[] visited = new boolean[datas.length]; dfs(new String[m], 0, visited, result); return result; } @@ -194,7 +192,7 @@ public class Arrangement implements Serializable { * @param m 选择的元素个数 * @return 排列迭代器 */ - public Iterable iterate(int m) { + public Iterable iterate(final int m) { return () -> new ArrangementIterator(datas, m); } @@ -208,10 +206,18 @@ public class Arrangement implements Serializable { private final String[] datas; private final int m; + private final int n; private final boolean[] visited; private final String[] buffer; - private final Deque stack = new ArrayDeque<>(); - boolean end = false; + + // 每一层记录当前尝试的下标,-1表示还未尝试 + private final int[] indices; + private int depth; + private boolean end; + + // 预取下一个元素 + private String[] nextItem; + private boolean nextPrepared; /** * 构造函数 @@ -219,62 +225,137 @@ public class Arrangement implements Serializable { * @param datas 数据数组 * @param m 选择的元素个数 */ - ArrangementIterator(String[] datas, int m) { + ArrangementIterator(final String[] datas, final int m) { this.datas = datas; this.m = m; - this.visited = new boolean[datas.length]; - this.buffer = new String[m]; - // 初始化 dfs 栈 - stack.push(0); + this.n = datas.length; + this.visited = new boolean[n]; + this.nextItem = null; + this.nextPrepared = false; + + if (m < 0 || m > n) { + // 无效或无解,直接结束 + this.indices = new int[Math.max(1, m)]; + this.buffer = new String[Math.max(1, m)]; + this.depth = -1; + this.end = true; + } else if (m == 0) { + // m == 0: 只返回一个空数组 + this.indices = new int[0]; + this.buffer = new String[0]; + this.depth = 0; + this.end = false; + } else { + this.indices = new int[m]; + Arrays.fill(this.indices, -1); + this.buffer = new String[m]; + this.depth = 0; + this.end = false; + } } @Override public boolean hasNext() { - return !end; + if (end) { + return false; + } + if (nextPrepared) { + return nextItem != null; + } + prepareNext(); + return nextItem != null; } @Override public String[] next() { - while (!stack.isEmpty()) { - int depth = stack.size() - 1; + if (end && !nextPrepared) { + throw new NoSuchElementException(); + } + if (!nextPrepared) { + prepareNext(); + } + if (nextItem == null) { + throw new NoSuchElementException(); + } + final String[] ret = nextItem; + // 清除预取缓存,下一次需要重新准备 + nextItem = null; + nextPrepared = false; + // 如果m == 0,该项是唯一项,迭代结束 + if (m == 0) { + end = true; + } + return ret; + } - int idx = stack.pop(); - if (idx >= datas.length) { - // 这一层遍历结束 - if (!stack.isEmpty()) { - int prev = stack.pop(); - stack.push(prev + 1); + /** + * 将状态推进到下一个可返回的排列并把它放入 nextItem。 + * 如果无更多排列,则将 end=true 并把 nextItem 置为 null。 + */ + private void prepareNext() { + // 已经准备过或已结束 + if (nextPrepared || end) { + nextPrepared = true; + return; + } + + // special-case m == 0 + if (m == 0) { + nextItem = new String[0]; + nextPrepared = true; + // do not set end here; end will be set after returning this element in next() + return; + } + + // 非递归模拟DFS,直到找到一个可返回的排列或穷尽 + while (depth >= 0) { + final int start = indices[depth] + 1; + boolean found = false; + for (int i = start; i < n; i++) { + if (!visited[i]) { + // 如果当前层之前有选过一个元素,要先取消之前选中的 visited + if (indices[depth] != -1) { + visited[indices[depth]] = false; + } + indices[depth] = i; + visited[i] = true; + buffer[depth] = datas[i]; + found = true; + break; } + } + + if (!found) { + // 本层没有可用元素,回溯 + if (indices[depth] != -1) { + visited[indices[depth]] = false; + indices[depth] = -1; + } + depth--; continue; } - // 如果该元素未使用 - if (!visited[idx]) { - visited[idx] = true; - buffer[depth] = datas[idx]; - - if (depth == m - 1) { - // 输出一个排列 - visited[idx] = false; - - // 下一次从 idx+1 继续 - stack.push(idx + 1); - - return Arrays.copyOf(buffer, m); - } else { - // 继续下一层 - stack.push(idx + 1); // 当前层下一个起点 - stack.push(0); // 下一层起点 - continue; + // 若已达到输出深度,准备输出(但不抛出) + if (depth == m - 1) { + nextItem = Arrays.copyOf(buffer, m); + // 取消当前visited,为下一次在同一层寻找下一个候选做准备 + visited[indices[depth]] = false; + // 保持 depth 不变(下一次 prepare 会从 indices[depth]+1 开始寻找) + nextPrepared = true; + return; + } else { + // 向下一层深入:初始化下一层为-1并继续循环 + depth++; + if (depth < m) { + indices[depth] = -1; } } - - // 已访问则跳过 - stack.push(idx + 1); } + // 若循环结束,说明已经穷尽所有可能 end = true; - return null; + nextItem = null; + nextPrepared = true; } } @@ -286,7 +367,7 @@ public class Arrangement implements Serializable { * @param visited 标记数组,记录哪些索引已经被使用了 * @param result 结果集 */ - private void dfs(String[] current, int depth, boolean[] visited, List result) { + private void dfs(final String[] current, final int depth, final boolean[] visited, final List result) { if (depth == current.length) { result.add(Arrays.copyOf(current, current.length)); return; diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/math/ArrangementTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/math/ArrangementTest.java index f92f400c52..30d8718d1a 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/math/ArrangementTest.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/math/ArrangementTest.java @@ -20,6 +20,7 @@ import cn.hutool.v7.core.lang.Console; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -152,4 +153,51 @@ public class ArrangementTest { assertArrayEquals(new String[]{"1", "2"}, all.get(3)); assertArrayEquals(new String[]{"1", "2", "3"}, all.get(9)); } + + // ---------------------------------------------------- + // 迭代器测试 + // ---------------------------------------------------- + @Test + public void iteratorTest() { + final Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"}); + + // 测试 m=2 的情况 + final List iterResult = new ArrayList<>(); + for (final String[] perm : arrangement.iterate(2)) { + iterResult.add(perm); + } + + assertEquals(6, iterResult.size()); + assertArrayEquals(new String[]{"1", "2"}, iterResult.get(0)); + assertArrayEquals(new String[]{"1", "3"}, iterResult.get(1)); + assertArrayEquals(new String[]{"2", "1"}, iterResult.get(2)); + assertArrayEquals(new String[]{"2", "3"}, iterResult.get(3)); + assertArrayEquals(new String[]{"3", "1"}, iterResult.get(4)); + assertArrayEquals(new String[]{"3", "2"}, iterResult.get(5)); + } + + @Test + public void iteratorFullTest() { + final Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"}); + + // 测试全排列的情况 + final List iterResult = new ArrayList<>(); + for (final String[] perm : arrangement.iterate(3)) { + iterResult.add(perm); + } + + assertEquals(6, iterResult.size()); + } + + @Test + public void iteratorBoundaryTest() { + final Arrangement arrangement = new Arrangement(new String[]{"1", "2", "3"}); + + // 测试 m > n 的情况 + final List iterResult = new ArrayList<>(); + for (final String[] perm : arrangement.iterate(5)) { + iterResult.add(perm); + } + assertTrue(iterResult.isEmpty()); + } } diff --git a/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/Excel03SaxReader.java b/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/Excel03SaxReader.java index b618eba0fb..d41272f165 100644 --- a/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/Excel03SaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/Excel03SaxReader.java @@ -133,7 +133,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader - *
  • Excel03中没有rid概念,如果传入'rId'开头,直接去除rId前缀,按照sheetIndex对待
  • + *
  • Excel03中没有rid概念,如果传入'rId'开头,rid从1开始计数,直接去除rId前缀并减1,转换为sheetIndex
  • *
  • 传入纯数字,表示sheetIndex
  • + *
  • 传入sheet名称,例如'sheet1',则读取sheetName,sheetIndex使用-1表示。
  • * * - * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,从0开始,rid必须加rId前缀,例如rId0,如果为-1处理所有编号的sheet - * @return sheet索引,从0开始 + * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,从0开始,rid必须加rId前缀,例如rId1,从1开始,如果为-1处理所有编号的sheet * @since 5.5.5 */ - private int getSheetIndex(final String idOrRidOrSheetName) { + private void initSheetIndexOrSheetName(final String idOrRidOrSheetName) { Assert.notBlank(idOrRidOrSheetName, "id or rid or sheetName must be not blank!"); // rid直接处理 if (StrUtil.startWithIgnoreCase(idOrRidOrSheetName, RID_PREFIX)) { - return Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, RID_PREFIX)); + // rid从1开始计数,此处转换为从0开始的索引 + this.sheetIndex = Integer.parseInt(StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, RID_PREFIX)) - 1; } else if(StrUtil.startWithIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX)){ // since 5.7.10,支持任意名称 this.sheetName = StrUtil.removePrefixIgnoreCase(idOrRidOrSheetName, SHEET_NAME_PREFIX); } else { + // 传入纯数字,表示sheetIndex try { - return Integer.parseInt(idOrRidOrSheetName); + this.sheetIndex = Integer.parseInt(idOrRidOrSheetName); } catch (final NumberFormatException ignore) { // 如果用于传入非数字,按照sheet名称对待 this.sheetName = idOrRidOrSheetName; } } - - return -1; } // ---------------------------------------------------------------------------------------------- Private method end } diff --git a/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/ExcelSaxReader.java b/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/ExcelSaxReader.java index 49b94c2622..dbb2ab2ba1 100644 --- a/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/ExcelSaxReader.java +++ b/hutool-poi/src/main/java/cn/hutool/v7/poi/excel/sax/ExcelSaxReader.java @@ -24,9 +24,9 @@ import java.io.InputStream; /** * Sax方式读取Excel接口,提供一些共用方法 - * @author Looly * * @param 子对象类型,用于标记返回值this + * @author Looly * @since 3.2.0 */ public interface ExcelSaxReader { @@ -41,29 +41,41 @@ public interface ExcelSaxReader { String SHEET_NAME_PREFIX = "sheetName:"; /** - * 开始读取Excel + * 开始从文件中读取Excel * - * @param file Excel文件 - * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,rid必须加rId前缀,例如rId1,如果为-1处理所有编号的sheet + * @param file Excel文件 + * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,规则如下: + *
      + *
    • 如果为-1,处理所有编号的sheet
    • + *
    • 如果为rId开头,例如rId1,表示读取指定编号的sheet,从1计数,即rId1表示第一个sheet
    • + *
    • 如果为sheet名称,例如sheet1,直接读取名车给对应sheet
    • + *
    • 如果为纯数字,在03中表示index,从0开始,07中表示sheet id,从1开始
    • + *
    * @return this * @throws POIException POI异常 */ T read(File file, String idOrRidOrSheetName) throws POIException; /** - * 开始读取Excel,读取结束后并不关闭流 + * 开始从流中读取Excel,读取结束后并不关闭流 * - * @param in Excel流 - * @param idOrRidOrSheetName Excel中的sheet id或者rid编号,rid必须加rId前缀,例如rId1,如果为-1处理所有编号的sheet + * @param in Excel流 + * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,规则如下: + *
      + *
    • 如果为-1,处理所有编号的sheet
    • + *
    • 如果为rId开头,例如rId1,表示读取指定编号的sheet,从1计数,即rId1表示第一个sheet
    • + *
    • 如果为sheet名称,例如sheet1,直接读取名车给对应sheet
    • + *
    • 如果为纯数字,在03中表示index,从0开始,07中表示sheet id,从1开始
    • + *
    * @return this * @throws POIException POI异常 */ T read(InputStream in, String idOrRidOrSheetName) throws POIException; /** - * 开始读取Excel,读取所有sheet + * 开始从路径中读取Excel,读取所有sheet * - * @param path Excel文件路径 + * @param path Excel文件路径,如果是相对路径,则相对classpath * @return this * @throws POIException POI异常 */ @@ -72,7 +84,7 @@ public interface ExcelSaxReader { } /** - * 开始读取Excel,读取所有sheet + * 开始从文件中读取Excel,读取所有sheet * * @param file Excel文件 * @return this @@ -83,7 +95,7 @@ public interface ExcelSaxReader { } /** - * 开始读取Excel,读取所有sheet,读取结束后并不关闭流 + * 开始从流中读取Excel,读取所有sheet,读取结束后并不关闭流 * * @param in Excel包流 * @return this @@ -94,22 +106,28 @@ public interface ExcelSaxReader { } /** - * 开始读取Excel + * 开始从路径中读取Excel * - * @param path 文件路径 - * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,rid必须加rId前缀,例如rId1,如果为-1处理所有编号的sheet + * @param path 文件路径,如果是相对路径,则相对classpath + * @param idOrRid Excel中的sheet id或者rid编号,rid必须加rId前缀,例如rId1,如果为-1处理所有编号的sheet * @return this * @throws POIException POI异常 */ - default T read(final String path, final int idOrRidOrSheetName) throws POIException { - return read(FileUtil.file(path), idOrRidOrSheetName); + default T read(final String path, final int idOrRid) throws POIException { + return read(FileUtil.file(path), idOrRid); } /** * 开始读取Excel * - * @param path 文件路径 - * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,rid必须加rId前缀,例如rId1,如果为-1处理所有编号的sheet + * @param path 文件路径 + * @param idOrRidOrSheetName Excel中的sheet id或者rid编号或sheet名称,规则如下: + *
      + *
    • 如果为-1,处理所有编号的sheet
    • + *
    • 如果为rId开头,例如rId1,表示读取指定编号的sheet,从1计数,即rId1表示第一个sheet
    • + *
    • 如果为sheet名称,例如sheet1,直接读取名车给对应sheet
    • + *
    • 如果为纯数字,在03中表示index,从0开始,07中表示sheet id,从1开始
    • + *
    * @return this * @throws POIException POI异常 */ @@ -118,26 +136,34 @@ public interface ExcelSaxReader { } /** - * 开始读取Excel + * 开始从文件中读取Excel * - * @param file Excel文件 - * @param rid Excel中的sheet rid编号,如果为-1处理所有编号的sheet + * @param file Excel文件 + * @param idOrRid Excel中的sheet id或者rid编号,规则如下: + *
      + *
    • 如果为-1,处理所有编号的sheet
    • + *
    • 如果为纯数字,在03中表示index,从0开始,07中表示sheet id,从1开始
    • + *
    * @return this * @throws POIException POI异常 */ - default T read(final File file, final int rid) throws POIException{ - return read(file, String.valueOf(rid)); + default T read(final File file, final int idOrRid) throws POIException { + return read(file, String.valueOf(idOrRid)); } /** - * 开始读取Excel,读取结束后并不关闭流 + * 开始从流中读取Excel,读取结束后并不关闭流 * - * @param in Excel流 - * @param rid Excel中的sheet rid编号,如果为-1处理所有编号的sheet + * @param in Excel流 + * @param idOrRid Excel中的sheet id或者rid编号,规则如下: + *
      + *
    • 如果为-1,处理所有编号的sheet
    • + *
    • 如果为纯数字,在03中表示index,从0开始,07中表示sheet id,从1开始
    • + *
    * @return this * @throws POIException POI异常 */ - default T read(final InputStream in, final int rid) throws POIException{ - return read(in, String.valueOf(rid)); + default T read(final InputStream in, final int idOrRid) throws POIException { + return read(in, String.valueOf(idOrRid)); } }