什么是移位M个位置的圆形数组的最快算法?
例如,[3 4 5 2 3 1 4]
移动 M = 2 个位置应该是 [1 4 3 4 5 2 3]
。
非常感谢。
什么是移位M个位置的圆形数组的最快算法?
例如,[3 4 5 2 3 1 4]
移动 M = 2 个位置应该是 [1 4 3 4 5 2 3]
。
非常感谢。
如果你想要O(n)的时间复杂度和不使用额外的内存(因为已经指定了数组),可以使用Jon Bentley在他的书《编程珠玑 第2版》中介绍的算法。该算法交换所有元素两次。虽然不像使用链表那么快,但它使用的内存更少,并且概念上更简单。
shiftArray( theArray, M ):
size = len( theArray )
assert( size > M )
reverseArray( theArray, 0, size - 1 )
reverseArray( theArray, 0, M - 1 )
reverseArray( theArray, M, size - 1 )
reverseArray( anArray, startIndex, endIndex ) 函数将从startIndex到endIndex(含)的元素顺序反转。
问题要求最快的方法。反转三次是最简单的方法,但它将每个元素移动两次,需要O(N)时间和O(1)空间。我们可以通过循环移位数组,每个元素只移动一次,并且在O(N)时间和O(1)空间内完成。
我们可以使用一个循环来将长度为N=9
的数组进行M=1
次循环移位:
tmp = arr[0]; arr[0] = arr[1]; ... arr[7] = arr[8]; arr[8] = tmp;
N=9
,M=3
,我们可以以三个周期进行循环移位:
tmp = arr[0]; arr[0] = arr[3]; arr[3] = tmp;
tmp = arr[1]; arr[1] = arr[4]; arr[4] = tmp;
tmp = arr[2]; arr[2] = arr[5]; arr[5] = tmp;
N=9, M=3
的移位图示N
和 M
的最大公约数(GCD)。如果 GCD 是 3,我们从 {0,1,2}
中的每个位置开始一个周期。使用二进制 GCD 算法可以快速计算 GCD。// n is length(arr)
// shift is how many place to cycle shift left
void cycle_shift_left(int arr[], int n, int shift) {
int i, j, k, tmp;
if(n <= 1 || shift == 0) return;
shift = shift % n; // make sure shift isn't >n
int gcd = calc_GCD(n, shift);
for(i = 0; i < gcd; i++) {
// start cycle at i
tmp = arr[i];
for(j = i; 1; j = k) {
k = j+shift;
if(k >= n) k -= n; // wrap around if we go outside array
if(k == i) break; // end of cycle
arr[j] = arr[k];
}
arr[j] = tmp;
}
}
// circle shift an array left (towards index zero)
// - ptr array to shift
// - n number of elements
// - es size of elements in bytes
// - shift number of places to shift left
void array_cycle_left(void *_ptr, size_t n, size_t es, size_t shift)
{
char *ptr = (char*)_ptr;
if(n <= 1 || !shift) return; // cannot mod by zero
shift = shift % n; // shift cannot be greater than n
// Using GCD
size_t i, j, k, gcd = calc_GCD(n, shift);
char tmp[es];
// i is initial starting position
// Copy from k -> j, stop if k == i, since arr[i] already overwritten
for(i = 0; i < gcd; i++) {
memcpy(tmp, ptr+es*i, es); // tmp = arr[i]
for(j = i; 1; j = k) {
k = j+shift;
if(k >= n) k -= n;
if(k == i) break;
memcpy(ptr+es*j, ptr+es*k, es); // arr[j] = arr[k];
}
memcpy(ptr+es*j, tmp, es); // arr[j] = tmp;
}
}
// cycle right shifts away from zero
void array_cycle_right(void *_ptr, size_t n, size_t es, size_t shift)
{
if(!n || !shift) return; // cannot mod by zero
shift = shift % n; // shift cannot be greater than n
// cycle right by `s` is equivalent to cycle left by `n - s`
array_cycle_left(_ptr, n, es, n - shift);
}
// Get Greatest Common Divisor using binary GCD algorithm
// http://en.wikipedia.org/wiki/Binary_GCD_algorithm
unsigned int calc_GCD(unsigned int a, unsigned int b)
{
unsigned int shift, tmp;
if(a == 0) return b;
if(b == 0) return a;
// Find power of two divisor
for(shift = 0; ((a | b) & 1) == 0; shift++) { a >>= 1; b >>= 1; }
// Remove remaining factors of two from a - they are not common
while((a & 1) == 0) a >>= 1;
do
{
// Remove remaining factors of two from b - they are not common
while((b & 1) == 0) b >>= 1;
if(a > b) { tmp = a; a = b; b = tmp; } // swap a,b
b = b - a;
}
while(b != 0);
return a << shift;
}
编辑:当N
很大而M
很小时,这个算法也可能比数组翻转有更好的性能,因为我们是以小步长遍历数组,从而提高了缓存局部性。
最后说明:如果你的数组很小,三重翻转就足够简单。如果你有一个大数组,那么通过计算GCD来减少移动次数的开销值得一试。 参考:http://www.geeksforgeeks.org/array-rotation/
size_t gcd(size_t a, size_t b) {return b == 0 ? a : gcd(b, a % b);}
。 - Cris Luengo这只是一种表示方法。将当前索引保留为整数变量,遍历数组时使用模运算符来确定何时换行。移位仅更改当前索引的值,将其包装到数组的大小中。当然,这是O(1)。
例如:
int index = 0;
Array a = new Array[SIZE];
get_next_element() {
index = (index + 1) % SIZE;
return a[index];
}
shift(int how_many) {
index = (index+how_many) % SIZE;
}
这个算法的时间复杂度为O(n),空间复杂度为O(1)。
其思想是追踪位移中的每个循环群(由nextGroup
变量编号)。
var shiftLeft = function(list, m) {
var from = 0;
var val = list[from];
var nextGroup = 1;
for(var i = 0; i < list.length; i++) {
var to = ((from - m) + list.length) % list.length;
if(to == from)
break;
var temp = list[to];
list[to] = val;
from = to;
val = temp;
if(from < nextGroup) {
from = nextGroup++;
val = list[from];
}
}
return list;
}
list[] -> val
,list[] -> tmp
,val -> list[]
,tmp -> val
。如果你改变移动事物的顺序,你可以将一个循环的第一个元素复制到 val
,然后直接将下一个元素向前复制(list[] -> list[]
),重复此过程,直到最后一个元素,在那里写入 val
。请参见此答案:https://dev59.com/cXNA5IYBdhLWcg3wpvtg#32698823 - Cris Luengo一种非常简单的解决方案。这是一种非常快速的方式,我在这里使用一个与原始数组大小相同的临时数组,并在最后将其附加到原始变量上。 此方法使用O(n)时间复杂度和O(n)空间复杂度,并且实现起来非常简单易懂。
int[] a = {1,2,3,4,5,6};
int k = 2;
int[] queries = {2,3};
int[] temp = new int[a.length];
for (int i = 0; i<a.length; i++)
temp[(i+k)%a.length] = a[i];
a = temp;
def shift(nelements, k):
result = []
length = len(nelements)
start = (length - k) % length
for i in range(length):
result.append(nelements[(start + i) % length])
return result
这段代码即使在负移位k的情况下也能正常工作。
这是一个简单而高效的C++通用就地旋转函数,不到10行代码。
这段代码摘自我在另一篇问题的回答中。 如何旋转数组?
#include <iostream>
#include <vector>
// same logic with STL implementation, but simpler, since no return value needed.
template <typename Iterator>
void rotate_by_gcd_like_swap(Iterator first, Iterator mid, Iterator last) {
if (first == mid) return;
Iterator old = mid;
for (; mid != last;) {
std::iter_swap(first, mid);
++first, ++mid;
if (first == old) old = mid; // left half exhausted
else if (mid == last) mid = old;
}
}
int main() {
using std::cout;
std::vector<int> v {0,1,2,3,4,5,6,7,8,9};
cout << "before rotate: ";
for (auto x: v) cout << x << ' '; cout << '\n';
int k = 7;
rotate_by_gcd_like_swap(v.begin(), v.begin() + k, v.end());
cout << " after rotate: ";
for (auto x: v) cout << x << ' '; cout << '\n';
cout << "sz = " << v.size() << ", k = " << k << '\n';
}
C数组右移函数。如果shift为负数,则该函数将数组向左移动。 它被优化以使用更少的内存。运行时间为O(n)。
void arrayShiftRight(int array[], int size, int shift) {
int len;
//cut extra shift
shift %= size;
//if shift is less then 0 - redirect shifting left
if ( shift < 0 ) {
shift += size;
}
len = size - shift;
//choosing the algorithm which needs less memory
if ( shift < len ) {
//creating temporary array
int tmpArray[shift];
//filling tmp array
for ( int i = 0, j = len; i < shift; i++, j++ ) {
tmpArray[i] = array[j];
}
//shifting array
for ( int i = size - 1, j = i - shift; j >= 0; i--, j-- ) {
array[i] = array[j];
}
//inserting lost values from tmp array
for ( int i = 0; i < shift; i++ ) {
array[i] = tmpArray[i];
}
} else {
//creating temporary array
int tmpArray[len];
//filling tmp array
for ( int i = 0; i < len; i++ ) {
tmpArray[i] = array[i];
}
//shifting array
for ( int i = 0, j = len; j < size; i++, j++ ) {
array[i] = array[j];
}
//inserting lost values from tmp array
for ( int i = shift, j = 0; i < size; i++, j++ ) {
array[i] = tmpArray[j];
}
}
}
result = original[-n:]+original[:-n]
我知道哈希查找在理论上不是O(1),但我们现在是实际操作,而不是理论操作,至少我希望如此...
assert(size>M)
替换为M = M % size
,并检查M==0
。这样可以使函数更加灵活。 - Georg Schölly