Циклические срезы и сдвиги коллекций

от автора

Одна из типовых задач при работе с коллекциями и массивами — выборка n элементов, начиная с i-того индекса, или же, как вариант, выборка элементов с i-того по j-й индекс. На языке C# с использованием методов-расширений библиотеки Linq решается она очень просто:

var selectedItems = items.Skip(i).Take(n).ToArray(); var selectedItems = items.Skip(i).Take(j - i).ToArray(); 

Для строк предусмотрен специальный метод Substing:

var target = source.Substing(i, n); var target = source.Substing(i, j - i); var target = source.Substing(i); // from i to end 

Часто начинающие разработчики изобретают свои велосипеды для подобных целей из-за недостающих знаний, поэтому на собеседованиях технические интервьюверы иногда задают вопросы по этой теме. В некоторых языках программирования также существует родственная и весьма интересная концепция срезов (slices), детальное пояснение которой с примерами следует далее.

image
Пускай дан массив букв, упорядоченных по алфавиту. Каждой букве соответствует не только привычный положительный индекс исчисляемый слева-направо (от головы), но и отрицательный, исчисляемый справа-налево (от хвоста):

var bodyLetters = new[] {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'I'}; var headIndexes = new[] { 0 ,  1 ,  2 ,  3 ,  4 ,  5 ,  6 ,  7 }; var tailIndexes = new[] {-8 , -7 , -6 , -5 , -4 , -3 , -2 , -1 }; 

Выбор элементов с четвёртого по шестой включительно можно осуществить несколькими способами:

// хвост не включается в результат bodyLetters.Slice(3, 7); // 'D', 'E', 'F', 'G' bodyLetters.Slice(-5, 7); // 'D', 'E', 'F', 'G' bodyLetters.Slice(3, -1); // 'D', 'E', 'F', 'G' bodyLetters.Slice(-5, -1); // 'D', 'E', 'F', 'G' 

Хвост удобно не включать, поскольку длина выборки тогда легко вычисляется по индексам без лишнего инкремента на единицу и не вызывает путаницы (5-2 = 3 вместо 5-2+1 = 4)

Но возникает вопрос, как в таком случае включить последний элемент исходной коллекции в результирующую выборку, ведь элемента с индексом 8 нет, а отрицательного соответствия ему даже не найти (разве что целочисленный -0, который не отличить от +0)? Тут возникает некоторая рассогласованность. Конечно, можно сделать включение хвоста, усложнив вычисление длины, либо искусственно использовать индексы за пределами массива, как поступили, например, в языке Python.

Чтобы принять оптимальное решение, давайте рассмотрим и другие моменты, как должен вести себя метод, когда индекс хвоста находится раньше индекса головы?

bodyLetters.Slice(7, 3); // ??? bodyLetters.Slice(-1, -5); // ??? 

Самые элементарные решения — выдать пустую выборку, бросить исключение или же толерантно поменять индексы хвоста и головы местами, а затем продолжить обработку, быть может, вывести результирующую выборку в обратном порядке?

А как поступить, когда индексы хвоста и головы совпадают? По нашим правилам мы включаем голову, но исключаем хвост, однако в таком случае хвост и голова — это одно и то же! Возникает логическое противоречие. Возвращаться к случаю с включением хвоста?

Давайте подумаем… Что если замкнуть, зациклить массив сам на себя? То есть предположить, что сразу за последним индексом 7 идёт снова 0, и если передать в метод, скажем, параметры 6 и 2, то вывести следующий результат ‘G’, ‘I’, ‘A’, ‘B’. Тогда чтобы включить последний элемент исходной коллекции в выборку достаточно написать Slice(6, 0) // ‘G’ ‘I’, а запись Slice(5, 5) приведёт к тому, что мы получим исходную коллекцию циклически сдвинутую на пять индексов влево — ‘F’, ‘G’, ‘I’, ‘A’, ‘B’, ‘C’, ‘D’, ‘E’ — вот чудо!

Для того, чтобы получить пустую выборку достаточно написать Slice(5, 6), плюс никаких трудностей с отрицательными индексами, а если вдруг выйдем за границы массива, то получим ожидаемое исключение. Как красиво всё получилось! Если же вдруг потребуется вывести срезанную выборку в обратном порядке, то метод Reverse в помощь. Мы нашли общее решение сразу для двух классов задач лишённое противоречий и очень естественное…

// хвост не включается в результат bodyLetters.Slice(6, 2); // 'G', 'I', 'A', 'B' bodyLetters.Slice(5, 5); // 'F', 'G', 'I', 'A', 'B', 'C', 'D', 'E' bodyLetters.Slice(5, 6); //  bodyLetters.Slice(5, 0); // 'F', 'G', 'I' bodyLetters.Slice(5); // 'F', 'G', 'I' 

Что ж, осталось его реализовать!

        public static IEnumerable<T> Slice<T>(this IEnumerable<T> collection, int head, int tail = 0)         {             var items = collection as T[] ?? collection.ToArray();             var count = items.Count();             head = head < 0 ? count + head : head;             tail = tail < 0 ? count + tail : tail;              if (head < 0 || count - 1 < head) throw new ArgumentOutOfRangeException("head");             if (tail < 0 || count - 1 < tail) throw new ArgumentOutOfRangeException("tail");              if (head < tail)             {                 foreach (var item in items.Skip(head).Take(tail - head))                 {                     yield return item;                 }             }             else             {                 foreach (var item in items.Skip(head))                 {                     yield return item;                 }                                  foreach (var item in items.Skip(0).Take(tail))                 {                     yield return item;                 }                }         } 

Вот и пригодился нам yield. И совсем немного кода получилось. Мне нравится, а вам?

P.S. И это не всё, данная статья написана намеренно. Метод Slice — это маленькая часть из синтаксического сахара мощной и функциональной библиотеки Aero Framework, которая предоставляет абстракции и концепции более высокого уровня настолько филигранно согласованные между собой, что при должном терпении в изучении эта красота вас поразит… Правда, код Aero Framework — это самое совершенное, что мне посчастливилось написать в жизни на текущий момент. А эта статья про циклические срезы и сдвиги только лишь замануха! Изучайте Aero Framework и вы его обязательно оцените.

ссылка на оригинал статьи http://habrahabr.ru/post/272743/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *