简体   繁体   中英

Sorting a list: numbers in ascending, letters in descending

This question is actually adapted from one previously asked by Mat.S ( image ). Although it was deleted, I thought it was a good question, so I am reposting it with clearer requirements and my own solution.


Given a list of letters and numbers, say

['a', 2, 'b', 1, 'c', 3]

The requirement is to sort the numbers in ascending and letters in descending, without altering the relative position of letters and numbers. By this I mean if the unsorted list is:

[L, D, L, L, D]    # L -> letter; # D -> digit 

Then, the sorted list must also be

[L, D, L, L, D] 
  1. The letters and digits do not necessarily alternate in a regular pattern - they can appear in any arbitrary order

  2. After sorting - numbers are ascending, letters are descending.

So for the example above the output is

['c', 1, 'b', 2, 'a', 3]

Another example:

 In[]: [5, 'a', 'x', 3, 6, 'b']
Out[]: [3, 'x', 'b', 5, 6, 'a']

What would be a good way to do this?

Here is an optimized approach using defaultdict() and bisect() :

In [14]: lst = [5, 'a', 'x', 3, 6, 'b']
In [15]: from collections import defaultdict       
In [16]: import bisect

In [17]: def use_dict_with_bisect(lst):
             d = defaultdict(list)
             for i in lst:
                 bisect.insort(d[type(i)], i)
             # since bisect doesn't accept key we need to reverse the sorted integers
             d[int].sort(reverse=True)
             return [d[type(i)].pop() for i in lst]
   .....:  

Demo :

In [18]: lst
Out[18]: [5, 'a', 'x', 3, 6, 'b']

In [19]: use_dict_with_bisect(lst)
Out[19]: [3, 'x', 'b', 5, 6, 'a']

In case you're dealing with larger lists it's more optimized to drop using bisect which has a complexity about O(n 2 )and just use python built-in sort() function with Nlog(n) complexity.

In [26]: def use_dict(lst):
             d = defaultdict(list)
             for i in lst:
                 d[type(i)].append(i)
             d[int].sort(reverse=True); d[str].sort()
             return [d[type(i)].pop() for i in lst]

Benchmark with other answers that shows the latest approach using dict and built-in sort is almost 1ms faster than the other approaches:

In [29]: def use_sorted1(lst):
              letters = sorted(let for let in lst if isinstance(let,str))
              numbers = sorted((num for num in lst if not isinstance(num,str)), reverse = True)
              return [letters.pop() if isinstance(elt,str) else numbers.pop() for elt in lst]
   .....: 

In [31]: def use_sorted2(lst):
              f1 = iter(sorted(filter(lambda x: isinstance(x, str), lst), reverse=True))
              f2 = iter(sorted(filter(lambda x: not isinstance(x, str), lst)))
              return [next(f1) if isinstance(x, str) else next(f2) for x in lst]
   .....: 

In [32]: %timeit use_sorted1(lst * 1000)
100 loops, best of 3: 3.05 ms per loop

In [33]: %timeit use_sorted2(lst * 1000)
100 loops, best of 3: 3.63 ms per loop

In [34]: %timeit use_dict(lst * 1000)   # <-- WINNER
100 loops, best of 3: 2.15 ms per loop

Here is a benchmark that shows how using bisect can slow down the process for long lists:

In [37]: %timeit use_dict_with_bisect(lst * 1000)
100 loops, best of 3: 4.46 ms per loop

Look ma, no iter :

lst = ['a', 2, 'b', 1, 'c', 3]
letters = sorted(let for let in lst if isinstance(let,str))
numbers = sorted((num for num in lst if not isinstance(num,str)), reverse = True)
lst = [(letters if isinstance(elt,str) else numbers).pop()for elt in lst]

I'm looking for a way to turn this into a (horrible) one-liner, but no luck so far - suggestions welcome!

I took a crack at this by creating two generators and then taking from them conditionally:

f1 = iter(sorted(filter(lambda x:     isinstance(x, str), lst), reverse=True))
f2 = iter(sorted(filter(lambda x: not isinstance(x, str), lst)))

[next(f1) if isinstance(x, str) else next(f2) for x in lst]
# ['c', 1, 'b', 2, 'a', 3]

在一行中:

list(map(list, sorted(zip(lst[::2], lst[1::2]), key=lambda x: x[1] if hasattr(x[0], '__iter__') else x[0])))

Totally not recommended, but I had fun coding it.

from collections import deque
from operator import itemgetter

lst = ['a', 2, 'b', 1, 'c', 3]
is_str = [isinstance(e, str) for e in lst]
two_heads = deque(map(itemgetter(1), sorted(zip(is_str, lst))))
[two_heads.pop() if a_str else two_heads.popleft() for a_str in is_str]

Why don't we just sort list in ascending order, but ensure that numbers come before letters:

[D, D, L, L, L]    # L -> letter; # D -> digit 

We can achieve that in such a way:

>>> lst = [5, 'a', 'x', 3, 6, 'b']
>>> sorted(lst, key=lambda el: (isinstance(el, str), el))
[3, 5, 6, 'a', 'b', 'x']

Then we look over original array from left to right and if we encounter number, we pick element from the beginning of sorted array, otherwise from the end. The full verbose solution will then be:

def one_sort(lst):
    s = sorted(lst, key=lambda el: (isinstance(el, str), el))
    res = []
    i, j = 0, len(s)
    for el in lst:
        if isinstance(el, str):
            j -= 1
            res.append(s[j])
        else:
            res.append(s[i])
            i += 1
    return res

lst = [5, 'a', 'x', 3, 6, 'b']
print(one_sort(lst)) # [3, 'x', 'b', 5, 6, 'a']

Much shorter but cryptic solution will be:

def one_sort_cryptic(lst):
    s = sorted(lst, key=lambda el: (isinstance(el, str), el))
    return [s.pop(-isinstance(el, str)) for el in lst]

lst = [5, 'a', 'x', 3, 6, 'b']
print(one_sort_cryptic(lst)) # [3, 'x', 'b', 5, 6, 'a']

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM