简体   繁体   中英

Python: Find all additive inverse pairs in a list and remove them

I have a few lists that contain additive inverse pairs (adding both pairs give you zero), and for each list I am trying to remove those pairs.

Below are a few examples, where I am trying to go from this:

lst1 = [326, -326, 336, -336, 336]
lst2 = [-35, 35, 838, -838, 440]
lst3 = [544, -544, 544]
lst4 = [-25, 25, 32, -32, -32, 56, 79]

To this:

lst1 = [336]
lst2 = [440]
lst3 = [544]
lst4 = [-32, 56, 79]

You can see that in each list, the additive inverse pairs are removed, leaving the values that have no additive inverse, or the 1 extra non-mirrored value.

I wish I could share what I have tried, but I have been at it for a few days and any logic I come up with falls apart with the example in lst4 .

I seem to have found a solution that yields exactly what I need, however it is written in R which I have no experience in, so I cannot understand the logic.

Any ideas on how I can overcome this?

You can use defaultdict to keep track of additive pairs as well as handle duplicates:

from collections import defaultdict
from itertools import chain
def remove_additive_pairs(sequence: list) -> list:
    """ Remove additive pairs from given sequence. """
    result = defaultdict(list)
    
    for element in sequence:
        if -element in result:
            del result[-element]
        else:
            result[element].append(element)
            
    return list(chain.from_iterable(result.values()))

>>> lst5 = [-25, 25, 32, -25, 25, 32]
>>> for seq in [lst1, lst2, lst3, lst4, lst5]:
        print(remove_additive_pairs(seq))

[336]
[440]
[544]
[-32, 56, 79]
[32, 32]

Of course you can do the same without importing itertools.chain or collections.defaultdict :

def chain(args):
    for lst in args:
        for elem in lst:
            yield elem

            
def remove_additive_pairs(sequence: list) -> list:
    """ Remove additive pairs from given sequence. """
    result = dict()
    
    for element in sequence:
        if -element in result:
            del result[-element]
        else:
            result.setdefault(element, []).append(element)
            
    return list(chain(result.values()))

defaultdict is a dict-like container that can provide default values for missing keys . For example:

>>> d = defaultdict(list)
>>> d[5]
[]

So defining defaultdict(list) ensures that when we get a number whose inverse is not already in the keys, we can add that number as a key to the dictionary, as well as append it to a list in its values in one step.

First, count how many times each value appears in the list. Then iterate through the list, either populating a new list or throwing the value away and decrementing the counter of the inverse value.

from collections import Counter

def remove_inverses(lst):
  value_counts = Counter(lst)
  result = []
  for element in lst:
    if value_counts.get(-element):
      value_counts[-element] -= 1
    else:
      result.append(element)
  return result

You could keep track of a running total of the elements you've seen in a (default)dictionary documentation

A dictionary is a data structure that holds key-value pairs and allows quick lookup. In this instance, the elements of our input list will be our keys, and the values will be a count. You access values in a dictionary like so: counter[key] = value .

A default dictionary simply makes it so that when key is not already a part of the dictionary, it automatically generates a default value for it instead of throwing a KeyError . So we define counter = collections.defaultdict(int) . The int specifies the default factory function for the values of yet-undefined keys.

Every time you see an unpaired number, you increment the counter for that number. Every time you find a number that has a pair in the counter, you decrement the counter for the pair. At the end, the keys of the counter will be the elements of your list, and the values will be the number of unpaired instances of that element were found, so we translate that into a list as required. Note that this will not preserve the order of the elements in the original list.

import collections

def filterInverse(lst):
    counter = collections.defaultdict(int)
    for num in lst:
        inverse = -num
        if counter[inverse] > 0:
            # Inverse was found previously in the counter
            # Decrement counter[inverse]
            counter[inverse] -= 1
            # And do nothing else with this number
        else: # Inverse wasn't found in the counter
            # This is an unpaired number so add it to the counter
            counter[num] += 1
    # Finally, go over the dict and return the keys whose values are nonzero
    # Repeat each item by its count
    # print(counter)
    rv = []
    for num, ct in counter.items():
        for _ in range(ct):
            rv.append(num)
    return rv

Running this with the inputs in your question and that we discussed in the comments,

>>> filterInverse(lst1)
Out: [336]

>>> filterInverse(lst2)
Out: [440]

>>> filterInverse(lst3)
Out: [544]

>>> filterInverse(lst4)
Out: [-32, 56, 79]

>>> lst5 = [-1, 0, 1, -1, -1]
>>> filterInverse(lst5)
Out: [-1, -1, 0]

If you're dead set against using defaultdict , you can get the same behavior by using a regular dict.get(key, default) documentation :

counter = dict()

...
    ...
        if counter.get(inverse, 0) > 0:
            # Inverse was found previously in the counter
            # Decrement counter[inverse]
            counter[inverse] = counter.get(inverse, 0) - 1
            # And do nothing else with this number
        else: # Inverse wasn't found in the counter
            # This is an unpaired number so add it to the counter
            counter[num] = counter.get(num, 0) + 1
    ...
...

Using the awesome collections.Counter :

from collections import Counter
from itertools import repeat

def remove_opposite_pairs(seq):
    dplus = Counter(seq)
    dminus = Counter(-x for x in seq)
    for x,n in (dplus - dminus).items():
        yield from repeat(x, n)

lsts = [[326, -326, 336, -336, 336],
        [-35, 35, 838, -838, 440],
        [544, -544, 544],
        [-25, 25, 32, -32, -32, 56, 79],
        [-1, 0, 1, -1, -1]]

for l in lsts:
    print(list(remove_opposite_pairs(l)))
# [336]
# [440]
# [544]
# [-32, 56, 79]
# [-1, -1]

Note the weird behaviour on 0, because 0 is its own opposite. I could fix it, but first you need to edit your question to specify exactly how to handle 0.

Here is a possible fix:

from collections import Counter
from itertools import repeat

def remove_opposite_pairs(seq):
    dplus = Counter(seq)
    dminus = Counter(-x for x in seq)
    for x,n in (dplus - dminus).items():
        yield from repeat(x, n)
    if dplus[0] % 2 == 1:
        yield 0

lsts = [[-1, 0, 1, -1, -1],
        [0, 0],
        [0, 0, 0]]

for l in lsts:
    print(list(remove_opposite_pairs(l)))
# [-1, -1, 0]
# []
# [0]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM