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

у меня есть список строк, которые уже отсортированы в регистр порядка. Я хотел бы вставить новую строку в список. Один из способов сделать это-добавить элемент, а затем отсортировать список, вот так:

myList.append('Something')
myList.sort(key=lambda s: s.lower())

но мне было интересно, есть ли способ просто вставить элемент в правильное положение, не сортируя все это снова.

Я нашел этот вопрос: вставьте элемент в отсортированном списке в Python. Он указывает на Python bisect модуль. Но этот модуль не выглядит так, как будто он может поддерживать нечувствительность к регистру.


редактировать: я проверил несколько ответов, перечисленных здесь.

  • добавление элемента в конец и сортировка всего списка (как предложено в исходном вопросе) была самой медленной.
  • ответ Моинуддина Квадри был быстрее, чем сортировка всего списка, но он все еще был довольно медленным, из-за запуска lower() по каждому пункту список.
  • Стефан Pochmann был на порядок быстрее, чем сортировка всего списка.
  • ответ Джареда Гогена был самым быстрым для повторных вставок. В первый раз, однако, он работает lower() на каждый элемент.

Я был близок к тому, чтобы принять ответ. В конце концов, я пошел с ответом Стефана Почманна, потому что он был лучшим для одноразовой вставки, и доступ к результирующему списку не требует доступа к члену переменная. Однако варианты использования различаются, поэтому обязательно изучите все ответы.

6 ответов


это хорошая возможность снова практиковать двоичный поиск (или просто копировать и вставлять и изменять bisect.insort, что я и сделала):

def insort_case_insensitive(a, x):
    key = x.lower()
    lo, hi = 0, len(myList)
    while lo < hi:
        mid = (lo + hi) // 2
        if key < a[mid].lower():
            hi = mid
        else:
            lo = mid + 1
    a.insert(lo, x)

демо:

myList = ['a', 'b', 'c', 'd', 'e']
for x in 'A', 'B', 'C', 'D', 'E':
    insort_case_insensitive(myList, x)
print myList

принты:

['a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E']

Это O (n), как и append+sort, но только из-за a.insert(lo, x) в конце. Что мертво-просто и сделано в C, так что это супер быстро. Двоичный поиск, конечно, принимает только шаги O(log n), так что это очень быстро. Добавление + сортировка способ вызовет .lower() на все элементы и сравнить их, оба из которых гораздо медленнее. Первое решение @MoinuddinQuadri также намного медленнее из-за вызова .lower() по всем элементам.

см. мой другой ответ Для сравнения бенчмаркинга.


вы можете использовать bisect.bisect в нижнем регистре отсортированного списка как:

from bisect import bisect
my_list = ["aa", "bb", "Dd", "ee"]
insert_string = "CC"

#                 convert all the items in list to lower case for
#               v finding the correct location via. bisect
index = bisect([i.lower() for i in my_list], insert_string.lower())
#                bisect based on lower-cased string for  ^
#                case-insensitive behavior

my_list.insert(index, insert_string)

где обновленное содержание my_list будет:

['aa', 'bb', 'CC', 'Dd', 'ee']

вы можете создать свой собственный тип для инкапсуляции этого поведения (в сочетании с bisect как предложено в другом ответе).

from bisect import bisect

class CaseInsensitiveSortedList:
    def __init__(self, iterable):
        self.with_case = list(sorted(iterable, key=lambda s: s.lower()))
        self.without_case = [s.lower() for s in self.with_case]

    def insert_in_order(self, s):
        s_lower = s.lower()
        index = bisect(self.without_case, s_lower)
        self.without_case.insert(index, s_lower)
        self.with_case.insert(index, s)


test_list = CaseInsensitiveSortedList(['a', 'B', 'cd', 'E', 'fff'])

test_list.insert_in_order('D')
print(test_list.with_case) # ['a', 'B', 'cd', 'D', 'E', 'fff']

test_list.insert_in_order('ee')
print(test_list.with_case) # ['a', 'B', 'cd', 'D', 'E', 'ee', 'fff']

