mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-12-08 01:59:14 +08:00
ArrangementTest
This commit is contained in:
parent
bad0c4c982
commit
6cbdf89fe6
@ -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;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',则读取sheetName,sheetIndex使用-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
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user