有没有更好的方法来排列字符串?

时间:2010-01-03 15:46:46

标签: c++ algorithm string permutation

void permute(string elems, int mid, int end)
{
    static int count;
    if (mid == end) {
        cout << ++count << " : " << elems << endl;
        return ;
    }
    else {
    for (int i = mid; i <= end; i++) {
            swap(elems, mid, i);
            permute(elems, mid + 1, end);
            swap(elems, mid, i);
        }
    }
}

上述函数显示str的排列(str[0..mid-1]作为稳定前缀,str[mid..end]作为可置换后缀)。因此,我们可以使用permute(str, 0, str.size() - 1)来显示一个字符串的所有排列。

但该函数使用递归算法;也许它的表现可以改善?

是否有更好的方法来置换字符串?

20 个答案:

答案 0 :(得分:63)

这是来自unordered generation of permutations的维基百科条目的C ++中的非递归算法。对于长度为s的字符串n,对于从k0的任何n! - 1,以下内容修改s以提供唯一的排列(也就是说,与为该范围内的任何其他k值生成的那些不同)。要生成所有排列,请为所有n运行它! k值是s的原始值。

#include <algorithm>

void permutation(int k, string &s) 
{
    for(int j = 1; j < s.size(); ++j) 
    {
        std::swap(s[k % (j + 1)], s[j]); 
        k = k / (j + 1);
    }
}

这里swap(s, i, j)交换字符串s的位置i和j。

答案 1 :(得分:48)

为什么不尝试std::next_permutation()std::prev_permutation()

链接:

std::next_permutation()
std::prev_permutation()

一个简单的例子:

#include<string>
#include<iostream>
#include<algorithm>

int main()
{
   std::string s="123";
   do
   {

      std::cout<<s<<std::endl;

   }while(std::next_permutation(s.begin(),s.end()));
}

输出:

123
132
213
231
312
321

答案 2 :(得分:23)

我想第二个Permaquid's answer。他引用的算法与已提供的各种置换枚举算法的工作方式完全不同。它不会生成n个对象的所有排列,它会生成一个不同的特定排列,给定0 and n!-1之间的整数。如果你只需要一个特定的排列,它比枚举它们然后选择一个更快。

即使您确实需要所有排列,它也提供了单个排列枚举算法所不具备的选项。我曾经写过一个蛮力的cryptarithm cracker,它试图将所有可能的字母分配给数字。对于base-10问题,这是足够的,因为只有10!个排列可供尝试。但是对于base-11问题花了几分钟而base-12问题花了将近一个小时。

我使用引用的Permaquid算法替换了我一直使用的简单i=0--to--N-1 for循环的排列枚举算法。结果只是稍微慢了一点。但后来我将整数范围拆分为四分之一,并同时运行四个for循环,每个循环都在一个单独的线程中。在我的四核处理器上,生成的程序运行速度快了近四倍。

正如使用置换枚举算法找到单独的排列一样困难,生成所有排列集的描绘子集也是困难的。 Permaquid引用的算法使这两者非常容易

答案 3 :(得分:11)

特别需要std::next_permutation

void permute(string elems, int mid, int end)
{
  int count = 0;
  while(next_permutation(elems.begin()+mid, elems.end()))
    cout << << ++count << " : " << elems << endl;
}

......或类似的东西......

答案 4 :(得分:4)

任何用于生成排列的算法都将在多项式时间内运行,因为n长度字符串中字符的排列数为(n!)。也就是说,有一些非常简单的就地算法用于生成排列。查看Johnson-Trotter algorithm

答案 5 :(得分:4)

Knuth random shuffle algorithm值得研究。

// In-place shuffle of char array
void shuffle(char array[], int n)
{
    for ( ; n > 1; n--)
    {
        // Pick a random element to move to the end
        int k = rand() % n;  // 0 <= k <= n-1  

        // Simple swap of variables
        char tmp = array[k];
        array[k] = array[n-1];
        array[n-1] = tmp;
    }
}

答案 6 :(得分:3)

任何利用或生成所有排列的算法都会花费O(N!* N)时间,O(N!)至少生成所有排列而O(N)使用结果,这真的很慢。请注意,打印字符串也是O(N)afaik。

