0

I'm trying to solve "Remove all elements from a linked list of integers that have value val" question with a helper recursion function but it does not work.

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

My Solution:

# 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

Test case which fails:

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

        

When I return the recursive checkHead function's value as head = self.checkHead(head, val), it states that head is equal to "1" but when I debug it, I can see that program returns from checkHead as None. I wonder that where is the problem.

2
  • 2
    The line “elif head.val and not head.next:” is suspicious. Do you miss a == ? Commented Feb 27, 2021 at 16:24
  • As @Stefan said, probably head.val == val is needed in the elif statement of checkHead. Commented Feb 27, 2021 at 16:45

3 Answers 3

2

fundamental flaw

There is a fundamental flaw attempting to solve this with iteration and mutation. Try as hard as you might, remove_elements can adjust the head and next properties, but it cannot change a ListNode to None -

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

We expect to see None as the result, but this is impossible without at least one of the following -

  1. the function call must change
  2. the data structure for ListNode must change

Presuming you want to maintain the shape of ListNode, we must adjust the function call to -

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

Now using the return value of remove_elements we can catch the scenario where ListNode is demoted to a plain None. Using your first example -

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->∅

And your second example -

q = node(1, node(1))

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

Here is the implementation of a mutable singly linked list -

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 + "∅"

know your origins

This question is tagged with recursion and rightfully so. The linked list is a recursive data structure and choosing a recursive program to process it harmonizes the structure of our program with the structure of our data. However, recursion is a functional heritage and so using it with functional style yields the best results. This means avoiding things like mutation, variable reassignment, and other side effects. Instead of destroying existing values, a new value is created -

  1. If the input list, t, is empty, the base case has been reached. Return the empty list
  2. (inductive) the input is not empty; it has at least one element. If the first element, t.head, matches the value to remove, val, return the result of the sub-problem, t.next
  3. (inductive) the input is not empty and the first element does not match the value to remove. Construct a new list node of t.head and the result of the sub-problem 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

For your first example -

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->∅

Using your other example -

q = node(1, node(1))

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

Here's the implementation of an immutable (persistent) singly linked list -

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)

attention

The immutable implementation does not mutate existing values. That means new values are created -

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

When we write q = remove_elements(p, 6), a new list is created and stored to q and p is unchanged -

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

disentangle

Notice each implementation of remove_elements is a plain function and disentangled from a class. If your program requires a class-based solution, it becomes a simple wrapper around our plain function -

class solution:
  class remove_elements(self, t, val):
    return remove_elements(t, val)
Sign up to request clarification or add additional context in comments.

Comments

1

If you're going to use recursion, you could use it to relink the elements as you go. This would only require the removeElements function itself to recurse:

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

3 Comments

really enjoying your answers. i still find this function very odd as it mutates the input and requires the caller to reassign the input to the function's output.
To understand what I mean, consider s = solution() and some list, x = node(1,node(2,node(3,node(1)))). First print(to_str(x)) gives 1->2->3->1->∅ and print(to_str(s.removeElements(x, 1))) is correct, 2->3->∅, but now x is rubbish when we re-print(to_str(x)), we see 1->2->3->∅. This is the basis of my post which says we must write x = removeElements(x, 1) where removeElements is used for mutation and its return value.
I understand your point and I agree that best practice should avoid mutating the original list. However, if the intent is indeed to mutate the original, there will be a need to return a new head (potentially). So, if the requirement is to mutate the original, returning the new head will have to be part of the function signature. That's how I interpreted the requirement but it wasn't really stated explicitely.
0

Here are the issues in your code:

  • When you call checkHead recursively, you don't capture the value it returns, and so the recursive call is useless. The only utility it has, is in what it returns. So just like you already do correctly in removeElements, you should also here assign the returned value to head:

    head = self.checkhead(head,val)
    
  • elif head.val is wrong. It should be elif head.val == val. It is a pity you need this elif at all. If you would make the if condition (above it) test head instead of head.next, you wouldn't need the elif case at all.

  • The while curr loop does not treat well the case where the searched value occurs twice in a row. In that case you also move cur to cur.next, and the next iteration will not look at cur.val, but at cur.next.val. So the value at cur is never inspected. So... you should do cur = cur.next in an else block.

  • The final return is wrong. You should not return None, since the while cur loop could have kept some nodes in the list. So return head.

With these changes it is also not really necessary to have two if not head checks: both can now be omitted.

Here is the corrected code:

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

This will work. Still, it is a bit strange to see a mix of recursive and iterative code, which is also the reason why you needed a separate method besides removeElements. You can actually do all with recursion:

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

3 Comments

see my comment on Alain's answer. this implementation is effectively the same and has the same "problem". the function mutates (destroys) the input and requires the caller to use reassignment.
Indeed, the OP has not been clear about this, but I recognise a leetcode challenge, which I am boldly assuming the OP is looking at. In that context the mutation of nodes is fine, as also the OP's own code assumes.
the point i am trying to make is subtle. it's not that mutation on it's own is a problem, or that reassignment on it's own is a problem. it's that this interface requires both otherwise weird state happens. as a leetcode challenge, you're right, there's not much that can be changed about it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.