如果我有一个N大小的对象数组,并且我有1 ... N范围内的唯一数字数组,是否有任何算法可以重新排列对象数组就地由数字列表指定的顺序,但在O(N)时间内执行此操作?
上下文:我正在对大小相当大的对象进行快速排序算法,因此在索引上进行交换比在对象本身上进行交换更快,并且只在最后一次传递中移动对象。我只是想知道我是否可以做最后一次传递而不为单独的数组分配内存。
编辑:我不询问如何在O(N)时间内进行排序,而是如何进行后排序重新排列在O(N)时间内有O(1)空间。很抱歉没有说清楚。
答案 0 :(得分:16)
我认为应该这样做:
static <T> void arrange(T[] data, int[] p) {
boolean[] done = new boolean[p.length];
for (int i = 0; i < p.length; i++) {
if (!done[i]) {
T t = data[i];
for (int j = i;;) {
done[j] = true;
if (p[j] != i) {
data[j] = data[p[j]];
j = p[j];
} else {
data[j] = t;
break;
}
}
}
}
}
注意:这是Java。如果您使用不带垃圾回收的语言执行此操作,请务必删除done
。
如果您关心空间,可以使用BitSet作为done
。我假设您可以为每个元素支付额外的一点,因为您似乎愿意使用排列数组,这是该数量的几倍。
该算法复制T n + k次的实例,其中k是置换中的周期数。您可以跳过那些p [i] = i。
的i,将其减少到最佳副本数答案 1 :(得分:7)
你的意思是你有一个对象数组O [1..N]然后你有一个数组P [1..N],其中包含数字1..N的排列,最后你想要得到一个对象的数组O1,使得所有k = 1..N的O1 [k] = O [P [k]]
例如,如果您的对象是字母A,B,C ...,Y,Z并且您的数组P是[26,25,24,..,2,1]则是您想要的输出Z,Y ,... C,B,A?
如果是,我相信你可以仅使用O(1)额外内存在线性时间内完成。反转数组的元素是此方案的特例。一般来说,我认为您需要考虑将置换P分解为循环,然后使用它来移动原始数组O []的元素。
如果这就是你要找的东西,我可以详细说明。
编辑:其他人在我睡觉的时候已经提出了很好的解决方案,所以不需要在这里重复。 ^ _ ^编辑:我的O(1)额外空间确实不完全正确。我只考虑“数据”元素,但事实上你还需要为每个置换元素存储一位,所以如果我们精确,我们需要O(log n)额外位。但是大部分时间使用符号位(如J.F.Sebastian建议的那样)都很好,所以在实践中我们可能不需要比现有更多的东西。
答案 2 :(得分:7)
方法是遵循排列的“置换周期”,而不是从左到右索引数组。但是既然你必须从某个地方开始,每当需要一个新的排列周期时,搜索unpermuted元素是从左到右:
// Pseudo-code N : integer, N > 0 // N is the number of elements swaps : integer [0..N] data[N] : array of object permute[N] : array of integer [-1..N] denoting permutation (used element is -1) next_scan_start : integer;
next_scan_start = 0;
while (swaps < N ) { // Search for the next index that is not-yet-permtued. for (idx_cycle_search = next_scan_start; idx_cycle_search < N; ++ idx_cycle_search) if (permute[idx_cycle_search] >= 0) break;
next_scan_start = idx_cycle_search + 1;
// This is a provable invariant. In short, number of non-negative // elements in permute[] equals (N - swaps) assert( idx_cycle_search < N );
// Completely permute one permutation cycle, 'following the // permutation cycle's trail' This is O(N) while (permute[idx_cycle_search] >= 0) { swap( data[idx_cycle_search], data[permute[idx_cycle_search] ) swaps ++; old_idx = idx_cycle_search; idx_cycle_search = permute[idx_cycle_search]; permute[old_idx] = -1; // Also '= -idx_cycle_search -1' could be used rather than '-1' // and would allow reversal of these changes to permute[] array } }
答案 3 :(得分:1)
如果您不介意为额外的索引哈希分配内存,则可以保留原始位置到当前位置的映射,以获得接近O(n)的时间复杂度。这是Ruby中的一个例子,因为它是可读的和伪代码的。 (这可能是更短或更具惯用性的Ruby-ish,但为了清楚起见,我已将其写出来。)
#!/usr/bin/ruby
objects = ['d', 'e', 'a', 'c', 'b']
order = [2, 4, 3, 0, 1]
cur_locations = {}
order.each_with_index do |orig_location, ordinality|
# Find the current location of the item.
cur_location = orig_location
while not cur_locations[cur_location].nil? do
cur_location = cur_locations[cur_location]
end
# Swap the items and keep track of whatever we swapped forward.
objects[ordinality], objects[cur_location] = objects[cur_location], objects[ordinality]
cur_locations[ordinality] = orig_location
end
puts objects.join(' ')
这显然确实涉及哈希的一些额外内存,但因为它只是针对索引而不是你的“相当大”的对象,所以希望这是可以接受的。由于哈希查找是O(1),即使由于项目已被多次向前交换并且您必须多次重写cur_location
而导致复杂性略有下降,因此算法为整体应该合理地接近O(n)。
如果你想要你可以提前建立原始到当前位置的完整哈希值,或者保持当前到原始位置的反向哈希值,并稍微修改算法以使其严格地达到O(n)。它会有点复杂并占用更多的空间,所以这是我写的版本,但修改应该不难。
编辑:实际上,我非常确定时间复杂度只是O(n),因为每个标准最多只能有一个跳跃关联,因此查找的最大数量限制为n。
答案 4 :(得分:1)
#!/usr/bin/env python
def rearrange(objects, permutation):
"""Rearrange `objects` inplace according to `permutation`.
``result = [objects[p] for p in permutation]``
"""
seen = [False] * len(permutation)
for i, already_seen in enumerate(seen):
if not already_seen: # start permutation cycle
first_obj, j = objects[i], i
while True:
seen[j] = True
p = permutation[j]
if p == i: # end permutation cycle
objects[j] = first_obj # [old] p -> j
break
objects[j], j = objects[p], p # p -> j
算法(正如我在编写之后所注意到的)与@meriton's answer in Java中的算法相同。
以下是代码的test
函数:
def test():
import itertools
N = 9
for perm in itertools.permutations(range(N)):
L = range(N)
LL = L[:]
rearrange(L, perm)
assert L == [LL[i] for i in perm] == list(perm), (L, list(perm), LL)
# test whether assertions are enabled
try:
assert 0
except AssertionError:
pass
else:
raise RuntimeError("assertions must be enabled for the test")
if __name__ == "__main__":
test()
答案 5 :(得分:0)
有histogram sort,但运行时间略高于O(N)(N log log n)。
答案 6 :(得分:0)
我可以使用O(N)临时空间 - 复制到新数组并复制回来。
编辑:我知道将继续存在的算法存在。我们的想法是在整数1..N数组上执行交换,同时镜像大型对象数组上的交换。我现在找不到算法。答案 7 :(得分:0)
问题在于使用最小的O(1)额外存储来应用置换:“原位置换”。
可解决,但算法预先不明显。
它被简要描述为Knuth中的练习,为了工作,我必须解读它并弄清楚它是如何工作的。看看5.2#13。
关于这个问题的一些更现代的工作,使用伪代码:
答案 8 :(得分:0)
我最终为此编写了一个不同的算法,它首先生成一个交换列表以应用订单,然后通过交换运行以应用它。优点是,如果您将排序应用于多个列表,则可以重用交换列表,因为交换算法非常简单。
void make_swaps(vector<int> order, vector<pair<int,int>> &swaps)
{
// order[0] is the index in the old list of the new list's first value.
// Invert the mapping: inverse[0] is the index in the new list of the
// old list's first value.
vector<int> inverse(order.size());
for(int i = 0; i < order.size(); ++i)
inverse[order[i]] = i;
swaps.resize(0);
for(int idx1 = 0; idx1 < order.size(); ++idx1)
{
// Swap list[idx] with list[order[idx]], and record this swap.
int idx2 = order[idx1];
if(idx1 == idx2)
continue;
swaps.push_back(make_pair(idx1, idx2));
// list[idx1] is now in the correct place, but whoever wanted the value we moved out
// of idx2 now needs to look in its new position.
int idx1_dep = inverse[idx1];
order[idx1_dep] = idx2;
inverse[idx2] = idx1_dep;
}
}
template<typename T>
void run_swaps(T data, const vector<pair<int,int>> &swaps)
{
for(const auto &s: swaps)
{
int src = s.first;
int dst = s.second;
swap(data[src], data[dst]);
}
}
void test()
{
vector<int> order = { 2, 3, 1, 4, 0 };
vector<pair<int,int>> swaps;
make_swaps(order, swaps);
vector<string> data = { "a", "b", "c", "d", "e" };
run_swaps(data, swaps);
}