无论您使用何种方法,只需一秒钟即可实际处理最多10或11个字符的字符串。由于11!* 11 = 439084800次迭代(在大多数机器上执行这么多次推动它)和12!* 12 = 5748019200次迭代。因此,即使是最快的实现,在12个字符上也需要大约30到60秒。

因为你希望通过编写更快的实现获得任何东西,因此增长太快,你最多只能获得一个角色。所以我建议Prasoon的建议。编码很容易,速度也很快。虽然坚持你的代码也完全没问题。

我建议您注意不要在字符串中无意中添加额外的字符,例如空字符。因为这会使你的代码慢一点。

答案 7 :(得分:1)

我认为这不是更好,但它确实有效并且不使用递归:

#include <iostream>
#include <stdexcept>
#include <tr1/cstdint>

::std::uint64_t fact(unsigned int v)
{
   ::std::uint64_t output = 1;
   for (unsigned int i = 2; i <= v; ++i) {
      output *= i;
   }
   return output;
}

void permute(const ::std::string &s)
{
   using ::std::cout;
   using ::std::uint64_t;
   typedef ::std::string::size_type size_t;

   static unsigned int max_size = 20;  // 21! > 2^64

   const size_t strsize = s.size();

   if (strsize > max_size) {
      throw ::std::overflow_error("This function can only permute strings of size 20 or less.");
   } else if (strsize < 1) {
      return;
   } else if (strsize == 1) {
      cout << "0 : " << s << '\n';
   } else {
      const uint64_t num_perms = fact(s.size());
      // Go through each permutation one-by-one
      for (uint64_t perm = 0; perm < num_perms; ++perm) {
         // The indexes of the original characters in the new permutation
         size_t idxs[max_size];

         // The indexes of the original characters in the new permutation in
         // terms of the list remaining after the first n characters are pulled
         // out.
         size_t residuals[max_size];

         // We use div to pull our permutation number apart into a set of
         // indexes.  This holds what's left of the permutation number.
         uint64_t permleft = perm;

         // For a given permutation figure out which character from the original
         // goes in each slot in the new permutation.  We start assuming that
         // any character could go in any slot, then narrow it down to the
         // remaining characters with each step.
         for (unsigned int i = strsize; i > 0; permleft /= i, --i) {
            uint64_t taken_char = permleft % i;
            residuals[strsize - i] = taken_char;

            // Translate indexes in terms of the list of remaining characters
            // into indexes in terms of the original string.
            for (unsigned int o = (strsize - i); o > 0; --o) {
               if (taken_char >= residuals[o - 1]) {
                  ++taken_char;
               }
            }
            idxs[strsize - i] = taken_char;
         }
         cout << perm << " : ";
         for (unsigned int i = 0; i < strsize; ++i) {
            cout << s[idxs[i]];
         }
         cout << '\n';
      }
   }
}

有趣的是,它从排列到排列使用的唯一状态是排列的数量,排列的总数和原始字符串。这意味着它可以很容易地封装在迭代器或类似的东西中,而不必小心保存完全正确的状态。它甚至可以是随机访问迭代器。

当然:: std :: next_permutation将状态存储在元素之间的关系中,但这意味着它无法处理无序的事情,如果你在序列中有两个相同的东西,我真的很想知道它是做什么的。你可以通过置换索引来解决这个问题,但这会增加更多的复杂性。

Mine可以使用任何随机访问迭代器范围,只要它足够短。如果不是这样,你无论如何也永远无法完成所有的排列。

该算法的基本思想是可以枚举N个项的每个排列。总数是N!或fact(N)。并且任何给定的排列可以被认为是源索引从原始序列到新序列中的一组目的地索引的映射。一旦列举了所有排列,剩下要做的就是将每个排列数映射到实际的排列中。

