Эта статья включает список общих ссылок , но в ней отсутствуют соответствующие встроенные цитаты . ( Февраль 2024 ) |
В программировании преобразование Шварца — это метод, используемый для повышения эффективности сортировки списка элементов. Эта идиома [1] подходит для сортировки на основе сравнения , когда упорядочение фактически основано на упорядочении определенного свойства ( ключа ) элементов, где вычисление этого свойства — интенсивная операция, которая должна выполняться минимальное количество раз. Преобразование Шварца примечательно тем, что оно не использует именованные временные массивы.
Преобразование Шварца — это версия идиомы Lisp, известной как decorate-sort-undecorate , которая избегает повторного вычисления ключей сортировки, временно связывая их с входными элементами. Этот подход похож на memoization , который избегает повторного вычисления ключа, соответствующего определенному входному значению. Для сравнения, эта идиома гарантирует, что ключ каждого входного элемента вычисляется ровно один раз, что все равно может привести к повторению некоторых вычислений, если входные данные содержат повторяющиеся элементы.
Идиома названа в честь Рэндала Л. Шварца , который впервые продемонстрировал ее в Perl вскоре после выпуска Perl 5 в 1994 году. Термин «преобразование Шварца» применялся исключительно к программированию на Perl в течение ряда лет, но позже был принят некоторыми пользователями других языков , таких как Python , для обозначения похожих идиом в этих языках. Однако алгоритм уже использовался в других языках (без определенного названия) до того, как он был популяризирован среди сообщества Perl в форме этой конкретной идиомы Шварцем. Термин «преобразование Шварца» указывает на конкретную идиому, а не на алгоритм в целом.
Например, чтобы отсортировать список слов ("aaaa","a","aa") по длине слова: сначала создайте список (["aaaa",4],["a",1],["aa",2]), затем отсортируйте его по числовым значениям, получив (["a",1],["aa",2],["aaaa",4]), затем отбросьте числа и получите ("a","aa","aaaa"). Это был алгоритм в целом, поэтому он не считается преобразованием. Чтобы сделать это настоящим преобразованием Шварца, это будет сделано в Perl следующим образом:
@sorted = map { $_ -> [ 0 ] } sort { $a -> [ 1 ] <=> $b -> [ 1 ] или $a -> [ 0 ] cmp $b -> [ 0 ] } # Использовать числовое сравнение, вернуться к строковой сортировке на исходной map { [ $_ , length ( $_ )] } # Вычислить длину строки @unsorted ;
Общая форма преобразования Шварца имеет вид:
@sorted = map { $_ -> [ 0 ] } sort { $a -> [ 1 ] cmp $b -> [ 1 ] или $a -> [ 0 ] cmp $b -> [ 0 ] } map { [ $_ , foo ( $_ )] } @unsorted ;
Здесь foo($_)
представлено выражение, которое берет $_
(каждый элемент списка по очереди) и выдает соответствующее значение, которое должно сравниваться вместо него.
Читаем справа налево (или снизу вверх):
@unsorted
передается в map
операцию, которая оборачивает каждый элемент в массив (ссылку на анонимный 2-элементный), состоящий из него самого и вычисленного значения, которое будет определять его порядок сортировки (список элементов становится списком [элемент, значение]);map
подается в sort
, которая сортирует его в соответствии с ранее вычисленными значениями (список [элемент, значение] ⇒ отсортированный список [элемент, значение]);map
операция извлекает значения (из анонимного массива), используемые для сортировки, создавая элементы исходного списка в отсортированном порядке (отсортированный список [элемент, значение] ⇒ отсортированный список элементов).Использование анонимных массивов гарантирует, что память будет освобождена сборщиком мусора Perl сразу после завершения сортировки.
Без преобразования Шварца сортировка в приведенном выше примере была бы записана на Perl следующим образом:
@sorted = sort { foo ( $a ) cmp foo ( $b ) } @unsorted ;
Хотя код короче, наивный подход здесь может быть гораздо менее эффективным, если функция ключа (называемая foo в примере выше) требует больших вычислительных затрат. Это связано с тем, что код внутри скобок оценивается каждый раз, когда необходимо сравнить два элемента. Оптимальная сортировка сравнением выполняет O ( n log n ) сравнений (где n — длина списка) с 2 вызовами foo на каждое сравнение, что приводит к O ( n log n ) вызовам foo . Для сравнения, используя преобразование Шварца, мы делаем только 1 вызов foo на элемент на начальном этапе отображения , что в общей сложности дает n вызовов foo .
Однако если функция foo относительно проста, то дополнительные накладные расходы на преобразование Шварца могут оказаться неоправданными.
Например, чтобы отсортировать список файлов по времени их изменения , наивный подход может быть следующим:
функция naiveCompare(файл a, файл b) { return modificationTime(a) < modificationTime(b) } // Предположим, что sort(list, comparisonPredicate) сортирует заданный список, используя // comparisonPredicate для сравнения двух элементов. sortedArray := sort(filesArray, naiveCompare)
Если время модификации не запоминается для каждого файла, этот метод требует повторного вычисления его каждый раз, когда файл сравнивается в сортировке. Используя преобразование Шварца, время модификации вычисляется только один раз для каждого файла.
Преобразование Шварца включает в себя функциональную идиому, описанную выше, которая не использует временные массивы.
Тот же алгоритм можно записать процедурно, чтобы лучше проиллюстрировать, как он работает, но это требует использования временных массивов и не является преобразованием Шварца. Следующий пример псевдокода реализует алгоритм таким образом:
для каждого файла в filesArray вставить массив (файл, время модификации (файл)) в конец преобразованного массива функция simpleCompare(массив a, массив b) { return a[2] < b[2] } преобразованныйМассив := сортировка(преобразованныйМассив, простоеСравнение) для каждого файла в transformedArray вставить файл[1] в конец sortedArray
Первое известное появление преобразования Шварца в сети — это сообщение Рэндала Шварца от 16 декабря 1994 года в теме в группе новостей Usenet comp.unix.shell , перенесенное в comp.lang.perl. (Текущая версия Perl Timeline неверна и относится к более поздней дате в 1995 году.) Тема началась с вопроса о том, как отсортировать список строк по их «последнему» слову:
прим.:Джошуа Нгadktk:KaLap Тимоти Квонгadmg:Mahalingam Gobieramananadmln:Марта Л. Нангалама
Шварц ответил:
#!/usr/bin/perl require 5 ; # Новые возможности, новые ошибки! print map { $_ -> [ 0 ] } sort { $a -> [ 1 ] cmp $b -> [ 1 ] } map { [ $_ , /(\S+)$/ ] } <> ;
Этот код выдает результат:
admg:Mahalingam Gobieramananadktk:KaLap Тимоти Квонгadmln:Марта Л. Нангаламаприм.:Джошуа Нг
Шварц отметил в посте, что он «разговаривал на языке lisp в Perl», имея в виду происхождение идиомы из языка lisp .
Сам термин «преобразование Шварца» был придуман Томом Кристиансеном в последующем ответе. Более поздние посты Кристиансена ясно дали понять, что он не намеревался давать название конструкции, а просто ссылаться на нее из исходного поста: его попытка наконец назвать ее «Черное преобразование» не прижилась («Черное» здесь — каламбур от «schwar[t]z», что в переводе с немецкого означает черный).
Некоторые другие языки предоставляют удобный интерфейс для той же оптимизации, что и преобразование Шварца:
sort
принимает #:key
аргумент ключевого слова с функцией, которая извлекает ключ, и дополнительный #:cache-keys?
запрос на кэширование полученных значений во время сортировки. Например, удобный способ перетасовать список — .(sort l < #:key (λ (_) (random)) #:cache-keys? #t)
функция сортировки_массивов_пространства ( массив & $a ) : void { array_walk ( $a , функция ( & $v , $k ) { $v = массив ( $v , $k ); }); сортировка ( $a ); array_walk ( $a , функция ( & $v , $_ ) { $v = $v [ 0 ]; }); }
@a . sort ( { $^a . Str } ) # или короче: @a.sort(*.Str)
@a . sort ( { $^a . Str cmp $^b . Str } )
sortOn
из базовой библиотеки выполняет преобразование Шварца.Эта идиома также известна как «преобразование Шварца» по аналогии с родственной идиомой Perl.