一、问题
设计一个算法,把一个含有N个元素的数组循环右移K位,要求时间复杂度为O(N),且只允许使用两个附加变量。
问题分析:
输入:长度为N的原数组,K
输出:循环右移K位后的数组
约束:时间复杂度要求O(N),且只允许两个附加变量
二、解法
版本一:数组中的每个元素一次移到位(利用数据移位的变化规律)
思考:既然时间复杂度要求时O(n),且只允许两个附加变量,那么就是说,算法可以按照某种顺序对数组利用一个附加变量进行遍历,每次移位的结果必须移动到正确的最终位置。同时,我们利用一个附加变量把最开头的数组数据先存储下来,全部移位结束后我们再最把最后的一个数组中数利用这个附加变量赋值为正确的数据。
在某个数组长度下,我们可以发现,对于数组中某一位置a,不断循环向右移动K位,最后移动到的这些位置(a, a+K.a+2K…)不一定能覆盖数组的全部位置,但是在位置a进行X次偏移(a, a+ 1, a+2, …, a+i (i < X))后并重复上述操作后,可以将数组的全部位置覆盖。遍历时只要我们将能够将数组的全部位置覆盖,就可以通过赋值完成整个数组的循环右移。因此,我们在数组中,对某一位置a,可以以K为间隔对数组中的数据进行移位,一次循环,然后对a再进行X-1次偏移并重复上述操作,最后整个数组循环右移K位。
关键是上面X值的求解,我们通过举例进行测试和分析可以发现,X值实际上是数组长度N和位移值K的最大公约数。
另外,我们可以发现:当K > N时,我们实际上只需要移动 K % N位即可,不必完整移动K位。这个比较好理解,K > N时数组向右移动K位和向右移动K % N位的效果是相同的。
最终设计了如下的算法:
/**
* @brief get greatest commont divisor(gcd) for value m and n.
*
* @param[in] m first value
* @param[in] n second value
*
* @return gcd value
*/
TYPE gcd(TYPE m, TYPE n)
{
if (m == 0)
return n;
if (n == 0)
return m;
if (m < n) {
TYPE temp = m;
m = n;
n = temp;
}
while (n != 0) {
TYPE temp = m % n;
m = n;
n = temp;
}
return m;
}
/**
* @brief cyclic shift for an array
*
* @param[in,out] array shift array
* @param[in] count array length
* @param[in] right_shift right shift count
*/
void array_cyclic_shift(TYPE* array, TYPE count, TYPE right_shift)
{
assert(array != NULL && right_shift >= 0 && count >= 1);
/// let right shift smaller than count
right_shift %= count;
TYPE i, k, temp;
/// get the gcd value from right_shift and count to set cyclic shift times
TYPE cyclic_count = gcd(right_shift, count);
for (k = 0; k < cyclic_count; k++) {
/// index begin from the top value
i = count - 1 - k;
/// save top value to buffer
temp = array[i];
/// set value to value right_shift distance before it in a cycle
while (((i - right_shift + count) % count) != count - 1 - k) {
array[i] = array[(i - right_shift + count) % count];
i = (i - right_shift + count) % count;
}
/// set last value from buffer
array[i] = temp;
}
return;
}
分析:
附加变量:很显然这个算法用了不止两个附加变量,不考虑GCD函数调用的附加变量,算法也用去4个附加变量,优化后可以变为3个(减去上面的k和cyclic_count其中一个),但仍不满足问题约束条件。
时间复杂度:我们不去考虑GCD对算法运算效率的影响(设O(1)),由于我们每次移位时均能把数组中的数据从某一位置正确移动到另一个偏移后的相应位置,并不会进行多余的移动,因此这部分的算法复杂度是O(N)的。
这个算法是通过寻找数组数据进行移位时他位置的变化规律而写成的,里面也对数组长度N和位移K进行最小公倍数的求解,这个最小公倍数起初并没有发现,一开始是从数组长度和位移K的奇偶性出发的,在测试过程中发现了错误,发现数组并不总能进行正确的循环移位,然后用例子深入探讨上面讲述的X趟移位的这个X值的特点,最后分析才发现X就是数组长度N和位移K的最小公倍数。
下面的版本二和版本三是参考着《编程之美》一书进行学习:
版本二:数组中的每个元素逐渐移位
把一个含有N个元素的数组循环右移K位,可以这么做:每次数组都整体向右移动1位,共移动K次。
这里也考虑 K > N的情况,令K = K % N(和版本一一样)。
算法C实现:
/**
* @brief cyclic shift for an array
*
* @param[in,out] array shift array
* @param[in] count array length
* @param[in] right_shift right shift count
*/
void array_cyclic_shift(TYPE* array, TYPE count, TYPE right_shift)
{
assert(array != NULL && right_shift >= 0 && count >= 1);
/// let right shift smaller than count
right_shift %= count;
TYPE i, temp;
/// shift right_shift times
while (right_shift--) {
/// save top value to buffer
temp = array[count - 1];
/// set higher value by lower value
for (i = count - 1; i > 0; i--)
array[i] = array[i - 1];
/// set bottom value from buffer
array[0] = temp;
}
return;
}
附加变量:这里只用了两个附加变量,满足题目要求。
算法复杂度:O(N^2),(因为算法中循环次数T = K % N,得:T < N)
(如果对循环次数T不采用:T = K % N 而直接使用 T = K,那么算法复杂度将会变为:O(KN),K可能大于N。)
版本三: 数组中的元素以段为整体进行翻转
举例:abcd1234 右移3位 后得到 234abcd1(N = 8 ,K = 3)
把数组分成两段,一段长度为N-K,一段长度为K,把这两段看成整体考虑,右移K位的过程就是把数组的这两段交换位置,可用如下算法完成:
1.对数组首(N-K)=5个元素段进行逆序排列,得到:1dcba234;
2.对数组尾K=3个元素段进行逆序排列,得到:1dcba432;
3.对整个数组进行逆序排列:得到最终结果:234abcd1。
算法C实现:
/**
* @brief reverse an array from index_begin to index_end
*
* @param[in,out] array array to be reversed
* @param[in] index_begin index begin of the array(included)
* @param[in] index_end index end of the array(included)
*/
void reverse(TYPE* array, TYPE index_begin, TYPE index_end)
{
TYPE temp;
/// swap between top index value and bottom index value
for (; index_begin < index_end; index_begin++, index_end--) {
temp = array[index_begin];
array[index_begin] = array[index_end];
array[index_end] = temp;
}
}
/**
* @brief cyclic shift for an array
*
* @param[in,out] array shift array
* @param[in] count array length
* @param[in] right_shift right shift count
*/
void array_cyclic_shift(TYPE* array, TYPE count, TYPE right_shift)
{
assert(array != NULL && right_shift >= 0 && count >= 1);
/// let right shift smaller than count
right_shift %= count;
/// reverse the first (count - right_shift) elements
reverse(array, 0, count - right_shift - 1);
/// reverse the remain elements
reverse(array, count - right_shift, count - 1);
/// reverse all the elements
reverse(array, 0, count - 1);
return;
}
附加变量:这里只用了两个附加变量,满足题目要求。
算法复杂度:第一次逆序排列需要扫描数组(N-K)/2长度,第二次逆序排列需要扫描数组K/2长度,最后一次逆序排列需要扫描数组N/2长度,算法总共扫描数组N长度,故时间复杂度为O(N)。
关键:这里实际上分析了数组位移的特点,利用已有的数组空间,通过逆序操作(空间翻转)使数组循环右移。
三、拓展学习
更多的求解方法学习参考资料: