使用递归Python计算项目在序列中出现的次数

时间:2016-03-09 15:25:38

标签: python recursion count

我正在尝试计算一个项目在序列中出现的次数,无论它是数字列表还是字符串,它适用于数字但我在尝试找到像{{1}这样的字母时遇到错误在一个字符串中:

"i"
  

TypeError:+:'int'和'NoneType'不支持的操作数类型

5 个答案:

答案 0 :(得分:2)

使用递归比使用递归更常用:使用内置的count方法计算出现次数。

def count(str, item):
    return str.count(item)
>>> count("122333444455555", "4")
4

但是,如果您想使用 iteration 进行此操作,则可以应用类似的原则。将其转换为列表,然后遍历列表。

def count(str, item):
    count = 0
    for character in list(str):
        if character == item:
            count += 1
    return count

答案 1 :(得分:1)

问题是您的第一个if,其中显式检查输入是否为空列表:

if s == []: 
    return 0

如果您希望它与strlist一起使用,您只需使用:

if not s:
    return s

简而言之,根据truth value testing in Python,任何空序列都被视为假,并且任何非空序列都被视为真。如果您想了解更多信息,我添加了相关文档的链接。

你也可以在这里省略while循环,因为它不必要,因为它总会在第一次迭代中返回,因此离开循环。

所以结果将是这样的:

def count(f, s):
    if not s: 
        return 0
    elif f == s[0]:
        return 1 + count(f, s[1:])
    else:
        return 0 + count(f, s[1:])

示例:

>>> count('i', 'what is it')
2

如果您不仅对使其成功感兴趣,而且还有兴趣让它变得更好,那么有几种可能性。

布尔值来自整数的子类

在Python中,布尔值只是整数,所以当你算术时它们的行为就像整数一样:

>>> True + 0
1
>>> True + 1
2
>>> False + 0
0
>>> False + 1
1

因此,您可以轻松地内联if else

def count(f, s):
    if not s: 
        return 0
    return (f == s[0]) + count(f, s[1:])

因为f == s[0]返回True(行为类似于1),如果它们相等或False(行为类似于0),如果它们不是。括号不是必需的,但为了清楚起见,我添加了它们。因为基本情况总是返回一个整数,所以这个函数本身总会返回一个整数。

以递归方式避免副本

由于以下原因,您的方法会创建大量输入副本:

s[1:]

除第一个元素外,这将创建整个列表(或字符串,...)的浅表副本。这意味着你实际上有一个操作使用O(n)(其中n是元素的数量)时间和内存在每个函数调用中,因为你递归地执行这个操作,时间和内存的复杂性将是{{1 }}

您可以避免这些副本,例如,通过传递索引:

O(n**2)

因为我需要传入当前索引,所以我添加了另一个获取索引的函数(我假设你可能不希望索引在你的公共函数中传入),但如果你想要你可以使用它一个可选参数:

def _count_internal(needle, haystack, current_index):
    length = len(haystack)
    if current_index >= length:
        return 0
    found = haystack[current_index] == needle
    return found + _count_internal(needle, haystack, current_index + 1)

def count(needle, haystack):
    return _count_internal(needle, haystack, 0)

然而,可能有更好的方法。你可以将序列转换为迭代器并在内部使用它,在函数的开头你从迭代器中弹出下一个元素,如果没有元素就结束递归,否则你比较元素然后递归到剩余的迭代器:

def count(needle, haystack, current_index=0):
    length = len(haystack)
    if current_index >= length:
        return 0

    return (haystack[current_index] == needle) + count(needle, haystack, current_index + 1)

当然,如果你想避免def count(needle, haystack): # Convert it to an iterator, if it already # is an (well-behaved) iterator this is a no-op. haystack = iter(haystack) # Try to get the next item from the iterator try: item = next(haystack) except StopIteration: # No element remained return 0 return (item == needle) + count(needle, haystack) 调用开销,你也可以使用内部方法,只有在第一次调用函数时才需要。然而,这可能不会导致显着更快执行的微优化:

iter

这两种方法都具有以下优点:它们不会使用(多)额外的内存并且可以避免副本。所以它应该更快,占用更少的内存。

但是对于长序列,由于递归会导致问题。 Python有一个递归限制(可调,但只是在某种程度上):

def _count_internal(needle, haystack):
    try:
        item = next(haystack)
    except StopIteration:
        return 0

    return (item == needle) + _count_internal(needle, haystack)

def count(needle, haystack):
    return _count_internal(needle, iter(haystack))

使用divide-and-conquer

递归

有一些方法可以缓解(只要你使用递归就无法解决递归深度问题)这个问题。经常使用的方法是分而治之。它基本上意味着你将你拥有的任何序列分成2个(有时更多)部分,然后用这些部分中的每个部分调用函数。当只剩下一个项目时,递归窗台结束:

>>> count('a', 'a'*10000)
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-9-098dac093433> in <module>()
----> 1 count('a', 'a'*10000)

<ipython-input-5-5eb7a3fe48e8> in count(needle, haystack)
     11     else:
     12         add = 0
---> 13     return add + count(needle, haystack)

... last 1 frames repeated, from the frame below ...

<ipython-input-5-5eb7a3fe48e8> in count(needle, haystack)
     11     else:
     12         add = 0
---> 13     return add + count(needle, haystack)

RecursionError: maximum recursion depth exceeded in comparison

递归深度现在从def count(needle, haystack): length = len(haystack) # No item if length == 0: return 0 # Only one item remained if length == 1: # I used the long version here to avoid returning True/False for # length-1 sequences if needle == haystack[0]: return 1 else: return 0 # More than one item, split the sequence in # two parts and recurse on each of them mid = length // 2 return count(needle, haystack[:mid]) + count(needle, haystack[mid:]) 更改为n,这允许进行之前失败的调用:

log(n)

然而,因为我使用切片,它将再次创建大量副本。使用迭代器会很复杂(或者不可能),因为迭代器不具有(通常)大小但是它易于使用的索引:

>>> count('a', 'a'*10000)
10000

使用带递归的内置方法

在这种情况下使用内置方法(或函数)可能看起来很愚蠢,因为已经有一个内置的方法来解决问题而没有递归但是在这里它是使用def _count_internal(needle, haystack, start_index, end_index): length = end_index - start_index if length == 0: return 0 if length == 1: if needle == haystack[start_index]: return 1 else: return 0 mid = start_index + length // 2 res1 = _count_internal(needle, haystack, start_index, mid) res2 = _count_internal(needle, haystack, mid, end_index) return res1 + res2 def count(needle, haystack): return _count_internal(needle, haystack, 0, len(haystack)) 方法字符串和列表都有:

index

使用迭代而不是递归

递归真的很强大但是在Python中你必须对抗递归限制,因为在Python中没有tail call优化它通常很慢。这可以通过使用迭代而不是递归来解决:

def count(needle, haystack):
    try:
        next_index = haystack.index(needle)
    except ValueError:  # the needle isn't present
        return 0

    return 1 + count(needle, haystack[next_index+1:])

使用内置函数的迭代方法

如果您更有利,可以将{(3)}与<{3}}一起使用生成器表达式:

