Найти тему

#19 Решаем задачи на слайсы в Go

Оглавление

Примерно в 100% случаях на собеседованиях на Go разработчика попадаются задачи, где дан какой-то кусок кода, в котором производятся операции над слайсом. Такие задачи обычно идут в начале и их нужно решать без ошибок, так как это показывает ваше знание основ языка. Для начала немного теории.

Структура слайса включает в себя:

array unsafe.Pointer: указатель на массив, содержащий элементы слайса.

len int: длина слайса, т.е. количество элементов в слайсе.

cap int: емкость слайса, т.е. максимальное количество элементов, которое слайс может вместить без перевыделения памяти​​.

Операции над слайсами:

ссылка на исходники

-2

makeslicecopy: выделяет слайс с заданной длиной tolen, а затем копирует fromlen элементов из существующего слайса. Другими словами функция создает копию другого слайса со всеми его элементами.

makeslice: создает новый слайс заданной длины и ёмкости. Функция выделяет память под слайс и возвращает указатель на эту память​​.

growslice: копирует существующие элементы в новое хранилище и возвращает обновлённый слайс с новой длиной и ёмкостью​​.

slicecopy: функция для копирования элементов из одного слайса в другой. Эта операция особенно полезна при работе со слайсами без указателей​​.

Задачи

#1

Создаем массив arr из пяти целочисленных элементов [1, 2, 3, 4, 5].

-3

C помощью операции среза [:] создаем слайс sl. В этом случае не происходит копирования элементов массива arr, слайс sl просто ссылается на те же данные. Поэтому изменения в слайсе sl также отразятся на массив arr.

Итог: [1, 2, 3, 4, 5] -> [3, 2, 3, 4, 5]

#2

Тот же массив, но теперь делаем срез, начиная с индекса 1, то есть со второго элемента массива [1:] -> [2, 3, 4, 5]

-4

sl ссылается на ту же область памяти, что и arr, начиная со второго элемента массива. Его длина и емкость равны 4. Первый элемент слайса sl - это второй элемент массива arr, соответственно, меняя первый элемент sl на 3, мы меняем второй элемент arr.

Итог: arr [1, 2, 3, 4, 5] -> [1, 3, 3, 4, 5] ; sl [2, 3, 4, 5] -> [3, 3, 4, 5]

#3

Делаем срез в один элемент [1:2]. Слайс может расширяться до конца массива, то есть его capacity равна 4, несмотря на длину 1. Если бы мы сделали срез, например, [3:4], то вместимость слайса считалась, начиная с 3 элемента включительно и до конца вместимости массива: 3, 4, 5 -> 3.

-5

Присоединяем элемента со значением 4 к слайсу sl. Здесь используется операция append, которая может вызвать growslice, если требуется увеличить емкость слайса. Однако, в данном случае, поскольку емкость sl достаточна для добавления ещё одного элемента, growslice не вызывается, и arr изменяется напрямую, так как sl ссылается на его элементы.

Итог: arr [1, 2, 3, 4, 5] -> [1, 2, 4, 4, 5] ; sl [2] -> [2, 4]

#4

Создаем срез из одного элемента, как в примере выше.

-6

append добавляет 4 к слайсу sl и этими значениями инициализируем новый слайс sl2. Так как емкость sl достаточна для добавления одного элемента (cap(sl) == 4), append модифицирует существующий слайс sl. Это означает, что sl2 ссылается на ту же область памяти, что и sl, а следовательно, и на ту же область, что и arr. Изменение первого элемент слайса sl2 на 8 отражается и в sl и в arr, так как sl2 ссылается на ту же область памяти.

Итог: arr [1, 2, 3, 4, 5] -> [1, 8, 4, 4, 5] ; sl [2] -> [8]; sl2 [2, 4] -> [8, 4]

#5

Слайс sl ссылается на массив arr из 5 элементов. Емкость sl равна 4. В слайс добавляются элементы 9, 8, и 7. После каждого добавления sl продолжает указывать на ту же область памяти, что и arr. Поэтому изменения в sl отражаются в arr, изменяя arr в [1 2 9 8 7].

-7

После добавления 6 в sl его емкость полностью использована. Выделяется область памяти под новый слайс с увеличенной ёмкостью (вдвое больше) и туда копируются элементы. Теперь sl не ссылается на arr, поэтому дальнейшие изменения sl не влияют на arr. Изменение первых двух элементов sl на 0 не отражается на arr.

Итог: arr [1, 2, 3, 4, 5] -> [1, 2, 9, 8, 7] ; sl [2, 9, 8, 7, 6] -> [0, 0, 8, 7, 6]

ссылка на Playground

#6

Эта задача с реального собеседования.

К слайсу x последовательно добавляются элементы 0, 1 и 2. При каждом вызове append, если в слайсе недостаточно места, создается новый слайс с увеличенной ёмкостью и в него копируются старые элементы. После добавления трех элементов, x становится [0, 1, 2] с ёмкостью 4.

-8

Создаются два новых слайса y и z с помощью append(x, 3) и append(x, 4) соответственно. Поскольку емкость x равна 4, оба слайса y и z фактически указывают на одну и ту же область памяти, что и x.

Когда z модифицируется, изменения отражаются и в y, так как они разделяют ту же область памяти. Таким образом, оба слайса y и z становятся [0, 1, 2, 4].

ссылка на Playground

-9

Вывод указателя для каждого элемента, подтверждает, что ячейки в памяти у элементов совпадают.

#7

Рассмотрим примеры модификации слайса в функции.

Когда передаем слайс в функцию, внутри ее создается новый слайс и в него копируются свойства исходного слайса - длина, емкость и указатель на массив. Таким образом слайс снаружи функции и внутри нее ссылаются на один и тот же массив. Изменение значений слайса в функции отражается на исходном слайсе:

-10

Когда мы делаем append внутри функции, то выходим за емкость исходного слайса. Происходит аллокация памяти под новый массив, а емкость слайса расширяется в 2 раза. Слайс внутри функции изменился, а за ее пределами - нет.

-11

Вернем модифицированный слайс a из функции и присвоим его исходному x. Теперь исходный слайс ссылается на измененный слайс.

-12

Можно еще рассмотреть пример с передачей слайса в функцию по указателю. В таком случае все изменения внутри функции отразятся на исходном слайсе.

-13