置换列表中的第一个元素可以是原始列表中的N个元素中的任何一个。第二个元素可以是N-1个剩余元素中的任何一个,依此类推。该算法使用%运算符将置换数分解为这种性质的一组选择。首先,它通过N来模数置换数,以从[0,N)得到一个数字。它通过除以N来丢弃余数,然后按照列表的大小对其进行模数 - 1从[0,N-1)得到一个数字,依此类推。这就是for (i =循环正在做的事情。

第二步是将每个数字转换为原始列表的索引。第一个数字很简单,因为它只是一个直的索引。第二个数字是列表的索引,该列表包含除第一个索引处删除的元素之外的所有元素,依此类推。这就是for (o =循环正在做的事情。

residuals是连续较小列表中的索引列表。 idxs是原始列表中的索引列表。 residualsidxs中的值之间存在一对一的映射关系。它们在不同的“坐标空间”中表示相同的值。

你选择的答案指出的答案具有相同的基本思想,但是比我的字面和暴力方法有更优雅的方法来完成映射。这种方式会比我的方法略快,但它们的速度大致相同,并且它们都具有随机访问排列空间的相同优势,这使得整个事情变得更加简单,包括(正如您所选择的答案所指出的那样)并行算法。

答案 8 :(得分:1)

实际上你可以使用Knuth shuffling algo来做到这一点!

// find all the permutations of a string
// using Knuth radnom shuffling algorithm!

#include <iostream>
#include <string>

template <typename T, class Func>
void permutation(T array, std::size_t N, Func func)
{
    func(array);
    for (std::size_t n = N-1; n > 0; --n)
    {
        for (std::size_t k = 0; k <= n; ++k)
        {
            if (array[k] == array[n]) continue;
            using std::swap;
            swap(array[k], array[n]);
            func(array);
        }
    }
}

int main()
{
    while (std::cin.good())
    {
        std::string str;
        std::cin >> str;
        permutation(str, str.length(), [](std::string const &s){ 
            std::cout << s << std::endl; });
    }
}

答案 9 :(得分:1)

您想要遍历所有排列,还是计算排列数?

对于前者,请按照其他人的建议使用std::next_permutation。每个排列需要O(N)时间(但是较少的摊销时间)并且除了其callframe,vs O(N)时间和O(N)内存之外没有内存用于递归函数。正如其他人所说,整个过程是O(N!)并且你不能做得比这更好,因为你不能在不到O(X)的时间内从程序得到超过O(X)的结果!无论如何,没有量子计算机。

对于后者,您只需要知道字符串中有多少个唯一元素。

big_int count_permutations( string s ) {
    big_int divisor = 1;
    sort( s.begin(), s.end() );
    for ( string::iterator pen = s.begin(); pen != s.end(); ) {
        size_t cnt = 0;
        char value = * pen;
        while ( pen != s.end() && * pen == value ) ++ cnt, ++ pen;
        divisor *= big_int::factorial( cnt );
    }
    return big_int::factorial( s.size() ) / divisor;
}

速度受到查找重复元素的操作的限制,char可以使用查找表在O(N)时间内完成。

答案 10 :(得分:1)

唯一显着提高性能的方法是找到一种方法来避免首先迭代所有排列!

置换是一个不可避免的缓慢操作(O(n!),或者更糟,取决于你对每个排列的处理方式),遗憾的是,你所做的一切都不会改变这一事实。

另外,请注意,在启用优化时,任何现代编译器都会使递归变平,因此手动优化的(小)性能提升会进一步降低。

答案 11 :(得分:1)

我最近写了一个排列算法。它使用类型为T(模板)的向量而不是字符串,并且它不是超快的,因为它使用递归并且有很多复制。但也许你可以为代码吸取一些灵感。您可以找到代码here

答案 12 :(得分:0)

这篇文章:http://cplusplus.co.il/2009/11/14/enumerating-permutations/处理几乎任何东西,而不仅仅是字符串。帖子本身和下面的评论非常有用,我不想复制和粘贴..

答案 13 :(得分:0)

如果您对排列生成感兴趣,我前一段时间就做了一篇研究论文:http://www.oriontransfer.co.nz/research/permutation-generation

它附带了源代码,并且实现了大约5种不同的方法。

答案 14 :(得分:0)

即使我发现很难理解第一次的递归版本,我花了一些时间来搜索一个berre way.Better方法找到(我能想到的)是使用{{{ 3}}。基本思路是:

  1. 首先以非递减顺序对给定字符串进行排序,然后从词典中找到小于其下一个字符的第一个元素的索引。将此元素索引称为'firstIndex'。
  2. 现在找到'firstIndex'中元素最大的最小字符。将此元素索引称为'ceilIndex'。
  3. 现在交换'firstIndex'和'ceilIndex'的元素。
  4. 将从索引'firstIndex + 1'开始的字符串部分反转到字符串的结尾。
  5. (而不是第4点)您还可以将字符串的一部分从索引'firstIndex + 1'排序到字符串的末尾。
  6. 第4点和第5点做同样的事情但是第4点情况下的时间复杂度是O(n * n!),而在点5的情况下是O(n ^ 2 * n!)。

    上述算法甚至可以应用于字符串中有重复字符的情况。 :

    显示字符串所有排列的代码:

    #include <iostream>
    
    using namespace std;
    
    void swap(char *a, char *b)
    {
        char tmp = *a;
        *a = *b;
        *b = tmp;
    }
    
    
    int partition(char arr[], int start, int end)
    {
        int x = arr[end];
        int i = start - 1;
        for(int j = start; j <= end-1; j++)
        {
            if(arr[j] <= x)
            {
                i = i + 1;
                swap(&arr[i], &arr[j]);
            }
        }
        swap(&arr[i+1], &arr[end]);
        return i+1;
    }
    
    void quickSort(char arr[], int start, int end)
    {
        if(start<end)
        {
            int q = partition(arr, start, end);
            quickSort(arr, start, q-1);
            quickSort(arr, q+1, end);
        }
    }
    
    int findCeilIndex(char *str, int firstIndex, int n)
    {
        int ceilIndex;
        ceilIndex = firstIndex+1;
    
        for (int i = ceilIndex+1; i < n; i++)
        {
            if(str[i] >= str[firstIndex] && str[i] <= str[ceilIndex])
                ceilIndex = i;
        }
        return ceilIndex;
    }
    
    void reverse(char *str, int start, int end)
    {
        while(start<=end)
        {
            char tmp = str[start];
            str[start] = str[end];
            str[end] = tmp;
            start++;
            end--;
        }
    }
    
    void permutate(char *str, int n)
    {
        quickSort(str, 0, n-1);
        cout << str << endl;
        bool done = false;
        while(!done)
        {
            int firstIndex;
            for(firstIndex = n-2; firstIndex >=0; firstIndex--)
            {
                if(str[firstIndex] < str[firstIndex+1])
                    break;
            }
            if(firstIndex<0)
                done = true;
            if(!done)
            {
                int ceilIndex;
                ceilIndex = findCeilIndex(str, firstIndex, n);
                swap(&str[firstIndex], &str[ceilIndex]);
                reverse(str, firstIndex+1, n-1);
                cout << str << endl;
            }
        }
    }
    
    
    int main()
    {
        char str[] = "mmd";
        permutate(str, 3);
        return 0;
    }
    

答案 15 :(得分:0)

这是我刚刚躲过的那个!!

void permute(const char* str, int level=0, bool print=true) {

    if (print) std::cout << str << std::endl;

    char temp[30];
    for (int i = level; i<strlen(str); i++) {

        strcpy(temp, str);

        temp[level] = str[i];
        temp[i] = str[level];

        permute(temp, level+1, level!=i);
    }
}

int main() {
    permute("1234");

    return 0;
}

答案 16 :(得分:0)

这不是最好的逻辑,但是,我是初学者。如果有人向我提供有关此代码的建议,我会非常高兴并且有责任

#include<iostream.h>
#include<conio.h>
#include<string.h>
int c=1,j=1;


int fact(int p,int l)
{
int f=1;
for(j=1;j<=l;j++)
{
f=f*j;
if(f==p)
return 1;

}
return 0;
}


void rev(char *a,int q)
{
int l=strlen(a);
int m=l-q;
char t;
for(int x=m,y=0;x<q/2+m;x++,y++)
{
t=a[x];
a[x]=a[l-y-1];
a[l-y-1]=t;
}
c++;
cout<<a<<"  ";
}

int perm(char *a,int f,int cd)
{
if(c!=f)
{
int l=strlen(a);
rev(a,2);
cd++;
if(c==f)return 0;
if(cd*2==6)
{
for(int i=1;i<=c;i++)
{
if(fact(c/i,l)==1)
{
rev(a,j+1);
rev(a,2);
break;
}
}
cd=1;
}
rev(a,3);
perm(a,f,cd);
}
return 0;
}

void main()
{
clrscr();
char *a;
cout<<"\n\tEnter a Word";
cin>>a;
int f=1;

for(int o=1;o<=strlen(a);o++)
f=f*o;

perm(a,f,0);
getch();
}

答案 17 :(得分:0)

**// Prints all permutation of a string**

    #include<bits/stdc++.h>
    using namespace std;


    void printPermutations(string input, string output){
        if(input.length() == 0){
            cout<<output <<endl;
            return;
        }

        for(int i=0; i<=output.length(); i++){
            printPermutations(input.substr(1),  output.substr(0,i) + input[0] + output.substr(i));
        }
    }

    int main(){
        string s = "ABC";
        printPermutations(s, "");
        return 0;
    }

答案 18 :(得分:0)

这里还有另一个用于字符串排列的递归函数:

void permute(string prefix, string suffix, vector<string> &res) {
    if (suffix.size() < 1) {
        res.push_back(prefix);
        return;
    }
    for (size_t i = 0; i < suffix.size(); i++) {
        permute(prefix + suffix[i], suffix.substr(0,i) + suffix.substr(i + 1), res);
    }
}


int main(){
    string str = "123";
    vector<string> res;
    permute("", str, res);
}

该函数收集向量res中的所有排列。 可以使用模板和迭代器将这种想法推广到不同类型的容器中:

template <typename Cont1_t, typename Cont2_t>
void permute(typename Cont1_t prefix,
    typename Cont1_t::iterator beg, typename Cont1_t::iterator end,
    Cont2_t &result)
{
    if (beg == end) {
        result.insert(result.end(), prefix);
        return;
    }
    for (auto it = beg; it != end; ++it) {
        prefix.insert(prefix.end(), *it);
        Cont1_t tmp;
        for (auto i = beg; i != end; ++i)
            if (i != it)
                tmp.insert(tmp.end(), *i);

        permute(prefix, tmp.begin(), tmp.end(), result);
        prefix.erase(std::prev(prefix.end()));
    }
}

int main()
{
    string str = "123";
    vector<string> rStr;
    permute<string, vector<string>>("", str.begin(), str.end(), rStr);

    vector<int>vint = { 1,2,3 };
    vector<vector<int>> rInt;
    permute<vector<int>, vector<vector<int>>>({}, vint.begin(), vint.end(), rInt);

    list<long> ll = { 1,2,3 };
    vector<list<long>> vlist;
    permute<list<long>, vector<list<long>>>({}, ll.begin(), ll.end(), vlist);
}

这可能是一个有趣的编程练习,但是在生产代码中,您应该使用非递归版本的置换,例如next_permutation。

答案 19 :(得分:-1)

  //***************anagrams**************//


  //************************************** this code works only when there are no   
  repeatations in the original string*************//
  #include<iostream>
  using namespace std;

  int counter=0;

  void print(char empty[],int size)
  {

  for(int i=0;i<size;i++)
  {
    cout<<empty[i];
  }
  cout<<endl;
  }


  void makecombination(char original[],char empty[],char comb[],int k,int& nc,int size)
{
nc=0;

int flag=0;
for(int i=0;i<size;i++)
{
    flag=0;                                                                   // {
    for(int j=0;j<k;j++)
    {
        if(empty[j]==original[i])                                                                // remove this code fragment
        {                                                                                        // to print permutations with repeatation
            flag=1;
            break;
        }
    }
    if(flag==0)                                                                // }
    {
        comb[nc++]=original[i];
    }
}
//cout<<"checks  ";
//    print(comb,nc);
}


void recurse(char original[],char empty[],int k,int size)
{
char *comb=new char[size];


int nc;


if(k==size)
{
    counter++;
    print(empty,size);
    //cout<<counter<<endl;

}
else
{
    makecombination(original,empty,comb,k,nc,size);
    k=k+1;
    for(int i=0;i<nc;i++)
    {
        empty[k-1]=comb[i];

        cout<<"k = "<<k<<" nc = "<<nc<<" empty[k-1] = "<<empty[k-1]<<endl;//checks the  value of k , nc, empty[k-1] for proper understanding
        recurse(original,empty,k,size);
    }
}

}

int main()
{
const int size=3;
int k=0;
char original[]="ABC";

char empty[size];
for(int f=0;f<size;f++)
empty[f]='*';

recurse(original,empty,k,size);

cout<<endl<<counter<<endl;
return 0;
}
相关问题