def count(needle, haystack):
    found = 0
    for item in haystack:
        if needle == item:
            found += 1
    return found

同样,这依赖于布尔表现为整数的事实,因此def count(needle, haystack): return sum(needle == item for item in haystack) 将所有出现(1)与所有非出现(零)相加,从而得出总计数。

但如果一个人已经在使用内置插件,那么更不用说内置方法(字符串和列表都有):sum

count

此时你可能不再需要将它包装在一个函数中,而只需直接使用该方法。

如果您想进一步计算所有元素,可以使用内置集合模块中的def count(needle, haystack): return haystack.count(needle)

Counter

效果

我经常提到副本及其对记忆和表现的影响,我实际上想要提供一些定量结果,以表明它实际上有所作为。

我在这里使用了一个有趣的项目>>> from collections import Counter >>> Counter('abcdab') Counter({'a': 2, 'b': 2, 'c': 1, 'd': 1}) (它是第三方软件包,所以如果你想运行它,你必须安装它):

simple_benchmarks

sum

它的对数日志缩放以有意义的方式显示值范围,而更低意味着更快。

可以清楚地看到原始方法对于长输入变得非常慢(因为它复制了它在def count_original(f, s): if not s: return 0 elif f == s[0]: return 1 + count_original(f, s[1:]) else: return 0 + count_original(f, s[1:]) def _count_index_internal(needle, haystack, current_index): length = len(haystack) if current_index >= length: return 0 found = haystack[current_index] == needle return found + _count_index_internal(needle, haystack, current_index + 1) def count_index(needle, haystack): return _count_index_internal(needle, haystack, 0) def _count_iterator_internal(needle, haystack): try: item = next(haystack) except StopIteration: return 0 return (item == needle) + _count_iterator_internal(needle, haystack) def count_iterator(needle, haystack): return _count_iterator_internal(needle, iter(haystack)) def count_divide_conquer(needle, haystack): length = len(haystack) if length == 0: return 0 if length == 1: if needle == haystack[0]: return 1 else: return 0 mid = length // 2 return count_divide_conquer(needle, haystack[:mid]) + count_divide_conquer(needle, haystack[mid:]) def _count_divide_conquer_index_internal(needle, haystack, start_index, end_index): length = end_index - start_index if length == 0: return 0 if length == 1: if needle == haystack[start_index]: return 1 else: return 0 mid = start_index + length // 2 res1 = _count_divide_conquer_index_internal(needle, haystack, start_index, mid) res2 = _count_divide_conquer_index_internal(needle, haystack, mid, end_index) return res1 + res2 def count_divide_conquer_index(needle, haystack): return _count_divide_conquer_index_internal(needle, haystack, 0, len(haystack)) def count_index_method(needle, haystack): try: next_index = haystack.index(needle) except ValueError: # the needle isn't present return 0 return 1 + count_index_method(needle, haystack[next_index+1:]) def count_loop(needle, haystack): found = 0 for item in haystack: if needle == item: found += 1 return found def count_sum(needle, haystack): return sum(needle == item for item in haystack) def count_method(needle, haystack): return haystack.count(needle) import random import string from functools import partial from simple_benchmark import benchmark, MultiArgument funcs = [count_divide_conquer, count_divide_conquer_index, count_index, count_index_method, count_iterator, count_loop, count_method, count_original, count_sum] # Only recursive approaches without builtins # funcs = [count_divide_conquer, count_divide_conquer_index, count_index, count_iterator, count_original] arguments = { 2**i: MultiArgument(('a', [random.choice(string.ascii_lowercase) for _ in range(2**i)])) for i in range(1, 12) } b = benchmark(funcs, arguments, 'size') b.plot() 中执行的列表),而其他方法表现为线性。可能看起来很奇怪的是,分而治之的方法执行速度较慢,但​​这是因为它们需要更多的函数调用(并且函数调用在Python中很昂贵)。但是,在它们达到递归限制之前,它们可以处理比迭代器和索引变体更长的输入。

改变分而治之的方法很容易让它跑得更快,想到一些可能性:

  • 当序列很短时,切换到非分治。
  • 每个函数调用始终处理一个元素,只划分序列的其余部分。

但鉴于这可能只是一种递归练习,有点超出了范围。

然而,它们的表现都比使用迭代方法更糟糕:

enter image description here

特别是使用列表的O(n**2)方法(也是字符串之一)和手动迭代要快得多。

答案 2 :(得分:1)

错误是因为有时你只是没有返回值。因此,在函数末尾返回0可修复此错误。在python中有很多更好的方法可以做到这一点,但我认为它只是用于训练递归编程。

答案 3 :(得分:0)

在我看来,你正在以艰难的方式做事。

你可以使用集合中的Counter来做同样的事情。

from collections import Counter

def count(f, s):
    if s == None:
        return 0
    return Counter(s).get(f)

Counter将返回一个dict对象,该对象包含s对象中所有内容的计数。在dict对象上执行.get(f)将返回您要搜索的特定项目的计数。这适用于数字列表或字符串。

答案 4 :(得分:0)

如果你受到约束并决定用递归来做,我会强烈建议将问题减半,而不是逐个削减它。减半允许您处理更大的案例,而不会遇到堆栈溢出。

g = O(f)