Leetcode 189. 轮转数组

这道题题面很简单:给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

这道题另辟空间的做法很简单,就不赘述了。本篇文章记录的是官方题解中介绍的一种就地方法。

这种方法描述的基本操作是很符合基本直觉的:比如以下标 1 举例,把小标 1 的数移到下标 1 + k 处,再把原先下标 1 + k 中的数移到下标 1 + k + k 中,……,以此反复。但是由此带来了一个当时百思不得其解的问题:

1. 以上反复就地轮转的操作会破坏原先存储的内容么,会不会有被重复交换的区域?

代码

先直接上代码,结合说明实际的算法。如代码清单 1 所示,实际的做法是:从第一个数开始轮转,它最终会回到第一个数;再从第二个数开始轮转,它最终也会回到第二个数;……;以此反复,直到记录到每个位置都被轮转过了,就结束。

代码清单 1
  1. void rotate(vector<int>& nums, int k) {
  2.     int len = nums.size();
  3.     int cnt = 0;
  4.     int index, i, j, num;
  5.     for (index = 0; cnt < len && index < len; index++)
  6.     {
  7.         i = index;
  8.         num = nums[i];
  9.         do
  10.         {
  11.             j = (i + k) % len;
  12.             std::swap(nums[j], num);
  13.  
  14.             i = j;
  15.             cnt++;
  16.         } while (i != index);
  17.     }
  18. }

以上的方法非常让我费劲,由此带来另外两个问题:

2. 由解法可知,非但不会有 1 中担心的重复轮换的问题,而且从哪处开始轮换就会重新回到轮换的位置!

3. 一整轮轮换可能不会把所有数都轮换过,为什么从下一个紧邻的数开始轮换,下一个数难道不会在上一轮中已经被轮换过了么?!

解释

当时翻遍了力扣下的题解,没有能清楚“说服”我以上问题的,不过评论里有提到和 Polya 计数 相关。我准备从这处入手,发现 Polya 计数还没看完,看前置基础知识的时候,已经能解决问题 12 了。

我们画一下四个数情况下的所有轮换情况,只有四种轮换情况:往后移动 0、1、2、3 个位置。比如往后移动 2 位的情况是:1 移到 3,3 移到 1;2 移到 4,4 移到 2。

图1 k = 0
图2 k = 1
图3 k = 2
图4 k = 3

从图 1 至 图 4 中,我们可以看到所有的情况都是按环的形式呈现的。其实轮转就是一种置换,某个元素会从原先的位置置换出去,原先的位置还会置换得到新的元素。可以理解成每个位置(或者每个元素)的出度和入度有且仅能是 1。那么呈现的形式只能是环。

因为呈现形式是环,出度入度都是 1,那么能很好的解释问题 12:是环所以从哪里开始会从哪里结束;一轮轮转中不会重复轮转,否则会破坏环的结构。

以上的证明方法虽然不够严谨,但已经足够能说服自己了。

接下来要证明问题 3。问题 3 换个更个例的问题是,为什么相邻的数在前一个环中,就代表全部数已经都轮转完成了;更普遍的问题是,整个数组会有多少个环,各个环中的元素会是什么。

我们从更普遍的问题入手。以位置 1 举例,\(1+kx\ mod\ n\) 是以 1 为代表的环。取余运算,让问题变得抽象难以处理,所以我们“化环为直”,直接看 1 往后偏移 \(r\) 个位置,谁和它在一个环里,即求 \(r\) 满足:

\(1+kx\ mod\ n = 1 + r\)

以上式子可以变型为:

\(1+kx=ny+1+r\)

最终化简成:

\(kx-ny=r\)

这就是 裴蜀定理。要想求解成立,即方程有解,\(r\) 只能满足:

\(r\ |\ gcd(k,n)\)

至此问题就解决了,以 1 举例,环中有:

\(\{1,1+gcd(k,n),1+2*gcd(k,n),\cdots\}\)

以 2 为首的环同理。更特例的举例,\(gcd(k,n)=1\),一轮轮换就全部遍历到;更普遍的答案,总共有\(gcd(k,n)\)个环。

以上证明是看到裴蜀定理后,发现公式形式上和这个问题有点像,试着往上靠了一下,发现竟然能求解😂

之前代码清单 1 中是按计数的方法,确定每个元素是否都轮转完成。现在我们额外知道会有多少个环了,可以对代码进行改造一下。

代码清单 2 gcd(k,n) 个环
  1. int gcd(int x, int y)
  2. {
  3.     return x == 0 ? y : gcd(y % x, x);
  4. }
  5.  
  6. void rotate(vector<int>& nums, int k) {
  7.     int len = nums.size();
  8.     int cnt = gcd(std::min(k, len), std::max(k, len));
  9.     int index, i, j, num;
  10.  
  11.     for (index = 0; index < cnt; index++)
  12.     {
  13.          i = index;
  14.          num = nums[i];
  15.          do
  16.          {
  17.              j = (i + k) % len;
  18.              std::swap(nums[j], num);
  19.  
  20.              i = j;
  21.          } while (i != index);
  22.     }
  23. }