Як сортувати масив у порядку зменшення в Ruby


282

У мене є масив хешів:

[
  { :foo => 'foo', :bar => 2 },
  { :foo => 'foo', :bar => 3 },
  { :foo => 'foo', :bar => 5 },
]

Я намагаюся сортувати цей масив у порядку зменшення відповідно до значення :barкожного хеша.

Я використовую sort_byдля сортування вище масиву:

a.sort_by { |h| h[:bar] }

Однак це сортує масив у порядку зростання. Як зробити його сортування у порядку зменшення?

Одним з рішень було зробити наступне:

a.sort_by { |h| -h[:bar] }

Але цей негативний знак не здається доречним.


4
З огляду на інші варіанти, я все ще думаю, -h [: bar] є найелегантнішим. Що вам не подобається в цьому?
Майкл Коль

2
Мене набагато більше зацікавило передання наміру коду.
Waseem

1
@Waseem, чи можу я зашкодити вам оновити прийняту відповідь?
коллін

7
@Waseem У поточній відповіді нічого поганого. Там просто трапляється набагато краща відповідь. Відповідь Олов'яного чоловіка набагато ретельніша і показує, що sort_by.reverseце значно ефективніше, ніж прийнята на даний момент відповідь. Я вважаю, що це також краще вирішує проблему, яку ви згадали вище, щодо "передачі наміру коду". Крім цього, Людина з жерсті оновив свою відповідь на поточну версію рубіну. Це питання переглядали понад 15 тис. Разів. Якщо ви можете зекономити навіть одну секунду часу кожного глядача, я думаю, це того варте.
коллін

3
@collindo Спасибі, що я це зробив. :)
Waseem

Відповіді:


566

Завжди просвітливо робити орієнтир на різні запропоновані відповіді. Ось що я дізнався:

#! / usr / bin / ruby

вимагають "орієнтиру"

арі = []
1000.times { 
  ary << {: bar => rand (1000)} 
}

n = 500
Benchmark.bm (20) do | x |
  x.report ("сортування") {n.times {ary.sort {| a, b | b [: bar] <=> a [: bar]}}}
  x.report ("сортувати зворотний") {n.times {ary.sort {| a, b | a [: bar] <=> b [: bar]} .реверс}}
  x.report ("sort_by -a [: bar]") {n.times {ary.sort_by {| a | -a [: бар]}}}
  x.report ("sort_by a [: bar] * - 1") {n.times {ary.sort_by {| a | a [: бар] * - 1}}}
  x.report ("sort_by.reverse!") {n.times {ary.sort_by {| a | a [: bar]} .реверс}}
кінець

                          система користувача загальна реальна
сортувати 3,960000 0,010000 3,970000 (3,990886)
сортувати реверс 4,040000 0,000000 4,040000 (4,038849)
sort_by -a [: бар] 0.690000 0.000000 0.690000 (0.692080)
sort_by a [: bar] * - 1 0.700000 0.000000 0.700000 (0.699735)
sort_by.reverse! 0,650000 0,000000 0,650000 (0.654447)

Я думаю, що цікаво, що @ Пабло sort_by{...}.reverse!найшвидший. Перед тим, як запустити тест, я подумав, що це буде повільніше, ніж " -a[:bar]", але, заперечуючи значення, знадобиться більше часу, ніж це для зворотного перетворення всього масиву за один прохід. Це не велика різниця, але кожен маленький прискорення допомагає.


Зауважте, що результати Ruby 1.9 відрізняються

Ось результати для Ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-darwin10.8.0]:

                           user     system      total        real
sort                   1.340000   0.010000   1.350000 (  1.346331)
sort reverse           1.300000   0.000000   1.300000 (  1.310446)
sort_by -a[:bar]       0.430000   0.000000   0.430000 (  0.429606)
sort_by a[:bar]*-1     0.420000   0.000000   0.420000 (  0.414383)
sort_by.reverse!       0.400000   0.000000   0.400000 (  0.401275)

Вони є на старому MacBook Pro. Більш новіші або швидші машини матимуть менші значення, але відносні відмінності залишаться.


Ось трохи оновлена ​​версія новітнього обладнання та версії Ruby 2.1.1:

#!/usr/bin/ruby

