Python: переименовать дубликаты в списке с прогрессивными числами без сортировки списка

Учитывая такой список:

mylist = ["name", "state", "name", "city", "name", "zip", "zip"]

Я хотел бы переименовать дубликаты, добавив число, чтобы получить следующий результат:

mylist = ["name1", "state", "name2", "city", "name3", "zip1", "zip2"]

Я не хочу менять порядок исходного списка. Решения, предложенные для этого связанного вопроса, сортируют список, который я не хочу делать.

Ответы

Ответ 1

Вот как бы я это сделал. РЕДАКТИРОВАТЬ: Я написал это в более обобщенную функцию полезности, так как людям, кажется, нравится этот ответ.

mylist = ["name", "state", "name", "city", "name", "zip", "zip"]
check = ["name1", "state", "name2", "city", "name3", "zip1", "zip2"]
copy = mylist[:]  # so we will only mutate the copy in case of failure

from collections import Counter # Counter counts the number of occurrences of each item
from itertools import tee, count

def uniquify(seq, suffs = count(1)):
    """Make all the items unique by adding a suffix (1, 2, etc).

    'seq' is mutable sequence of strings.
    'suffs' is an optional alternative suffix iterable.
    """
    not_unique = [k for k,v in Counter(seq).items() if v>1] # so we have: ['name', 'zip']
    # suffix generator dict - e.g., {'name': <my_gen>, 'zip': <my_gen>}
    suff_gens = dict(zip(not_unique, tee(suffs, len(not_unique))))  
    for idx,s in enumerate(seq):
        try:
            suffix = str(next(suff_gens[s]))
        except KeyError:
            # s was unique
            continue
        else:
            seq[idx] += suffix

uniquify(copy)
assert copy==check  # raise an error if we failed
mylist = copy  # success

Если вы хотите добавить подчеркивание перед каждым подсчетом, вы можете сделать что-то вроде этого:

>>> mylist = ["name", "state", "name", "city", "name", "zip", "zip"]
>>> uniquify(mylist, (f'_{x!s}' for x in range(1, 100)))
>>> mylist
['name_1', 'state', 'name_2', 'city', 'name_3', 'zip_1', 'zip_2']

... или если вы хотите использовать вместо этого буквы:

>>> mylist = ["name", "state", "name", "city", "name", "zip", "zip"]
>>> import string
>>> uniquify(mylist, (f'_{x!s}' for x in string.ascii_lowercase))
>>> mylist
['name_a', 'state', 'name_b', 'city', 'name_c', 'zip_a', 'zip_b']

ПРИМЕЧАНИЕ: это не самый быстрый алгоритм; для этого обратитесь к ответу по ronakg. Преимущество функции выше в том, что ее легко понять и прочитать, и вы не увидите большой разницы в производительности, если у вас нет чрезвычайно большого списка.

РЕДАКТИРОВАТЬ: Вот мой оригинальный ответ в одну строку, однако порядок не сохраняется, и он использует метод .index, который является чрезвычайно неоптимальным (как объяснено в ответе DTing). Посмотрите ответ queezz для хорошего " двухслойного ", который сохраняет порядок.

[s + str(suffix) if num>1 else s for s,num in Counter(mylist).items() for suffix in range(1, num+1)]
# Produces: ['zip1', 'zip2', 'city', 'state', 'name1', 'name2', 'name3']

Ответ 2

Мое решение с map и lambda:

print map(lambda x: x[1] + str(mylist[:x[0]].count(x[1]) + 1) if mylist.count(x[1]) > 1 else x[1], enumerate(mylist))

Более традиционная форма

newlist = []
for i, v in enumerate(mylist):
    totalcount = mylist.count(v)
    count = mylist[:i].count(v)
    newlist.append(v + str(count + 1) if totalcount > 1 else v)

И последний

[v + str(mylist[:i].count(v) + 1) if mylist.count(v) > 1 else v for i, v in enumerate(mylist)]

Ответ 3

Любой метод, в котором count вызывается для каждого элемента, приведет к O(n^2) поскольку count равно O(n). Вы можете сделать что-то вроде этого:

# not modifying original list
from collections import Counter

