完美洗牌算法

完美洗牌问题: 给定一个数组a1,a2,a3,...an,b1,b2,b3..bn,把它最终设置为b1,a1,b2,a2,...bn,an这样的。 

分析: 首先,有的问题要求把它换成a1,b1,a2,b2,...an,bn。其实也差不多。我们可以:循环n次交换a1,b1,a2,b2, 把数组变为b1,b2...bn,a1,a2...an,时间复杂度O(n),再用完美洗牌问题的算法。或者  先用完美洗牌算法,再循环n次交换每组相邻的两个元素,也是O(n)。所以,我们只研究第一行给出的问题。为方便起见,我们考虑的是一个下标从1开始的数组,下标范围是[1..2n]。 我们看一下每个元素最终去了什么地方。

前n个元素 a1 -> a2  a2->a4.... 第i个元素去了 第(2 * i)的位置,后n个元素a(n + 1)->a1, a(n + 2)->a3... 第i个元素去了第 ((2 * (i - n-1) ) + 1) = (2 * i - (2 * n + 1)) = (2 * i) % (2 * n + 1) 个位置。统一一下,任意的第i个元素,我们最终换到了 (2 * i) % (2 * n + 1)的位置,这个取模很神奇,不会产生0。所有的位置编号还是从1到n。

一、完美洗牌算法1

代码如下:

<span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">// 时间O(n),空间O(n) 数组下标从1开始  </span>
<span class="keyword" style="font-weight: bold;">void</span> pefect_shuffle1(<span class="keyword" style="font-weight: bold;">int</span> *a,<span class="keyword" style="font-weight: bold;">int</span> n) {  
<span class="keyword" style="font-weight: bold;">int</span> n2 = n * <span class="number" style="color: rgb(0, 153, 153);">2</span>, i, b[N];  
    <span class="keyword" style="font-weight: bold;">for</span> (i = <span class="number" style="color: rgb(0, 153, 153);">1</span>; i <= n2; ++i) {  
        b[(i * <span class="number" style="color: rgb(0, 153, 153);">2</span>) % (n2 + <span class="number" style="color: rgb(0, 153, 153);">1</span>)] = a[i];  
    }  
    <span class="keyword" style="font-weight: bold;">for</span> (i = <span class="number" style="color: rgb(0, 153, 153);">1</span>; i <= n2; ++i) {  
        a[i] = b[i];  
    }  
}

二、 完美洗牌算法2---分治的力量

a1, a2,a3,a4,b1,b2,b3,b4

我们先要把前半段的后2个元素(a3,a4)与后半段的前2个元素(b1,b2)交换,得到a1,a2,b1,b2,a3,a4,b3,b4。

于是,我们分别求解子问题A (a1,a2,b1,b2)和子问题B (a3,a4,b3,b4)就可以了。

如果n = 5,是偶数怎么办?我们原始的数组是a1,a2,a3,a4,a5,b1,b2,b3,b4,b5,我们先把a5拎出来,后面所有元素前移,再把a5放到最后,变为a1,a2,a3,a4,b1,b2,b3,b4,b5,a5。可见这时最后两个元素b5,a5已经是我们要的结果了,所以我们只要考虑n=4就可以了。

那么复杂度怎么算? 每次,我们交换中间的n个元素,需要O(n)的时间,n是奇数的话,我们还需要O(n)的时间先把后两个元素调整好,但这步影响总体时间复杂度。所以,无论如何都是O(n)的时间复杂度。

于是我们有 T(n) = T(n / 2) + O(n)  这个就是跟归并排序一样的复杂度式子,最终复杂度解出来T(n) = O(nlogn)。空间的话,我们就在数组内部折腾的,所以是O(1)。(当然没有考虑递归的栈的空间)

代码:

<span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//时间O(nlogn) 空间O(1) 数组下标从1开始  </span>
<span class="keyword" style="font-weight: bold;">void</span> perfect_shuffle2(<span class="keyword" style="font-weight: bold;">int</span> *a,<span class="keyword" style="font-weight: bold;">int</span> n) {  
<span class="keyword" style="font-weight: bold;">int</span> t,i;  
    <span class="keyword" style="font-weight: bold;">if</span> (n == <span class="number" style="color: rgb(0, 153, 153);">1</span>) {  
        t = a[<span class="number" style="color: rgb(0, 153, 153);">1</span>];  
        a[<span class="number" style="color: rgb(0, 153, 153);">1</span>] = a[<span class="number" style="color: rgb(0, 153, 153);">2</span>];  
        a[<span class="number" style="color: rgb(0, 153, 153);">2</span>] = t;  
        <span class="keyword" style="font-weight: bold;">return</span>;  
    }  
    <span class="keyword" style="font-weight: bold;">int</span> n2 = n * <span class="number" style="color: rgb(0, 153, 153);">2</span>, n3 = n / <span class="number" style="color: rgb(0, 153, 153);">2</span>;  
    <span class="keyword" style="font-weight: bold;">if</span> (n % <span class="number" style="color: rgb(0, 153, 153);">2</span> == <span class="number" style="color: rgb(0, 153, 153);">1</span>) {  <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//奇数的处理  </span>
        t = a[n];  
        <span class="keyword" style="font-weight: bold;">for</span> (i = n + <span class="number" style="color: rgb(0, 153, 153);">1</span>; i <= n2; ++i) {  
            a[i - <span class="number" style="color: rgb(0, 153, 153);">1</span>] = a[i];  
        }  
        a[n2] = t;  
        --n;  
    }  
    <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//到此n是偶数  </span>
      
    <span class="keyword" style="font-weight: bold;">for</span> (i = n3 + <span class="number" style="color: rgb(0, 153, 153);">1</span>; i <= n; ++i) {  
        t = a[i];  
        a[i] = a[i + n3];  
        a[i + n3] = t;  
    }  
      
    <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">// [1.. n /2]  </span>
    perfect_shuffle2(a, n3);  
    perfect_shuffle2(a + n, n3);  
      
}

