跳轉到內容

演算法實現/排序/施瓦茨變換

來自華夏公益教科書,開放的書籍,開放的世界

Perl 的緊湊列表操作函式可以在單個語句中執行整個變換,儘管它必須反向編寫

@sorted_array = 
       map  { $_->[0] }              # extract original list elements
       sort { $a->[1] <=> $b->[1] }  # sort list by keys
       map  { [$_, -M $_] }          # pair up list elements with keys
       @files_array;

這是一個完整的施瓦茨變換,展示了基於大小和修改時間記憶鍵的多級排序。

my @sorted_files =
  map $_->[0], # extract original name
  sort {
       $a->[1] <=> $b->[1] # sort first numerically by size (smallest first)
    or $b->[2] <=> $a->[2] # then numerically descending by modtime age (oldest first)
    or $a->[0] cmp $b->[0] # then stringwise by original name
  }
  map [$_, -s $_, -M $_], # compute tuples of name, size, modtime
  glob "*"; # of all files in the directory

Python 程式設計師在排序操作可能很昂貴的排序中使用變換。

在這個例子中,f(x) 返回一個適合用於排序的鍵。例如,它可能返回 x 的長度,或者它可能根據 x 的值執行資料庫查詢。

# python2.4
new_list = sorted(old_list, key=f)

如果需要二級和更高階的排序鍵,鍵函式可以返回一個元組值,利用元組首先按第一個鍵排序,然後按第二個鍵排序(如果第一個鍵相同),依此類推。如果鍵的預設排序不夠,可以使用一個比較函式,並使用一個額外的關鍵字引數 cmp,例如

# python2.4; removed in python3.0
new_list = sorted(old_list, key=f, cmp=lambda a,b:cmp(a[0],b[0]) or cmp(b[1],a[1]))

注意,鍵函式 f 應該執行昂貴的操作,因為它只對每個鍵呼叫一次,而比較函式在排序演算法需要比較兩個元素時被呼叫。在 Python 3.0 中不能再使用比較函式;只可以用鍵。函式 sorted 是 Python 2.4 中的新功能,在此之前,只有方法 sort 可用,需要多個語句。鍵函式也是 Python 2.4 中的新功能,因此在 Python 2.3 中,必須以類似以下方式編寫變換,利用元組的第一個元素將首先用於比較

# python2.3
new_list = [(f(x), x) for x in old_list]
new_list.sort()
new_list = [x[1] for x in new_list]

如果需要,也可以在此處為排序提供比較函式(Python 2.4;在 Python 3.0 中刪除)。

以下是與 Perl 習慣用法類似的版本(除了結尾處的額外括號),再次利用元組的排序,將函式結果放在首位

new_list = map(lambda x: x[1],
           sorted(
           map(lambda x: (f(x), x),
               old_list)))

zip 內建函式可以用來代替內聯 lambda 函式,給出

new_list = zip(*
           sorted(
           zip(map(f, old_list), 
               old_list)))[1]

Ruby 程式設計師有自己的快捷方式。

new_list = old_list.sort_by {|file| file.mtime }

然而,這不是完整的施瓦茨變換,因為記憶鍵的比較是固定的。(雖然有一個解決方法。)

完整的施瓦茨變換

[編輯 | 編輯原始碼]

真正的施瓦茨變換還詳細說明了如何比較各種記憶鍵(作為字串,作為數字),包括惰性邏輯,它可以避免在二級和三級鍵中進行比較,如果主要鍵的區分就足夠了。

sorted_files = Dir["*"].                         # Get all files
                 collect{|f| [f, test(?s, f), test(?M, f)]}.   # compute tuples of name, size, modtime
                 sort {|a, b|                    # sort
                   a[1] <=> b[1] or              #   -- by increasing size
                   b[2] <=> a[2] or              #   -- by age descending
                   a[0] <=> b[0]                 #   -- by name
                 }.collect{|a| a[0]}             # extract original name

注意,由於 collect、sort 等都是方法呼叫,因此操作從左到右讀取,而不是像原始 Perl 中那樣從右到左讀取。

現在,讓我們來看一下使快捷方法能夠實現完整變換的所有功能的技巧。這個技巧是,在 Ruby 中,物件知道如何排序自身,並且陣列按字典順序排序(即透過比較第一個值、然後比較第二個值等等)。這意味著可以構造一些值,這些值將透過相當複雜的邏輯進行排序自身。

# Here is the helper class.
class InReverse
  include Comparable
  
  attr :value
  
  def initialize (value)
    @value = value
  end
  
  def <=> (other)
    other.value <=> @value
  end
end

# And now we can do the complex transform using sort_by.
sorted_files = Dir["*"].sort_by{|f| [test(?s, f), InReverse.new(test(?M, f)), f]}

這是 Haskell 中按元素的變換版本進行比較的慣用方式

import Data.List (sortBy)
import Data.Ord (comparing)

sortOn f = sortBy (comparing f)

這裡,“比較”函式接受一個函式,並返回一個基於比較函式應用結果的比較函式。它是這樣定義的

comparing f x y = compare (f x) (f y)

這裡有一個完全模仿傳統 Perl 習慣用法的版本

import Data.List (sortBy)
import Data.Ord (comparing)

sortOn' f = map fst . sortBy (comparing snd) . map (\x -> (x, f x))

但是,我們可以利用元組首先按第一個元素排序,然後按第二個元素排序,依此類推,將函式結果放在第一個元素位置,然後使用預設比較器

import Data.List (sort)

sortOn'' f = map snd . sort . map (\x -> (f x, x))

由於元組比較要求兩種元素型別都是Ord的例項,然而,最後這個實現有一個微妙的不同型別

sortOn, sortOn' :: (Ord b) => (a -> b) -> [a] -> [a]
sortOn'' :: (Ord a, Ord b) => (a -> b) -> [a] -> [a]

這可能是 OCaml 中最簡單的方法

let comparing f x y = compare (f x) (f y)

let newList = List.sort (comparing f) oldList

這裡有一個完全模仿傳統 Perl 習慣用法的版本(除了額外的括號)

let comparing f x y = compare (f x) (f y)

let newList = List.map fst (
              List.sort (comparing snd) (
              List.map (fun x -> x, f x)
                oldList))

但是,我們可以利用元組首先按第一個元素排序,然後按第二個元素排序,依此類推,將函式結果放在第一個元素位置,然後使用預設比較器

let newList = List.map snd (
              List.sort compare (
              List.map (fun x -> f x, x)
                oldList))

Javascript

[編輯 | 編輯原始碼]

在現代版本的 Javascript 中(Firefox 1.5、Internet Explorer 9、Opera 9.6、Chrome、Safari 3 等;在 ECMAScript 第 5 版中標準化),陣列在其原型中有一個 map 函式,允許人們輕鬆地編寫施瓦茨變換

function sortOn(arr, fun) {
    return arr.map(function (x) {
	    return [ x, fun(x) ];
	}).sort(function (a, b) {
            // If fun(a) always returns a number, this can be replaced by: return a[1] - b[1];
	    return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
	}).map(function (x) {
	    return x[0];
	});
}

示例用法

js> print(sortOn([ 'Charlie', 'alice', 'BOB' ], function (s) { return s.toUpperCase(); }));
alice,BOB,Charlie

對於舊版瀏覽器,可以使用 相容性地圖原型函式


華夏公益教科書