【问题标题】:Efficient algorithm to randomly find available places inside a list in Python在 Python 中随机查找列表中可用位置的有效算法
【发布时间】:2018-06-28 19:09:25
【问题描述】:

我需要随机将列表中的位置分配给输入。我需要先检查它是否没有被占用,然后再使用它。我能想到的最佳算法如下:

def get_random_addr(input_arr):

    while True:
        addr = random.randrange(1, len(input_arr))
        if input_arr[addr] is None:
            break
    return addr

这显然效率不高,因为当我们占用更多的槽时,循环需要更长的时间才能找到一个空槽,甚至可能需要很长时间(假设只剩下一个空槽)。你有更好的解决方案吗?

我是怎么做到的

根据选择的答案,这就是我最终这样做的方式。与搜索整个列表并找到None 元素并从检索到的集合中随机选择的解决方案相比,它非常快速和高效。我认为瓶颈是random.choice 方法,它似乎很慢。

# Create a list of indexes at the beginning when all the values are None 
available_index = list(range(1, len(input_arr)))
random.shuffle(available_index)

# To get a random index simply pop from shuffled available index
random_index = available_index.pop()

虽然这种方法有额外的 O(n) 内存复杂度,但在实践中它非常高效且快速。

【问题讨论】:

  • 如果您真的想高效地执行此操作,请使用numpy。否则,通过循环一次,保留所有 None 索引的列表,然后从该列表中弹出并分配您的值
  • 在引擎盖下 numpy 仍然是 O(N) 它只是一个更快的 O(N)

标签: python python-3.x algorithm python-2.7


【解决方案1】:

如果您不能使用 numpy,我会保留一组已知包含 None 的索引。每次添加或删除None 时,这组索引都会更新

【讨论】:

  • 这是一个很好的答案(+1) ...但我不认为 numpy 可以为他解决这个问题 ...我们认为 numpy 向量运算为 O(1) 但 A[A==None] 仍然是 @ 987654324@ 在引擎盖下......虽然我认为一组已知的空是正确的答案
  • @JoranBeasley numpy 确实是O(N)(它仍然没有使用魔法:))但是numpy 的引擎盖下循环更快(有时在几个数量级)
【解决方案2】:

您的函数可能需要任意长的时间才能返回。特别是,如果没有项目是None,您将进入无限循环。

相反,恢复所有None 的索引并使用random.choices 随机返回其中的k

import random

def get_random_addr(input_arr, k=1, target=None):
    return random.choices([i for i, v in enumerate(input_arr) if v is target], k=k)

用法

l = [0, None, 2, 3, None, None]

for i in get_random_addr(l, k=2):
    l[i] = i

print(l) # [0, None, 2, 3, 4, 5]

【讨论】:

  • 从长远来看,这与 OP 的代码一样低效
  • none_index 应该只计算一次,而不是每次调用
  • @DeepSpace 否,因为 OP 的答案可能会选择一个不是 None 的索引,而这不会
  • @OlivierMelançon 是的,但是这段代码也是 O(N)(它必须遍历整个列表)。顺便说一句,我相信你的意思是迭代 enumerate(input_arr)
  • @DeepSpace 没有办法不遍历列表。您的解决方案忽略了这样一个事实,即如果您保留一组可用索引,则会在每个项目设置上添加 O(1) 操作。根据最常用的操作,这可能会更长
【解决方案3】:

类似于 DeepSpace 的想法,除了 O(1) 内存和 O(n) 时间,但速度更快,因为它只迭代数组中超过一半的槽。

  1. 跟踪空槽的数量。
  2. 遍历列表。
  3. 如果一个槽是空的,返回你的新值,概率为1/number_empty_slots
  4. 如果我们没有返回并且槽是空的,则将概率质量重新分配到其他空槽

代码:

def get_random_addr(input_arr, num_empty_slots):
    # num_empty_slots contains the number of empty slots in input_arr
    for index, elem in enumerate(arr): 
        if elem is None: 
            if random.random() < 1 / num_empty_slots:
                return index
            num_empty_slots -= 1

【讨论】:

    【解决方案4】:

    只需先使用enumerate 为您的列表建立索引,过滤掉那些None,然后使用random.choice 选择一个可用空间。

    from random import choice
    def get_random_addr(input_arr):
        return choice([index for index, value in enumerate(input_arr) if value is None])
    print(get_random_addr([None, 1, None, 2]))
    

    这会随机输出02,如果没有更多可用空间,则输出None

    【讨论】:

    • 这个解决方案显着降低了我的代码速度!我正在处理相当大的列表,而代码刚刚停在这一行!不知道为什么!
    • @A23149577 是因为我使用了一个lambda函数作为过滤器,在python中效率很低。我已经编辑了我的答案以使用列表理解。感谢您的反馈。
    • 嗯,它仍然比我幼稚的解决方案慢得多,但我认为这是我应该为避免无限循环而付出的代价。
    • @A23149577 当有足够的可用空间时,您的幼稚解决方案肯定会更快,因为您不必先扫描整个列表以查找可用空间。但正如您也意识到的那样,当剩余空间较少时,您的解决方案会慢得多,因此扫描可用空间对于可预测的性能来说是必不可少的。
    【解决方案5】:

    在我的方法中,我在目标数组中选择一个任意地址,如果它是空闲的,我将其添加到输出列表中,但如果不是,我将该地址映射到包含None 的地址,最接近列表的末尾。数组中超出并包括该映射空闲地址的所有条目都将从该列表中删除,因为它们不是空的,或者已经在列表中的其他地方表示。我重复这个过程,削减目标列表的大小,使查找新的空地址变得越来越容易。还有一些其他的小细节可以让它全部工作,但我认为下面的代码可以比我用文字更好地解释这些。

    from random import random
    
    def randint(max_val):
        return int(random() * max_val)
    
    def assign(values, target):
        output = []
        mapping = dict()
        mmax = 0
        size = len(target)
        for val in values:
            idx = randint(size)
            while target[idx] != None:
                if idx in mapping:
                    idx = mapping.pop(idx)
                    mmax = max(mapping or [0])
                    break
    
                min_size = max(idx, mmax)
                try:
                    size -= target[size-1:min_size:-1].index(None)
                except:
                    size = min_size + 1
    
                if target[size-1] == None:
                    size -= 1
                    mapping[idx] = size
                    if idx > mmax:
                        mmax = idx
                elif size-1 in mapping:
                    size -= 1
                    mapping[idx] = mapping.pop(size)
                    mmax = max(mapping or [0])
    
                idx = randint(size)
            target[idx] = val
            output.append(idx)
        return output
    

    请注意,这会修改传递给它的目标列表。如果你不想修改它,你真的有两个选择:实现一些额外的逻辑来检查“空闲”地址是否已经被消耗,或者复制整个列表(在这种情况下,反转它并修补索引, 这样.index() 就可以直接在列表上工作,这无论如何都是主要的时间槽。

    我还建议验证它产生的解决方案是否有效。我已经做了一些测试,但我很可能错过了一些东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-03-16
      • 1970-01-01
      • 1970-01-01
      • 2020-10-09
      • 1970-01-01
      • 1970-01-01
      • 2017-01-02
      相关资源
      最近更新 更多