Каким был бы функциональный подход к смещению определенных элементов массива?
У меня есть приложение Scala со списком элементов с флажками, поэтому пользователь выбирает некоторые из них и нажимает кнопку, чтобы переместить их на одну позицию вверх (влево). Я решил написать функцию для сдвига элементов произвольного типа, которые соответствуют заданному предикату. Итак, если у вас есть эти элементы:
a b c D E f g h I
и предикат "прописные буквы", функция будет возвращать это:
a b D E c f g I h
Короче говоря, любая последовательность смежных элементов, которые соответствуют предикату, обменивается с единственным элементом слева от него.
Я придумал следующую безобразную императивную реализацию. Я хотел бы видеть хорошее и, надеюсь, читаемое, функциональное решение.
def shiftUp[T](a:Array[T], shiftable: T => Boolean) = {
val s = new Array[T](a.length)
var i = 0
var j = 0
while(i < a.length)
{
if(!shiftable(a(i)) && i < a.length - 1 && shiftable(a(i+1)))
{
var ii = i + 1
while(ii < a.length && shiftable(a(ii)))
{
s(j) = a(ii)
ii = ii+1
j = j+1
}
s(j) = a(i)
i = ii
}
else
{
s(j) = a(i)
i = i+1
}
j = j+1
}
s
}
РЕДАКТИРОВАТЬ: Спасибо всем, я надеюсь, вам понравилось упражнение!
9 ответов
Вот чисто функциональная реализация
def shiftElements[A](l: List[A], pred: A => Boolean): List[A] = {
def aux(lx: List[A], accum: List[A]): List[A] = {
lx match {
case Nil => accum
case a::b::xs if pred(b) && !pred(a) => aux(a::xs, b::accum)
case x::xs => aux(xs, x::accum)
}
}
aux(l, Nil).reverse
}
А вот тот, который использует изменчивость внутри, чтобы быть быстрее
import scala.collection.mutable.ListBuffer
def shiftElements2[A](l: List[A], pred: A => Boolean): List[A] = {
val buf = new ListBuffer[A]
def aux(lx: List[A]) {
lx match {
case Nil => ()
case a::b::xs if pred(b) && !pred(a) => {
buf.append(b)
aux(a::xs)
}
case x::xs => {
buf.append(x)
aux(xs)
}
}
}
aux(l)
buf.toList
}
Вы могли бы сделать это через foldLeft
(также известен как /:
):
(str(0).toString /: str.substring(1)) { (buf, ch) =>
if (ch.isUpper) buf.dropRight(1) + ch + buf.last else buf + ch
}
Требуется работа для обработки пустой строки, но:
def foo(Str: String)(p: Char => Boolean) : String = (str(0).toString /: str.substring(1)) {
(buf, ch) => if (p(ch) ) buf.dropRight(1) + ch + buf.last else buf + ch
}
val pred = (ch: Char) => ch.isUpper
foo("abcDEfghI")(pred) //prints abDEcfgIh
Я оставлю это в качестве упражнения о том, как изменить это в решение на основе массива
Это в основном императивный алгоритм с функциональным стилем.
def shifWithSwap[T](a: Array[T], p: T => Boolean) = {
def swap(i:Int, j:Int) = {
val tmp = a(i); a(i) = a(j); a(j) = tmp
}
def checkAndSwap(i:Int) = i match {
case n if n < a.length - 1 && !p(a(i)) && p(a(i+1)) => swap(i, i+1)
case _ =>
}
(0 until a.length - 1) map checkAndSwap
a
}
Он изменяет массив на месте, с побочным эффектом. Я думаю, что это действительно похоже на версию в вопросе, за исключением того, что ее легче читать. Императив не должен быть безобразным...
Изменить: штопать, не мог заснуть, пока я не записал это (так же, как выше, просто более компактно):
def shift[T](a: Array[T], p: T => Boolean) = {
for (i <- 0 until a.length - 1; if !p(a(i)) && p(a(i+1))) {
val tmp = a(i); a(i) = a(i+1); a(i+1) = tmp // swap
}
a
}
Я не знаю достаточно, чтобы написать это в Scala, но эта проблема специально для функций списка takeWhile
а также dropWhile
, Идея состоит в том, что вы разделяете список предметов на три части:
Левая часть, рассчитанная с
takeWhile
, содержит самые левые элементы, не удовлетворяющие предикату.Средняя часть - это группа элементов, которую вы хотите сдвинуть влево, вычисляя, отбрасывая левые элементы, а затем
takeWhile
остаток.Правая часть - это все, что осталось;
dropWhile
средние элементы.
Вот это в Хаскеле:
-- take first group of elements satisfying p and shift left one
shift :: (a -> Bool) -> [a] -> [a]
shift p l = case reverse left of
[] -> l
(a:as) -> reverse as ++ middle ++ a : shift p right
where left = takeWhile (not . p) l -- could be done with List.break
notLeft = dropWhile (not . p) l
middle = takeWhile p notLeft -- could be done with List.span
right = dropWhile p notLeft
И вот один тестовый модуль:
*Shiftup> shift (>9) [1, 2, 3, 44, 55, 6, 7, 8]
[1,2,44,55,3,6,7,8]
Программисты на Haskell могут использовать List.break
или же List.span
объединять звонки takeWhile
а также dropWhile
, но я не уверен, есть ли у Scala такие вещи. Кроме того, takeWhile
а также dropWhile
хорошие значимые имена, в то время как я, по крайней мере, нахожу break
а также span
менее заметен
РЕДАКТИРОВАТЬ: исправлен рекурсивный вызов, чтобы сделать shift p right
вместо right
сдвинуть вверх все группы.
Я не утверждаю, что этот материал ниже, чтобы быть эффективным или читабельным. К сожалению, все хорошие ответы, кажется, приняты, поэтому я иду за оригинальность.:-)
def shift[T](a: Seq[T], p: T => Boolean) = {
val (areP, notP) = a.zipWithIndex partition { case (t, index) => p(t) }
val shifted = areP map { case (t, index) => (t, index - 1) }
val others = notP map (shifted.foldLeft(_){
case ((t, indexNotP), (_, indexIsP)) =>
if (indexNotP == indexIsP) (t, indexNotP + 1) else (t, indexNotP)
})
(shifted ++ others).sortBy(_._2).map(_._1)
}
Итак, вот что происходит. Во-первых, я связываю каждый символ с его индексом (a.zipWithIndex
), а затем разделить затем на areP
а также notP
в зависимости от того, удовлетворяет ли персонаж p
или нет.
Итак, на данный момент у меня есть две последовательности, каждая из которых состоит из символа и его индекса в исходной последовательности.
Затем я просто сдвигаю индекс элементов в первой последовательности, вычитая 1, и вычисляю shifted
,
Вычислить новый индекс несмещенных элементов гораздо сложнее. Для каждого из этих элементов (notP map
) Я сделаю foldLeft
, Аккумулятором левого сгиба будет сам элемент (всегда с его индексом). Сложенная последовательность - это последовательность сдвинутых элементов, поэтому можно видеть, что для каждого несмещенного элемента я пересекаю всю последовательность сдвинутых элементов (крайне неэффективно!).
Итак, мы сравниваем индекс несмещенного элемента с индексом каждого сдвинутого элемента. Если они равны, увеличьте индекс несмещенного элемента. Потому что последовательность сдвинутых элементов упорядочена (partition
не меняет порядок), мы знаем, что сначала мы проверим на более низкие индексы, а затем на более высокие индексы, гарантируя, что индекс элемента будет увеличен по мере необходимости.
При этом мы объединяем две последовательности, упорядочиваем их по новым индексам и затем отображаем обратно на элемент.
Вот еще один вариант ответа Джеффа:
def shift[T](l: List[T], p: T => Boolean): List[T] = {
l match {
case a::b::t if ! p(a) && p(b) => b::shift(a::t, p)
case a::t => a::shift(t, p)
case Nil => l
}
}
Быстро протестировано с помощью
scala> def pred(c: Char) = c.isUpper
pred: (c: Char)Boolean
scala> shift("abcDEfghI".toList, pred)
res3: List[Char] = List(a, b, D, E, c, f, g, I, h)
scala> shift("AbCd".toList, pred)
res4: List[Char] = List(A, C, b, d)
scala> shift(Nil, pred)
res5: List[Nothing] = List()
Вот вторая версия
def shift[T](l: List[T], p: T => Boolean, r: List[T] = Nil): List[T] = {
l match {
case a::b::t if ! p(a) && p(b) => shift(a::t, p, b::r)
case a::t => shift(t, p, a::r)
case Nil => r.reverse
}
}
Не самый быстрый, но не только String и использующий ту же логику, что и @oxbow_lakes
def shift[T](iter: Iterable[T])(p: T=>Boolean): Iterable[T] =
iter.foldLeft(Iterable[T]())((buf, elm) =>
if (p(elm) && buf.nonEmpty)
buf.dropRight(1) ++ Iterable[T](elm) ++ Iterable[T](buf.last)
else
buf++Iterable[T](elm)
)
def upperCase(c:Char)=c.isUpper
shift("abcDEfghI")(upperCase).mkString
//scala> res86: String = abDEcfgIh
val array="abcDEfghI".toArray
shift(array)(upperCase).toArray
//res89: Array[Char] = Array(a, b, D, E, c, f, g, I, h)
def pair(i:Int)=i%2==0
val l=List(1,2,3,5,4,6,7,9,8)
shift(l)(pair)
//scala> res88: Iterable[Int] = List(2, 1, 3, 4, 6, 5, 7, 8, 9)
Решение в J:
('abcdefghijklmnopqrstuvwxyz';'ABCDEFGHIJKLMNOPQRSTUVWXYZ') (4 : '(y#~y e. >1{x)([: I. '' ''= ])} }._1&|.&.((1,~y e. >0{x)&#)y,'' ''') 'abcDEfghI'
abDEcfgIh
Давайте разберем это на именованные части для более легкого понимания. Последняя строкаabDEcfgIh
"Является результатом применения функции к строке"abcDEfghI
”, Который является правильным аргументом для функции. Пара алфавитов составляет левый аргумент функции (которая является началом части "(4 :
..."). Таким образом, вместо двухэлементного вектора в штучной упаковке мы можем назвать каждую из них по отдельности:
'lc uc'=. 'abcdefghijklmnopqrstuvwxyz';'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
Теперь, когда у нас есть две переменные "lc
" а также "uc
”Для строчных и прописных алфавитов, давайте рассмотрим тело функции подробно. Взяв логически связный фрагмент с правого конца, так как это будет оценено в первую очередь, мы можем назвать это так:
rmUCshift=: 4 : 0
}._1&|.&.((1,~y e. >0{x)&#)y,' '
)
Это определяет "rmUCshift
Как то, что требует правого и левого аргумента ("4 :
”Указывает это), когда тело начинается со следующей строки и продолжается до закрывающего парена. "4 : 0
"Форма, сопровождаемая телом, является вариантом"4 :
Форма "тело", показанная изначально. Этот глагол rmUCshift
может быть вызван независимо, как это:
(lc;'') rmUCshift 'abcDEfghI' NB. Remove upper-case, shift, then insert
ab cfg h NB. spaces where the upper-case would now be.
Вызов имеет три пробела с отступом, и результат сразу следует за ним. Левый аргумент (lc;'')
это двухэлементный вектор с пустым массивом, указанным в качестве второго элемента, потому что он не используется в этом фрагменте кода - мы могли бы использовать любое значение после точки с запятой, но две одинарные кавычки легко набрать.
Следующие части, чтобы назвать это (определения, сопровождаемые примерами):
ixSpaces=: [:I.' '=]
ixSpaces 'ab cfg h'
2 3 7
onlyUC=: 4 : 'y#~y e.>1{x'
('';uc) onlyUC 'abcDEfghI'
DEI
Объединение этих названных частей вместе дает нам это:
(lc;uc) (4 : '(x onlyUC y)(ixSpaces x rmUCshift y)}x rmUCshift y') 'abcDEfghI'
abDEcfgIh
Тем не менее, повторение "x rmUCshift y
"Является ненужным и может быть упрощено, чтобы дать нам это:
(lc;uc) (4 : '(x onlyUC y) ((ixSpaces ]) } ]) x rmUCshift y') 'abcDEfghI'
abDEcfgIh
Изменить: это на самом деле не решает поставленную проблему - это решает связанную, но другую проблему (увеличение приоритета отмеченных элементов на единицу). Я оставляю это здесь для справки, как бы то ни было.
Вот "одна строка", использующая массивы в соответствии с запросом, для Scala 2.8.
def shiftUp[T](a: Array[T], p: T => Boolean) = {
a.zipWithIndex.map(ci => {
(ci._1 , if (p(ci._1)) ci._2 - 1.5 else ci._2.toDouble)
}).sortWith((l,r) => l._2 < r._2).map(_._1)
}
scala> shiftUp(Array('h','E','l','l','O'),(c:Char)=>c.isUpper).toArray
res0: Array[Char] = Array(E, h, l, O, l)
scala> shiftUp("HeLlO".toArray,(c:Char)=>c.isUpper).toArray
res1: Array[Char] = Array(H, L, e, O, l)
Я оставляю это в качестве упражнения для читателя, чтобы выяснить, как это работает. (Если вы действительно хотите использовать дженерики с T, в Scala 2.8 он даст вам GenericArray; затем вы можете переписать его, если вам нужен Java-потенциально примитивный массив.)