【问题标题】:Remove Linked List Elements - Recursion problem删除链表元素 - 递归问题
【发布时间】:2021-05-29 17:17:21
【问题描述】:

我正在尝试使用辅助递归函数解决“从具有值 val 的整数的链接列表中删除所有元素”问题,但它不起作用。

Example:
Input:  1->2->6->3->4->5->6, val = 6
Output: 1->2->3->4->5

我的解决方案:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def checkHead(self, head, val):
        if head.val == val and head.next:
            head = head.next
            self.checkhead(head,val)
        elif head.val and not head.next:
            head = None
        
        return head
        
    def removeElements(self, head: ListNode, val: int) -> ListNode:
        if not head:
            return head
        
        head = self.checkHead(head, val)
        
        if not head:
            return head
        
        curr = head
        
        while curr and curr.next:
            if curr.next.val == val:
                curr.next = curr.next.next
            
            curr = curr.next

        return None

失败的测试用例:

Input:  1->1, val = 1
Output: []

        

当我将递归checkHead 函数的值返回为head = self.checkHead(head, val) 时,它指出head 等于“1”但是当我调试它时,我可以看到程序从checkHead 返回为None .我想知道问题出在哪里。

【问题讨论】:

  • 行“elif head.val and not head.next:”是可疑的。你错过了 == 吗?
  • 正如@Stefan 所说,elifelif 语句中可能需要head.val == valcheckHead

标签: python recursion data-structures linked-list singly-linked-list


【解决方案1】:

根本缺陷

试图通过迭代和变异来解决这个问题存在一个根本缺陷。尽你所能,remove_elements 可以调整headnext属性,但它不能ListNode更改为None -

w = node(1)
print(w)      # ListNode { head = 1, next = None }
remove(w, 1)
print(w)      # ???

我们希望看到None 作为结果,但如果没有以下至少一项,这是不可能的 -

  1. 函数调用必须改变
  2. ListNode 的数据结构必须改变

假设你想保持ListNode的形状,我们必须将函数调用调整为-

w = node(1)
print(w)
w = remove_elements(w, 1)   # <- reassign with result of call
print(w)

现在使用remove_elements 的返回值,我们可以捕捉到ListNode 被降级为普通None 的场景。使用您的第一个示例 -

p = node(6,node(1,node(2,node(6,node(3,node(4,node(5,node(6))))))))

print(to_str(p))
p = remove_elements(p, 6)   # <- reassign p
print(to_str(p))
6->1->2->6->3->4->5->6->∅
1->2->3->4->5->∅

还有你的第二个例子-

q = node(1, node(1))

print(to_str(q))
q = remove_elements(q, 1)    # <- reassign q
print(to_str(q))
1->1->∅
∅

这是一个可变单链表的实现-

class node:
  def __init__(self, head, next = None):
    self.head = head
    self.next = next

def remove_elements(t, val):
  # load stack
  s = []
  while t:
    if t.head != val:
      s.append(t)
    t = t.next

  # unload stack
  r = None
  while s:
    q = s.pop()
    q.next = r
    r = q
  
  # return
  return r

def to_str(t):
  r = ""
  while t:
    r = r + str(t.head) + "->"
    t = t.next
  return r + "∅"

了解你的出身

这个问题被标记为recursion,这是理所当然的。链表是一种递归数据结构,选择一个递归程序来处理它可以使我们的程序结构与我们的数据结构相协调。然而,递归是一种函数式遗产,因此将它与函数式风格一起使用会产生最好的结果。这意味着避免诸如突变、变量重新分配和其他副作用之类的事情。不是破坏现有值,而是创建一个 值 -

  1. 如果输入列表t 为空,则已达到基本情况。返回空列表
  2. (归纳)输入为空;它至少有一个元素。如果第一个元素 t.head 匹配要删除的值 val,则返回子问题的结果 t.next
  3. (归纳)输入不为空且第一个元素与要删除的值匹配。构造t.head的新列表节点和子问题t.next的结果-
def remove_elements(t, val):
  if not t:
    return None                                           # 1
  elif t.head == val:
    return remove_elements(t.next, val)                   # 2
  else:
    return ListNode(t.head, remove_elements(t.next, val)) # 3

对于您的第一个示例 -

p = node(6,node(1,node(2,node(6,node(3,node(4,node(5,node(6))))))))

print(to_str(p))
print(to_str(remove_elements(p, 6)))
6->1->2->6->3->4->5->6->∅
1->2->3->4->5->∅

使用您的其他示例 -

q = node(1, node(1))

print(to_str(q))
print(to_str(remove_elements(q, 1)))
1->1->∅
∅

这是一个不可变(持久)单链表的实现——

class node:
  def __init__(self, head, next = None):
    self.head = head
    self.next = next

def remove_elements(t, val):
  if not t:
    return None
  elif t.head == val:
    return remove_elements(t.next, val)
  else:
    return node(t.head, remove_elements(t.next, val))

def to_str(t):
  if not t:
    return "∅"
  else:
    return str(t.head) + "->" + to_str(t.next)

注意

