Коллекции scala: отображать список и нести какое-то состояние?

avatar
West_JR
8 апреля 2018 в 05:29
753
2
3

Кажется, я постоянно сталкиваюсь с этой проблемой. Я хочу изменить некоторые элементы в списке, но при этом мне нужно сохранить какое-то состояние, поэтому карта не работает.

Вот пример:

scala> val l1 = List("a","b","c","d","e","f","b","c","e","b","a")
l1: List[String] = List(a, b, c, d, e, f, b, c, e, b, a)

Я хочу изменить имя всех дубликатов. так что я хочу закончить с этим:

List(a1, b1, c1, d, e1, f, b2, c2, e2, b3, a2)

Получить копии легко :

scala> val d = l1.diff(l1.distinct).distinct
d: List[String] = List(b, c, e, a)

Теперь я застрял. Я заставил это работать, преобразовав d в HashMap со счетчиком и написав функцию для итерации по l1 и обновления ее и хэша перед рекурсией. Это прекрасно работает, но выглядит довольно уродливо для меня.

Но я всегда думал, что должен быть способ сделать это с классами коллекций.

Вот остальная часть моего решения, которое мне не нравится:

  val m = d.map( _ -> 1).toMap

  def makeIt(ms: Map[String, Int], ol: Iterator[String], res: List[String]=List[String]()) :List[String] = {
    if( !ol.hasNext) return res
    val no = ol.next()
    val (p,nm) = ms.get(no) match {
      case Some(v) => (s"$no$v", ms.updated(no,v+1))
      case None => (no,ms)
    }

    makeIt(nm,ol,res :+ p)
  }

  makeIt(m,l1.iterator)

Что дает мне то, что я хочу

res2: List[String] = List(a1, b1, c1, d, e1, f, b2, c2, e2, b3, a2)

Я чувствую, что мне нужен "mapWithState", где я могу просто что-то передать. Как фолд-иш. Может быть, он существует, просто я его еще не нашел?

Спасибо

-------ОБНОВЛЕНИЕ---------

Комментарий @Aluan Haddad указал мне на это направление. Что разрушает порядок, что нормально для моего случая. Но «состояние» несет zipWithIndex. Я ищу более общий случай, когда состояние потребует некоторых вычислений для каждого элемента. Но для этого простого случая мне это нравится:

l1.groupBy(x=>x).values.flatMap( v =>{
    if( v.length <= 1 ) v else {
      v.zipWithIndex.map{ case (s,i) => s"$s${i+1}"}
    }
  })
res7: Iterable[String] = List(e1, e2, f, a1, a2, b1, b2, b3, c1, c2, d)
Источник
Aluan Haddad
8 апреля 2018 в 05:41
0

Похоже, вы хотите groupBy

West_JR
8 апреля 2018 в 06:04
0

@aluan Думаю, я мог бы использовать groupBy только для посещения элементов, которые нужно изменить (и пропустить match), но я не понимаю, как это помогает мне сохранять состояние при изменении имен?

Aluan Haddad
8 апреля 2018 в 06:08
2

Я должен был сказать, что предлагал groupBy в качестве альтернативы отслеживанию состояния. Таким образом, вы можете написать что-то вроде values.groupBy(x => x).map((g => g.zipWithIndex.map(...

West_JR
8 апреля 2018 в 13:59
0

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

Ответы (2)

avatar
jwvh
8 апреля 2018 в 06:31
2

Сложность заключается в том, что элементы "d" и "f" не изменяются.

Вот что я придумал. Это немного более лаконично с точки зрения кода, но включает несколько обходов.

val l1: List[String] = List("a","b","c","d","e","f","b","c","e","b","a")

l1.reverse.tails.foldLeft(List[String]()){
  case (res, Nil) => res
  case (res, hd::tl) =>
    val count = tl.count(_ == hd)
    if (count > 0) s"$hd${count+1}" +: res
    else if (res.contains(hd+2)) (hd+1) +: res
    else hd +: res
}
//res0: List[String] = List(a1, b1, c1, d, e1, f, b2, c2, e2, b3, a2)

При использовании tails каждый элемент, hd, может видеть будущее, tl, и прошлое, res.

West_JR
8 апреля 2018 в 14:01
0

Если я правильно прочитал это, это будет работать только в том случае, если не более 3 одинаковых? Хотя мне нравится этот узор.

jwvh
8 апреля 2018 в 14:04
0

Не так. Он будет считать любое количество повторений.

West_JR
8 апреля 2018 в 15:35
0

так оно и есть. Прости.

avatar
Tim
8 апреля 2018 в 08:18
1

Простая, но медленная версия

l1.zipWithIndex.map{ case (elem, i) =>
  if (l1.count(_ == elem) == 1) {
    elem
  } else {
    val n = {l1.take(i+1).count(_ == elem)}
    s"$elem$n"
  }
}

Следующая версия длиннее, менее привлекательна и нефункциональна, но должна быть быстрее в том маловероятном случае, если вы обрабатываете очень длинный список.

def makeUniq(in: Seq[String]): Seq[String] = {
  // Count occurrence of each element
  val m = mutable.Map.empty[String, Int]

  for (elem <- in) {
    m.update(elem, m.getOrElseUpdate(elem, 0) + 1)
  }

  // Remove elements with a single occurrence
  val dupes = m.filter(_._2 > 1)

  // Apply numbering to duplicate elements
  in.reverse.map(e => {
    val idx = dupes.get(e) match {
      case Some(i) =>
        dupes.update(e, i - 1)
        i.toString
      case _ =>
        ""
    }

    s"$e$idx"
    }).reverse
  }

Код будет проще, если вы хотите применить счетчик к каждому элементу, а не только к неуникальным.

def makeUniq(in: Seq[String]): Seq[String] = {
  val m = mutable.Map.empty[String, Int]

  in.map{ e =>
    val i = m.getOrElseUpdate(e, 0) + 1
    m.update(e, i)

    s"$e$i"
  }
}
jwvh
8 апреля 2018 в 14:10
0

Хороший. Вместо slice из 0 почему бы и нет: val n = 1 + {l1.take(i).count(_ == elem)}

Tim
8 апреля 2018 в 17:29
0

@jwvh Спасибо за предложение, я изменил ответ.