想象一下,我必须检查一个字符串的所有字母是否在另一个字符串中。我想比较两个实现,一个是尾递归,另一个是使用hashMap。以下是两种实现:
private boolean isPossible(final String left, final String right) {
boolean toReturn = false;
if (left.isEmpty()) {
toReturn = true;
} else {
char charAt = left.charAt(0);
final int index = right.indexOf(charAt);
toReturn = index != -1 ? isPossible(left.substring(1), stringWithoutIndex(right, index)) : false;
}
return toReturn;
}
和hashMap解决方案:
public boolean isPossible(String left, String right) {
HashMap<Character, Integer> occurrencesMap = createOccurrenceMapFor(left);
HashMap<Character, Integer> withTheLettersInRightRemoved = removeLettersFoundIn(right, occurrencesMap);
return checkThatWeCanWriteTheMessage(withTheLettersInRightRemoved);
}
private HashMap<Character, Integer> removeLettersFoundIn(final String string, final HashMap<Character, Integer> occurrencesMap) {
HashMap<Character, Integer> lettersRemoved = new HashMap<>(occurrencesMap);
for (char c : string.toCharArray()) {
if (lettersRemoved.containsKey(c))
lettersRemoved.put(c, lettersRemoved.get(c).intValue() - 1);
}
return lettersRemoved;
}
private HashMap<Character, Integer> createOccurrenceMapFor(String string) {
HashMap<Character, Integer> occurrencesMap = new HashMap<>();
for (char c : string.toCharArray()) {
if (occurrencesMap.containsKey(c))
occurrencesMap.put(c, occurrencesMap.get(c).intValue() + 1);
else
occurrencesMap.put(c, 1);
}
return occurrencesMap;
}
private boolean checkThatWeCanWriteTheMessage(HashMap<Character, Integer> occurrencesMap) {
for (char c : occurrencesMap.keySet()){
if (withTheLettersInMagazineRemoved.get(c) > 0) {
return false;
}
}
return true;
}
我认为这两种解决方案都具有O(n)性能,因为它们都没有for循环等。但是,一旦我比较时间,我得到的hashMap解决方案比递归解决方案快得多。当然,这是有道理的,但我想知道为什么从理论上讲,两者都有O(n)。我是对的吗?
答案 0 :(得分:3)
第一个解决方案遍历第一个字符串中的每个字符,即O(N),但是对于每个字符,它搜索第二个字符串中的匹配字符,它给出另一个内部/嵌套O(N)和 O(N ^ 2) 总计。
第二个解决方案迭代第一个字符串O(N),然后迭代第二个字符串O(N),最后它遍历只包含有限范围的字符(某些常量)的hashmap。总数为O(N)+ O(N)+ C = O(N)
答案 1 :(得分:2)
aString.indexOf(aChar)
;因此它是O(aString.length
)。并且stringWithoutIndex(aString, anIndex)
无法比O(aString.length
)更好地实现。在最糟糕的情况下(当left
中的所有字符都显示在right
中)时,您将执行O(left.length
* right.length
)操作。
您的第一个代码片段相当于:
private boolean isPossible(String left, String right) {
// loop will repeat left.length times at most
while (true) {
if (left.isEmpty()) {
return true;
} else {
char first = left.charAt(0);
left = left.substring(1);
// indexOf: O(right.length)
int index = -1;
for (int i=0; i<right.length; i++) {
if (right.charAt(i) == first) {
index = i;
break;
}
}
if (index >= 0) {
// stringWithoutIndex: concatenating strings is O(size of result)
right = right.substring(0, index) + right.substring(index+1);
} else {
return false;
}
}
}
}
我只是将递归转换为迭代,并扩展了indexOf
和stringWithoutIndex
- 这使得复杂性更容易计算,因为循环很容易看到。
请注意,从递归到迭代(或反之亦然)的机械转换不会改变复杂性类;虽然,当代码可以写为尾递归时(如本例isPossible
),迭代代码可能会更快,并且不能耗尽堆栈(因为它不使用它)。因此,许多编译器将尾递归转换为幕后迭代。
可以对内联函数进行类似的论证(内联通常更快,但仍保留在相同的大复杂类中),尽管存在额外的大小权衡:内联使得编译的程序在代码存在时更大在许多地方使用过。
答案 2 :(得分:1)
第一种算法:O(n * m)
第二种算法:O(n) - 您可以在此处看到m
的大小。
请参阅下面的反馈。
递归函数不是尾递归的。没有尾巴电话。
但我认为您可以通过分离return
语句将其转换为尾递归函数。例如。 (我缩短了你的代码,但请尽快验证)
private boolean isPossible(final String left, final String right) {
if (left.isEmpty()) return true;
char firstChar = left.charAt(0);
int index = right.indexOf(charAt);
if (index == -1) return false;
return isPossible(left.substring(1), stringWithoutIndex(right, index));
}
不确定stringWithoutIndex(right, index)
究竟做了什么,但是知道时间复杂度会很好。我假设它只是返回正确的字符串而没有给定索引处的字符,即O(m)
(m
是正确字符串的长度)。
我用数字(1, 2, 3, 4 and 5
)表示函数内部的代码行。我假设左侧字符串的长度为n
,而右侧字符串的长度为m
。
O(1)
O(1)
O(m)
,因为在最糟糕的情况下,您会在最后一个索引处找到该字符。O(1)
O(m)
,删除了正确字符串的字符将这些添加到一起,您将获得O(m)
一次迭代。由于您迭代的次数与左侧字符串中的字符数一样多,因此该算法的时间复杂度为O(m*n)
。
m < n
,则小于O(n^2)
。m = n
,则为O(n^2)
。m > n
,则为O(n * m)
最后,此算法的复杂性类别为O(n*m)
。
它与时间复杂性无关,但在特定于实现的级别上,您可以提升性能。 从理论上讲,这只会提高算法复杂度等级的常数。
尾部递归函数(您可能已经知道)可以进行优化并提高性能。如果速度对您很重要,您可能有兴趣将其转换为尾递归函数。 See Wikipedia for information. 通常,您可以期望迭代函数比递归函数更快。 但是,使用 memoization 的递归函数可以击败迭代函数。 See here for example with Fibonacci numbers.
阅读并考虑本解释末尾的注释(将讨论用于分析的hashmap的复杂性)并注意这是一种纯理论但严谨的方法。与前一个示例中类似的行号:
isPossible(...)
:
O(n * k')
- 取决于createOccurrenceMapFor(...)
O(m * k + n)
- 取决于removeLettersFoundIn(...)
O((n-m) * l)
- 取决于checkThatWeCanWriteTheMessage(...)
removeLettersFoundIn(...)
:
O(n)
- 基于左侧字符串O(m)
- 循环右字符串中的字符数
O(k)
- 理论上插入hashmap的最坏情况是O(k)
您可以看到此函数的复杂度为O(m*k + n)
。
createOccurrenceMapFor(...)
:
O(n)
- 循环左侧字符串中的字符数
O(k')
- 理论上插入hashmap的最坏情况是O(k')
此功能具有复杂性O(n*k')
checkThatWeCanWriteTheMessage(...)
:
不确定withTheLettersInMagazineRemoved
到底是什么,但我认为它是一个大小为k''
的散列图。
O(n-m)
- 循环左侧字符串中的剩余字符数
O(k'')
- 在hashmap中检索的最坏情况是O(k'')
注意此功能在您的算法上下文中没有意义。至少我无法理解这一点。
最后,算法具有子函数总和的复杂性:
O(n*k' + m*k+n + (n-m)*l)
此实现为基本提供了恒定时间性能 操作(获取和放置),假设散列函数分散了 在桶之间正确的元素。迭代集合视图 要求时间与容量成比例&#34; HashMap实例的 (桶数)加上其大小(键值的数量 映射)。因此,不设置初始容量非常重要 如果迭代性能是太高(或负载因子太低) 重要的。
HashMap的一个实例有两个影响其性能的参数: 初始容量和负载系数。容量是数量 哈希表中的桶,而初始容量就是 创建哈希表时的容量。负载系数是a 衡量哈希表在其之前可以获得多长的度量 容量自动增加。当中的条目数 哈希表超出了加载因子和当前的乘积 容量,哈希表重新哈希(即内部数据 重建结构),以便哈希表大约两次 桶的数量。
你的密钥是字符,因此我认为构建一个恒定时间哈希函数并不困难。因此,让我们假设您的散列图(put
,get
和containsKey
)的操作在固定时间O(1)
而不是O(k)
,O(k')
运行}或O(k'')
。
最后,该算法的复杂性类别为O(n + m+n + (n-m)) = O(3*n) = O(n)
,假设哈希函数在O(1)
中运行,您可以很好地假设它。