在Python中将项插入到不区分大小写的排序列表中

时间:2017-01-27 21:03:13

标签: python python-2.7 sorting case-insensitive

我有一个已经按不区分大小写的顺序排序的字符串列表。我想在列表中插入一个新字符串。一种方法是附加项目,然后对列表进行排序,如下所示:

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

但是我想知道是否有办法将项目插入正确的位置而不再对整个事物进行排序。

我发现了这个问题:Insert an item into a sorted list in Python。它指向Python的bisect模块。但该模块看起来不支持不区分大小写。

修改:我测试了这里列出的几个答案。

  • 将项目追加到最后并对整个列表进行排序(如原始问题中所建议的那样)是最慢的。
  • Moinuddin Quadri的答案比排序整个列表更快,但由于在列表上的每个项目上运行lower(),它仍然很慢。
  • Stefan Pochmann的答案比整个清单排序快了一个数量级。
  • Jared Goguen的回答是反复插入最快的答案。但是,它第一次在每个元素上运行lower()

接听答案是一个很接近的电话。最后,我选择了Stefan Pochmann的答案,因为它是一次性插入的最佳选择,访问结果列表不需要访问成员变量。但是,用例会有所不同,因此请务必检查所有答案。

6 个答案:

答案 0 :(得分:3)

您可以在下层排序列表中使用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']

答案 1 :(得分:3)

您可以创建自己的类型来封装此行为(与另一个答案中建议的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

答案 2 :(得分:2)

这是再次练习二进制搜索的好机会(或者只是复制并粘贴和修改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),就像追加+排序一样,但仅仅是因为最后的a.insert(lo, x)。这很简单,用C语言完成,所以速度非常快。二进制搜索当然只需要O(log n)步,因此也非常快。 append + sort方法会在所有元素上调用.lower()并进行比较,两者都要慢得多。由于在所有元素上调用.lower(),@ MoinuddinQuadri的第一个解决方案也慢得多。

请参阅我的其他答案进行基准比较。

答案 3 :(得分:1)

我从未使用过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']

答案 4 :(得分:1)

基于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']

这里我们将低位字词列表二等分以找到索引,并根据索引将字符串插入原始列表。

答案 5 :(得分:1)

我看到你在测试中添加了测试结果。我现在做了一些基准测试并得到了类似的图片:

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)时间变得可以忽略不计,并且时间由insert调用的O(n)时间支配。我的解决方案只有其中一个,而另一个解决方案有两个。

另一个区别是空间复杂性,保留所有字符串的低级版本的额外列表并不是很好。

这是我的基准代码:

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