如何在不将二进制数转换为整数的情况下递归地增加二进制数字(以列表形式)?

7
例如,给定列表[1, 0, 1],代码将返回[1,1,0]。其他示例:
[1,1,1] -- > [1,0,0,0]
[1,0,0,1] --> [1,0,1,0]

我最困惑的是递归的基本情况应该是什么,以及如何实现(n-1)的情况。

def increment_helper(number):
    newNum = []
    if len(number) ==1:
        if number[0] == 1:
            carry = 1
            newNum.append(0)

        else:
            carry = 0
            newNum.append(1)
    else:
        return increment_helper(number-1)
    return newNum

我确定在这里有很多错误,特别是在我如何调用递归方面,因为我不知道如何在递归列表的同时存储已删除的数字。显然,else返回语句是不正确的,但我正在使用它作为占位符。我不确定该使用什么条件作为我的基本情况增量。我认为我应该使用一个carry变量来跟踪是否携带了一个1,但除此之外,我对如何继续进行感到困惑。


3
看起来您希望我们为您编写一些代码。虽然很多用户愿意帮助有困难的程序员编写代码,但他们通常只会在发布者已经尝试了解决问题后才提供帮助。展示这种努力的好方法是包括你已经编写的代码、示例输入(如果有的话)、期望的输出以及实际得到的输出(控制台输出、跟踪等)。提供的细节越多,您可能收到的答案就越多。请查看[FAQ]和[ask]。 - MooingRawr
3
递归、二进制数以列表形式表示,不进行转换。哇,有很多限制条件。我们可以假设这是一个作业吗? - Robᵩ
你最后一句话的意思是需要编写整个函数。请阅读并遵守帮助文档中的发布指南。在这里适用主题提问方式。 StackOverflow不是设计、编码、研究或教程服务。 - Prune
“递归”是这里的一部分规则吗?这没什么意义... - Patrick Artner
这是一个算法问题,规定必须使用递归。可迭代的解决方案对我来说非常容易理解,但这个问题有条件地要求使用递归。 - user5460917
显示剩余3条评论
10个回答

1
啊哈!好的,你对自己在做什么有一些想法。基本概述如下:
基本情况:我何时知道自己已经完成了?
当你用完数字时就完成了。number 应该是一个单独数字的列表;检查它的长度以确定何时不要再进行递归。
递归情况:接下来怎么办?
递归的一般概念是“做一些简单的事情,通过少量减少问题,并使用较小的问题进行递归”。在这部分中,您的工作是为一个数字执行加法。如果您需要继续(从那个数字是否有进位?),则进行递归。否则,您已经拥有完成所需的所有信息。
具体应用
您的递归步骤将涉及使用一个数字少的调用 increment_helper:不是 number - 1,而是 number[:-1]。
在每次递归返回后,您需要将刚完成的数字附加到结果中。例如,如果要增加1101,则第一次调用将看到右侧的数字增加了一个进位。新数字是0,您需要进行递归。暂时保留0,使用110调用自身,并获取该调用的结果。将保存的0附加到结果中,然后返回主程序。
这样是否对您有所帮助?

0

这个问题有点“棘手”,因为每一步都有两件事情:

  1. 我正在查看的当前数字是什么?(二进制问题0/1)
  2. 它应该被递增吗?(我们是否有进位?)(二进制问题是/否)

这导致了4种情况。

还有一个额外的“情况”,我们是否得到了一个列表,但它并不那么有趣。

所以我会描述如下:

if not carry:
    # technically meaningless so just return the entire list immediately
    return list

# we have "carry", things will change
if not list:
    # assumes [] == [0]
    return [1]

if list[0]:
    # step on `1` (make it 0 and carry)
    return [0] + increment(rest_of_list)

# step on `0` (make it 1 and no more carry)
return [1] + rest_of_list

我强烈建议将列表更改为元组并使用它们。

还要注意递归是在反转的列表上进行的,因此应按以下方式应用:

def increment_helper(lst, carry):
    if not carry:
        return lst

    if not lst:
        return [1]

    if lst[0]:
        return [0] + increment_helper(lst[1:], True)

    return [1] + lst[1:]