require 'benchmark'

puts "Running Ruby #{RUBY_VERSION}"

ary = []
1000.times {
  ary << {:bar => rand(1000)}
}

n = 500

puts "n=#{n}"
Benchmark.bm(20) do |x|
  x.report("sort")               { n.times { ary.dup.sort{ |a,b| b[:bar] <=> a[:bar] } } }
  x.report("sort reverse")       { n.times { ary.dup.sort{ |a,b| a[:bar] <=> b[:bar] }.reverse } }
  x.report("sort_by -a[:bar]")   { n.times { ary.dup.sort_by{ |a| -a[:bar] } } }
  x.report("sort_by a[:bar]*-1") { n.times { ary.dup.sort_by{ |a| a[:bar]*-1 } } }
  x.report("sort_by.reverse")    { n.times { ary.dup.sort_by{ |a| a[:bar] }.reverse } }
  x.report("sort_by.reverse!")   { n.times { ary.dup.sort_by{ |a| a[:bar] }.reverse! } }
end

# >> Running Ruby 2.1.1
# >> n=500
# >>                            user     system      total        real
# >> sort                   0.670000   0.000000   0.670000 (  0.667754)
# >> sort reverse           0.650000   0.000000   0.650000 (  0.655582)
# >> sort_by -a[:bar]       0.260000   0.010000   0.270000 (  0.255919)
# >> sort_by a[:bar]*-1     0.250000   0.000000   0.250000 (  0.258924)
# >> sort_by.reverse        0.250000   0.000000   0.250000 (  0.245179)
# >> sort_by.reverse!       0.240000   0.000000   0.240000 (  0.242340)

Нові результати із застосуванням вищевказаного коду за допомогою Ruby 2.2.1 на останніх Macbook Pro. Знову ж таки, точні цифри не важливі, це їх відносини:

Running Ruby 2.2.1
n=500
                           user     system      total        real
sort                   0.650000   0.000000   0.650000 (  0.653191)
sort reverse           0.650000   0.000000   0.650000 (  0.648761)
sort_by -a[:bar]       0.240000   0.010000   0.250000 (  0.245193)
sort_by a[:bar]*-1     0.240000   0.000000   0.240000 (  0.240541)
sort_by.reverse        0.230000   0.000000   0.230000 (  0.228571)
sort_by.reverse!       0.230000   0.000000   0.230000 (  0.230040)

Оновлено для Ruby 2.7.1 на MacBook Pro середини 2015 року:

Running Ruby 2.7.1
n=500     
                           user     system      total        real
sort                   0.494707   0.003662   0.498369 (  0.501064)
sort reverse           0.480181   0.005186   0.485367 (  0.487972)
sort_by -a[:bar]       0.121521   0.003781   0.125302 (  0.126557)
sort_by a[:bar]*-1     0.115097   0.003931   0.119028 (  0.122991)
sort_by.reverse        0.110459   0.003414   0.113873 (  0.114443)
sort_by.reverse!       0.108997   0.001631   0.110628 (  0.111532)

... зворотний метод насправді не повертає перевернутий масив - він повертає нумератор, який починається в кінці і працює назад.

Джерело для Array#reverse:

               static VALUE
rb_ary_reverse_m(VALUE ary)
{
    long len = RARRAY_LEN(ary);
    VALUE dup = rb_ary_new2(len);

    if (len > 0) {
        const VALUE *p1 = RARRAY_CONST_PTR_TRANSIENT(ary);
        VALUE *p2 = (VALUE *)RARRAY_CONST_PTR_TRANSIENT(dup) + len - 1;
        do *p2-- = *p1++; while (--len > 0);
    }
    ARY_SET_LEN(dup, RARRAY_LEN(ary));
    return dup;
}

do *p2-- = *p1++; while (--len > 0); - це копіювання покажчиків на елементи у зворотному порядку, якщо я правильно запам’ятав свій C, тому масив перевернуто.


45
Супер корисно. Дякую за додаткові зусилля.
Джошуа Пінтер

7
мені подобається, коли люди надають тест-орієнтовність, як це !! Дивовижно!
ktec

25
"Мені подобається, коли люди надають такий критерій, як це !!" Я теж роблю, бо тоді мені цього не потрібно.
Олов'яний чоловік

