我将问题概括为“将N个已排序数组合并为一个已排序数组”。
该问题的代码使用了泛型。但是,这会引入一个问题,因为数组不是类型安全的。简而言之,它们的行为有很大的差异:数组是协变的,而泛型是不变的。由于这个原因,当混用泛型和数组时,编译器无法识别出问题。避免使用泛型数组是一种好的实践。
另外,我考虑到这显然是一个算法问题(因此其受众比那些需要深入了解Java才能掌握基于泛型的实现的读者更广),我决定创建两种解决方案:一种仅使用数组,另一种使用泛型和集合框架。
非泛型版本
下面是如何合并任意数量的原始排序数组的描述:
- 找到所有元素的总数,并基于它创建一个结果数组;
- 定义一个数组,用于维护每个源数组中的当前位置;
- 对于结果数组中的每个位置,使用嵌套的for循环选择所有当前可访问值中最小的值。
这个算法的时间复杂度是O(n * m)(其中n是所有数组中的元素总数,m是数组数量)。
该实现可能看起来像这样:
public static int[] mergeNSorted(int[]... arrays) {
int[] result = new int[getTotalLength(arrays)];
int[] positions = new int[arrays.length];
for (int pos = 0; pos < result.length; pos++) {
int minCurVal = Integer.MAX_VALUE;
int curArr = 0;
for (int i = 0; i < arrays.length; i++) {
if (positions[i] < arrays[i].length && arrays[i][positions[i]] < minCurVal) {
minCurVal = arrays[i][positions[i]];
curArr = i;
}
}
result[pos] = minCurVal;
positions[curArr]++;
}
return result;
}
public static int getTotalLength(int[][] arrays) {
long totalLen = 0;
for (int[] arr : arrays) totalLen += arr.length;
if (totalLen > Integer.MAX_VALUE) throw new IllegalArgumentException("total length exceeded Integer.MAX_VALUE");
return (int) totalLen;
}
main()
- 演示
public static void main(String[] args) {
int[][] input =
{{1, 3}, {}, {2, 6, 7}, {10}, {4, 5, 8, 9}};
System.out.println(Arrays.toString(mergeNSorted(input)));
}
输出
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
通用版本
在此版本中,输入被视为包含多个泛型类型为T
的列表的列表,这些列表应实现Comparable
接口。
该解决方案增强了上面提供的基于数组的实现,将总时间复杂度降至O(n * log m)(其中n
是所有数组中元素的总数,m
是数组的数量)。
它不是为每个结果元素执行m
次迭代,而是维护一个PriorityQueue
,在本例中表示一个最小堆(即当从中检索头元素时,它将具有所有存在于队列中的元素中最低的值)。
队列中的每个元素都包装了从给定列表之一检索的特定元素的值,以及关于此值来源的数据(即列表的索引和此列表内部的位置)。
这个嵌套列表的元素包装器可以由下面所示的类来表示。
public class ElementWrapper<V extends Comparable<V>> implements Comparable<ElementWrapper<V>> {
private V value;
private int listIndex;
private int position;
public ElementWrapper(V value, int listIndex, int position) {
this.value = value;
this.listIndex = listIndex;
this.position = position;
}
@Override
public int compareTo(ElementWrapper<V> o) {
return value.compareTo(o.getValue());
}
}
注意,该类基于包装列表元素的值实现了
Comparable
接口。
队列将使用每个非空列表的第一个元素进行预填充。然后,直到队列为空为止,将删除其最低元素并添加到结果列表中。此外,如果从队列中检索的最新元素指向的列表具有更多元素,则将它们中的下一个元素添加到队列中。
请注意,根据
文档,将新元素添加到优先级队列
add()
和删除其头元素
remove()
的两个操作都需要O(n)时间成本(其中n是队列中元素的数量)。
通过使用
TreeSet
也可以实现相同的时间复杂度,但实际上
PriorityQueue
的性能更好,因为维护堆比维护红黑树更容易。
代码可能如下所示:
public static <T extends Comparable<T>> List<T> mergeNSorted(List<List<T>> lists) {
List<T> result = new ArrayList<>();
Queue<ElementWrapper<T>> queue = getInitializedQueue(lists);
while (!queue.isEmpty()) {
ElementWrapper<T> next = queue.remove();
result.add(next.getValue());
if (next.getPosition() + 1 < lists.get(next.getListIndex()).size()) {
queue.add(new ElementWrapper<>(lists.get(next.getListIndex()).get(next.getPosition() + 1),
next.getListIndex(),
next.getPosition() + 1));
}
}
return result;
}
public static <T extends Comparable<T>> Queue<ElementWrapper<T>> getInitializedQueue(List<List<T>> lists) {
Queue<ElementWrapper<T>> queue = new PriorityQueue<>();
for (int i = 0; i < lists.size(); i++) {
if (lists.get(i).isEmpty()) continue;
queue.add(new ElementWrapper<>(lists.get(i).get(0), i, 0));
}
return queue;
}
main()
- 演示
public static void main(String[] args) {
List<List<Integer>> genericInput =
List.of(List.of(1, 3), List.of(), List.of(2, 6, 7), List.of(10), List.of(4, 5, 8, 9));
System.out.println(mergeNSorted(genericInput));
}
输出
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]