Разделить список на несколько списков с фиксированным количеством элементов

119

Как разбить список элементов на списки, содержащие не более N элементов?

Пример: учитывая список из 7 элементов, создайте группы из 4, оставив последнюю группу, возможно, с меньшим количеством элементов.

split(List(1,2,3,4,5,6,"seven"),4)

=> List(List(1,2,3,4), List(5,6,"seven"))
Джонни Эверсон
источник

Ответы:

213

Думаю, ты ищешь grouped. Он возвращает итератор, но вы можете преобразовать результат в список,

scala> List(1,2,3,4,5,6,"seven").grouped(4).toList
res0: List[List[Any]] = List(List(1, 2, 3, 4), List(5, 6, seven))
Киптон Баррос
источник
25
В списках Scala есть что-то для всего.
J Atkin
У меня странный вопрос. В том же случае, если я конвертирую данные в последовательность, я получаю объект Stream. Это почему?
Rakshith
3
@Rakshith Звучит как отдельный вопрос. В Scala есть загадочный гном, который выбирает структуру данных, и он выбрал для вас поток. Если вам нужен список, вы должны запросить список, но вы также можете просто доверять мнению гнома.
Ион Фриман
12

Есть гораздо более простой способ выполнить задачу, используя метод скольжения. Это работает так:

val numbers = List(1, 2, 3, 4, 5, 6 ,7)

Допустим, вы хотите разбить список на более мелкие списки размером 3.

numbers.sliding(3, 3).toList

дам тебе

List(List(1, 2, 3), List(4, 5, 6), List(7))
Доржи
источник
9

Или, если вы хотите сделать свой собственный:

def split[A](xs: List[A], n: Int): List[List[A]] = {
  if (xs.size <= n) xs :: Nil
  else (xs take n) :: split(xs drop n, n)
}

Использование:

scala> split(List(1,2,3,4,5,6,"seven"), 4)
res15: List[List[Any]] = List(List(1, 2, 3, 4), List(5, 6, seven))

edit : просмотрев это 2 года спустя, я бы не рекомендовал эту реализацию, поскольку sizeэто O (n), и, следовательно, этот метод - O (n ^ 2), что объясняет, почему встроенный метод становится быстрее для больших списков, как указано в комментариях ниже. Вы можете эффективно реализовать следующее:

def split[A](xs: List[A], n: Int): List[List[A]] =
  if (xs.isEmpty) Nil 
  else (xs take n) :: split(xs drop n, n)

или даже (немного) более эффективно, используя splitAt:

def split[A](xs: List[A], n: Int): List[List[A]] =
  if (xs.isEmpty) Nil 
  else {
    val (ys, zs) = xs.splitAt(n)   
    ys :: split(zs, n)
  }
Луиджи Плиндж
источник
4
xs splitAt nявляется альтернативой комбинации xs take nиxs drop n
Киптон Баррос
1
это взорвет стек, рассмотрим рекурсивную реализацию
Джед Уэсли-Смит
@Kipton, правда, но вам нужно извлечь результаты во временные vals, чтобы он добавил пару строк в метод. Я провел быстрый тест, и похоже, что он использует splitAtвместо take/ dropповышает производительность в среднем на 4%; оба на 700-1000% быстрее .grouped(n).toList!
Луиджи Плиндж
@ Луиджи, Вау. Есть мысли о том, почему grouped-toListтак медленно? Звучит как ошибка.
Киптон Баррос,
@Jed В крайних случаях вы правы, но ваша реализация зависит от того, для чего вы ее используете. Для варианта использования OP (если groupedне существует :)) простота является решающим фактором. Для стандартной библиотеки стабильность и производительность должны быть важнее элегантности. Но есть множество примеров как в Программе на Scala, так и в стандартных библиотеках нормально-рекурсивных (а не хвостово-рекурсивных) вызовов; это стандартное и важное оружие в наборе инструментов FP.
Луиджи Плиндж
4

Я добавляю хвостовую рекурсивную версию метода разделения, поскольку было некоторое обсуждение хвостовой рекурсии по сравнению с рекурсией. Я использовал аннотацию tailrec, чтобы заставить компилятор жаловаться на случай, если реализация действительно не учитывает хвост. Я считаю, что хвостовая рекурсия превращается в цикл под капотом и поэтому не вызовет проблем даже для большого списка, поскольку стек не будет расти бесконечно.

import scala.annotation.tailrec


object ListSplitter {

  def split[A](xs: List[A], n: Int): List[List[A]] = {
    @tailrec
    def splitInner[A](res: List[List[A]], lst: List[A], n: Int) : List[List[A]] = {
      if(lst.isEmpty) res
      else {
        val headList: List[A] = lst.take(n)
        val tailList : List[A]= lst.drop(n)
        splitInner(headList :: res, tailList, n)
      }
    }

    splitInner(Nil, xs, n).reverse
  }

}

object ListSplitterTest extends App {
  val res = ListSplitter.split(List(1,2,3,4,5,6,7), 2)
  println(res)
}
Майк
источник
1
Этот ответ можно улучшить, добавив некоторые пояснения. Учитывая, что принятый ответ кажется каноническим, предполагаемым способом сделать это, вы должны объяснить, почему кто-то предпочел бы этот ответ.
Джеффри Босбум,
0

Я думаю, что это реализация с использованием splitAt вместо take / drop

def split [X] (n:Int, xs:List[X]) : List[List[X]] =
    if (xs.size <= n) xs :: Nil
    else   (xs.splitAt(n)._1) :: split(n,xs.splitAt(n)._2)
Hydrosan
источник