9
@theTinMan Чи можете ви надати TL; DR для вашої відповіді. Вся ця інформація про орієнтир є досить корисною. Але TL; DR зверху на відповідь буде корисним людям, які просто хочуть відповіді. Я знаю, що вони повинні прочитати все пояснення, і я думаю, що вони будуть. І все-таки TL; DR буде досить корисним IMHO. Дякуємо за ваші зусилля.
Waseem

8
Я згоден з @Waseem. Настільки добре досліджена, як ця відповідь, ОП не запитувала "який найшвидший спосіб зробити низхідний сорт у Рубі". TL; DR вгорі, що демонструє просте використання, а потім показники, покращить цю відповідь IMO.

89

Просто швидка річ, що позначає наміри порядку зменшення.

descending = -1
a.sort_by { |h| h[:bar] * descending }

(Буде думати про кращий спосіб в середній час);)


a.sort_by { |h| h[:bar] }.reverse!

Пабло, приємна робота з пошуку кращого способу! Дивіться тест, який я зробив.
Олов'яний чоловік

перший метод швидший (хоч може і хитріший), тому що він циклічить лише один раз. Щодо другого, вам не потрібно !, це для операцій на місці.
tokland

3
Якщо ви не використовуєте удар після реверсу, ви не будете реверсувати масив, але створите інший, який перевернуто.
Пабло Фернандес

56

Ви можете зробити:

a.sort{|a,b| b[:bar] <=> a[:bar]}

4
але весь сенс використання sort_byполягав у тому, що він уникав функції порівняння стільки разів
user102008

3
-1. sort_byє набагато ефективнішим і читабельнішим. Заперечення значення або здійснення зворотного в кінці буде швидше і легше для читання.
Марк-Андре Лафортун

1
Мені подобається ця відповідь, оскільки * -1працює не з усіма значеннями (наприклад, Час), і reverseбуде переупорядковувати значення, відсортовані як рівні
Abe Voelker

8

Я бачу, що у нас (крім інших) в основному два варіанти:

a.sort_by { |h| -h[:bar] }

і

a.sort_by { |h| h[:bar] }.reverse

Хоча обидва способи дають однаковий результат, коли ваш ключ сортування унікальний, майте на увазі, що reverseспосіб змінить порядок клавіш, рівний .

Приклад:

a = [{foo: 1, bar: 1},{foo: 2,bar: 1}]
a.sort_by {|h| -h[:bar]}
 => [{:foo=>1, :bar=>1}, {:foo=>2, :bar=>1}]
a.sort_by {|h| h[:bar]}.reverse
 => [{:foo=>2, :bar=>1}, {:foo=>1, :bar=>1}]

Хоча вам часто не потрібно про це дбати, іноді це робите. Щоб уникнути такої поведінки, ви можете ввести другий ключ сортування (який напевно повинен бути унікальним принаймні для всіх елементів, які мають однаковий ключ сортування):

a.sort_by {|h| [-h[:bar],-h[:foo]]}
 => [{:foo=>2, :bar=>1}, {:foo=>1, :bar=>1}]
a.sort_by {|h| [h[:bar],h[:foo]]}.reverse
 => [{:foo=>2, :bar=>1}, {:foo=>1, :bar=>1}]

+1 для вказівки на те, що семантика reverseрізниться. Я вірю, що це також зіпсує попередній сорт, у випадку, коли хтось намагається застосувати кілька видів у певному порядку.
johncip

6

А як на рахунок:

 a.sort {|x,y| y[:bar]<=>x[:bar]}

Це працює!!

irb
>> a = [
?>   { :foo => 'foo', :bar => 2 },
?>   { :foo => 'foo', :bar => 3 },
?>   { :foo => 'foo', :bar => 5 },
?> ]
=> [{:bar=>2, :foo=>"foo"}, {:bar=>3, :foo=>"foo"}, {:bar=>5, :foo=>"foo"}]

>>  a.sort {|x,y| y[:bar]<=>x[:bar]}
=> [{:bar=>5, :foo=>"foo"}, {:bar=>3, :foo=>"foo"}, {:bar=>2, :foo=>"foo"}]

