0

For the piece of code below.
I want to remove element which is equals to 3 from an array. But the code only removed the first element. When I debug the code,
I found that the iterator only loops though the array one time instead of two.
I am not sure what causes the problem. Any help will be appreciated.

nums = [3,3]
def remove_element(nums, val)
    nums.each_with_index do |num,index|
        if num == val
            nums.slice!(index)
        end
    end
    nums.length
end

remove_element(nums,3)
5
  • 6
    Don’t ever try to modify an array while iterating it. Enumerators got crazy. Commented Nov 9, 2016 at 7:52
  • 2
    Don't modify the same array you are iterating. Commented Nov 9, 2016 at 7:52
  • @mudasobwa Thanks, could you let me know the reason Commented Nov 9, 2016 at 8:08
  • This has been asked and answered many times before here on this site. The first iteration is at index 0. You delete the element. Now, the element that used to be at index 1 is at index 0, the element that used to be at index 2 is at index 1, and so on. The next iteration is at index 1, which is the element that used to be at index 2. See? The element that used to be at index 1 got skipped. In your case, the whole thing already stops after 1 iteration, because after the first iteration, the size of the array is 1, so there is no index 1 which could be visited in the second iteration. Commented Nov 9, 2016 at 8:37
  • @JörgWMittag Very helpful Commented Nov 9, 2016 at 8:46

3 Answers 3

3

As rightly commented, you have modified the array while iterating it, due to which it removed the first element, and concluded the iteration. If you put a print statment inside nums.each_with_index loop, you will see it gets printed only once.

A better way to remove an element can be using reject method like following:

nums.reject!{|item| item == 3}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, could you please let me know why iterator stops after deleting the first element
@MarcoSong, I think internally the iterator keep looking for next item to iterate, when you removed the first element, and iterator is on second element but it does not find anything next and returns. check here: apidock.com/ruby/Enumerator
3

As @steenslag has pointed out, the delete method will do what you want:

n = [1,2,3,3,4,5,6,3,4,5,3,2,1,8]
n.delete(3)
n

returns: [1, 2, 4, 5, 6, 4, 5, 2, 1, 8]

It's worth looking at this alternative to your code:

nums = [3,3]
def remove_element(nums, val)
    nums.each_with_index do |num,index|
        nums_before_slice = nums.clone
        if num == val
            sliced = nums.slice!(index)
        end
        puts "nums: #{nums_before_slice}, index: #{index}, sliced: #{sliced.inspect}"
    end
end

remove_element(nums,3)

puts "Result: #{nums.inspect}"

The output will be:

nums: [3, 3], index: 0, sliced: 3
Result: [3]

As you can see, the iteration only happens once, because the second element has been removed before it is time to do the second iteration.

Compare that result to this version of the code:

nums = [3,3]
def remove_element(nums, val)
    nums.clone.each_with_index do |num,index|
        nums_before_slice = nums.clone
        if num == val
            sliced = nums.slice!(index)
        end
        puts "nums: #{nums_before_slice}, index: #{index}, sliced: #{sliced.inspect}"
    end
end

remove_element(nums,3)

puts "Result: #{nums.inspect}"

Which results in:

nums: [3, 3], index: 0, sliced: 3
nums: [3], index: 1, sliced: nil
Result: [3]

This now runs the iteration over a copy of the original nums, but the result is the same, as on the second iteration - there is no second element to be removed.

3 Comments

Thanks for your help. Very easy to understand.
n.delete(3) .
@steenslag Yes - delete is another (and probably better) option.
2

What about method delete ?

nums = [3,3]
def remove_element(nums, val)
    nums.delete(val)
    nums.length
end
remove_element(nums, 3)
#=> 0

or delete_if ?

nums = [3,3]
def remove_element(nums, val)
    nums.delete_if { |element| element == val }
    nums.length
end
remove_element(nums, 3)
#=> 0

UPD

require 'benchmark'

array = Array.new(100000) { rand(5) }

Benchmark.bm do |x|
  x.report("delete: ") { array.delete(5) }
  x.report("delete_if: ") { array.delete_if { |e| e == 5 } }
  x.report("reject: ") { array.reject! { |e| e == 5 } }
end

#            user       system     total        real
# delete:    0.000000   0.000000   0.000000 (  0.004230)
# delete_if: 0.010000   0.000000   0.010000 (  0.006387)
# reject:    0.010000   0.000000   0.010000 (  0.007543)

3 Comments

Yes, I saw this solution from leetcode. It is really powerful and clean. But the code not runs very fast, I think the way delete method works might not be very efficient
Awesome, not slow at all
@MarcoSong I'm glad :) Next time you can check difference between speed of methods by using example in upd :)

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.