左旋转数组 C++
Posted
技术标签:
【中文标题】左旋转数组 C++【英文标题】:left rotate array in place C++ 【发布时间】:2018-07-27 13:20:18 【问题描述】:这就是我想要做的: 数组 A[] = 1,2,3,4,5 左旋转 2:A:3,4,5,1,2
我们是否有一个简单而好的解决方案来执行此操作?我希望数组 A 本身用这个左旋转值更新 - 没有额外的空间。
我尝试了各种方法,但各种测试用例的逻辑似乎不同,并且很难找到适合这个看似简单的任务的算法。
注意:我知道这可以很容易地通过创建一个带有左旋转值的新数组来完成。我正在尝试在输入数组本身中执行此操作。
请建议。简单的伪代码应该可以。
【问题讨论】:
看看std::rotate
还有std::swap
。
SO 是不是代码编写服务。请先说明您尝试过什么。
如果您不能使用std::rotate
,请参阅std::move 和std::copy。
@AlgirdasPreidžius OP 特别不要求代码。
【参考方案1】:
std::rotate() 将完全满足您的需求:
auto b = std::begin(A);
std::rotate( b, b + 2, std::end(A) );
【讨论】:
【参考方案2】:矢量旋转似乎是神秘算法的一个特别肥沃的土壤。其中大部分都可以在野外找到,但有变化;所有高效的都需要花点心思来理解它们的功能。
如果你只向左旋转一个元素,你可以非常高效地做到这一点:
template<typename Iter>
void rotate_one(Iter first, Iter last)
using Value = typename Iter::value_type;
if (first != last)
Value temp = std::move(*first);
for (Iter next = std::next(first);
next != last;
first = next, next = std::next(next))
*first = std::move(*next);
*first = std::move(temp);
您可以通过执行Δ
次(更准确地说,Δ % N
)来使用它来旋转delta
位置,但这需要时间 O(NΔ),对于任意 Δ,这实际上是 O(N²)。
虽然这种解决方案经常如上所示,但也可以在没有临时值对象的情况下实现它,使用交换而不是移动:
template<typename Iter>
void rotate_one(Iter first, Iter last)
if (first != last)
for (Iter next = std::next(first); next != last; ++first, ++next)
std::iterswap(first, next);
交换通常比移动需要更多的工作,但有可能存在一个高效的特定于容器的交换实现。无论如何,这个版本将有助于理解以后的实现。
一个众所周知且经常被引用的 O(N) 解决方案是执行三个反向操作:
template<typename Iter>
void rotate_one(Iter first, Iter newfirst, Iter last)
std::reverse(first, newfirst);
std::reverse(newfirst, last);
std::reverse(first, last);
在我看来,这真的很优雅。您可以通过在纸上尝试来了解它是如何工作的:
a b ... c d w x ... y z
d c ... b a w x ... y z first rotate
d c ... b a z y ... x w second rotate
w x ... y z a b ... c d third rotate
这是众所周知的“反转句子中单词顺序”解决方案的一个特例,它首先反转每个单词的字母,然后反转整个字符串。
但是,它存在几个问题。首先,当一次移动就足够时,它(几乎)将每个元素移动两次。其次,std::reverse
需要一个双向迭代器。这没有什么问题,但算法与任何前向迭代器一起工作会更好。
另一个简单的解决方案是注意,如果您使用第一个算法,但使用增量 Δ 而不是 1,并在迭代器到达末尾时将它们包装回开头,那么如果 Δ 和 N,您将正确旋转向量是相对优质的。但是,如果它们不是相对质数,则您只会旋转一些元素;指数为 0 模 gcd(N, Δ) 的那些。要旋转整个向量,您需要对向量中的每个前 gcd(N, Δ) 元素执行 gcd(N, Δ) 次。
这是一个包含 12 个元素和 Δ 为 3 的插图:
a b c d e f g h i j k l
\ / / /
\/ / /
/ \ / /
/ /\ /
/ / \/
/ / / \
d b c g e f j h i a k l first loop
\ / / /
\/ / /
/ \ / /
/ /\ /
/ / \/
/ / / \
d e c g h f j k i a b l second loop
\ / / /
\/ / /
/ \ / /
/ /\ /
/ / \/
/ / / \
d e f g h i j k l a b c third loop
使用随机访问迭代器更容易(这是一个缺陷);这是一个示例实现。 (变量count
计算已移动的元素个数;每个元素移动一次,所以当count
达到0 时,旋转完成。这避免了必须计算GCD 才能知道运行多少次外循环。)
template<typename Container>
void rotate(Container& A, typename Container::size_type delta)
using Value = typename Container::value_type;
using Index = typename Container::size_type;
Index n = A.size();
delta %= n;
for (Index loop = 0, count = n;
count;
++loop, --count)
Index dst = loop;
Value tmp = std::move(A[dst]);
for (Index src = loop + delta;
src != loop;
--count, dst = src, src += (src < n - delta ? delta : delta - n))
A[dst] = std::move(A[src]);
A[dst] = std::move(tmp);
如前所述,这依赖于具有随机访问迭代器的容器。
请注意,我们可以通过使用交换来消除对临时存储的需求,就像上面第一个算法的替代版本一样。如果我们这样做,那么我们可以并行执行所有外部循环,因为它们不会相互干扰。所以我们可以一次只向前移动一个元素,将每个元素与它的 Δ-next peer 交换。
这导致以the sample implementation of std::rotate
提供的巡回演出。它确实执行 N 次交换,这可能比上面的解决方案效率稍低(执行 N + gcd(N, Δ) 移动),但只需要前向迭代器和交换:(下面的代码稍作修改以更好地符合上面的例子。)
template <class Iter>
void rotate(Iter first, Iter newfirst, Iter last)
if(first == newfirst || newfirst == last) return;
Iter next = newfirst;
do
std::iter_swap(first++, next++);
if (first == newfirst) newfirst = next;
while (next != last);
for(next = newfirst; next != last; )
std::iter_swap(first++, next++);
if (first == newfirst) newfirst = next;
else if (next == last) next = newfirst;
上面唯一棘手的部分是环绕的处理。请注意first
和next
之间的(循环)距离始终相同(Δ)。 newfirst
用于跟踪环绕点:每次first
到达newfirst
,newfirst
前进Δ(通过将其分配给next
的值,始终超出first
Δ)。
next
在第一个循环结束时第一次回绕。一旦发生这种情况,它就在容器末端的Δ以内;第二个循环继续交换。在此过程中,通过欧几里得算法有效地计算了N和Δ的GCD。
【讨论】:
这是最好的答案,因为它不会卖光“使用 std::rotate”,从而允许 OP 学习。【参考方案3】:我尝试了 O(n) 中的解决方案。在这里,我制作了另一个相同大小的向量。假设你想左旋转 2,所以这里 d = 2。首先将元素从位置 2 复制到新数组中的位置 0 直到最后。然后将元素从第一个数组的开头复制到第二个数组的结尾,即从 n-d 位置。
int i = 0;
int r = d;
vector<int> b(n);
for(i=0; i< n-d; i++)
b[i] = a[r];
r++;
r = 0;
for(i=n-d; i<n; i++)
b[i] = a[r];
r++;
【讨论】:
感谢建筑师。然而,我正在寻找这是否可以在不分配额外空间的情况下完成。基本上在输入数组本身内。【参考方案4】:这种技术称为旋转。
让我们假设数组中的 0 元素是左边缘:
0 1 2 3 4 5 <-- array indices
+---+---+---+---+---+---+
| 3 | 1 | 4 | 1 | 5 | 9 |
+---+---+---+---+---+---+
操作是A[0] = A[1]
或A[i] = A[i + 1]
。
第一个槽和最后一个槽是两个例外。
首先,将左边的元素复制到一个临时变量中:
temp 0 1 2 3 4 5 <-- array indices
+---+ +---+---+---+---+---+---+
| 3 |<-- | 3 | 1 | 4 | 1 | 5 | 9 |
+---+ +---+---+---+---+---+---+
接下来,将剩下的所有元素都复制一个:
temp 0 1 2 3 4 5 <-- array indices
+---+ +---+---+---+---+---+---+
| 3 |<-- | 1 | 4 | 1 | 5 | 9 | 9 |
+---+ +---+---+---+---+---+---+
最后,将临时变量复制到最后一个槽:
0 1 2 3 4 5 temp
+---+---+---+---+---+---+ +---+
| 1 | 4 | 1 | 5 | 9 | 3 | <-- | 3 |
+---+---+---+---+---+---+ +---+
向右旋转将在另一个方向上起作用:
A[i - 1] = A[i]
编辑 1 要旋转多个位置,请执行两次旋转,或修改算法以跳过一个元素。
例如,向左旋转 2:A[i] = A[i + 2]
。
临时存储留给读者作为练习。 :-)
【讨论】:
感谢托马斯的帖子。对不起,如果我没有说得更清楚。我试图一次性做到这一点。我同意这也是一个很好的解决方案,但想知道这是否可以在数组的 1 遍中完成。 是的,使用A[i] = A[i+2]
。像我一样画出来,然后弄清楚你需要做什么。
谢谢托马斯!我还有一些顾虑。但是,如果这是我正在寻找的,让我再尝试一下您建议的方法。
@thomas:您的旋转两次修改并不像看起来那么简单。可以随走随走,但需要处理增量和长度有公因数的情况。
@rici 原来是退出情况(长度是 delta 的整数倍),这并不奇怪,因为 1
的转换总是立即生效。【参考方案5】:
毕竟是可以做到的。
@ThomasMatthews 的建议可以作为一个起点:您可以简单地开始交换 array[i]
和 array[i+rotate]
的元素,直到 i=0...last-rotate
。问题是最后一个rotate
元素的顺序会很忙,除非数组的长度是rotate
的整数倍,在这种情况下它似乎很好,虽然我不知道为什么(这只是第一次出现)。
使用这个 sn-p,您可以交互地检查这些元素的外观,您可能会注意到其余元素需要length of array % rotate
(即末尾的单个数字)量才能正确移动.我当然不知道为什么。
function test()
var count=document.getElementById("count").valueAsNumber;
var rotate=document.getElementById("rotate").valueAsNumber;
var arr=[...Array(count).keys()];
for(var i=0;i<count-rotate;i++)
var t=arr[i];
arr[i]=arr[i+rotate];
arr[i+rotate]=t;
document.getElementById("trivial").innerhtml=arr.slice(0,count-rotate).join();
document.getElementById("tail").innerHTML=arr.slice(count-rotate).join();
document.getElementById("remainder").innerHTML=count%rotate;
test();
<input type="number" id="count" oninput="test()" min="1" value="41"><br>
<input type="number" id="rotate" oninput="test()" min="1" value="12"><br>
<div id="trivial"></div>
<div id="tail"></div>
<div id="remainder"></div>
然后我只是添加了一些递归,还利用了右旋转可以作为左旋转的事实(移位量必须从元素的数量中减去),所以我不必单独编写方法,因为我很懒。块显示在单独的行中(部分结果正常显示,输入块在括号中),因此更容易理解。这里我使用 array.slice()
创建一个新数组,但它可以通过传递起始索引和长度来代替,因此它可以作为 C/C++ 中的就地操作。
function rot(arr,rotate)
var retbase="("+rotate+":"+arr.join()+")<br>"; // input in parentheses
for(var i=0;i<arr.length-rotate;i++)
var t=arr[i];
arr[i]=arr[i+rotate];
arr[i+rotate]=t;
var rightrot=arr.length % rotate; // amount of right-rotation missing
if(rightrot===0) // done
return retbase+arr.join();
else // needs fixing
retbase+=arr.slice(0,arr.length-rotate)+"<br>"; // partial result
arr=arr.slice(arr.length-rotate);
return retbase
+rot(arr,arr.length-rightrot); // flipping right-rotation left-rotation
function test()
var count=document.getElementById("count").valueAsNumber;
var rotate=document.getElementById("rotate").valueAsNumber;
var arr=[...Array(count).keys()];
document.getElementById("result").innerHTML=rot(arr,rotate);
test();
<input type="number" id="count" oninput="test()" min="1" value="41"><br>
<input type="number" id="rotate" oninput="test()" min="1" value="12"><br>
<div id="result"></div>
递归的深度/模式与 GCD 计算有一些相似之处,所以如果有人不得不谈论复杂性,我会开始寻找那个方向。
【讨论】:
您应该查看std::rotate
的示例实现(链接在其他答案中),这与您的解决方案基本相同(但迭代而不是递归)。它真的非常漂亮,即使它的工作原理并不完全清楚。
@rici 是的,我倾向于忘记 cppreference.com 经常有示例实现,所以我只是认为答案是“使用这个”,而不是“如何”。也许我应该开始删除它,因为它是一个仅限链接的答案:evilgrin:
如果问题是“我如何在 C++ 中做到这一点”,那么“使用标准库”在我看来是一个完全合理的答案,而实际的实现是有趣但不必要的信息。如果问题是“什么是矢量旋转的好/常见/有趣的算法”,那么标准库实现是几种可能性之一。我试图在扩展答案中列出并解释它们。以上是关于左旋转数组 C++的主要内容,如果未能解决你的问题,请参考以下文章