88

The array has lots of data and I need to delete two elements.

Below is the code snippet I am using,

my @array = (1,2,3,4,5,5,6,5,4,9);
my $element_omitted = 5;
@array = grep { $_ != $element_omitted } @array;
Sam
  • 7,252
  • 16
  • 46
  • 65
user21246
  • 1,774
  • 3
  • 15
  • 15

15 Answers15

94

Use splice if you already know the index of the element you want to delete.

Grep works if you are searching.

If you need to do a lot of these, you will get much better performance if you keep your array in sorted order, since you can then do binary search to find the necessary index.

If it makes sense in your context, you may want to consider using a "magic value" for deleted records, rather then deleting them, to save on data movement -- set deleted elements to undef, for example. Naturally, this has its own issues (if you need to know the number of "live" elements, you need to keep track of it separately, etc), but may be worth the trouble depending on your application.

Edit Actually now that I take a second look -- don't use the grep code above. It would be more efficient to find the index of the element you want to delete, then use splice to delete it (the code you have accumulates all the non-matching results..)

my $index = 0;
$index++ until $arr[$index] eq 'foo';
splice(@arr, $index, 1);

That will delete the first occurrence. Deleting all occurrences is very similar, except you will want to get all indexes in one pass:

my @del_indexes = grep { $arr[$_] eq 'foo' } 0..$#arr;

The rest is left as an excercise for the reader -- remember that the array changes as you splice it!

Edit2 John Siracusa correctly pointed out I had a bug in my example.. fixed, sorry about that.

SquareCog
  • 19,421
  • 8
  • 49
  • 63
  • 15
    if the string is not found, the loop will get stuck, so do my $index = 0; my $count = scalar @arr; $index++ until $arr[$index] eq 'foo' or $index==$count; splice(@arr, $index, 1); – Amir.F May 28 '13 at 11:07
  • 2
    or `my ($index) = grep { $arr[$_] eq 'foo' } 0..$#arr; if (defined $index) {splice(@arr, $index, 1); }` - for the first match – Reflective Feb 14 '17 at 15:57
15

splice will remove array element(s) by index. Use grep, as in your example, to search and remove.

spoulson
  • 21,335
  • 15
  • 77
  • 102
12

You can simply do this:

my $input_Color = 'Green';
my @array = qw(Red Blue Green Yellow Black);
@array = grep {!/$input_Color/} @array;
print "@array";
Chetan
  • 1,217
  • 2
  • 13
  • 27
8

Is this something you are going to be doing a lot? If so, you may want to consider a different data structure. Grep is going to search the entire array every time and for a large array could be quite costly. If speed is an issue then you may want to consider using a Hash instead.

In your example, the key would be the number and the value would be the count of elements of that number.

tvanfosson
  • 524,688
  • 99
  • 697
  • 795
5

if you change

my @del_indexes = grep { $arr[$_] eq 'foo' } 0..$#arr;

to