def increment(lst):
    # long but really just reverse input and output
    return increment_helper(lst[::-1], True)[::-1]

我在编程中使用了一些快捷方式,通过立即返回列表(短路),但您可以通过继续递归而不使用carry来使其更"纯粹"。


0

您可以通过从尾到头遍历数字(就像加法一样)来递归地处理(二进制)数字。

在执行递归之前,您必须检查两种特殊情况:

  • 不需要对当前数字进行增量。然后只需返回未修改的数字即可。
  • 仅剩一个数字(您位于数字的开头)。然后,您可能需要将溢出附加到开头。

对于其余情况,您可以增加当前数字,并以递归方式处理当前数字之前的所有数字。

def bin_incr(n, incr=True):
    if not incr:
        return n
    if len(n) == 1:
        return [1, 0] if n[0] == 1 else [1]
    return (
        # `n[-1] == 1` denotes an overflow to the next digit.
        bin_incr(n[:-1], n[-1] == 1)
        + [(n[-1] + 1) % 2]
    )

@Sean 在 len(n) == 1 的条件下,无需修改 incr,因为你将返回一个固定大小的列表:在溢出的情况下是 [1, 0],否则是 [1](因为必须发生增量)。 - a_guest
@Sean 是的,你说得很好,因为我实际上混淆了两种不同的类型。所以 incr 应该只表示是否要递增(所以它实际上是一个布尔变量,但出于简洁起见,我将其设置为 1;我们总是递增 1)。n[-1] == 1 部分仅表示是否有溢出到下一个数字,如果是这种情况,则计算结果为 True。因此,对于每个递归调用,incr 的值实际上是基于是否有溢出而为 TrueFalse。我会修复答案以保持一致性。 - a_guest
哦,那样就清楚多了。我喜欢你的解决方案!我发现我的问题是我一直在从头开始思考而不是从尾部开始遍历列表。这使得问题变得微不足道。非常感谢你的帮助! - user5460917
其实你测试过你的解决方案吗?当我使用以下断言进行断言时:assert bin_incr([1,1,1]) = [1,0,0,0], "ERROR",我得到了一个错误。 - user5460917
1
@Sean 是的,我试过了,对于你的例子我也没有收到错误。在你的断言中,你似乎使用了单个=进行比较相等性,应该使用两个==。这是一个错别字吗?另外,你收到什么错误?信息是什么? - a_guest
显示剩余4条评论

0
最好使用两个函数:一个用于检查是否简单增加最后一个位置就足够了,另一个用于在前一个尝试失败时执行递归。
vals = [[1, 0, 1], [1,1,1], [1,0,0,1]]
def update_full(d):
   if all(i in [1, 0] for i in d):
      return d
   start = [i for i, a in enumerate(d) if a > 1]
   return update_full([1]+[0 if i > 1 else i for i in d] if not start[0] else [a+1 if i == start[0] -1 else 0 if i == start[0] else a for i, a in enumerate(d)])

def increment(d):
    if not d[-1]:
       return d[:-1]+[1]
    return update_full(d[:-1]+[2])

print(list(map(increment, vals)))

输出:

[[1, 1, 0], [1, 0, 0, 0], [1, 0, 1, 0]]

d[-1]表示从尾部开始吗? - user5460917
@Sean 我的答案所使用的逻辑要求递归从尾部开始,因此是 d[-1] - Ajax1234

0

试试这个:

def add1(listNum):

    if listNum.count(0):
        oneArr = [[0] * (len(listNum) - 1)] + [1]
        sumArr = []
        for i in range(len(listNum)):
            sumArr.append(sum(listNum[i], oneArr[i]))
        newArr = []
        for j in range(len(sumArr) - 1):
            if sumArr[len(sumArr) - 1 - j] < 2:
                newArr.insert(0, sumArr[len(sumArr) - 1 - j])
            else:
                newArr.insert(0, 1)
                sumArr[len(sumArr) - 1 - j] += 1
        return sumArr

    else:
        return [1] + [[0] * len(listNum)]

对于像这样简单的程序,使用递归的原因并不多,这就是为什么我选择提供非递归解决方案的原因。

