Ruby 数组排序
最后修改于 2025 年 4 月 2 日
Ruby 提供了强大的数组排序方法,语法简单。本指南探讨了从基础到高级的排序技术、自定义比较和性能考虑。学习如何高效地对数字、字符串、对象和复杂数据结构进行排序。掌握数组排序对于有效的 Ruby 编程至关重要。
基本数组排序
Ruby 的 sort
方法是排序数组最简单的方式。它自然地处理数字和字符串,返回一个新的已排序数组。原始数组保持不变,除非你使用 sort!
。本示例展示了数字和字符串数组的基本排序。理解这些基本概念是掌握更高级排序技术的关键。
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
方法。本示例演示了数字和字符串的这两种方法。反向排序在现实应用中很常见,例如显示最高得分。理解这些技术可以扩展你的排序工具集。
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}"
第一种方法是将 sort
与 reverse
链接起来,这种方法很清晰,但会创建一个中间数组。第二种方法使用带负值的 sort_by
,对于大型数组来说更有效。对于字符串,第二种方法需要仔细处理大小写敏感性。
反向排序日期或年份遵循与数字相同的模式。示例显示,在降序排列中,2025 排在 2021 之前。选择最适合你的具体用例和性能要求的方法。
使用块进行自定义排序
Ruby 允许使用 sort
和 sort_by
的块进行自定义排序逻辑。这使得可以根据任何标准进行排序,包括复杂的对象属性。块应返回 -1、0 或 1 进行比较。本示例演示了按字符串长度和自定义数字条件进行排序。自定义排序对于特定的排序需求非常强大。
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
在这里非常有用,可以提取比较键。本示例展示了按多个字段对哈希数组进行排序。类似的技术适用于具有属性访问器的自定义对象。掌握这些技术可以对真实世界的数据结构进行排序。
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 的默认字符串排序是区分大小写的,这通常不是用户期望的。本示例演示了正确的不区分大小写的排序技术。我们将比较朴素的方法和健壮的解决方案。正确的字符串排序对于面向用户的应用程序至关重要。这些方法可以确保无论大小写如何都能保持一致的排序。
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)。此技术对于复杂排序非常有用。
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 类的这两种方法。一致的对象排序可以使面向对象应用程序中的代码更清晰。这些模式适用于任何自定义类。
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 的默认排序不保证稳定,但我们可以在需要时实现稳定排序。本示例演示了一种稳定排序的实现。当次要特征应保持顺序时,稳定排序很重要。该解决方案适用于任何可比较的元素。
# 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 的默认排序在大多数情况下使用快速排序。本示例对不同的排序方法进行了基准测试。理解性能有助于为大型数据集选择正确的方法。这些测量指导实际应用程序中的优化决策。
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
基准测试表明,在简单的数字排序方面,sort
和 sort_by
的性能相似。通过 sort!
进行的就地排序可以通过避免数组复制来稍微快一些。反向排序对基本排序操作的开销很小。
预排序数组比随机数组排序更快,因为算法得到了优化。示例表明,Ruby 的排序实现能够检测到接近排序的数据。对于关键的性能需求,请考虑 Ruby 内置方法之外的专用数据结构或算法。
高级:实现自定义排序算法
虽然 Ruby 的内置方法足以满足大多数需求,但实现排序算法可以展示计算机科学的基础知识。本示例展示了 Ruby 中的冒泡排序和归并排序实现。理解这些算法可以加深你对排序的认识。这些示例仅用于教育目的,而非生产使用。
# 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 本电子书。我在编程教学方面拥有十多年的经验。