my @del_indexes = reverse(grep { $arr[$_] eq 'foo' } 0..$#arr);

This avoids the array renumbering issue by removing elements from the back of the array first. Putting a splice() in a foreach loop cleans up @arr. Relatively simple and readable...

foreach $item (@del_indexes) {
   splice (@arr,$item,1);
}
dean
  • 51
  • 1
  • 1
5

You could use array slicing instead of splicing. Grep to return the indices you want keep and use slicing:

my @arr = ...;
# run through each item.
my @indicesToKeep = grep { $arr[$_] ne 'foo' } 0..$#arr;
@arr = @arr[@indicesToKeep];
Sean
  • 645
  • 1
  • 6
  • 21
oryan_dunn
  • 198
  • 1
  • 8
  • I particularly like the logic and elegance of this approach. – Keve Jan 03 '18 at 21:22
  • Yes indeed, you can even write it as a one-liner like: `@arr = @arr[grep ...]` which I particularly like. I'm not sure how efficient it is, but I'm gonna start using it because it can't be worse than the other solutions. – soger Dec 19 '19 at 14:54
4

The best I found was a combination of "undef" and "grep":

foreach $index ( @list_of_indexes_to_be_skiped ) {
      undef($array[$index]);
}
@array = grep { defined($_) } @array;

That does the trick! Federico

Federico
  • 41
  • 3
3

I think your solution is the simplest and most maintainable.

The rest of the post documents the difficulty of turning tests on elements into splice offsets. Thus, making it a more complete answer.

Look at the gyrations you have to go through to have an efficient (i.e. one-pass) algorithm to turn tests on list items into indexes. And it's not that intuitive at all.

sub array_remove ( \@& ) { 
    my ( $arr_ref, $test_block ) = @_;
    my $sp_start  = 0;
    my $sp_len    = 0;
    for ( my $inx = 0; $inx <= $#$arr_ref; $inx++ ) {
        local $_ = $arr_ref->[$inx];
        next unless $test_block->( $_ );
        if ( $sp_len > 0 && $inx > $sp_start + $sp_len ) {
            splice( @$arr_ref, $sp_start, $sp_len );
            $inx    = $inx - $sp_len;
            $sp_len = 0;
        }
        $sp_start = $inx if ++$sp_len == 1;
    }
    splice( @$arr_ref, $sp_start, $sp_len ) if $sp_len > 0;
    return;
}
brian d foy
  • 129,424
  • 31
  • 207
  • 592
Axeman
  • 29,660
  • 2
  • 47
  • 102
2

Delete all occurrences of 'something' if array.

Based on SquareCog answer's:

my @arr = ('1','2','3','4','3','2', '3','4','3');
my @dix = grep { $arr[$_] eq '4' } 0..$#arr;
my $o = 0;
for (@dix) {
    splice(@arr, $_-$o, 1);
    $o++;
}
print join("\n", @arr);

Each time we remove index from @arr, the next correct index to delete will be $_-current_loop_step.

António Almeida
  • 9,620
  • 8
  • 59
  • 66
Tom Lime
  • 1,154
  • 11
  • 15
2

You can use the non-capturing group and a pipe delim list of items to remove.


perl -le '@ar=(1 .. 20);@x=(8,10,3,17);$x=join("|",@x);@ar=grep{!/^(?:$x)$/o} @ar;print "@ar"'
Rich
  • 183
  • 5
1

I use:

delete $array[$index];

Perldoc delete.

António Almeida
  • 9,620
  • 8
  • 59
  • 66
Ariel Monaco
  • 3,695
  • 1
  • 23
  • 21
  • 10
    *delete* on array value is likely to be deprecated (see your doc) – Déjà vu Feb 23 '13 at 15:52
  • 3
    this just deletes the value stored at that array index. at least in my version of perl, (5.14) – Rooster Jul 02 '14 at 15:21
  • This does NOT really delete what you think. It only deletes the value, making it `undef`. Besides, from the doc linked by ringø : "WARNING: Calling delete on array values is strongly discouraged. The notion of deleting or checking the existence of Perl array elements is not conceptually coherent, and can lead to surprising behavior." (the previous paragraph in the doc has all the gory details). – mivk Sep 11 '17 at 17:20
  • my original answer is from 10 years ago – Ariel Monaco Aug 23 '23 at 01:31
1

Just to be sure I have benchmarked grep and map solutions, first searching for indexes of matched elements (those to remove) and then directly removing the elements by grep without searching for the indexes. I appears that the first solution proposed by Sam when asking his question was already the fastest.

    use Benchmark;
    my @A=qw(A B C A D E A F G H A I J K L A M N);
    my @M1; my @G; my @M2;
    my @Ashrunk;
    timethese( 1000000, {
      'map1' => sub {
          my $i=0;
          @M1 = map { $i++; $_ eq 'A' ? $i-1 : ();} @A;
      },
      'map2' => sub {
          my $i=0;
          @M2 = map { $A[$_] eq 'A' ? $_ : () ;} 0..$#A;
      },
      'grep' => sub {
          @G = grep { $A[$_] eq 'A' } 0..$#A;
      },
      'grem' => sub {
          @Ashrunk = grep { $_ ne 'A' } @A;
      },
    });

The result is:

Benchmark: timing 1000000 iterations of grem, grep, map1, map2...
  grem:  4 wallclock secs ( 3.37 usr +  0.00 sys =  3.37 CPU) @ 296823.98/s (n=1000000)
  grep:  3 wallclock secs ( 2.95 usr +  0.00 sys =  2.95 CPU) @ 339213.03/s (n=1000000)
  map1:  4 wallclock secs ( 4.01 usr +  0.00 sys =  4.01 CPU) @ 249438.76/s (n=1000000)
  map2:  2 wallclock secs ( 3.67 usr +  0.00 sys =  3.67 CPU) @ 272702.48/s (n=1000000)
M1 = 0 3 6 10 15
M2 = 0 3 6 10 15
G = 0 3 6 10 15
Ashrunk = B C D E F G H I J K L M N

As shown by elapsed times, it's useless to try to implement a remove function using either grep or map defined indexes. Just grep-remove directly.

Before testing I was thinking "map1" would be the most efficient... I should more often rely on Benchmark I guess. ;-)

0

A similar code I once wrote to remove strings not starting with SB.1 from an array of strings

my @adoSymbols=('SB.1000','RT.10000','PC.10000');
##Remove items from an array from backward
for(my $i=$#adoSymbols;$i>=0;$i--) {  
    unless ($adoSymbols[$i] =~ m/^SB\.1/) {splice(@adoSymbols,$i,1);}
}
BBT
  • 1
  • 2
0

If you know the array index, you can delete() it. The difference between splice() and delete() is that delete() does not renumber the remaining elements of the array.

Powerlord
  • 87,612
  • 17
  • 125
  • 175
0

This works well too:

my @array = (1,2,3,4,5,5,6,5,4,9);
my $element_omitted = 5;
for( my $i = 0; $i < scalar( @array ); $i++ )
{
    splice( @array, $i ), $i-- if( $array[$i] == $element_omitted );
}
say "@array"; # 1 2 3 4 6 4 9
Jacques
  • 991
  • 1
  • 12
  • 15