如果你感兴趣的话,我已经计算出了这个函数的时间复杂度,它是O(n)。


我没有看到递归步骤。而且,它似乎过于复杂了。 - Prune

0

另一种递归方法。

  • 当前输入是否为空列表?

    • 如果是,返回[1]
    • 如果不是,则继续
  • 列表中最后一个元素的和(value)加1是否大于1?

    • 如果是,则在不包括最后一个元素的列表上递归调用你的函数,并将[0]附加到结果中。
    • 如果不是,则将列表的最后一个元素设置为该和。
  • 返回number_list

代码

def increment_helper(number_list):
    if not number_list:
        return [1]

    value = number_list[-1] + 1

    if value > 1:
        number_list = increment_helper(number_list[:-1]) + [0]
    else:
        number_list[-1] = value

    return number_list

示例输出

numbers = [[1, 0, 1], [1,1,1], [1,0,0,1]]
for n in numbers:
    print("%r ---> %r" % (n, increment_helper(n)))
#[1, 0, 1] ---> [1, 1, 0]
#[1, 1, 1] ---> [1, 0, 0, 0]
#[1, 0, 0, 1] ---> [1, 0, 1, 0]

0

我知道你不想使用十进制加法,但如果你必须使用大端位序,那么先转换回十进制可能是最实际的方法。否则,你会得到不必要的reverse调用或者笨拙的负数组索引。

def binary (dec): # big endian bit order
  if dec < 2:
    return [ dec ]
  else:
    return binary (dec >> 1) + [ dec & 1 ]

def decimal (bin, acc = 0):
  if not bin:
    return acc
  else:
    return decimal (bin[1:], (acc << 1) + bin[0])

def increment (bin):
  # sneaky cheat
  return binary (decimal (bin) + 1)

for x in range (10):
  print (x, binary (x), increment (binary (x)))

# 0 [0] [1]
# 1 [1] [1, 0]
# 2 [1, 0] [1, 1]
# 3 [1, 1] [1, 0, 0]
# 4 [1, 0, 0] [1, 0, 1]
# 5 [1, 0, 1] [1, 1, 0]
# 6 [1, 1, 0] [1, 1, 1]
# 7 [1, 1, 1] [1, 0, 0, 0]
# 8 [1, 0, 0, 0] [1, 0, 0, 1]
# 9 [1, 0, 0, 1] [1, 0, 1, 0]

然而,如果您可以以小端位序表示二进制数,情况就会改变。不需要转换回十进制,increment 可以直接定义为一个美丽的递归函数。

def binary (dec): # little endian bit order
  if dec < 2:
    return [ dec ]
  else:
    return [ dec & 1 ] + binary (dec >> 1) 

def increment (bin):
  if not bin:
    return [1]
  elif bin[0] == 0:
    return [1] + bin[1:]
  else:
    return [0] + increment(bin[1:])

for x in range (10):
  print (x, binary (x), increment (binary (x)))

# 0 [0] [1]
# 1 [1] [0, 1]
# 2 [0, 1] [1, 1]
# 3 [1, 1] [0, 0, 1]
# 4 [0, 0, 1] [1, 0, 1]
# 5 [1, 0, 1] [0, 1, 1]
# 6 [0, 1, 1] [1, 1, 1]
# 7 [1, 1, 1] [0, 0, 0, 1]
# 8 [0, 0, 0, 1] [1, 0, 0, 1]
# 9 [1, 0, 0, 1] [0, 1, 0, 1]

另外:将小端表示转换回十进制有点不同。我提供这个示例以展示递归的用例无处不在。

def decimal (bin, power = 0):
  if not bin:
    return 0
  else:
    return (bin[0] << power) + decimal (bin[1:], power + 1)

这部分回答为您提供了蛋糕,并允许您同时享用。您可以获得大端位序和递归的increment,该递归按从左到右的顺序遍历位 - 对于许多原因,您应该使用上述任一实现,但这旨在向您展示即使您的问题很复杂,仍然可以递归地考虑它。在制作此函数时没有误用reversearr[::-1]

