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

Posted

技术标签:

【中文标题】有没有更好的方法来排列字符串?【英文标题】:Are there any better methods to do permutation of string? 【发布时间】:2011-01-01 00:09:41 【问题描述】:
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) 来显示一个字符串的所有排列。

但该函数使用递归算法;也许它的性能可以提高?

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

【问题讨论】:

你看过STL的next_permutation函数了吗? cplusplus.com/reference/algorithm/next_permutation 不确定您在寻找什么?我们在 STL 和 Boost 中都有用于排列的函数,是您对它们的性能不满意还是您对实现感兴趣。 现在我把所有的工作都放在了一个答案中,我希望有人在赏金到期之前注意到,即使它是为了告诉我这是多么可怕的黑客攻击。 :-) 添加了您要求的解释。 【参考方案1】:

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

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);

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

【讨论】:

【参考方案2】:
**// 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;
    

【讨论】:

【参考方案3】:

这不是最好的逻辑,但是,我是初学者。如果有人对此代码提出建议,我将非常高兴和义务

#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();

【讨论】:

【参考方案4】:

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

#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。

【讨论】:

有人选择了表示您的答案是最佳答案的答案。 sigh 你的答案是最好的。 生活就是这样。最好的计划是老鼠和男人一头扎进船尾。 已经四年了,***的文章现在已经改变了,所以请您详细说明为什么这会起作用!究竟为什么它保证是第 k 个唯一排列? @Harshdeep 我猜en.wikipedia.org/wiki/… 是解释它的地方...【参考方案5】:

这是一个我刚刚沙沙作响的!

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;

【讨论】:

【参考方案6】:

特别是,你想要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;

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

【讨论】:

【参考方案7】:

你为什么不试试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

【讨论】:

请记住,为了获得所有排列,您的初始字符串/数组必须按升序排序。 我认为每次调用 STL 时都必须重新检查序列。问题中的代码不需要做任何比较,所以我认为这可能更有效(另外,它甚至可以在不支持&lt; 的类型上工作)。 欧米瑞:不正确。它进入一个循环。最大排列的下一个排列是最小排列。 请记住,STL 是由疯狂的数学家发明的。说真的,如果你正确地应用算法,你会得到很高的效率。这都是 C++ 的一部分! 如果 STL 真的是疯狂的数学,它会有这些:en.wikipedia.org/wiki/Fibonacci_heap【参考方案8】:

即使我第一次发现递归版本也很难理解,我花了一些时间寻找一种berre方式。更好的方法找到(我能想到的)是使用@987654321提出的算法@。基本思路是:

    首先按非递减顺序对给定字符串进行排序,然后按字典顺序查找从末尾开始小于其下一个字符的第一个元素的索引。将此元素索引称为“firstIndex”。 现在找到比“firstIndex”处的元素更大的最小字符。将此元素索引称为“ceilIndex”。 现在交换“firstIndex”和“ceilIndex”处的元素。 将字符串中从索引“firstIndex+1”开始的部分反转到字符串末尾。 (代替第 4 点)您还可以将字符串中从索引 'firstIndex+1' 到字符串末尾的部分进行排序。

第 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;

【讨论】:

【参考方案9】:
  //***************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;

【讨论】:

【参考方案10】:

实际上,您可以使用 Knuth 洗牌算法来做到这一点!

// 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; );
    

【讨论】:

我知道这已经有几年历史了 - 但这个解决方案并没有给你所有的排列。你知道 - 是个问题。【参考方案11】:

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

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

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

正如使用排列枚举算法很难找到一个单独的排列一样,生成所有排列集合的描述子集也很困难。 Permaquid 引用的算法使这两个都变得非常简单

【讨论】:

另一个想法 - 该算法将排列映射到 0 和 n!-1 之间的整数,这很快就会溢出任何合理的整数大小。如果您需要使用更大的排列,则需要扩展整数表示。在这种情况下,阶乘表示将为您提供最好的服务。在阶乘表示中,不是每个数字代表 10^k 的倍数,而是每个数字代表 k! 的倍数。有一种将阶乘表示映射到排列的直接算法。您可以在en.wikipedia.org/wiki/Factoradic#Permutations找到详细信息【参考方案12】:

如果您对置换生成感兴趣,我不久前曾发表过一篇研究论文:http://www.oriontransfer.co.nz/research/permutation-generation

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

【讨论】:

【参考方案13】:

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

#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 将状态存储在元素之间的关系中,但这意味着它不能处理无序的事物,我真的想知道如果序列中有两个相等的事物它会做什么。当然,您可以通过排列索引来解决这个问题,但这会稍微复杂一些。