Так, це насправді працює, але я думаю, що PO хоче показати наміри з кодом (у нього вже є робоче рішення).
Пабло Фернандес

Хоча це sortбуде працювати, це лише швидше при сортуванні безпосередніх значень. Якщо вам доведеться копати їх, sort_byто швидше. Дивіться орієнтир.
Бляшаний чоловік

3

Щодо згаданого набору еталонів, ці результати також стосуються відсортованих масивів.

sort_by/ reverseце:

# foo.rb
require 'benchmark'

NUM_RUNS = 1000

# arr = []
arr1 = 3000.times.map { { num: rand(1000) } }
arr2 = 3000.times.map { |n| { num: n } }.reverse

Benchmark.bm(20) do |x|
  { 'randomized'     => arr1,
    'sorted'         => arr2 }.each do |label, arr|
    puts '---------------------------------------------------'
    puts label

    x.report('sort_by / reverse') {
      NUM_RUNS.times { arr.sort_by { |h| h[:num] }.reverse }
    }
    x.report('sort_by -') {
      NUM_RUNS.times { arr.sort_by { |h| -h[:num] } }
    }
  end
end

І результати:

$: ruby foo.rb
                           user     system      total        real
---------------------------------------------------
randomized
sort_by / reverse      1.680000   0.010000   1.690000 (  1.682051)
sort_by -              1.830000   0.000000   1.830000 (  1.830359)
---------------------------------------------------
sorted
sort_by / reverse      0.400000   0.000000   0.400000 (  0.402990)
sort_by -              0.500000   0.000000   0.500000 (  0.499350)

Ви повинні мати змогу зробити sort_by {}. Реверс! (реверс без удару створює новий масив, і я б очікував, що це буде повільніше, звичайно)
bibstha

2

Просте рішення від висхідного до низхідного та навпаки:

STRINGS

str = ['ravi', 'aravind', 'joker', 'poker']
asc_string = str.sort # => ["aravind", "joker", "poker", "ravi"]
asc_string.reverse # => ["ravi", "poker", "joker", "aravind"]

ЦИФРИ

digit = [234,45,1,5,78,45,34,9]
asc_digit = digit.sort # => [1, 5, 9, 34, 45, 45, 78, 234]
asc_digit.reverse # => [234, 78, 45, 45, 34, 9, 5, 1]

1

Для тих, хто любить вимірювати швидкість в IPS;)

require 'benchmark/ips'

ary = []
1000.times { 
  ary << {:bar => rand(1000)} 
}

Benchmark.ips do |x|
  x.report("sort")               { ary.sort{ |a,b| b[:bar] <=> a[:bar] } }
  x.report("sort reverse")       { ary.sort{ |a,b| a[:bar] <=> b[:bar] }.reverse }
  x.report("sort_by -a[:bar]")   { ary.sort_by{ |a| -a[:bar] } }
  x.report("sort_by a[:bar]*-1") { ary.sort_by{ |a| a[:bar]*-1 } }
  x.report("sort_by.reverse!")   { ary.sort_by{ |a| a[:bar] }.reverse }
  x.compare!
end

І результати:

Warming up --------------------------------------
                sort    93.000  i/100ms
        sort reverse    91.000  i/100ms
    sort_by -a[:bar]   382.000  i/100ms
  sort_by a[:bar]*-1   398.000  i/100ms
    sort_by.reverse!   397.000  i/100ms
Calculating -------------------------------------
                sort    938.530   1.8%) i/s -      4.743k in   5.055290s
        sort reverse    901.157   6.1%) i/s -      4.550k in   5.075351s
    sort_by -a[:bar]      3.814k  4.4%) i/s -     19.100k in   5.019260s
  sort_by a[:bar]*-1      3.732k  4.3%) i/s -     18.706k in   5.021720s
    sort_by.reverse!      3.928k  3.6%) i/s -     19.850k in   5.060202s

Comparison:
    sort_by.reverse!:     3927.8 i/s
    sort_by -a[:bar]:     3813.9 i/s - same-ish: difference falls within error
  sort_by a[:bar]*-1:     3732.3 i/s - same-ish: difference falls within error
                sort:      938.5 i/s - 4.19x  slower
        sort reverse:      901.2 i/s - 4.36x  slower
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.