【问题标题】:Argsorting values in a list of lists对列表列表中的值进行排序
【发布时间】:2018-11-06 10:13:22
【问题描述】:

我有一个列表列表A,长度为mA 的每个列表都包含来自 {1, 2, ..., n} 的正数。下面是一个例子,其中m = 3n = 4

A = [[1, 1, 3], [1, 2], [1, 1, 2, 4]]

我将A 中的每个数字x 表示为一对(i, j),其中A[i][j] = x。我想以非递减顺序对A 中的数字进行排序;按最低的第一指数打破平局。也就是说,如果A[i1][j1] == A[i2][j2],那么(i1, j1)(i2, j2) iff i1 <= i2 之前。

在示例中,我想返回对:

(0, 0), (0, 1), (1, 0), (2, 0), (2, 1), (1, 1), (2, 2), (0, 2), (2, 3)

表示排序后的数字

1, 1, 1, 1, 1, 2, 2, 3, 4

我所做的是一种天真的方法,其工作原理如下:

  • 首先我对A 中的每个列表进行排序。
  • 然后我迭代{1, 2, ..., n} 和列表A 中的数字并添加对。

代码:

for i in range(m): 
    A[i].sort()
S = []
for x in range(1, n+1):
    for i in range(m):
        for j in range(len(A[i])):
            if A[i][j] == x:
                S.append((i, j))

我认为这种方法不好。我们能做得更好吗?