只要它足够短,我的将适用于任何随机访问迭代器范围。如果不是这样,您将永远无法完成所有排列。

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

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

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

residuals 是一个连续更小的列表的索引列表。 idxs 是原始列表中的索引列表。 residualsidxs 中的值之间存在一对一的映射。它们各自代表不同“坐标空间”中的相同值。

您选择的答案所指向的答案具有相同的基本思想,但完成映射的方式比我相当文字和蛮力的方法要优雅得多。这种方式会比我的方法稍微快一点,但它们的速度差不多,而且它们都具有随机访问排列空间的相同优势,这使得很多事情变得更容易,包括(正如你选择的答案所指出的)并行算法。

【讨论】:

你能再解释一下吗。这个算法对我来说很难。 这是实际选择的答案的一个糟糕的变体。 还有一个小问题:你使用什么编译器?&lt;tr1/stdint&gt; 中的 tr1 是什么? 我使用 g++ 4.4.x。 TR1 是一个临时标准,用于向 C++ 标准库添加一些内容。属于 TR1 的所有标题都将在它们前面有 tr1/。见en.wikipedia.org/wiki/C%2B%2B_Technical_Report_1 而新的 C99 stdint.h 标头不是 C++ 的一部分,但在 TR1 中,他们将其添加为 &lt;tr1/cstdint&gt;【参考方案14】:

这篇文章:http://cplusplus.co.il/2009/11/14/enumerating-permutations/ 处理几乎任何东西的置换,而不仅仅是字符串。帖子本身和下面的 cmets 信息量很大,我不想复制和粘贴..

【讨论】:

【参考方案15】:

你想遍历所有的排列,还是计算排列的数量?

对于前者,请按照其他人的建议使用std::next_permutation。每个排列都需要 O(N) 时间(但摊销时间更少),除了它的调用帧之外没有内存,而递归函数需要 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;

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

【讨论】:

任何建设性的批评,或失败的输入示例? while ( pen != s.end() &amp;&amp; * pen == value ) ++ cnt 会导致无限循环。 啊,正确。顺便说一句,您想要唯一元素的排列,(n!)总数,作为您的算法返回,还是唯一排列,以此计算? 实际上,我之前不认为是唯一的,我假设输入字符串的元素在我的算法中是唯一的。 请注意您的算法中还有许多其他问题。这是我计算唯一排列的版本:code.google.com/p/jcyangs-alg-trunk/source/browse/trunk/recur/…【参考方案16】:

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;
    

【讨论】:

哦,没关系,我没有仔细阅读这个问题。 OP 想要 所有 排列,而不仅仅是一个。 您仍然可以获得基于 Knuth 洗牌算法的所有排列!我刚刚修改了您的解决方案并将其发布在下面;-)【参考方案17】:

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

置换是一个不可避免的缓慢操作(O(n!),或更糟,取决于您对每个置换所做的事情),不幸的是,您无能为力。

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

【讨论】:

【参考方案18】:

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

【讨论】:

n!不是多项式,因此没有算法能够在多项式时间内运行。【参考方案19】:

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

【讨论】:

concat 只是v.insert(v.begin(), item) 的低级版本。 GetPermutations 只是和 OP 的代码做同样的事情,它不如 std::next_permutation 的循环。 我从来没有声称我的解决方案是优越的 :) 也就是说,我看不出我的 GetPermutations 函数与 OP 的代码有何相同。 每次调用都会将字符串划分为一个稳定的和递归排列的部分。【参考方案20】:

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

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

阶乘增长太快,以至于您无法希望通过编写更快的实现来获得任何东西,您最多只能获得一个字符。所以我建议 Prasoon 的建议。它很容易编码,而且速度非常快。尽管坚持使用您的代码也完全没问题。

我只是建议您注意不要无意中在字符串中包含额外的字符,例如空字符。因为这会使你的代码慢 N 倍。

【讨论】:

以上是关于有没有更好的方法来排列字符串?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 2.0 中检查 UITextField 是不是为空白?有没有更好的方法来计算字符串? [复制]

有没有更好的方法来实现模拟有限字符显示的水平滚动文本效果?

有没有更好的方法来使用没有 eval 的字符串来获取对动作脚本中影片剪辑的引用

使用hashmap确定一个字符串是否是另一个字符串的排列(最佳解决方案)

查找没有重复的字符串的排列[重复]

用零填充字符串的更好方法[重复]