ZetCode

Ruby 数组排序

最后修改于 2025 年 4 月 2 日

Ruby 提供了强大的数组排序方法,语法简单。本指南探讨了从基础到高级的排序技术、自定义比较和性能考虑。学习如何高效地对数字、字符串、对象和复杂数据结构进行排序。掌握数组排序对于有效的 Ruby 编程至关重要。

基本数组排序

Ruby 的 sort 方法是排序数组最简单的方式。它自然地处理数字和字符串,返回一个新的已排序数组。原始数组保持不变,除非你使用 sort!。本示例展示了数字和字符串数组的基本排序。理解这些基本概念是掌握更高级排序技术的关键。

basic_sorting.rb
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
strings = ["apple", "banana", "cherry", "date"]

sorted_numbers = numbers.sort
sorted_strings = strings.sort

puts "Original numbers: #{numbers}"
puts "Sorted numbers: #{sorted_numbers}"

puts "\nOriginal strings: #{strings}"
puts "Sorted strings: #{sorted_strings}"

# In-place sorting
numbers.sort!
puts "\nAfter sort!: #{numbers}"

# Additional example: Mixed case strings
mixed_case = ["Apple", "banana", "Cherry"]
puts "\nMixed case sort: #{mixed_case.sort}"

sort 方法按升序排列数字,按字母顺序排列字符串。请注意,在 Ruby 的默认字符串比较中,大写字母排在小写字母之前。原始数组保持不变,除非你使用带感叹号的版本 sort!,它会直接修改原始数组。

对于数字数组,排序很简单,元素按从小到大的顺序排列。字符串排序遵循 ASCII/Unicode 顺序,这会影响混合大小写字符的比较。示例显示“Apple”排在“banana”之前,因为大写字母 'A' 的 ASCII 值比小写字母 'b' 低。

反向排序

Ruby 提供了两种对数组进行降序排序的方法:使用 sort.reverse 或使用带有负号的 sort_by 方法。本示例演示了数字和字符串的这两种方法。反向排序在现实应用中很常见,例如显示最高得分。理解这些技术可以扩展你的排序工具集。

reverse_sorting.rb
numbers = [5, 2, 8, 3, 1]
words = ["zebra", "apple", "monkey", "banana"]

# Method 1: sort then reverse
desc_numbers = numbers.sort.reverse
desc_words = words.sort.reverse

# Method 2: sort_by with negation
desc_numbers2 = numbers.sort_by { |n| -n }
desc_words2 = words.sort_by { |w| -w.downcase.ord }

puts "Descending numbers (method 1): #{desc_numbers}"
puts "Descending numbers (method 2): #{desc_numbers2}"

puts "\nDescending words (method 1): #{desc_words}"
puts "Descending words (method 2): #{desc_words2}"

# Additional example: Sorting dates in reverse
dates = [2023, 2021, 2025, 2020]
puts "\nDescending years: #{dates.sort.reverse}"

第一种方法是将 sortreverse 链接起来,这种方法很清晰,但会创建一个中间数组。第二种方法使用带负值的 sort_by,对于大型数组来说更有效。对于字符串,第二种方法需要仔细处理大小写敏感性。

反向排序日期或年份遵循与数字相同的模式。示例显示,在降序排列中,2025 排在 2021 之前。选择最适合你的具体用例和性能要求的方法。

使用块进行自定义排序

Ruby 允许使用 sortsort_by 的块进行自定义排序逻辑。这使得可以根据任何标准进行排序,包括复杂的对象属性。块应返回 -1、0 或 1 进行比较。本示例演示了按字符串长度和自定义数字条件进行排序。自定义排序对于特定的排序需求非常强大。

custom_sorting.rb
fruits = ["apple", "banana", "cherry", "date", "elderberry"]

# Sort by string length
length_sorted = fruits.sort_by { |fruit| fruit.length }
puts "Sorted by length: #{length_sorted}"

# Custom numeric sort
numbers = [42, 17, 99, 23, 56]
custom_sorted = numbers.sort do |a, b|
  if a.even? && b.odd?
    -1
  elsif a.odd? && b.even?
    1
  else
    a <=> b
  end
end

puts "\nEven numbers first: #{custom_sorted}"

# Additional example: Sort by last character
last_char_sorted = fruits.sort_by { |fruit| fruit[-1] }
puts "\nSorted by last character: #{last_char_sorted}"