不可变实现不会改变现有值。这意味着创造了新的价值 -

p = node(6,node(1,node(2,node(6,node(3,node(4,node(5,node(6))))))))
q = remove_elements(p, 6)

当我们写q = remove_elements(p, 6)时,一个列表被创建并存储到qp不变-

print(to_str(q))
print(to_str(p))
1->2->3->4->5->∅                # q is a new list
6->1->2->6->3->4->5->6->∅       # p is unchanged

解开

请注意,remove_elements 的每个实现都是一个普通函数,并且与一个类分离。如果您的程序需要基于类的解决方案,它会成为我们普通函数的简单包装器 -

class solution:
  class remove_elements(self, t, val):
    return remove_elements(t, val)

【讨论】:

    【解决方案2】:

    如果您要使用递归,您可以随时使用它来重新链接元素。这只需要 removeElements 函数本身进行递归:

    def removeElements(self,head,val):
        if not head: return None                           # end of list
        if head.val == val:
            head = self.removeElements(head.next,val)      # remove head
        else:
            head.next = self.removeElements(head.next,val) # keep head 
        return head
    

    【讨论】:

    • 真的很喜欢你的回答。我仍然觉得这个函数很奇怪,因为它改变了输入并且要求调用者将输入重新分配给函数的输出。
    • 要理解我的意思,请考虑s = solution() 和一些列表x = node(1,node(2,node(3,node(1))))。首先print(to_str(x)) 给出1-&gt;2-&gt;3-&gt;1-&gt;∅print(to_str(s.removeElements(x, 1))) 是正确的,2-&gt;3-&gt;∅,但是现在x 是垃圾,当我们重新print(to_str(x)) 时,我们看到1-&gt;2-&gt;3-&gt;∅。这是我的帖子的基础,它说我们必须写 x = removeElements(x, 1) 其中removeElements 用于突变它的返回值。
    • 我理解您的观点,并且我同意最佳做法应避免更改原始列表。但是,如果确实是要改变原始的,则需要返回一个新的头(可能)。所以,如果需要改变原始的,返回新的头部将必须是函数签名的一部分。这就是我对要求的解释,但并没有真正明确地说明。
    【解决方案3】:

    以下是代码中的问题:

    • 当您递归调用checkHead 时,您不会捕获它返回的值,因此递归调用是无用的。它拥有的唯一实用程序在于它返回。所以就像你在removeElements中已经做的那样,你也应该在这里将返回的值分配给head

      head = self.checkhead(head,val)
      
    • elif head.val 是错误的。应该是elif head.val == val。很遗憾你需要这个elif。如果您将if 条件(在其上方)测试head 而不是head.next,则根本不需要elif 案例。

    • while curr 循环不能很好地处理搜索值连续出现两次的情况。在这种情况下,您还将cur 移动到cur.next,下一次迭代将不会查看cur.val,而是cur.next.val。所以cur 的值永远不会被检查。所以...你应该在else 块中执行cur = cur.next

    • 最后的return 是错误的。您不应该返回None,因为while cur 循环可能会在列表中保留一些节点。所以返回head

    通过这些更改,实际上也没有必要进行两个if not head 检查:现在都可以省略。

    以下是更正后的代码:

    class Solution:
        def checkHead(self, head, val):
            # Check that head is not None, instead of checking head.next
            if head and head.val == val:
                # Should use the value returned by recursive call
                head = self.checkHead(head.next, val)
            return head
    
        def removeElements(self, head: ListNode, val: int) -> ListNode:
            head = self.checkHead(head, val)
            curr = head 
            while curr and curr.next:
                if curr.next.val == val:
                    curr.next = curr.next.next
                else: # Only move to next when no match
                    curr = curr.next
            return head  # Should return head, not None
    

    这会奏效。尽管如此,看到混合使用递归和迭代代码还是有点奇怪,这也是为什么除了removeElements 之外还需要一个单独的方法的原因。你实际上可以用递归来做所有事情:

    class Solution:
        def removeElements(self, head: ListNode, val: int) -> ListNode:
            if head:
                if head.val == val:
                    return self.removeElements(head.next, val)
                else:
                    head.next = self.removeElements(head.next, val)
                    return head
    

    【讨论】:

    • 查看我对 Alain 回答的评论。这个实现实际上是相同的并且具有相同的“问题”。该函数改变(破坏)输入并要求调用者使用重新分配。
    • 确实,OP 对此并不清楚,但我认识到 leetcode 挑战,我大胆假设 OP 正在查看。在这种情况下,节点的突变很好,正如 OP 自己的代码所假设的那样。
    • 我想表达的观点很微妙。不是它自己的突变是一个问题,或者它自己的重新分配是一个问题。就是这个接口需要 both 否则会发生奇怪的状态。作为 leetcode 挑战,你是对的,没有什么可以改变的。
    猜你喜欢
    • 1970-01-01
    • 2014-05-02
    • 1970-01-01
    • 2021-12-20
    • 2021-05-14
    • 1970-01-01
    • 2014-12-08
    • 2014-08-17
    • 1970-01-01
    相关资源
    最近更新 更多