Нахождение минимума и максимума¶
Задача¶
В наборе узлов требуется найти узел (или узлы) с минимальным (или максимальным) числовым значением.
Решение¶
XPath 1.0¶
В проекте EXSLT определены следующие функции для выполнения этих операций: exsl:min
, exsl:max
, exsl:lowest
и exsl:highest
. Функции min
и max
ищут узел с минимальным и максимальным числовым значением соответственно.
В EXSLT функция exsl:min
специфицирована следующим образом:
- Минимальное значение определяется так: набор узлов, переданный в качестве аргумента, сортируется в порядке возрастания, как это сделала бы команда
xsl:sort
в применении к типу данныхnumber
. Минимумом считается результат преобразования строкового значения первого узла в отсортированном списке в число с помощью функцииnumber
. - Если набор узлов пуст или преобразование строкового значения любого узла в число дает результат
NaN
, возвращаетсяNaN
.
Функция exsl:max
определяется аналогично. На сайте EXSLT предлагаются реализации на чистом XSLT, буквально отражающие эти определения (пример 3.8).
Пример 3.8. Реализации функций min
и max
, следующие непосредственно из определений EXSLT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Возможно, вам непонятен смысл значения по умолчанию параметра nodes(select="/..")
. Это просто идиоматический способ инициализировать пустой набор узлов (у корневого узла по определению не существует родителя).
Хотя в определениях ckbk:min
и ckbk:max
используется команда xsl:sort
, реализацию не обязательно строить именно так. Если ваш процессор XSLT оптимизирует хвостовую рекурсию, то следующий вариант окажется более эффективным (пример 3.9).
Пример 3.9. Реализация функций min
и max
с помощью метода «разделяй и властвуй»
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Как правило, приведенные выше реализации работают быстрее, чем основанные на команде xsl:sort
. Но в некоторых вырожденных случаях они оказываются медленнее. Причина в том, что эффективность достигается за счет исключения из рассмотрения в среднем половины узлов на каждом шаге рекурсии. Однако возможна ситуация, когда на каждом проходе aNode
является минимальным (в случае ckbk:max
) или максимальным (в случае ckbk:min
) узлом. Если это так, то при каждом рекурсивном вызове будет удаляться только один узел, и производительность окажется очень низкой. К счастью, данные чаще всего бывают либо предварительно упорядочены, либо случайны, а в этих случаях быстродействие будет на высоте.
Реализация EXSLT, основанная на xsl:sort
, корректно обрабатывает нечисловые значения. Дело в том, что стандарт XSLT требует, чтобы в случае, когда тип данных равен number
, нечисловые значения предшествовали числовым после сортировки.
Не пытайтесь использовать выражение not(number($var))
для проверки на NaN
! Я часто ловил себя на этой ошибке, которая на первый взгляд выглядит совершенно корректно. Функция number
не проверяет, что аргумент является числом, а пытается преобразовать его в число. А это совсем не то, что вам нужно, – такая проверка сообщит, что 0
– не число, поскольку 0
преобразуется в false
. Правильное условие выглядит так: number($var) != number($var)
. Она работает, потому что NaN
никогда не равно NaN
, тогда как всякое число равно само себе. Не поддавайтесь искушению сократить это условие до number($var) != $var
. Это будет работать только, если $var
не является пустым набором узлов. Если хотите, можете применить совсем прямолинейный подход: string(numer($var)) = 'NaN'
.
В EXSLT функция ckbk:lowest
определена следующим образом:
- Функция
ckbk:lowest
возвращает те узлы из набора узлов, значения которых минимальны в данном наборе. Минимум по набору узлов вычисляется так же, как в функцииckbk:min
. Узел имеет такое минимальное значение, если результат преобразования его строкового значения в число, как это делает функцияnumber
, равен минимальному значению, причем равенство определяется в терминах числового сравнения оператором=
. - Если хотя бы один узел в наборе имеет нечисловое значение, то функция
ckbk:min
возвращаетNaN
. Из определения сравнения чисел следует, чтоNaN != NaN
. Поэтому, если хотя бы один узел в наборе имеет нечисловое значение, то функцияckbk:lowest
возвращает пустой набор узлов. Предлагаемая на сайте EXSLT реализация дословно следует этому определению, что может оказаться не слишком эффективно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Команда xsl:if
проверяет, имеются ли в наборе узлы с нечисловыми значениями. Далее узлы сортируются для нахождения минимума, а затем собираются все узлы, значения которых совпадают с найденным минимумом. В примере 3.10 приведен код, в котором повторно используется функция ckbk:min
, что делает сортировку ненужной.
Пример 3.10. Реализация lowest
с повторным использованием ckbk:min
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Наконец, ckbk:lowest
можно реализовать с одним проходом по набору узлов, если вы готовы отказаться от повторного использования ckbk:min
(пример 3.11).
Пример 3.11. Реализация lowest
без использования ckbk:min
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
Интересно, что эта реализация работает хуже, возможно, из-за дополнительного копирования. В тестах, содержащих 10000 узлов с различным распределением данных (отсортированные в порядке возрастания, в порядке убывания, полуслучайные и случайные), реализация, основанная на ckbk:min
, работала быстрее альтернативной в среднем на 40% (а часто и еще лучше). Рекурсивная реализация без использования ckbk:min
оказалась на 24% медленнее.
Определение и реализации функции ckbk:highest
получаются из ckbk:lowest
обращением условий сравнения. Особо я их обсуждать не буду.
XSLT 2.0¶
В XPath 2.0 уже встроены функции min
и max
.
Обсуждение¶
Минимальное и максимальное значение в наборе узлов можно найти с помощью простых выражений XPath <xsl:value-of select="$nodes[not($nodes < .)]"/>
и <xsl:value-of select="$nodes[not($nodes > .)]"/>
соответственно. На обычном языке это означает: «Отобрать все узлы, для которых не существует узла с меньшим значением» и «Отобрать все узлы, для которых не существует узла с большим значением».
Однако вычислительная сложность этих, по видимости очень простых, выражений составляет O(N2), где N – количество узлов. Поэтому, если вы не уверены, что узлов будет заведомо немного, не прибегайте к такой идиоме. Впрочем, иногда другого выхода просто не остается, например, если нужно найти min/max внутри атрибута select
команды xsl:sort
или атрибута use
команды xsl:key
(для которых нельзя вызвать шаблон).
В одной из публикаций по XSLT предлагается следующая реализация отыскания минимумов, которая, как утверждается, более эффективна, чем основанная на xsl:sort
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Но это не так ни в одной из реализаций XSLT, которые я тестировал, и я просто не могу себе представить, при каких условиях это было бы правдой. Причина, по которой эта реализация, скорее всего, будет работать медленнее, заключается в том, что на каждом шаге из рассмотрения исключается всего один узел. Даже при оптимизации хвостовой рекурсии число операций копирования узлов слишком велико. Можно легко впасть в заблуждение, думая, что такое рекурсивное решение так же эффективно, как итерация по индексированному массиву в языке C или Java. Однако увеличение индекса – совсем не то же самое, что создание нового набора узлов путем удаления из исходного начального элемента. А именно это мы и делаем в выражении $nodes[position() > 1]
.
Во многих случаях, когда необходимо найти минимум, требуется также и максимум. Хорошо бы иметь функцию, которая дает оба значения по цене одного. Ее можно написать ценой небольшого усложнения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
|
Тестирование показывает, что и этот подход превосходит по скорости решение, основанное на сортировке.
При обсуждении минимумов и максимумов мы рассматривали только один частный случай: когда узлы содержат числа. Более общая задача – найти минимум и максимум из значений функции, определенной на узлах. Возьмем, к примеру, случай, когда есть множество положительных и отрицательных чисел, а нужно найти минимальный квадрат значения. Хотя эту задачу легко решить путем небольшой модификации показанного выше кода, хотелось бы иметь единую повторно используемую реализацию. В главе 14 мы вернемся к этой проблеме и опишем несколько способов создания обобщенных решений.