生成字符串排列的复杂性
Complexity for generating permutations of a string
我想弄清楚用于生成给定字符串的所有排列的代码(来自 Cracking the Coding Interview 一书)的复杂度是 O(n!)。
我知道这是最好的复杂度,因为我们有 n!排列,但我想以代码方式理解它,因为并非每个执行此操作的算法都是 O(n!)。
代码:
import java.util.*;
public class QuestionA {
public static ArrayList<String> getPerms(String str) {
if (str == null) {
return null;
}
ArrayList<String> permutations = new ArrayList<String>();
if (str.length() == 0) { // base case
permutations.add("");
return permutations;
}
char first = str.charAt(0); // get the first character
String remainder = str.substring(1); // remove the first character
ArrayList<String> words = getPerms(remainder);
for (String word : words) {
for (int j = 0; j <= word.length(); j++) {
String s = insertCharAt(word, first, j);
permutations.add(s);
}
}
return permutations;
}
public static String insertCharAt(String word, char c, int i) {
String start = word.substring(0, i);
String end = word.substring(i);
return start + c + end;
}
public static void main(String[] args) {
ArrayList<String> list = getPerms("abcde");
System.out.println("There are " + list.size() + " permutations.");
for (String s : list) {
System.out.println(s);
}
}
}
这是我到现在为止的想法:
在任何函数调用中,可用的单词数为 (n-1) ;假设我们在余数长度为 (n-1) 的地方。现在为所有这些 (n-1) 个单词在所有可能的位置插入第 n 个元素需要 (n-1)*(n-1) 时间。
所以跨过执行,应该是(n-1)^2+(n-2)^2+(n-3)^2+....2^2+1^2次操作,我认为这不是 n!。
我错过了什么?
我觉得getPerms
的时间复杂度是O((n + 1)!)
.
我们用T(n)
表示getPerms
的运行时间,其中n
是输入的长度。
============================================= ======================
两个 if
分支和 char first = str.charAt(0)
行需要 O(1)
时间。下面这行需要 O(n)
时间:
String remainder = str.substring(1); // remove the first character
下一行需要时间T(n - 1)
:
ArrayList<String> words = getPerms(remainder);
现在我们考虑嵌套for-loops
的运行时间。外for-loop
的大小是(n-1)!
:
for (String word : words) {
内部for-loop
的大小是n + 1
:
for (int j = 0; j <= word.length(); j++) {
并且insertCharAt
的复杂度也是O(n)
。
所以嵌套for-loops
的总运行时间是(n + 1) * (n - 1)! * O(n) = O((n + 1)!)
.
因此我们有如下关系:
T(n) = T(n - 1) + O(n) + O((n + 1)!)
= T(n - 1) + O(n) + O((n + 1)!)
= (T(n - 2) + O(n - 1) + O(n!) + O(n) + O((n + 1)!)
= T(n - 2) + ( O(n - 1) + O(n) ) + ( O(n!) + O((n + 1)!) )
= ...
= O(n2) + (1 + ... + O(n!) + O((n + 1)!) )
= O((n + 1)!)
如果你正在研究这个,最好学习一般的解决方案,而不是仅仅学习你的例子中给出的实现。塞奇威克做了我所知道的最好的分析。我在 class.
中教它
https://www.cs.princeton.edu/~rs/talks/perms.pdf
生成函数每次调用的复杂度为O(n)。因此成本是 O(n!).
您提供的代码效率极低。有一个巨大的常数因子,因为您正在创建大量字符串对象和数组,这是您在 Java.
中可以做的最低效的事情之一。
如果您只想遍历所有排列,排列单个实体,请不要生成列表。这是一个更快的实现:
public class Permute {
private int[] a;
private void swap(int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public Permute(int n) {
a = new int[n];
for (int i = 0; i < n; i++)
a[i] = i+1;
this.generate(n);
}
public void generate(int N) {
// System.out.println("generate(" + N + ")");
if (N == 0) doit();
for (int c = 0; c < N; c++) {
// System.out.print("Swapping: " + c + "," + N);
swap(c, N-1); //swap(0, 7)
generate(N-1);
// System.out.print("Swapping: " + c + "," + N);
swap(c, N-1);
}
}
public void doit() {
for (int i = 0; i < a.length; i++)
System.out.print(a[i] + " ");
System.out.println();
}
public static void main(String[] args) {
Permute p = new Permute(4);
}
}
Sedgewick 展示的另一种方法是 Heaps,每个排列只有一个交换而不是 2 个。这是一个 C++ 实现:
#include <vector>
#include <iostream>
using namespace std;
class Heaps {
private:
vector<int> p;
public:
Heaps(int n) {
p.reserve(n);
for (int i = 0; i < n; i++)
p.push_back(i+1);
generate(n);
}
void doit() {
cout << "doit size=" << p.size() << '\n';
for (int i = 0; i < p.size(); i++)
cout << p[i];
cout << '\n';
}
void generate(int N) {
// cout << "generate(" << N << ")\n";
if (N == 0)
doit();
for (int c = 0; c < N; c++) {
generate(N-1);
swap(p[N % 2 != 0 ? 0 : c], p[N-1]);
}
}
};
int main() {
Heaps p(4);
}
我想弄清楚用于生成给定字符串的所有排列的代码(来自 Cracking the Coding Interview 一书)的复杂度是 O(n!)。
我知道这是最好的复杂度,因为我们有 n!排列,但我想以代码方式理解它,因为并非每个执行此操作的算法都是 O(n!)。
代码:
import java.util.*;
public class QuestionA {
public static ArrayList<String> getPerms(String str) {
if (str == null) {
return null;
}
ArrayList<String> permutations = new ArrayList<String>();
if (str.length() == 0) { // base case
permutations.add("");
return permutations;
}
char first = str.charAt(0); // get the first character
String remainder = str.substring(1); // remove the first character
ArrayList<String> words = getPerms(remainder);
for (String word : words) {
for (int j = 0; j <= word.length(); j++) {
String s = insertCharAt(word, first, j);
permutations.add(s);
}
}
return permutations;
}
public static String insertCharAt(String word, char c, int i) {
String start = word.substring(0, i);
String end = word.substring(i);
return start + c + end;
}
public static void main(String[] args) {
ArrayList<String> list = getPerms("abcde");
System.out.println("There are " + list.size() + " permutations.");
for (String s : list) {
System.out.println(s);
}
}
}
这是我到现在为止的想法: 在任何函数调用中,可用的单词数为 (n-1) ;假设我们在余数长度为 (n-1) 的地方。现在为所有这些 (n-1) 个单词在所有可能的位置插入第 n 个元素需要 (n-1)*(n-1) 时间。
所以跨过执行,应该是(n-1)^2+(n-2)^2+(n-3)^2+....2^2+1^2次操作,我认为这不是 n!。
我错过了什么?
我觉得getPerms
的时间复杂度是O((n + 1)!)
.
我们用T(n)
表示getPerms
的运行时间,其中n
是输入的长度。
============================================= ======================
两个 if
分支和 char first = str.charAt(0)
行需要 O(1)
时间。下面这行需要 O(n)
时间:
String remainder = str.substring(1); // remove the first character
下一行需要时间T(n - 1)
:
ArrayList<String> words = getPerms(remainder);
现在我们考虑嵌套for-loops
的运行时间。外for-loop
的大小是(n-1)!
:
for (String word : words) {
内部for-loop
的大小是n + 1
:
for (int j = 0; j <= word.length(); j++) {
并且insertCharAt
的复杂度也是O(n)
。
所以嵌套for-loops
的总运行时间是(n + 1) * (n - 1)! * O(n) = O((n + 1)!)
.
因此我们有如下关系:
T(n) = T(n - 1) + O(n) + O((n + 1)!) = T(n - 1) + O(n) + O((n + 1)!) = (T(n - 2) + O(n - 1) + O(n!) + O(n) + O((n + 1)!) = T(n - 2) + ( O(n - 1) + O(n) ) + ( O(n!) + O((n + 1)!) ) = ... = O(n2) + (1 + ... + O(n!) + O((n + 1)!) ) = O((n + 1)!)
如果你正在研究这个,最好学习一般的解决方案,而不是仅仅学习你的例子中给出的实现。塞奇威克做了我所知道的最好的分析。我在 class.
中教它https://www.cs.princeton.edu/~rs/talks/perms.pdf
生成函数每次调用的复杂度为O(n)。因此成本是 O(n!).
您提供的代码效率极低。有一个巨大的常数因子,因为您正在创建大量字符串对象和数组,这是您在 Java.
中可以做的最低效的事情之一。如果您只想遍历所有排列,排列单个实体,请不要生成列表。这是一个更快的实现:
public class Permute {
private int[] a;
private void swap(int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public Permute(int n) {
a = new int[n];
for (int i = 0; i < n; i++)
a[i] = i+1;
this.generate(n);
}
public void generate(int N) {
// System.out.println("generate(" + N + ")");
if (N == 0) doit();
for (int c = 0; c < N; c++) {
// System.out.print("Swapping: " + c + "," + N);
swap(c, N-1); //swap(0, 7)
generate(N-1);
// System.out.print("Swapping: " + c + "," + N);
swap(c, N-1);
}
}
public void doit() {
for (int i = 0; i < a.length; i++)
System.out.print(a[i] + " ");
System.out.println();
}
public static void main(String[] args) {
Permute p = new Permute(4);
}
}
Sedgewick 展示的另一种方法是 Heaps,每个排列只有一个交换而不是 2 个。这是一个 C++ 实现:
#include <vector>
#include <iostream>
using namespace std;
class Heaps {
private:
vector<int> p;
public:
Heaps(int n) {
p.reserve(n);
for (int i = 0; i < n; i++)
p.push_back(i+1);
generate(n);
}
void doit() {
cout << "doit size=" << p.size() << '\n';
for (int i = 0; i < p.size(); i++)
cout << p[i];
cout << '\n';
}
void generate(int N) {
// cout << "generate(" << N << ")\n";
if (N == 0)
doit();
for (int c = 0; c < N; c++) {
generate(N-1);
swap(p[N % 2 != 0 ? 0 : c], p[N-1]);
}
}
};
int main() {
Heaps p(4);
}