sort_by 方法对于字符串长度等简单转换通常更简洁,它使用单个值进行比较。完整的 sort 块在你需要复杂的条件逻辑时提供了完全的控制,如偶数/奇数示例所示。

Ruby 的“飞船操作符”(<=>)根据比较返回 -1、0 或 1,这在自定义排序中很有用。示例演示了按最后一个字符对水果进行排序,展示了 Ruby 排序方法的灵活性。这些技术适用于任何可比较的对象。

排序哈希和复杂对象

排序哈希或对象的集合需要指定要比较的属性。Ruby 的 sort_by 在这里非常有用,可以提取比较键。本示例展示了按多个字段对哈希数组进行排序。类似的技术适用于具有属性访问器的自定义对象。掌握这些技术可以对真实世界的数据结构进行排序。

hash_sorting.rb
people = [
  { name: "Alice", age: 30, city: "New York" },
  { name: "Bob", age: 25, city: "Chicago" },
  { name: "Charlie", age: 25, city: "Boston" }
]

# Sort by single field
age_sorted = people.sort_by { |person| person[:age] }
puts "Sorted by age:"
age_sorted.each { |p| puts "#{p[:name]} (#{p[:age]})" }

# Sort by multiple fields
multi_sorted = people.sort_by { |person| [person[:age], person[:city]] }
puts "\nSorted by age then city:"
multi_sorted.each { |p| puts "#{p[:name]}, #{p[:age]}, #{p[:city]}" }

# Additional example: Case-insensitive name sort
name_sorted = people.sort_by { |person| person[:name].downcase }
puts "\nSorted by name (case-insensitive):"
name_sorted.each { |p| puts p[:name] }

使用 sort_by 按单个键排序哈希很简单,只需提取相关值即可。对于多字段排序,请按优先级顺序返回一个值数组——Ruby 会逐个元素地比较数组。示例显示 25 岁的按城市排序(年龄之后)。

不区分大小写的排序需要使用 downcase 对字符串进行规范化。此技术适用于任何不应区分大小写的基于字符串的比较。当使用访问器方法而不是哈希键对对象数组进行排序时,原则也是相同的。

不区分大小写的字符串排序

Ruby 的默认字符串排序是区分大小写的,这通常不是用户期望的。本示例演示了正确的不区分大小写的排序技术。我们将比较朴素的方法和健壮的解决方案。正确的字符串排序对于面向用户的应用程序至关重要。这些方法可以确保无论大小写如何都能保持一致的排序。

case_insensitive.rb
words = ["Apple", "banana", "apple", "Banana", "cherry"]

# Problematic approach (still case-sensitive)
naive_sort = words.sort
puts "Naive sort: #{naive_sort}"

# Correct case-insensitive sort
proper_sort = words.sort_by { |w| w.downcase }
puts "\nCase-insensitive sort: #{proper_sort}"

# Alternative with sort block
proper_sort2 = words.sort { |a, b| a.casecmp(b) }
puts "\nUsing casecmp: #{proper_sort2}"

# Additional example: Mixed strings with numbers
mixed = ["file1", "File10", "file2", "File3"]
puts "\nNatural sort: #{mixed.sort_by { |s| s.downcase }}"

朴素的方法之所以失败,是因为大写字母的 ASCII 值低于小写字母。sort_by 解决方案在比较前规范化大小写,而 casecmp 提供了内置的不区分大小写的比较。这两种方法都能正确地交错“Apple”和“apple”。

对于包含数字的混合字符串(如“File2”与“File10”),简单的忽略大小写是不够的——需要自然排序算法。示例显示了此限制,其中“File10”在字母顺序上排在“File3”之前。需要更高级的技术才能实现真正的自然排序。

使用 Schwartzian 变换进行排序

Schwartzian 变换通过缓存比较键来优化昂贵的排序操作。Ruby 的 sort_by 自动实现了这一点。本示例比较了直接排序和 Schwartzian 风格排序。该变换将 O(n log n) 的键计算减少到 O(n)。此技术对于复杂排序非常有用。

schwartzian.rb
require 'benchmark'

books = [
  "War and Peace",
  "The Great Gatsby",
  "Moby Dick",
  "To Kill a Mockingbird"
]

# Expensive operation to simulate complex key calculation
def title_key(title)
  sleep(0.01) # Simulate expensive computation
  title.gsub(/^(A|An|The)\s+/i, '').downcase
