<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>西瓜田 &#187; 循环位移</title>
	<atom:link href="http://blog.thpiano.com/?feed=rss2&#038;tag=%E5%BE%AA%E7%8E%AF%E4%BD%8D%E7%A7%BB" rel="self" type="application/rss+xml" />
	<link>http://blog.thpiano.com</link>
	<description>无复洛城东</description>
	<lastBuildDate>Tue, 19 Jan 2021 03:54:37 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.4.2</generator>
		<item>
		<title>数组循环移位，你能想到多少种算法？</title>
		<link>http://blog.thpiano.com/?p=251</link>
		<comments>http://blog.thpiano.com/?p=251#comments</comments>
		<pubDate>Fri, 11 Nov 2011 16:15:27 +0000</pubDate>
		<dc:creator>suika</dc:creator>
				<category><![CDATA[编程]]></category>
		<category><![CDATA[C/C++]]></category>
		<category><![CDATA[循环位移]]></category>
		<category><![CDATA[编程之美]]></category>

		<guid isPermaLink="false">http://172.18.126.172/blog/?p=251</guid>
		<description><![CDATA[设计一个算法，把一个含有 N 个元素的数组循环右移K 位 例如： abcd12345循环右移5位，得到12345abcd 第一次遇到这个问题，还是一个朋友当成考研题来问我的，并给出了进一步的限制：时间复杂度为O（N），且只允许使用两个附加变量。 后来在《编程珠玑》、《编程之美》上都看到了这道题 想来按照那个限制的难度，肯定是不会出现在考研卷上的吧，不适合做考题，所以就先无视这个限制 &#160; 抛开各种算法不谈，就题目本身，也可以得到一些优化信息： 1.虽然题目是右移，但是左移的话也可以一样完成（数组长度为N，右移K位相当于左移N-K位） 在这种情况下，可以分别写出左移和右移的算法，然后根据K与N-K的大小关系，调用移动较少的那个 2.在K大于N的情况下，可以将右移K位简化为右移K%N位 &#160; 自己当初也想出了多种烂算法，就当抛砖引玉啦，后面会附上几个大牛给出的做法 共有3个自己想的烂算法，3个大牛算法 ============================================================== 烂算法1： 是自己最先想到的算法，时间复杂度O(N)，空间复杂度O(1) 实际上是普通排序算法的变异，以两数交换为主，只不过临时变量做了小改动 还是以abcd12345右移5位为例： a右移5位，跑到2的位置，同时临时变量记录2，此时原串变为abcd1a345 再从2的位置右移5位（位置取模），让2跑到b的位置，同时临时变量记录b，此时原串变为a2cd1a345 再从b的位置右移5位，让b跑到3的位置，同时临时变量记录3…… 如此循环数组长度N次之后即可，有个缺点是取模运算比较多 ============================================================== 烂算法2： 想要避免取模运算，该怎么办呢？ 干脆直接把数组叠两遍，变成abcd12345abcd12345，然后从第5个位置开始截取一段连续的就行了！ 其实最后一个元素不用叠，到abcd12345abcd1234就足够了 需要三次memcpy，空间复杂度比其他算法高，需要新开辟空间 ============================================================== 烂算法3： 应该是最原始的算法，每次将原数组循环右移一次 temp=a[0]; for (i=0;i&#60;length-1;++i)     a[i]=a[i+1]; a[length-1]=temp; 移动距离为K就这么做K次 时间复杂度O(K*N) ===================笨蛋和牛人的分界线========================= 牛人提供的算法4： 如果有频繁的移位操作需求的话，干脆做一个封装用来获取元素，而不是直接取下标 这样的话所有的访问都从封装走，每次位置取个余就行了，直接一步到位了 时间复杂度O(1)，空间复杂度O(1) ============================================================== 牛人提供的算法5：（个人认为是最佳算法） 对于abcd12345，将其根据位移长度（5）分为两部分，第一部分为第1个元素至第N-K个元素： abcd和12345 然后先将这两个部分内部分别倒序，得到dcba 54321 然后再将整个数组倒序，就能得到结果12345abcd 这个大概是符合那位同学所说的“时间复杂度为O（N），且只允许使用两个附加变量”变态要求的答案了。 [...]]]></description>
			<content:encoded><![CDATA[<p>设计一个算法，把一个含有 N 个元素的数组循环右移K 位</p>
<p>例如： abcd12345循环右移5位，得到12345abcd<span id="more-251"></span></p>
<p>第一次遇到这个问题，还是一个朋友当成考研题来问我的，并给出了进一步的限制：时间复杂度为O（N），且只允许使用两个附加变量。</p>
<p>后来在《编程珠玑》、《编程之美》上都看到了这道题</p>
<p>想来按照那个限制的难度，肯定是不会出现在考研卷上的吧，不适合做考题，所以就先无视这个限制</p>
<p>&nbsp;</p>
<p>抛开各种算法不谈，就题目本身，也可以得到一些优化信息：<br />
1.虽然题目是右移，但是左移的话也可以一样完成（数组长度为N，右移K位相当于左移N-K位）<br />
在这种情况下，可以分别写出左移和右移的算法，然后根据K与N-K的大小关系，调用移动较少的那个<br />
2.在K大于N的情况下，可以将右移K位简化为右移K%N位</p>
<p>&nbsp;</p>
<p>自己当初也想出了多种烂算法，就当抛砖引玉啦，后面会附上几个大牛给出的做法<br />
共有3个自己想的烂算法，3个大牛算法</p>
<p>==============================================================</p>
<p>烂算法1：</p>
<p>是自己最先想到的算法，时间复杂度O(N)，空间复杂度O(1)</p>
<p>实际上是普通排序算法的变异，以两数交换为主，只不过临时变量做了小改动</p>
<p>还是以abcd12345右移5位为例：</p>
<p>a右移5位，跑到2的位置，同时临时变量记录2，此时原串变为abcd1a345</p>
<p>再从2的位置右移5位（位置取模），让2跑到b的位置，同时临时变量记录b，此时原串变为a2cd1a345</p>
<p>再从b的位置右移5位，让b跑到3的位置，同时临时变量记录3……</p>
<p>如此循环数组长度N次之后即可，有个缺点是取模运算比较多</p>
<p>==============================================================</p>
<p>烂算法2：</p>
<p>想要避免取模运算，该怎么办呢？</p>
<p>干脆直接把数组叠两遍，变成abcd12345abcd12345，然后从第5个位置开始截取一段连续的就行了！</p>
<p>其实最后一个元素不用叠，到abcd12345abcd1234就足够了</p>
<p>需要三次memcpy，空间复杂度比其他算法高，需要新开辟空间</p>
<p>==============================================================</p>
<p>烂算法3：</p>
<p>应该是最原始的算法，每次将原数组循环右移一次</p>
<pre>temp=a[0];

for (i=0;i&lt;length-1;++i)

    a[i]=a[i+1];

a[length-1]=temp;</pre>
<p>移动距离为K就这么做K次<br />
时间复杂度O(K*N)</p>
<p>===================笨蛋和牛人的分界线=========================</p>
<p>牛人提供的算法4：</p>
<p>如果有频繁的移位操作需求的话，干脆做一个封装用来获取元素，而不是直接取下标</p>
<p>这样的话所有的访问都从封装走，每次位置取个余就行了，直接一步到位了<br />
时间复杂度O(1)，空间复杂度O(1)</p>
<p>==============================================================</p>
<p><span style="color: #0000ff;">牛人提供的算法5：（个人认为是最佳算法）</span></p>
<p>对于abcd12345，将其根据位移长度（5）分为两部分，第一部分为第1个元素至第N-K个元素：</p>
<p>abcd和12345</p>
<p>然后先将这两个部分内部分别倒序，得到dcba 54321</p>
<p>然后再将整个数组倒序，就能得到结果12345abcd</p>
<p>这个大概是符合那位同学所说的“时间复杂度为O（N），且只允许使用两个附加变量”变态要求的答案了。</p>
<p>其原理为UV=((V-1) (U-1))-1（-1为上标，表示取逆）</p>
<p>真是太巧妙了。</p>
<p>&nbsp;</p>
<p>顺便说一下，STL里面的rotate函数&lt;algorithm&gt;，就是用这种方法实现的</p>
<pre>template&lt;class _BidIt&gt; inline
void _Rotate(_BidIt _First, _BidIt _Mid, _BidIt _Last,   bidirectional_iterator_tag)
{
    std::reverse(_First, _Mid);
    std::reverse(_Mid, _Last);
    std::reverse(_First, _Last);
}</pre>
<p>===============================================================</p>
<p>牛人提供的算法6：（很长很长，涉及数学理论，只好一股脑转载了。这也是一个时间复杂度O(N)的算法）<br />
（将此算法与烂算法1进行对比，可深刻体会笨蛋与牛人的区别）<br />
循环置换分解定理：对于给定数组A[0..N-1]向后循环换位N-K位运算，可分解为恰好gcd(K,N-K)个循环置换，且0,...,gcd(K,N-K)-1中的每个数恰属于一个循环置换。其中gcd(x,y)表示x和y的最大公因数。</p>
<p>我们从头开始分析这个问题，对于数组A[0..n-1]，要将其向后循环移动k位元素。因为每个元素右移n位后又回到了原来的位置上，所以右移k位等于右移k mod n位。考虑每个元素右移k位后的最终位置，比如对于A[0]，右移k位后在k mod n位置上，原来在k mod n位置上的元素右移k位后到了2*k mod n的位置上，把如此因为A[0]的移动而受到连环影响必须移动的位置列出来，就是下面这样一个位置序列：0,k,2*k,...,(t-1)k。其中每一项都是在模n的意义下的位置。t*k mod n 的结果是0。t是使得t*k mod n的结果为0的最小正整数。</p>
<p>这个位置序列实质上是模n加法群中由元素k生成的一个循环子群。由群论中的结论(该结论的证明见最后)知，循环子群(k)的周期为n / gcd(k,n)，元数为n / gcd(k,n)，其陪集个数为gcd(k,n)。换句话说，A[0..n-1]循环右移k位的结果是循环子群(k)以及它的所有陪集循环右移一位。例如，将A[0..5] = {1,2,3,4,5,6}循环右移4位，这里n = 6, k = 4, gcd(k, n) = 2。A[0]的最终位置是4,A[4]的最终位置是2,A[2]的最终位置是0，这样，位置0,4,2便是由k=4生成的循环群，周期为6 / gcd(4,6) = 6 / 2 = 3，这样的循环子群共有gcd(4,6) = 2个。</p>
<p>完整代码如下：<br />
// 数组的循环移位</p>
<pre>#include &lt;cstdio&gt;

int gcd(int m, int n) {

    int r;

    while(r = m % n) {

        m = n; n = r;

    }

    return n;
}

void shiftArray(int A[], int n, int k) {

    // 因为左移的代码比右移的代码好实现的多，而右移k位

    // 等价于左移-k位，-k = n - k。以下代码是通过左移-k位来实现右移k位

    k = n - (k % n);

    for(int i = 0, cnt = gcd(n, k); i &lt; cnt; i++) {

        int t = A[i], p = i, j = (k+i) % n;

        while(j != i) {

            A[p] = A[j]; p = j; j = (k + p) % n;

        }

        A[p] = t;

    }

}

void printArray(int A[], int n) {
    for(int i = 0; i &lt; n; i++) {

        printf("%-3d", A[i]);

        if((i+1)%10 == 0) printf("/n");

    }

}

int main() {

    int A[] = {1,2,3,4,5,6, 7};

    shiftArray(A, 7, 4);

    printArray(A, 7);

    return 0;

}</pre>
<p>上述所用到的那个群论结论的证明：</p>
<p>结论：设G是一个循环群，其中一个生成元素为a，若r和n的最大公约数为d，则a^r的周期为n / d。</p>
<p><img src="http://p.blog.csdn.net/images/p_blog_csdn_net/jcwKyl/EntryImages/20090210/zm633698809141718750.JPG" alt="" width="422" height="240" border="0" /></p>
<p>在模n加法群中，1是一个生成元素，任意元素k=1*k，所以任意元素k生成的循环子群(k)的周期为n / gcd(k,n)。因为gcd(k,n)=gcd(k,n-k)，所以也可以写成n / gcd(k, n-k)。</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.thpiano.com/?feed=rss2&#038;p=251</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