三、 完美洗牌算法

这个算法源自一篇文章,文章很数学,可以只记结论就好了…… 这个算法的具体实现还是依赖于算法1,和算法2的。 首先,对于每一个元素,它最终都会到达一个位置,我们如果记录每个元素应到的位置会形成圈。 为什么会形成圈? 比如原来位置为a的元素最终到达b,而b又要达到c……,因为每个新位置和原位置都有一个元素,所以一条链 a->b->c->d……这样下去的话,必然有一个元素会指向a,(因为中间那些位置b,c,d……都已经被其它元素指向了)。 这就是圈的成因。

比如 6个元素  原始是(1,2,3,4,5,6), 最终是(4,1,5,2,6,3),我们用a->b表示原来下标为a的元素,新下标为b了。


1->2

2->4

3->6

4->1

5->3

6->5

<span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//数组下标从1开始,from是圈的头部,mod是要取模的数 mod 应该为 2 * n + 1,时间复杂度O(圈长)</span>
<span class="keyword" style="font-weight: bold;">void</span> cycle_leader(<span class="keyword" style="font-weight: bold;">int</span> *a,<span class="keyword" style="font-weight: bold;">int</span> <span class="keyword" style="font-weight: bold;">from</span>, <span class="keyword" style="font-weight: bold;">int</span> mod) {
<span class="keyword" style="font-weight: bold;">int</span> last = a[<span class="keyword" style="font-weight: bold;">from</span>],t,i;
    
    <span class="keyword" style="font-weight: bold;">for</span> (i = <span class="keyword" style="font-weight: bold;">from</span> * <span class="number" style="color: rgb(0, 153, 153);">2</span> % mod;i != <span class="keyword" style="font-weight: bold;">from</span>; i = i * <span class="number" style="color: rgb(0, 153, 153);">2</span> % mod) {
        t = a[i];
        a[i] = last;
        last = t;
        
    }
    a[<span class="keyword" style="font-weight: bold;">from</span>] = last;
}

那么如何找到每个圈的头部呢?引用一篇论文,名字叫:

A Simple In-Place Algorithm for In-Shuffle.   Peiyush Jain, Microsoft Corporation.  

利用数论知识,包括原根、甚至群论什么的,论文给出了一个出色结论(*):对于2 * n = (3^k - 1),这种长度的数组,恰好只有k个圈,并且每个圈的头部是1,3,9,...3^(k - 1)。这样我们就解决了这种特殊的n作为长度的问题。那么,对于任意的n怎么办?我们利用算法2的思路,把它拆成两部分,前一部分是满足结论(*)。后一部分再单独算。

为了把数组分成适当的两部分,我们同样需要交换一些元素,但这时交换的元素个数不相等,不能简单地循环交换,我们需要更强大的工具——循环移。假设满足结论(*)的需要的长度是2 * m = (3^k - 1), 我们需要把n分解成m和n - m两部分,按下标来说,是这样:

原先的数组(1..m) (m + 1.. n) (n + 1..n + m)(n + m + 1..2 * n),我们要达到的数组 (1..m)(n + 1.. n + m)(m + 1..n)(n + m + 1..2  * n)。可见,中间那两段长度为(n - m)和m的段需要交换位置,这个相当于把(m + 1..n + m)的段循环右移m次,而循环右移是有O(长度)的算法的, 主要思想是把前(n - m)个元素和后m个元素都翻转一下,再把整个段翻转一下。

循环移位的代码:

<span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//翻转字符串时间复杂度O(to - from)</span>
<span class="keyword" style="font-weight: bold;">void</span> reverse(<span class="keyword" style="font-weight: bold;">int</span> *a,<span class="keyword" style="font-weight: bold;">int</span> <span class="keyword" style="font-weight: bold;">from</span>,<span class="keyword" style="font-weight: bold;">int</span> to) {
<span class="keyword" style="font-weight: bold;">int</span> t;
    <span class="keyword" style="font-weight: bold;">for</span> (; <span class="keyword" style="font-weight: bold;">from</span> < to; ++<span class="keyword" style="font-weight: bold;">from</span>, --to) {
        t = a[<span class="keyword" style="font-weight: bold;">from</span>];
        a[<span class="keyword" style="font-weight: bold;">from</span>] = a[to];
        a[to] = t;
    }
    
}