【问题讨论】:

    标签: python list sorting


    【解决方案1】:

    list.sort

    您可以生成索引列表,然后使用key 调用list.sort

    B = [(i, j) for i, x in enumerate(A) for j, _ in enumerate(x)]
    B.sort(key=lambda ix: A[ix[0]][ix[1]])
    

    print(B)
    [(0, 0), (0, 1), (1, 0), (2, 0), (2, 1), (1, 1), (2, 2), (0, 2), (2, 3)]
    

    请注意,在支持可迭代解包函数的 python-2.x 上,您可以稍微简化 sort 调用:

    B.sort(key=lambda (i, j): A[i][j])
    

    sorted

    这是上述版本的替代方案,它生成 两个 列表(一个在内存中,sorted 然后处理,返回另一个副本)。

    B = sorted([
           (i, j) for i, x in enumerate(A) for j, _ in enumerate(x)
        ], 
        key=lambda ix: A[ix[0]][ix[1]]
    )
    
    print(B)
    [(0, 0), (0, 1), (1, 0), (2, 0), (2, 1), (1, 1), (2, 2), (0, 2), (2, 3)]
    

    性能

    应大众需求,添加一些时间和情节。

    从图中,我们看到调用list.sortsorted 更有效。这是因为list.sort 执行就地排序,因此创建sorted 拥有的数据副本没有时间/空间开销。

    函数

    def cs1(A):
        B = [(i, j) for i, x in enumerate(A) for j, _ in enumerate(x)]
        B.sort(key=lambda ix: A[ix[0]][ix[1]]) 
    
        return B
    
    def cs2(A):
        return sorted([
               (i, j) for i, x in enumerate(A) for j, _ in enumerate(x)
            ], 
            key=lambda ix: A[ix[0]][ix[1]]
        )
    
    def rorydaulton(A):
    
        triplets = [(x, i, j) for i, row in enumerate(A) for j, x in enumerate(row)]
        pairs = [(i, j) for x, i, j in sorted(triplets)]
    
        return pairs
    
    def jpp(A):
        def _create_array(data):
            lens = np.array([len(i) for i in data])
            mask = np.arange(lens.max()) < lens[:,None]
            out = np.full(mask.shape, max(map(max, data))+1, dtype=int)  # Pad with max_value + 1
            out[mask] = np.concatenate(data)
            return out
    
        def _apply_argsort(arr):
            return np.dstack(np.unravel_index(np.argsort(arr.ravel()), arr.shape))[0]
    
        return _apply_argsort(_create_array(A))[:sum(map(len, A))]
    
    def agngazer(A):
        idx = np.argsort(np.fromiter(chain(*A), dtype=np.int))
        return np.array(
           tuple((i, j) for i, r in enumerate(A) for j, _ in enumerate(r))
        )[idx]
    

    性能基准代码

    from timeit import timeit
    from itertools import chain
    
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    
    res = pd.DataFrame(
           index=['cs1', 'cs2', 'rorydaulton', 'jpp', 'agngazer'],
           columns=[10, 50, 100, 500, 1000, 5000, 10000, 50000],
           dtype=float
    )
    
    for f in res.index: 
        for c in res.columns:
            l = [[1, 1, 3], [1, 2], [1, 1, 2, 4]] * c
            stmt = '{}(l)'.format(f)
            setp = 'from __main__ import l, {}'.format(f)
            res.at[f, c] = timeit(stmt, setp, number=30)
    
    ax = res.div(res.min()).T.plot(loglog=True) 
    ax.set_xlabel("N"); 
    ax.set_ylabel("time (relative)");
    
    plt.show();
    

    【讨论】:

    【解决方案2】:

    您可以将(x, i, j) 制成三元组,对这些三元组进行排序,然后提取索引(i, j)。这是有效的,因为三元组包含排序所需的所有信息,并按排序所需的顺序包含在最终列表中。 (这被称为“装饰-排序-不装饰”习语,与 Schwartzian 变换有关 - 向@Morgen 表示名称和概括以及我解释这种技术的普遍性的动机。)这可以结合起来成一个语句,但为了清楚起见,我在这里将其拆分。

    A = [[1, 1, 3], [1, 2], [1, 1, 2, 4]]
    
    triplets = [(x, i, j) for i, row in enumerate(A) for j, x in enumerate(row)]
    pairs = [(i, j) for x, i, j in sorted(triplets)]
    print(pairs)
    

    这是打印结果:

    [(0, 0), (0, 1), (1, 0), (2, 0), (2, 1), (1, 1), (2, 2), (0, 2), (2, 3)]
    

    【讨论】:

    • 这看起来像是 en.m.wikipedia.org/wiki/Schwartzian_transform(也称为 decorate-sort-undecorate)的具体示例,提及它可能会使您的答案更普遍适用
    • @Morgen:感谢您提供的信息——我不知道名称或特定的转换。如果我正确理解您的文章,我没有使用转换本身,而是使用类似的“装饰-排序-不装饰”成语。我首先在群论背景下想到了这个习语,而不是在计算机科学中,并在 80 年代初在我的魔方解决方案中使用了它。 (我的“排序”概念更笼统,不是很排序。)
    • 公平点。我可能应该找到一篇关于 decorate-sort-undecorate 的文章,我主要使用那篇文章,因为它很容易记住找到它
    【解决方案3】:

    为了好玩,这里有一个通过 3rd 方库numpy 的方法。由于昂贵的填充步骤,性能比 @coldspeed 的解决方案慢约 10%。

    致谢:对于这个解决方案,我改编了@Divakar 的array-from-jagged-list 配方,并逐字复制了@AshwiniChaudhary 的multi-dimension argsort 解决方案。

    import numpy as np
    
    A = [[1, 1, 3], [1, 2], [1, 1, 2, 4]]
    
    def create_array(data):
    
        """Convert jagged list to numpy array; pad with max_value + 1"""
    
        lens = np.array([len(i) for i in data])
        mask = np.arange(lens.max()) < lens[:,None]
        out = np.full(mask.shape, max(map(max, data))+1, dtype=int)  # Pad with max_value + 1
        out[mask] = np.concatenate(data)
        return out
    
    def apply_argsort(arr):
    
        """Flatten, argsort, extract indices, then stack into a single array"""
    
        return np.dstack(np.unravel_index(np.argsort(arr.ravel()), arr.shape))[0]
    
    # limit only to number of elements in A
    res = apply_argsort(create_array(A))[:sum(map(len, A))]
    
    print(res)
    
    [[0 0]
     [0 1]
     [1 0]
     [2 0]
     [2 1]
     [1 1]
     [2 2]
     [0 2]
     [2 3]]
    

    【讨论】:

      【解决方案4】:

      仅仅因为@jpp 玩得很开心:

      from itertools import chain
      import numpy as np
      def agn(A):
          idx = np.argsort(np.fromiter(chain(*A), dtype=np.int))
          return np.array(tuple((i, j) for i, r in enumerate(A) for j, _ in enumerate(r)))[idx]
      

      计时测试:

      测试 1:

      与@coldspeed 的最快方法比较:

      In [1]: import numpy as np
      
      In [2]: print(np.__version__)
      1.13.3
      
      In [3]: from itertools import chain
      
      In [4]: import sys
      
      In [5]: print(sys.version)
      3.5.5 |Anaconda, Inc.| (default, Mar 12 2018, 16:25:05) 
      [GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]
      
      In [6]: A = [[1],[0, 0, 0, 1, 1, 3], [1, 2], [1, 1, 2, 4]] * 10000
      
      In [7]: %timeit np.array(tuple((i, j) for i, r in enumerate(A) for j, _ in enumerate(r)))[np.argsort(np.fromit
         ...: er(chain(*A), dtype=np.int))]
      89.4 ms ± 718 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
      
      In [8]: %timeit B = [(i, j) for i, x in enumerate(A) for j, _ in enumerate(x)]; B.sort(key=lambda ix: A[ix[0]]
         ...: [ix[1]])
      93.5 ms ± 1.65 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
      

      测试 2a:

      这个测试使用一个随机生成的大数组A(每个子列表都被排序,因为这就是OP列表的样子):

      In [20]: A = [sorted([random.randint(1, 100) for _ in range(random.randint(1,1000))]) for _ in range(10000)]
      
      In [21]: def agn(A):
          ...:     idx = np.argsort(np.fromiter(chain(*A), dtype=np.int))
          ...:     return np.array(tuple((i, j) for i, r in enumerate(A) for j, _ in enumerate(r)))[idx]
          ...:     
      
      In [22]: %timeit agn(A)
      3.1 s ± 62.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      
      In [23]: %timeit cs1(A)
      3.2 s ± 89.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      

      测试 2.b

      与测试 2.b 类似,但使用未排序的数组 A

      In [25]: A = [[random.randint(1, 100) for _ in range(random.randint(1,1000))] for _ in range(10000)]
      
      In [26]: %timeit cs1(A)
      4.24 s ± 215 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      
      In [27]: %timeit agn(A)
      3.44 s ± 49.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
      

      【讨论】:

      • @coldspeed 感谢您的计时测试!我还在答案中添加了自己的测试(A 略有不同)。
      猜你喜欢
      • 1970-01-01
      • 2018-12-22
      • 2013-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-16
      • 2016-08-03
      • 1970-01-01
      相关资源
      最近更新 更多