开发者

Problem with sorting algorithm in RailsWizard

RailsWizard, a tool to generate rails templates, allows setting order of recipes by specifying which recipes should run before (run_before option) and after (run_after) given recipe, as described in https://github.com/intridea/rails_wizard/wiki/Recipe-Configuration

This is implemented in Recipe class:

module RailsWizard
  class Recipe
    extend Comparable

    def self.<=>(another)
      return -1 if another.run_after.开发者_运维百科include?(self.key) || self.run_before.include?(another.key)
      return 1 if another.run_before.include?(self.key) || self.run_after.include?(another.key)
      self.key <=> another.key # simple sort by name
    end
  end
end

However, this algorithm often provides a wrong order, see corresponding issue and a demo script for this problem: https://gist.github.com/987025

How would you fix this algorithm?


The problem with the algorithm is that the comparison operator does not define a Strict Weak Ordering.

Specifically, it lacks transitivity (if x < y, and y < z, then x < z). In your example gist:

  • n < l by the before instruction
  • l < m by the sort by keys
  • m < n by the sort by keys

That last one, m < n, is a contradiction, since by transitivity, n < l < m. This breaks your sort.

We first need to fix the operator so that it never provides contradictory information. Second, I think we need to either propagate the transitive ordering so that transitivity is maintained across non-adjacent items, or we need to use an O(n^2) algorithm that compares every item against every other item.

Selection sort meets that criterion, so here's your code using selection sort, and a before? comparison operator.

require 'yaml'
hash = YAML.load '
a:
  after: 
  before: 
l:
  after: 
  before: 
n:
  after: 
  before: l
b:
  after: 
  before: 
m:
  after: 
  before: 
c:
  after: 
  before: 
z:
  after: 
  before: 
'
hash.each do |k,v|
  v.each do |vk,vv|
    v[vk] = (vv||'').split
  end
end

class Hash
  def without *ks
    delete_if { |k,v| ks.include? k }
  end
  def delete! *ks
    #print "\n\nwithout: #{ks.join(', ')}"
    replace without *ks
  end
end

def print_keys hash
  puts hash.map(&:first).join('  ')
end

def sort hash
  array = hash.to_a
  selection_sort array
  print_keys array
  Hash[array]
end

def selection_sort array
  (0...array.length).each do |i|
    min = i
    ((i+1)...array.length).each do |j|
      min = j if before?(array[j], array[min])
    end
    temp = array[min]
    array[min] = array[i]
    array[i] = temp
  end
end

def before? a, b
  b.last['after'].include?(a.first) || a.last['before'].include?(b.first)
end

def test hash
  puts nil,nil
  print_keys hash
  puts '-'*hash.count*3
  hash.count.times { hash = sort hash }
end

test hash

And this gives:

$ ruby sort_test.rb 


a  l  n  b  m  c  z
---------------------
a  n  l  b  m  c  z
a  n  l  b  m  c  z
a  n  l  b  m  c  z
a  n  l  b  m  c  z
a  n  l  b  m  c  z
a  n  l  b  m  c  z
a  n  l  b  m  c  z
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