end

# Without optimization
time1 = Benchmark.measure do
  sorted1 = books.sort { |a, b| title_key(a) <=> title_key(b) }
  puts "\nDirect sort (slow): #{sorted1}"
end

# With Schwartzian Transform (sort_by)
time2 = Benchmark.measure do
  sorted2 = books.sort_by { |title| title_key(title) }
  puts "\nSchwartzian sort (fast): #{sorted2}"
end

puts "\nTimings:"
puts "Direct sort: #{time1.real.round(2)}s"
puts "Schwartzian: #{time2.real.round(2)}s"

# Additional example: Sorting by word count
puts "\nSorted by word count: #{books.sort_by { |t| t.split.size }}"

直接排序会调用昂贵的 title_key 方法 O(n log n) 次,而 sort_by 只调用它 O(n) 次。基准测试显示了显著的性能差异——Schwartzian 的速度可以快几个数量级。这种优化在键计算成本很高时尤为重要。

Ruby 的 sort_by 自动实现了 Schwartzian 变换,使其成为复杂排序的首选。该示例还展示了按单词数量排序,这也是 sort_by 表现出色的另一个例子。当比较键需要计算时,请始终考虑使用 sort_by

自定义对象排序

在 Ruby 中排序自定义对象需要定义比较逻辑。这可以通过实现“飞船操作符”(<=>)或使用 sort_by 来完成。本示例展示了 Person 类的这两种方法。一致的对象排序可以使面向对象应用程序中的代码更清晰。这些模式适用于任何自定义类。

object_sorting.rb
class Person
  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  # Implement spaceship operator for default sorting
  def <=>(other)
    age <=> other.age
  end

  def to_s
    "#{name} (#{age})"
  end
end

people = [
  Person.new("Alice", 32),
  Person.new("Bob", 25),
  Person.new("Charlie", 40)
]

# Sort using <=>
age_sorted = people.sort
puts "Sorted by age (<=>):"
puts age_sorted

# Sort by name using sort_by
name_sorted = people.sort_by { |person| person.name }
puts "\nSorted by name (sort_by):"
puts name_sorted

# Additional example: Reverse sort by age
reverse_age = people.sort_by { |person| -person.age }
puts "\nReverse age sort:"
puts reverse_age

实现 <=> 可以通过 sort 实现自然排序,使对象表现得像内置类型。示例使用此运算符按年龄对 Person 对象进行排序。sort_by 在不修改类的情况下提供了更大的灵活性,如姓名排序示例所示。

对象反向排序与原始类型相同——要么对排序键取负,要么与 reverse 链接。示例通过取负键演示了按年龄反向排序。选择最适合你的类设计和使用模式的方法。

稳定排序

稳定排序会保留相等元素的相对顺序。Ruby 的默认排序不保证稳定,但我们可以在需要时实现稳定排序。本示例演示了一种稳定排序的实现。当次要特征应保持顺序时,稳定排序很重要。该解决方案适用于任何可比较的元素。

stable_sort.rb
# Ruby's default sort isn't guaranteed stable
items = [
  {value: 5, order: 1},
  {value: 3, order: 2},
  {value: 5, order: 3},
  {value: 2, order: 4}
]

# Regular sort might not preserve order for equal values
unstable = items.sort_by { |item| item[:value] }
puts "Unstable sort (order might change):"
unstable.each { |item| puts "Value: #{item[:value]}, Original order: #{item[:order]}" }

# Stable sort implementation
def stable_sort(array)
  array.each_with_index.sort_by { |x, i| [x[:value], i] }.map(&:first)
end

stable = stable_sort(items)
puts "\nStable sort (original order preserved):"
stable.each { |item| puts "Value: #{item[:value]}, Original order: #{item[:order]}" }

# Additional example: Stable sort with strings
words = ["banana", "apple", "cherry", "Apple"]
stable_words = words.each_with_index.sort_by { |w, i| [w.downcase, i] }.map(&:first)
puts "\nStable case-insensitive sort: #{stable_words}"

稳定排序实现将原始索引添加为次要排序键,确保具有相等值的元素保持其相对位置。这是通过对 [值, 原始索引] 数组进行排序来实现的。示例显示值为 5 的项保留了其原始的 1, 3 顺序。