def binary (dec): # big endian bit order
  if dec < 2:
    return [ dec ]
  else:
    return binary (dec >> 1) + [ dec & 1 ]

def increment (bin, cont = lambda b, carry: [1] + b if carry else b):
  if bin == [0]:
    return cont ([1], 0)
  elif bin == [1]:
    return cont ([0], 1)
  else:
    n, *rest = bin
    return increment (rest, lambda b, carry:
                              cont ([n ^ carry] + b, n & carry))

for x in range (10):
  print (x, binary (x), increment (binary (x)))

# 0 [0] [1]
# 1 [1] [1, 0]
# 2 [1, 0] [1, 1]
# 3 [1, 1] [1, 0, 0]
# 4 [1, 0, 0] [1, 0, 1]
# 5 [1, 0, 1] [1, 1, 0]
# 6 [1, 1, 0] [1, 1, 1]
# 7 [1, 1, 1] [1, 0, 0, 0]
# 8 [1, 0, 0, 0] [1, 0, 0, 1]
# 9 [1, 0, 0, 1] [1, 0, 1, 0]

我们首先将问题分解成更小的部分;n是第一个问题,而rest则是其余的问题。但是使用延续(如上面的cont)思考的关键是要有大局观。

在这个特定的问题中,n的更新取决于rest是否更新。因此,我们立即对rest进行递归,并传递一个将接收子问题结果的延续。我们的延续接收子问题b的答案,以及该子问题是否导致carry

...
  else:
    n, *rest = bin
    return increment (rest, lambda b, carry:
                              cont ([n ^ carry] + b, n & carry))

n^carryn&carry表达式决定了这个子问题的答案和下一个进位是什么。以下真值表显示了^&分别对我们的answernext_carry进行编码。例如,如果n为0且carry为1,则可以消耗进位。answer将是子问题的答案加上[1]+,而下一个进位将为0。

n   carry   (answer, next_carry)   n ^ carry   n & carry
0   0       ([0] + b, 0)           0           0
0   1       ([1] + b, 0)           1           0
1   0       ([1] + b, 0)           1           0
1   1       ([0] + b, 1)           0           1

基本情况很简单。如果子问题是[0],则答案为[1]且没有进位0。如果子问题是[1],则答案为[0],并带有进位1
...
  if bin == [0]:
    return cont ([1], 0)
  elif bin == [1]:
    return cont ([0], 1)

最后,设计默认的继续操作 - 如果问题的答案 b 导致进位,则只需在答案前面添加 [1],否则只需返回答案。
cont = lambda b, carry: [1] + b if carry else b

0
您正在寻找一个可以生成序列的增量/后继/下一个函数。由于其他人已经提供了代码,我将为开发此类函数提供一种通用方法。
首先,开发一个多重递归(2个或更多递归调用)来计算长度为N的所有类型序列。对于二进制序列(bs),从N个0到N个1的大端顺序,基本情况bs(0)表达式是[[]],包含一个没有二进制位的序列。bs(n)的双重递归是在bs(n-1)的基础上,将[0]连接到bs(n-1)的所有成员(按顺序)中,再加上[1]连接到bs(n-1)的所有成员。
接下来,专注于相邻递归调用返回的子序列之间的转换。这里只有一个:0、1、…、1到1、0、…、0。为了跨越这个边界进行增量,我们需要将0后面跟着0或更多个1替换为1,然后是相同数量的0。我们通过从右侧扫描查找第一个0来找到这样的中断,正如其他人所展示的。

事实证明,每个增量都会穿越相邻bs(k)调用之间的边界,这意味着在双重递归产生的调用树的某个级别上。

就我所知,同样的想法适用于设计灰码序列、组合序列(从n个物品中取k个)和排列序列的增量函数。

注1:1->0的转换可以一次完成或逐个完成。

注2:二进制位测试和翻转是计数+1的图灵机算法。斯蒂芬·沃尔夫拉姆在《新科学的一种》中,在TM章节中提出了我记得有3种不同的实现方法。

注3(编辑添加):如果从首先添加0切换到首先添加1,或者从前缀切换到后缀,或两者都切换,则会得到另外3个序列的序列,以及3个其他的增量函数。


