【问题标题】:Counting elements in a list that only occur once: how do I optimise my performance? It's very slow计算列表中仅出现一次的元素:如何优化性能?很慢
【发布时间】:2020-02-16 16:12:07
【问题描述】:

我有一个包含用户名的大列表(大约 60,000 个字符串)。每个用户名代表一个提交。有些用户只提交了一次,即他们是“一次性用户”,因此他们的用户名在此列表中只出现一次。其他人已经多次提交(回访用户),因此他们的用户名可以在此列表中多次出现。我想计算有多少这些一次性用户,并据此获得一些统计数据。以下是我目前正在获取的变量:

import time

start_time = time.time()

users = ["UserA", "UserB", "UserC", "UserA", "UserA", "UserA", "UserB", "UserB", "UserD"] # ...just a sample, this goes up to ~60,000 elements
print(f"1: got users list. Time elapsed: {time.time() - start_time}")

one_time_users = [user for user in users if users.count(user) == 1]
print(f"2: got one-time users list. Time elapsed: {time.time() - start_time}")

returning_users = [user for user in users if users.count(user) != 1]
print(f"3: got returning users list. Time elapsed: {time.time() - start_time}")

frequencies = [users.count(user) for user in set(users)]
print(f"4: calculated frequencies list. Time elapsed: {time.time() - start_time}")

sorted_frequencies = sorted(frequencies, reverse=True) # Descending order, largest first
print(f"5: got sorted frequencies list. Time elapsed: {time.time() - start_time}")

top_ten_frequencies_sum = sum(sorted_frequencies[:10])
print(f"6: got top 10 frequencies sum. Time elapsed: {time.time() - start_time}")

top_ten_frequencies_percentage = round(((top_ten_frequencies_sum / len(users)) * 100), 2)
print(f"7: got top 10 frequencies percentage. Time elapsed: {time.time() - start_time}")

average_submissions_per_user = round(len(users) / len(set(users)), 2)
print(f"8: got average submissions per user. Time elapsed: {time.time() - start_time}")

这个操作非常慢。这是我的输出:

1: got users list. Time elapsed: 0.41695237159729004
2: got one-time users list. Time elapsed: 48.26731848716736
3: got returning users list. Time elapsed: 101.88410639762878
4: calculated frequencies list. Time elapsed: 104.39784860610962
5: got sorted frequencies list. Time elapsed: 104.39850783348083
6: got top 10 frequencies sum. Time elapsed: 104.39853930473328
7: got top 10 frequencies percentage. Time elapsed: 104.39856457710266
8: got average submissions per user. Time elapsed: 104.4005241394043

如您所见,列表推导式花费的时间最多。谁能给我解释一下:

  1. 为什么它的时间复杂度如此之慢。
  2. collections.Counter() 是否会是更好的选择以及如何最好地在此处应用它。

谢谢!

【问题讨论】:

  • 在循环中使用.count 是一种二次时间算法。是的,collections.Counter 方法明显更快,它需要线性时间。

标签: python python-3.x list time-complexity list-comprehension


【解决方案1】:

您可以通过使用Counter 来改进,在2. 中为您迭代整个列表的每个元素,如果一个用户出现多次,您可以为同一个用户多次执行此操作。
请注意,当您使用 users.count(user) 时,您会迭代所有用户列表以计算用户出现的次数。这意味着关于列表长度的二次复杂度。
使用计数器,您可以解决线性复杂度问题。
此外,在 4. 中,您正在迭代和再次计数,而您可以从整个用户中删除刚刚计算出的那些。
示例。

>>> one_time_users = {user for user,cnt in Counter(users).items() if cnt == 1}
{'UserC', 'UserD'}
>>> returning_users = set(users) - one_time_users
>>> returning_users
{'UserB', 'UserA'}

或者更直接

one_time_users, returning_users  = [], []
for user,cnt in Counter(users).items():
   if cnt==1:
      one_time_users.append(user)
   else:
      returning_users.append(user)

这里是l.count(el)Counter(l) 的比较。

>>> l = random.choices(range(500), k=60000)
>>> timeit.timeit('[el for el in l if l.count(el) == 1]',setup='from __main__ import l',number=1)
71.70409394335002
>>> timeit.timeit('[el for el,cnt in Counter(l).items() if cnt == 1]',setup='from __main__ import l, Counter',number=1)
0.005492186639457941

【讨论】:

    【解决方案2】:

    正如您在自己的评论中提到的,Counter 在这里明显更快。从你自己的时间可以看出,创建一组结果大约需要 10 毫秒才能完成(#8->#9),这也大致是 Counter 所需的时间。

    使用计数器,您可以查看 N 个元素中的每一个,然后查看每个唯一元素(最多 N 个)一次。

    当您使用.count() 时,您会遍历整个列表(一个快速实现,但仍然是整个列表)。您对每个元素都执行此操作,因此您查看了 N 个元素中的每一个 N 次。

    每当您的列表变大 1000 倍时,Counter 方法需要 1000 倍的时间,而 .count 版本则需要 1000000 倍。

    【讨论】:

    • 是的!在我的原始示例中,有 10 对 100,但它并没有以我想要的方式传达二次增长
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-10-25
    • 2020-06-09
    • 1970-01-01
    • 1970-01-01
    • 2011-08-10
    • 1970-01-01
    相关资源
    最近更新 更多