此技术适用于任何排序标准,包括示例中所示的不区分大小写的字符串排序。稳定版本在不区分大小写排序的同时,能够正确地保持“apple”和“Apple”的原始顺序。只要元素顺序比主键更重要,就应实现稳定排序。

性能注意事项

排序性能因算法和数据特性而异。Ruby 的默认排序在大多数情况下使用快速排序。本示例对不同的排序方法进行了基准测试。理解性能有助于为大型数据集选择正确的方法。这些测量指导实际应用程序中的优化决策。

performance.rb
require 'benchmark'

# Generate large dataset
large_array = Array.new(100_000) { rand(1..100_000) }

Benchmark.bm(15) do |x|
  x.report("sort:") { large_array.sort }
  x.report("sort_by:") { large_array.sort_by { |n| n } }
  x.report("reverse sort:") { large_array.sort.reverse }
  x.report("sort! (in-place):") { large_array.dup.sort! }
end

# Additional example: Pre-sorted array
sorted_array = (1..100_000).to_a
Benchmark.bm(15) do |x|
  x.report("sorted sort:") { sorted_array.sort }
  x.report("shuffled sort:") { sorted_array.shuffle.sort }
end

基准测试表明,在简单的数字排序方面,sortsort_by 的性能相似。通过 sort! 进行的就地排序可以通过避免数组复制来稍微快一些。反向排序对基本排序操作的开销很小。

预排序数组比随机数组排序更快,因为算法得到了优化。示例表明,Ruby 的排序实现能够检测到接近排序的数据。对于关键的性能需求,请考虑 Ruby 内置方法之外的专用数据结构或算法。

高级:实现自定义排序算法

虽然 Ruby 的内置方法足以满足大多数需求,但实现排序算法可以展示计算机科学的基础知识。本示例展示了 Ruby 中的冒泡排序和归并排序实现。理解这些算法可以加深你对排序的认识。这些示例仅用于教育目的,而非生产使用。

custom_algorithms.rb
# Bubble sort (O(n^2))
def bubble_sort(array)
  arr = array.dup
  n = arr.length
  loop do
    swapped = false
    (n-1).times do |i|
      if arr[i] > arr[i+1]
        arr[i], arr[i+1] = arr[i+1], arr[i]
        swapped = true
      end
    end
    break unless swapped
  end
  arr
end

# Merge sort (O(n log n))
def merge_sort(array)
  return array if array.size <= 1

  mid = array.size / 2
  left = merge_sort(array[0...mid])
  right = merge_sort(array[mid..-1])

  merge(left, right)
end

def merge(left, right)
  result = []
  until left.empty? || right.empty?
    result << (left.first <= right.first ? left.shift : right.shift)
  end
  result + left + right
end

numbers = [5, 3, 8, 1, 9, 4]
puts "Original: #{numbers}"
puts "Bubble sort: #{bubble_sort(numbers)}"
puts "Merge sort: #{merge_sort(numbers)}"
puts "Ruby sort: #{numbers.sort}"

# Additional example: Quick sort
def quick_sort(array)
  return array if array.size <= 1
  pivot = array.delete_at(rand(array.size))
  left, right = array.partition { |x| x < pivot }
  quick_sort(left) + [pivot] + quick_sort(right)
end

puts "\nQuick sort: #{quick_sort(numbers.dup)}"

冒泡排序展示了最简单的排序概念,但性能较差,为 O(n²)。归并排序展示了一种更有效的 O(n log n) 分而治之的方法。示例包括用于合并已排序子数组的辅助方法。

快速排序是另一种 O(n log n) 算法,由于引用局部性,在实践中通常更快。附加示例展示了如何通过随机选择枢轴来实现它。所有自定义排序都匹配 Ruby 的内置结果,但在大多数 Ruby 应用程序中,它们都用于教育目的而不是实际用途。

最佳实践

对于复杂排序,请使用 sort_by 来受益于 Schwartzian 变换优化。在适当的情况下,实现 <=> 来进行自然对象排序。考虑稳定性要求,如果需要,请添加索引键。对于大型数据集,请对不同方法进行基准测试以找到最佳性能。请记住,Ruby 的内置方法对于大多数用例都经过高度优化。

资料来源

从这些资源中了解更多:Ruby 数组文档Enumerable 模块,以及排序算法

作者

我叫 Jan Bodnar,是一名热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直撰写编程文章。迄今为止,我已撰写了 1,400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。