串或序列的rotate操作

这里的rotate操作,也就是指循环移位。比如将串“ABCDEFG”以D为中心旋转,就相当将该串向左循环移位,直到第一个元素为D为止,最后得到新串“DEFGABC”。要想方便的完成rotate操作,一个常见的技巧是这样的:先将前半部分反转,再将后半部分反转,最后再将整个串反转即可(这里的前半部分与后半部分是以旋转中心来划分的)。还是以串“ABCDEFG”以D为中心旋转为例,以D为分割点,将先半部分与后半部分分别反转后,得“CBAGFED”,最后将整个串反转即得“DEFGABC”。这个算法在很多书上都提到过,相信大家一定都很熟悉了。那么其效率如何呢,假设串的长度为t,那么完成整个旋转过程需要约t次swap操作,也即是说需要大概3t次赋值,同时只需要常量的临时空间。因为其实现简单,所以还是差强人意。所以SGI STL在处理双向迭代器容器时也正是使用了该算法。但是进一步观察STL源码可以发现,在处理存储在拥有随机访问能力的容器中的串时,SGI STL却是采用了另外一种算法,而这个算法的原理在《STL源码剖析》中恰恰被hjj无视了,所以我在这里再简单地梳理一下。

首先,先帖出这个算法的源代码:

ContractedBlock.gif ExpandedBlockStart.gif Code
template<class RandomAccessIterator, class Distance> 
void __rotate(RandomAccessIterator first, 
      RandomAccessIterator middle, 
      RandomAccessIterator last, Distance
*
      random_access_iterator_tag) 

     Distance n 
= __gcd(last - first, middle - first); 
    
while (n--
         __rotate_cycle(first, last, frist 
+ n, middle - first, value_type(first)); 


template
<class EuclideanRingElement> 
EuclideanRingElement __gcd(EuclideanRingElement m, EuclideanRingElement n) 

    
while (n != 0
     { 
       EuclideanRingElement t 
= m % n; 
       m 
= n; 
       n 
= t; 
     } 
    
return m; 


template
<class _RandomAccessIterator, class Distance, class T> 
void __rotate_cycle(RandomAccessIterator first, RandomAccessIterator last, 
                     RandomAccessIterator initial, Distance shift, T
*

     T value 
= *initial; 
     RandomAccessIterator ptr1 
= initial; 
     RandomAccessIterator ptr2 
= ptr1 + shift; 
    
while (ptr2 != initial) 
     { 
         
*ptr1 = *ptr2; 
         ptr1 
= ptr2; 
        
if (last - ptr2 > shift) 
             ptr2 
+= shift; 
        
else 
             ptr2 
= first + (shift - (last - ptr2)); 
     } 
     
*ptr1 = value; 
}

 

上面只涉及到三个函数:__rotate、__gcd、__rotate_cycle。后两个函数都比较容易理解:__gcd没什么好说的,XXX嘛,当然是求最大公约数了。__rotate_cycle,是从某个初始元素开始,依次将其替换成其后相隔固定距离的元素。如果后面没有足够的偏移距离了,则又返回头部继续计算(相当于求模)。直到最后形成一个置换圈为止。

现在来仔细观察函数__rotate,这个函数实际上也不复杂,才区区三行:先是求出偏移距离和串总长的最大公约数,然后循环n次,分别以串的前n个元素为起点进行__rotate_cycle操作,over。但这怎么就保证了能正确地完成对输入串的rotate操作呢?

这就涉及到数论中的一个小定理:若有两个正整数m、n,且gcd(m,n)=d,那么序列{m%n, 2m%n, 3m%n,..., nm%n}一定是{0, d, 2d,..., n-d}的某个排列并重复出现d次,其中%号代表求模操作。比如若m=6, n=8,d=gcd(m,n)=2,那么{6%8, 12%8, 18%8,..., 48%8}即为{0,2,4,6}的某个排列并重复两次,事实上也正是{6,4,2,0, 6,4,2, 0}。特别地,若m、n互素,d=1,那么序列{m%n,2m%n,3m%n,...,(n-1)m%n}实际上就是{1, 2, 3,..., n-1}的某个排列。这个定理的证明过程可以很多书中找到(比如具体数学4.8节),这里不再详述。

了解这个引理后,就很容易看出__rotate函数的内涵了。若第一步求得的最大公约数n为1,那么只需一次__rotate_cycle就可以遍历到所有的元素,并将每个元素正确的替换为其后相距某个距离的元素,于是也就完成了循环左移操作。若n大于1,那么每一次__rotate_cycle只能将t/n的元素正确的左移,其中t为串的总长度,而这些被移动的元素是以d为等间距的,所以循环n次,并分别以串的前n个元素为起点进行__rotate_cycle操作,就能保证将所有的元素都移动到正确的位置上。

在这个新的算法中,每次__rotate_cycle需要t/n+1次赋值,n次循环,所以总共只需要t+n次赋值操作,显然是要比前面所说的三次反转的算法快上许多。

比如考虑当串a = { 1, 2, 3, 4, 5} 循环左移2位,即期望得到串{ 3, 4, 5, 1, 2},那么该算法的赋值过程如下:

tmp = a[0] -> tmp = 1
a[0] = a[2] ->{ 3, 2, 3, 4, 5}
a[2] = a[4] ->{ 3, 2, 5, 4, 5}
a[4] = a[1] ->{ 3, 2, 5, 4, 2}
a[1] = a[3] ->{ 3, 4, 5, 4, 2}
a[3] = tmp ->{ 3, 4, 5, 1, 2}

这里因为2与5互素,所以6次赋值就已搞定,而如果是用三次翻转的算法则需要大约3*5次赋值(实际上为12次)。

转载于:https://www.cnblogs.com/atyuwen/archive/2009/11/08/rotate.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python 中字符求子序列可以通过递归方法来实现。具体做法如下: 首先,编写一个函数 `find_subsequence(s, subsequence)`,其中 `s` 表示原始字符,`subsequence` 表示当前找到的子序列。函数的作用是找到字符 `s` 中的所有长度大于等于1的子序列。 在函数内部,分别考虑两种情况:当前字符 `s[0]` 参与子序列和不参与子序列。 1. 如果当前字符 `s[0]` 参与子序列,将其加入到 `subsequence` 中,并递归调用 `find_subsequence(s[1:], subsequence + s[0])`,即在剩余的字符中寻找子序列。 2. 如果当前字符 `s[0]` 不参与子序列,直接递归调用 `find_subsequence(s[1:], subsequence)`,即在剩余的字符中寻找子序列。 需要注意的是,递归调用 `find_subsequence` 时,需要传递更新后的 `s` 和 `subsequence`。 在递归的过程中,需要判断以下结束条件: 1. 当字符 `s` 的长度为0时,表示已经遍历完了原始字符,将 `subsequence` 加入到结果列表中。 2. 当字符 `s` 的长度为1时,表示只剩下一个字符了,此时需要将当前字符加入到 `subsequence` 中,并将 `subsequence` 加入到结果列表中。 最后,在主程序中调用 `find_subsequence` 函数,并打印结果列表即可完成字符求子序列操作。 需要注意的是,这种方法可能会产生大量的重复子序列,因此可以使用集合(set)来存储结果,避免重复。另外,也可以对结果进行排序,使得结果按照一定的顺序输出。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值