вы могли бы расширить list непосредственно и сделать это немного "более естественным" или сделать все, что вы хотите с ним. Это просто идея, чтобы избежать вызова str.lower на каждом элементе для каждой вставки.


Я никогда не работал с bisect, но вот мой удар по нему. Первая функция, которую я беру непосредственно из bisect страница, которую вы перешли по ссылке:

def index(a, x):
    'Locate the leftmost value exactly equal to x'
    i = bisect_left(a, x)
    if i != len(a) and a[i] == x:
        return i
    raise ValueError

def insert_into_sorted(char, my_list):
    marker = chr(ord(char) + 1)
    spot = index(my_list, marker)
    my_list[spot:spot] = char
    return my_list

x = ['a', 'b', 'd', 'e']

insert_into_sorted('c', x)

>>['a', 'b', 'c', 'd', 'e']

на основе комментария от OP, так как это нормально, чтобы поддерживать промежуточный список для удержания строк в нижнем регистре (один раз); это может быть достигнуто как:

from bisect import bisect
my_list = ["aa", "bb", "Dd", "ee"]

lower_list = [i.lower() for i in my_list]  # list of lower-cased strings.
                                           # one time operation
insert_string = "CC"  # word to insert

# find index based on lower-cased list
index = bisect(lower_list, insert_string.lower())

my_list.insert(index, insert_string)  # insert word in original list
lower_list.insert(index, insert_string.lower())   # insert lower-cased word
                                                  # in lower-cased list

где конечное значение my_list будет lower_list будет:

>>> my_list   # original list
['aa', 'bb', 'CC', 'Dd', 'ee']

>>> lower_list
['aa', 'bb', 'cc', 'dd', 'ee']

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


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

Insorting 20000 words:
 80.224 seconds with insort_sorting
  0.166 seconds with insort_own_binary_search
 70.294 seconds with insort_lower_all
  0.094 seconds with insort_keep_lower

вы несколько ошибаетесь насчет быстрых двух, хотя. При большем количестве вставок моя становится быстрее. Примерно в два раза быстрее:

Insorting 1000000 words:
 92.712 seconds with insort_own_binary_search
173.577 seconds with insort_keep_lower

это потому, что время o(log n) для поиска индекса становится незначительным, и время доминирует время O (n) для insert звонки. И мое решение имеет только один из них, а другой решение имеет два.

другое отличие-сложность пространства, сохранение дополнительного списка пониженных версий всех строк не так хорошо.

вот мой код бенчмаркинга:

import random, string, time

#--------------------------------------------------------------
def insort_sorting(a, x):
    a.append(x)
    a.sort(key=str.lower)
#--------------------------------------------------------------
def insort_own_binary_search(a, x):
    key = x.lower()
    lo, hi = 0, len(myList)
    while lo < hi:
        mid = (lo + hi) // 2
        if key < a[mid].lower():
            hi = mid
        else:
            lo = mid + 1
    a.insert(lo, x)
#--------------------------------------------------------------
from bisect import bisect
def insort_lower_all(a, x):
    index = bisect([i.lower() for i in a], x.lower())
    a.insert(index, x)
#--------------------------------------------------------------
from bisect import bisect
def insort_keep_lower(a, x, lower=[]):
    x_lower = x.lower()
    index = bisect(lower, x_lower)
    a.insert(index, x)
    lower.insert(index, x_lower)
#--------------------------------------------------------------

# Generate random words    
words = [''.join(random.choice(string.ascii_letters) for _ in range(10))
         for _ in range(20000)]
#         for _ in range(1000000)]

# Compare the solutions
print 'Insorting', len(words), 'words:'
reference = None
for insort in insort_sorting, insort_own_binary_search, insort_lower_all, insort_keep_lower:
#for insort in insort_own_binary_search, insort_keep_lower:
    t0 = time.time()
    myList = []
    for word in words:
        insort(myList, word)
    print '%7.3f seconds with %s' % (time.time() - t0, insort.__name__)
    if reference is None:
        reference = myList
    else:
        assert myList == reference