0

在这种情况下,您不需要递归。 让我们从最简单的实现开始:

  1. 实现一个全加器,它将在位上操作。
  2. 使用全加器实现一个涟漪加法器,它将在2个位列表上操作。

全加器的实现很直接。

def full_adder(a,b,cin):
    sum_ = a^(b^cin)
    cout = (a&b) | (a|b)&cin
    return sum_, cout

测试以确保全加器符合规格:

>>> zero_one = (0,1)
>>> [full_adder(*x) for x in [(a,b,c) for a in zero_one for b in zero_one for c in zero_one]]
[(0, 0), (1, 0), (1, 0), (0, 1), (1, 0), (0, 1), (0, 1), (1, 1)]

由于涟波加法器的参数是列表,因此在进行加法运算之前,我们需要确保列表长度匹配。这可以通过在较短的列表前面填充零来实现。

def ripple_adder(xs,ys):
    x, y = map(len, (xs, ys))
    alen = max(x, y)
    ax, by = map(lambda f: f if len(f) == alen else [0]*(alen-len(f)) + f, (xs, ys))
    cout = 0
    res = [0]*(alen)
    for i in range(alen-1, -1, -1):
        a, b, cin = ax[i], by[i], cout
        s, cout = full_adder(a, b, cin)
        res[i] = s
    if cout:
        res = [1] + res
    return res

最后,我们通过涟漪加法器定义bin_inc二进制增量函数。
def bin_inc(bin_lst):
    return ripple_adder(bin_lst, [1])

>>> bin_inc([1,1,1])
[1, 0, 0, 0]
>>> bin_inc([1,0,0,1])
[1, 0, 1, 0]

现在我们提供一个更简单但需要一点点见识的解决方案。考虑以下输入xs和输出ys的观察结果:

  1. 如果xs全部为1,则ys的长度将为L+1ys的第一个元素将为1,其余元素将为零。

  2. 如果xs有一个零元素,让p成为xs中最后一个零元素的索引。 然后ys将是由xs的前p个元素组成的列表,后跟一个长度为L-p的1和0。也就是说 ys = xs[:p] + [1] + [0]*(L-p)

我们可以很容易地将其转换为Python代码。我们使用Python的list.index方法通过对列表进行反转来查找零的最后出现位置,并相应地调整算法:

def bin_inc_2(xs):
    if all(xs):
        return [1] + [0]*len(xs)
    p = xs[::-1].index(0)
    return xs[:-p-1] + [1] + [0]*p

这个实现方式比基于ripple_adder的实现更简单有效。你可能会注意到一个小缺点是,当xs有一个零元素时,我们需要遍历它来检查它是否全为1,然后再遍历一次以找到第一个零出现的位置。

我们可以通过使用一个try except块来简化实现并同时使其更符合Python语言风格:

def bin_inc_3(bin_lst):
    try:
        p = bin_lst[::-1].index(0)
        return bin_lst[:-p-1] + [1] + [0]*p
    except ValueError: 
        return [1] + [0]*len(bin_lst)

这个实现在源代码和Python语言方面都非常简单。现在我们将对其进行测试,以确保它能够与基于ripple_adder的加法器良好地配合使用。

>>> z_n = (0,1)
>>> xs = [[a,b,c,d,e,f,g,h] for a in z_n for b in z_n for c in z_n for d in z_n
                            for e in z_n for f in z_n for g in z_n for h in z_n ]

>>> print(all(ripple_adder(x, [1]) == bin_inc_3(x) for x in xs))
True

太棒了,它按预期工作,并通过测试正确处理前导零(测试将每个数字从0增加到255)。


0

只有在进位时才需要递归:

def f(n):
  # Base cases
  if not n:
    return [1]
  if n == [1]:
    return [1, 0]
  if n[-1] == 0:
    return n[:-1] + [1]
  if n[-2:] == [0, 1]:
    return n[:-2] + [1, 0]

  # Recurse
  return f(n[:-2]) + [0, 0]

numbers = [[1, 0, 1], [1,1,1], [1,0,0,1], [1, 0, 1, 1]]
for n in numbers:
  print n, f(n)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接