Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 753
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
306 Глава 14 • Работа со списками сконцентрироваться на xs как на источнике сопоставления с образцом.
Самое частое сопоставление в отношении списков просто отличает пустой список от непустого. Итак, для метода append вырисовывается следующая схема:
def append[T](xs: List[T], ys: List[T]): List[T] =
xs match case List() => ???
case x :: xs1 => ???
Остается лишь заполнить два места, обозначенных
???
1
. Первое такое ме
сто — альтернатива для случая, когда входной список xs пуст. В таком случае результатом конкатенации будет второй список:
case List() => ys
Второе место, оставленное незаполненным, — альтернатива для случая, когда входной список xs состоит из некоего head
элемента x
, за которым следует остальная часть xs1
. В таком случае результатом тоже будет непустой спи
сок. Чтобы сконструировать непустой список, нужно знать, какой должна быть его «голова» (head), а каким — «хвост» (tail). Вам известно, что первый элемент получающегося в результате списка — x
. Что касается остальных элементов, то их можно вычислить, добавив второй список, ys
, к оставшейся части первого списка, xs1
Это завершает проектирование и дает:
def append[T](xs: List[T], ys: List[T]): List[T] =
xs match case List() => ys case x :: xs1 => x :: append(xs1, ys)
Вычисление второй альтернативы — иллюстрация той части принципа, ко
торая называется «властвуй»: сначала нужно продумать форму желаемого результата, затем вычислить отдельные части этой формы, используя, где возможно, рекурсивные вызовы алгоритма. И наконец, сконструировать из этих частей вывод.
1
???
— это метод, который генерирует ошибку scala.NotImplementedError и имеет результирующий тип
Nothing
. Он может применяться в качестве временной реа
лизации в процессе разработки приложения.
14 .6 . Методы первого порядка класса List 307
Получение длины списка: length
Метод length вычисляет длину списка:
List(1, 2, 3).length // 3
Определение длины списков, в отличие от массивов, — довольно затратная операция. Чтобы ее выполнить, нужно пройти по всему списку в поисках его конца, и на это затрачивается время, пропорциональное количеству элементов списка. Поэтому вряд ли имеет смысл заменять xs.isEmpty вы
ражением xs.length
==
0
. Результаты будут получены одинаковые, но второе выражение будет выполняться медленнее, в частности, если список xs имеет большую длину.
Обращение к концу списка: init и last
Вам уже известны основные операции head и tail
, в результате выполне
ния которых извлекаются соответственно первый элемент списка и весь остальной список, за исключением первого элемента. У каждой из них есть обратная по смыслу операция: last возвращает последний элемент непустого списка, а init
— список, состоящий из всех элементов, за исключением по
следнего:
val abcde = List('a', 'b', 'c', 'd', 'e')
abcde.last // e abcde.init // List(a, b, c, d)
Подобно методам head и tail
, эти методы, примененные к пустому списку, генерируют исключение:
scala> List().init java.lang.UnsupportedOperationException: init of empty list at ...
scala> List().last java.util.NoSuchElementException: last of empty list at ...
В отличие от head и tail
, на выполнение которых неизменно затрачивается одно и то же время, для вычисления результата методы init и last должны обойти весь список. То есть их выполнение требует времени, пропорцио
нального длине списка.
308 Глава 14 • Работа со списками
Неплохо было бы организовать ваши данные таким образом, чтобы основная часть обращений приходилось на головной, а не на послед
ний элемент списка.
Реверсирование списков: reverse
Если в какойто момент вычисления алгоритма требуется часто обращаться к концу списка, то порой более разумным будет сначала перестроить список в обратном порядке и работать уже с результатом. Получить такой список можно следующим образом:
abcde.reverse // List(e, d, c, b, a)
Как и все остальные операции со списками, reverse создает новый список, а не изменяет тот, с которым работает. Поскольку списки неизменяемы, сде
лать это все равно бы было невозможно. Чтобы убедиться в этом, убедитесь, что исходный список abcde после операции reverse не изменился:
abcde // List(a, b, c, d, e)
Операции reverse
, init и last подчиняются ряду законов, с помощью кото
рых можно анализировать вычисления и упрощать программы.
1. Операция reverse является собственной инверсией:
xs.reverse.reverse равно xs
2. Операция reverse превращает init в tail
, а last в head
, за исключением того, что все элементы стоят в обратном порядке:
xs.reverse.init равно xs.tail.reverse xs.reverse.tail равно xs.init.reverse xs.reverse.head равно xs.last xs.reverse.last равно xs.head
Реверсирование можно реализовать, воспользовавшись конкатенацией (
:::
), как в следующем методе по имени rev
:
def rev[T](xs: List[T]): List[T] =
xs match case List() => xs case x :: xs1 => rev(xs1) ::: List(x)
14 .6 . Методы первого порядка класса List 309
Но этот метод, вопреки предположениям, менее эффективен. Чтобы убе
диться в высокой вычислительной сложности rev
, представьте, будто спи
сок xs имеет длину n. Обратите внимание: придется делать n рекурсивных вызовов rev
. Каждый вызов, за исключением последнего, влечет за собой конкатенацию списков. На конкатенацию xs
:::
ys затрачивается время, пропорциональное длине ее первого аргумента xs
. Следовательно, общая вычислительная сложность rev выражается так:
n + (n – 1) + ... + 1 = (1 + n)
× n / 2.
Иными словами, rev имеет квадратичную вычислительную сложность по длине его входного аргумента. Если сравнить это обстоятельство со стандарт
ным реверсированием изменяемого связного списка, имеющего линейную вычислительную сложность, то оно вызывает разочарование. Но данная реа
лизация rev
— не самая лучшая из возможных. Пример, который начинается на с. 321, позволит вам увидеть, как можно ускорить все это.
Префиксы и суффиксы: drop, take и splitAt
Операции drop и take обобщают tail и init в том смысле, что возвращают произвольные префиксы или суффиксы списка. Выражение xs.take(n)
возвращает первые
n
элементов списка xs
. Если
n
больше xs.length
, то воз
вращается весь список xs
. Операция xs.drop(n)
возвращает все элементы списка xs
, за исключением первых
n
элементов. Если
n
больше xs.length
, то возвращается пустой список.
Операция splitAt разбивает список по заданному индексу, возвращая пару из двух списков
1
. Она определяется следующим равенством:
xs.splitAt(
n
) равно (xs.take(
n
), xs.drop(
n
))
Но операция splitAt избегает двойного прохода по элементам списка. При
меры применения этих трех методов выглядят следующим образом:
abcde.take(2) // List(a, b)
abcde.drop(2) // List(c, d, e)
abcde.splitAt(2) // (List(a, b),List(c, d, e))
1
Как уже упоминалось в разделе 10.12, понятие пары — неформальное название для
Tuple2.
310 Глава 14 • Работа со списками
Выбор элемента: apply и indices
Произвольный выбор элемента поддерживается методом apply
, но эта опе
рация менее востребована, чем аналогичная операция для массивов:
abcde.apply(2) // в Scala используется довольно редко
Что же касается всех остальных типов, то подразумевается, что apply встав
ляется в вызове метода, когда объект появляется в позиции функции. По
этому показанную ранее строку кода можно сократить до следующей:
abcde(2) // в Scala используется довольно редко
Одной из причин того, что выбор произвольного элемента менее популярен для списков, чем для массивов, является то, что на выполнение кода xs(n)
затрачивается время, пропорциональное величине значения индекса
n
. Фак
тически метод apply определен сочетанием методов drop и head
:
xs.apply(
n
) равно (xs.drop(
n
)).head
Из этого определения также становится понятно, что индексы списков, как и индексы массивов, задаются в диапазоне от 0 до длины списка минус один.
Метод indices возвращает список, состоящий из всех допустимых индексов заданного списка:
abcde.indices // Диапазон от 0 до 5
Линеаризация списка списков: flatten
Метод flatten принимает список списков и линеаризует его в единый спи
сок:
List(List(1, 2), List(3), List(), List(4, 5)).flatten
// List(1, 2, 3, 4, 5)
fruit.map(_.toList).flatten
// List(a, p, p, l, e, s, o, r, a, n, g, e,
// s, p, e, a, r, s)
Он применим только к тем спискам, все элементы которых являются спи
сками. Попытка линеаризации других списков выдаст ошибку компиляции:
scala> List(1, 2, 3).flatten
1 |List(1, 2, 3).flatten
| ˆ
14 .6 . Методы первого порядка класса List 311
| No implicit view available from
| Int => IterableOnce[B]
| where, B is a type variable.
Объединение списков: zip и unzip
Операция zip получает два списка и формирует список из пар их значений:
abcde.indices.zip(abcde)
// Vector((0,a), (1,b), (2,c), (3,d), (4,e))
Если списки разной длины, то все элементы без пары отбрасываются:
val zipped = abcde.zip(List(1, 2, 3))
// List((a,1), (b,2), (c,3))
Особо полезен вариант объединения в пары списка с его индексами. Наи
более эффективно оно выполняется с помощью метода zipWithIndex
, со
ставляющего пары из каждого элемента списка и той позиции, в которой он появляется в этом списке.
abcde.zipWithIndex
// List((a,0), (b,1), (c,2), (d,3), (e,4))
Любой список кортежей можно превратить обратно в кортеж списков с по
мощью метода unzip
:
zipped.unzip // (List(a, b, c),List(1, 2, 3))
Методы zip и unzip реализуют один из способов одновременной работы с несколькими списками. Более эффективный способ сделать то же самое показан в разделе 14.9.
Отображение списков: toString и mkString
Операция toString возвращает каноническое строковое представление списка:
abcde.toString // List(a, b, c, d, e)
Если требуется иное представление, то можно воспользоваться методом mkString
. Операция xs mkString
(pre,
sep,
post)
задействует четыре опе
ранда: отображаемый список xs
, префиксную строку pre
, отображаемую перед всеми элементами, строковый разделитель sep
, отображаемый между
1 ... 29 30 31 32 33 34 35 36 ... 64
312 Глава 14 • Работа со списками последовательно выводимыми элементами, и постфиксную строку, отобра
жаемую в конце.
Результатом операции будет следующая строка:
pre + xs(0) + sep + . . . + sep + xs(xs.length - 1) + post
У метода mkString имеется два перегруженных варианта, которые позволяют отбрасывать некоторые или все его аргументы. Первый вариант получает только строковый разделитель:
xs.mkString(sep) равно xs.mkString("", sep, "")
Второй вариант позволяет опустить все аргументы:
xs.mkString равно xs.mkString("")
Рассмотрим несколько примеров:
abcde.mkString("[", ",", "]") // [a,b,c,d,e]
abcde.mkString("") // abcde abcde.mkString // abcde abcde.mkString("List(", ", ", ")") // List(a, b, c, d, e)
Есть также вариант mkString
, называющийся addString
; он не возвра
щает созданную строку в качестве результата, а добавляет ее к объекту
StringBuilder
1
:
val buf = new StringBuilder abcde.addString(buf, "(", ";", ")") // (a;b;c;d;e)
Методы mkString и addString наследуются из супертрейта
Iterable класса
List
, поэтому их можно применять ко всем другим коллекциям.
Преобразование списков: iterator, toArray, copyToArray
Чтобы выполнить преобразование данных между линейным миром массивов и рекурсивным миром списков, можно воспользоваться методом toArray в классе
List и методом toList в классе
Array
:
val arr = abcde.toArray // Array(a, b, c, d, e)
arr.toList // List(a, b, c, d, e)
1
Имеется в виду класс scala.StringBuilder
, а не java.lang.StringBuilder
14 .6 . Методы первого порядка класса List 313
Есть также метод copyToArray
, который копирует элементы списка в после
довательные позиции массива внутри некоего результирующего массива.
Операция xs.copyToArray(arr, start)
копирует все элементы списка xs в массив arr
, начиная с позиции start
Нужно обеспечить достаточную длину результирующего массива arr
, чтобы в нем мог поместиться весь список. Рассмотрим пример:
val arr2 = new Array[Int](10)
// Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
List(1, 2, 3).copyToArray(arr2, 3)
arr2 // Array(0, 0, 0, 1, 2, 3, 0, 0, 0, 0)
И наконец, если нужно получить доступ к элементам списка через итератор, то можно воспользоваться методом iterator
:
val it = abcde.iterator it.next() // a it.next() // b
Пример: сортировка слиянием
Ранее представленная сортировка вставками записывается кратко, но эф
фективность ее невысока. Ее усредненная вычислительная сложность про
порциональна квадрату длины входного списка. Более эффективен алгоритм сортировки слиянием.
УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ
Этот пример — еще одна иллюстрация карринга и принципа «разделяй и властвуй» . Кроме того, в нем говорится о вычислительной сложности алгоритма, что может оказаться полезным . Если же вы хотите поскорее перейти к практике, то раздел 14 .7 можно спокойно пропустить .
Сортировка слиянием работает следующим образом: если список имеет один элемент или не имеет никаких элементов, то он уже отсортирован, поэтому может быть возвращен в исходном виде. Более длинные списки разбива
ются на два подсписка, в каждом из которых содержится около половины элементов исходного списка. Каждый подсписок сортируется с помощью рекурсивного вызова функции sort
, затем два отсортированных списка объ
единяются в ходе операции слияния.