mylist = ["name", "state", "name", "city", "name", "zip", "zip"]
counts = {k:v for k,v in Counter(mylist).items() if v > 1}
newlist = mylist[:]

for i in reversed(range(len(mylist))):
    item = mylist[i]
    if item in counts and counts[item]:
        newlist[i] += str(counts[item])
        counts[item]-=1
print(newlist)

# ['name1', 'state', 'name2', 'city', 'name3', 'zip1', 'zip2']

# modifying original list
from collections import Counter

mylist = ["name", "state", "name", "city", "name", "zip", "zip"]
counts = {k:v for k,v in Counter(mylist).items() if v > 1}      

for i in reversed(range(len(mylist))):
    item = mylist[i]
    if item in counts and counts[item]:
        mylist[i] += str(counts[item])
        counts[item]-=1
print(mylist)

# ['name1', 'state', 'name2', 'city', 'name3', 'zip1', 'zip2']

Это должно быть O(n).

Другие предоставленные ответы:

mylist.index(s) на элемент вызывает O(n^2)

mylist = ["name", "state", "name", "city", "name", "zip", "zip"]

from collections import Counter
counts = Counter(mylist)
for s,num in counts.items():
    if num > 1:
        for suffix in range(1, num + 1):
            mylist[mylist.index(s)] = s + str(suffix) 

count(x[1]) на элемент вызывает O(n^2)
Он также используется несколько раз для каждого элемента вместе со срезкой списка.

print map(lambda x: x[1] + str(mylist[:x[0]].count(x[1]) + 1) if mylist.count(x[1]) > 1 else x[1], enumerate(mylist))

тесты:

http://nbviewer.ipython.org/gist/dting/c28fb161de7b6287491b

Ответ 4

Здесь очень простое решение O(n). Просто перейдите в список, в котором хранится индекс элемента в списке. Если мы уже видели этот элемент, используйте ранее сохраненные данные, чтобы добавить значение вхождения.

Этот подход решает проблему, просто создав еще один словарь для обратного просмотра. Избегает выполнения внешнего вида, чтобы мы не создавали временные фрагменты списка.

mylist = ["name", "state", "name", "city", "city", "name", "zip", "zip", "name"]

dups = {}

for i, val in enumerate(mylist):
    if val not in dups:
        # Store index of first occurrence and occurrence value
        dups[val] = [i, 1]
    else:
        # Special case for first occurrence
        if dups[val][1] == 1:
            mylist[dups[val][0]] += str(dups[val][1])

        # Increment occurrence value, index value doesn't matter anymore
        dups[val][1] += 1

        # Use stored occurrence value
        mylist[i] += str(dups[val][1])

print mylist

# ['name1', 'state', 'name2', 'city1', 'city2', 'name3', 'zip1', 'zip2', 'name4']

Ответ 5

Вы можете использовать хеш-таблицу для решения этой проблемы. Определить словарь d. key - это строка и значение (first_time_index_in_the_list, times_of_appearance). Каждый раз, когда вы видите слово, просто проверяйте словарь, а если значение равно 2, используйте first_time_index_in_the_list, чтобы добавить "1" к первому элементу и добавьте time_of_appearance к текущему элементу. Если больше 2, просто добавьте time_of_appearance к текущему элементу.

Ответ 6

Меньше фантазии.

from collections import defaultdict
mylist = ["name", "state", "name", "city", "name", "zip", "zip"]
finalList = []
dictCount = defaultdict(int)
anotherDict = defaultdict(int)
for t in mylist:
   anotherDict[t] += 1
for m in mylist:
   dictCount[m] += 1
   if anotherDict[m] > 1:
       finalList.append(str(m)+str(dictCount[m]))
   else:
       finalList.append(m)
print finalList

Ответ 7

Версия для понимания списка ответа Рика Тичи "двухстрочный":

from collections import Counter

m = ["name", "state", "name", "city", "name", "zip", "zip"]

d = {a:list(range(1, b+1)) if b>1 else '' for a,b in Counter(m).items()}
[i+str(d[i].pop(0)) if len(d[i]) else i for i in m]
#['name1', 'state', 'name2', 'city', 'name3', 'zip1', 'zip2']