ArrangementTest

This commit is contained in:
Looly 2025-11-28 09:57:04 +08:00
parent bad0c4c982
commit 6cbdf89fe6
4 changed files with 239 additions and 84 deletions

View File

@ -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<String[]> result = new ArrayList<>(capacity);
boolean[] visited = new boolean[datas.length];
final List<String[]> 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<String[]> iterate(int m) {
public Iterable<String[]> 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<Integer> 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<String[]> result) {
private void dfs(final String[] current, final int depth, final boolean[] visited, final List<String[]> result) {
if (depth == current.length) {
result.add(Arrays.copyOf(current, current.length));
return;

View File

@ -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<String[]> 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<String[]> 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<String[]> iterResult = new ArrayList<>();
for (final String[] perm : arrangement.iterate(5)) {
iterResult.add(perm);
}
assertTrue(iterResult.isEmpty());
}
}

View File

@ -133,7 +133,7 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
* @throws POIException IO异常包装
*/
public Excel03SaxReader read(final POIFSFileSystem fs, final String idOrRidOrSheetName) throws POIException {
this.sheetIndex = getSheetIndex(idOrRidOrSheetName);
initSheetIndexOrSheetName(idOrRidOrSheetName);
formatListener = new FormatTrackingHSSFListener(new MissingRecordAwareHSSFListener(this));
final HSSFRequest request = new HSSFRequest();
@ -373,33 +373,33 @@ public class Excel03SaxReader implements HSSFListener, ExcelSaxReader<Excel03Sax
/**
* 获取sheet索引从0开始
* <ul>
* <li>Excel03中没有rid概念如果传入'rId'开头直接去除rId前缀按照sheetIndex对待</li>
* <li>Excel03中没有rid概念如果传入'rId'开头rid从1开始计数直接去除rId前缀并减1转换为sheetIndex</li>
* <li>传入纯数字表示sheetIndex</li>
* <li>传入sheet名称例如'sheet1'则读取sheetNamesheetIndex使用-1表示</li>
* </ul>
*
* @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
}

View File

@ -24,9 +24,9 @@ import java.io.InputStream;
/**
* Sax方式读取Excel接口提供一些共用方法
* @author Looly
*
* @param <T> 子对象类型用于标记返回值this
* @author Looly
* @since 3.2.0
*/
public interface ExcelSaxReader<T> {
@ -41,29 +41,41 @@ public interface ExcelSaxReader<T> {
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名称规则如下
* <ul>
* <li>如果为-1处理所有编号的sheet</li>
* <li>如果为rId开头例如rId1表示读取指定编号的sheet从1计数即rId1表示第一个sheet</li>
* <li>如果为sheet名称例如sheet1直接读取名车给对应sheet</li>
* <li>如果为纯数字在03中表示index从0开始07中表示sheet id从1开始</li>
* </ul>
* @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名称规则如下
* <ul>
* <li>如果为-1处理所有编号的sheet</li>
* <li>如果为rId开头例如rId1表示读取指定编号的sheet从1计数即rId1表示第一个sheet</li>
* <li>如果为sheet名称例如sheet1直接读取名车给对应sheet</li>
* <li>如果为纯数字在03中表示index从0开始07中表示sheet id从1开始</li>
* </ul>
* @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<T> {
}
/**
* 开始读取Excel读取所有sheet
* 开始从文件中读取Excel读取所有sheet
*
* @param file Excel文件
* @return this
@ -83,7 +95,7 @@ public interface ExcelSaxReader<T> {
}
/**
* 开始读取Excel读取所有sheet读取结束后并不关闭流
* 开始从流中读取Excel读取所有sheet读取结束后并不关闭流
*
* @param in Excel包流
* @return this
@ -94,22 +106,28 @@ public interface ExcelSaxReader<T> {
}
/**
* 开始读取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名称规则如下
* <ul>
* <li>如果为-1处理所有编号的sheet</li>
* <li>如果为rId开头例如rId1表示读取指定编号的sheet从1计数即rId1表示第一个sheet</li>
* <li>如果为sheet名称例如sheet1直接读取名车给对应sheet</li>
* <li>如果为纯数字在03中表示index从0开始07中表示sheet id从1开始</li>
* </ul>
* @return this
* @throws POIException POI异常
*/
@ -118,26 +136,34 @@ public interface ExcelSaxReader<T> {
}
/**
* 开始读取Excel
* 开始从文件中读取Excel
*
* @param file Excel文件
* @param rid Excel中的sheet rid编号如果为-1处理所有编号的sheet
* @param file Excel文件
* @param idOrRid Excel中的sheet id或者rid编号规则如下
* <ul>
* <li>如果为-1处理所有编号的sheet</li>
* <li>如果为纯数字在03中表示index从0开始07中表示sheet id从1开始</li>
* </ul>
* @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编号规则如下
* <ul>
* <li>如果为-1处理所有编号的sheet</li>
* <li>如果为纯数字在03中表示index从0开始07中表示sheet id从1开始</li>
* </ul>
* @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));
}
}