<span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//循环右移num位 时间复杂度O(n)</span>
<span class="keyword" style="font-weight: bold;">void</span> right_rotate(<span class="keyword" style="font-weight: bold;">int</span> *a,<span class="keyword" style="font-weight: bold;">int</span> num,<span class="keyword" style="font-weight: bold;">int</span> n) {
    reverse(a, <span class="number" style="color: rgb(0, 153, 153);">1</span>, n - num);
    reverse(a, n - num + <span class="number" style="color: rgb(0, 153, 153);">1</span>,n);
    reverse(a, <span class="number" style="color: rgb(0, 153, 153);">1</span>, n);
}

再用a和b举例一下,设n = 7这样m = 4, k = 2

原先的数组是a1,a2,a3,a4,(a5,a6,a7),(b1,b2,b3,b4),b5,b6,b7。


结论(*)是说m = 4的部分可以直接搞定,也就是说我们把中间加括号的那两段(a5,a6,a7) (b1,b2,b3,b4)交换位置,也就是把(a5,a6,a7,b1,b2,b3,b4)整体循环右移4位就可以得到:(a1,a2,a3,a4,b1,b2,b3,b4)(a5,a6,a7,b5,b6,b7)

于是前m = 4个由算法cycle_leading算法直接搞定,n的长度减小了4。

所以这也是一种分治算法。算法流程:

输入数组 a[1..2 * n]

step 1 找到 2 * m = 3^k - 1 使得 3^k <= 2 * n < 3^(k +1)

step 2 把a[m + 1..n + m]那部分循环移m位

step 3 对每个i = 0,1,2..k - 1,3^i是个圈的头部,做cycle_leader算法,数组长度为m,所以对2 * m + 1取模。

step 4 对数组的后面部分a[2 * m + 1.. 2 * n]继续使用本算法,这相当于n减小了m。

时间复杂度分析:

 1 因为循环不断乘3的,所以时间复杂度O(logn)

 2 循环移位O(n)

 3 每个圈,每个元素只走了一次,一共2*m个元素,所以复杂度omega(m), 而m < n,所以 也在O(n)内。

 4 T(n - m)

因此 复杂度为 T(n) = T(n - m) + O(n)      m = omega(n)  解得:总复杂度T(n) = O(n)。

算法代码:

<span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//时间O(n),空间O(1)</span>
<span class="keyword" style="font-weight: bold;">void</span> perfect_shuffle3(<span class="keyword" style="font-weight: bold;">int</span> *a,<span class="keyword" style="font-weight: bold;">int</span> n) {
<span style="white-space: pre;">  </span><span class="keyword" style="font-weight: bold;">int</span> n2, m, i, k,t;
    <span style="white-space: pre;">	</span><span class="keyword" style="font-weight: bold;">for</span> (;n > <span class="number" style="color: rgb(0, 153, 153);">1</span>;) {
        <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">// step 1</span>
        n2 = n * <span class="number" style="color: rgb(0, 153, 153);">2</span>;
        <span class="keyword" style="font-weight: bold;">for</span> (k = <span class="number" style="color: rgb(0, 153, 153);">0</span>, m = <span class="number" style="color: rgb(0, 153, 153);">1</span>; n2 / m >= <span class="number" style="color: rgb(0, 153, 153);">3</span>; ++k, m *= <span class="number" style="color: rgb(0, 153, 153);">3</span>);
        m /= <span class="number" style="color: rgb(0, 153, 153);">2</span>;
        <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">// 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1)</span>
        <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">// step 2</span>
        right_rotate(a + m, m, n);
        <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">// step 3</span>
        <span class="keyword" style="font-weight: bold;">for</span> (i = <span class="number" style="color: rgb(0, 153, 153);">0</span>, t = <span class="number" style="color: rgb(0, 153, 153);">1</span>; i < k; ++i, t *= <span class="number" style="color: rgb(0, 153, 153);">3</span>) {
            cycle_leader(a , t, m * <span class="number" style="color: rgb(0, 153, 153);">2</span> + <span class="number" style="color: rgb(0, 153, 153);">1</span>);    
        }
        <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">//step 4</span>
        a += m * <span class="number" style="color: rgb(0, 153, 153);">2</span>;
        n -= m;
    }
    <span class="comment" style="color: rgb(153, 153, 136); font-style: italic;">// n = 1</span>
    t = a[<span class="number" style="color: rgb(0, 153, 153);">1</span>];
    a[<span class="number" style="color: rgb(0, 153, 153);">1</span>] = a[<span class="number" style="color: rgb(0, 153, 153);">2</span>];
    a[<span class="number" style="color: rgb(0, 153, 153);">2</span>] = t;  
}
以上所有代码已经测试过  
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值