#julia
Вопрос:
У меня есть массив x
, и я хотел бы повторить каждую запись x
несколько раз, указанное соответствующими записями другого массива y
, той же длины x
.
x = [1, 2, 3, 4, 5] # Array to be repeated
y = [3, 2, 1, 2, 3] # Repetitions for each element of x
# result should be [1, 1, 1, 2, 2, 3, 4, 4, 5, 5, 5]
Есть ли способ сделать это в Julia?
Комментарии:
1. О, по-видимому, можно сделать
vcat(fill.(x, y)...)
, хотя я не совсем понимаю, как это работает
Ответ №1:
Ваши x
и y
векторы представляют собой то, что называется кодированием вектора по длине [1, 1, 1, 2, 2, 3, 4, 4, 5, 5, 5]
. Итак, если вы возьмете обратную кодировку длины выполнения, вы получите искомый вектор. Пакет StatsBase.jl содержит функции rle
и inverse_rle
. Мы можем использовать inverse_rle
так:
julia> using StatsBase
julia> x = [1, 2, 3, 4, 5];
julia> y = [3, 2, 1, 2, 3];
julia> inverse_rle(x, y)
11-element Vector{Int64}:
1
1
1
2
2
3
4
4
5
5
5
Комментарии:
1. Это определенно лучший вариант здесь. Быстрый (немного превосходит даже мой ответ «ручным способом»), простой в понимании и обслуживании — вполне оправдывает стоимость одной (довольно стандартной) зависимости пакета.
2. @sundar-RememberMonica Просто просматривает исходный код, он выглядит довольно близко к вашему «ручному способу», вложенный цикл написан немного по-другому, но, насколько я могу судить, он выполняет те же итерации.
3. @BatWannaBe Да, и поучительно увидеть различия, которые компенсируют эти последние различия в производительности. Избегание
enumerate
, не отбрасываяlength
результат вверху, кажется большим, но маленькие, такие как выведение индексации массива из внутреннего цикла, также, похоже, складываются.
Ответ №2:
Вы дали то, что я бы предложил в качестве ответа, уже в своем комментарии:
vcat(fill.(x, y)...)
Как это работает? Начните с fill
:
help?> fill
fill(x, dims::Tuple)
fill(x, dims...)
Create an array filled with the value x. For example, fill(1.0, (5,5)) returns a 5×5 array of floats, with each element initialized to 1.0.
Это немного сложнее, чем нужно для нашего случая (где у нас есть только одно измерение для заполнения), поэтому давайте рассмотрим простой пример:
julia> fill(1, 3)
3-element Vector{Int64}:
1
1
1
это fill(1, 3)
просто означает «возьмите число один и поместите это число в одномерный массив 3 раза».
Это, конечно, именно то, что мы хотим сделать здесь: для каждого элемента in x
нам нужен массив, который содержит этот элемент несколько раз, с кратным, заданным соответствующим элементом in y
. Поэтому мы могли бы перебирать x
и y
и делать что-то вроде:
julia> for (xᵢ, yᵢ) ∈ zip(x, y)
fill(xᵢ, yᵢ)
end
Теперь этот цикл ничего не возвращает, поэтому нам придется предварительно выделить некоторое хранилище и назначить его внутри цикла. Более кратким способом записи этого при автоматическом возврате объекта было бы понимание:
julia> [fill(xᵢ, yᵢ) for (xᵢ, yᵢ) ∈ zip(x, y)]
5-element Vector{Vector{Int64}}:
[1, 1, 1]
[2, 2]
[3]
[4, 4]
[5, 5, 5]
и еще более кратко, мы можем просто использовать широковещательную передачу:
julia> fill.(x, y)
5-element Vector{Vector{Int64}}:
[1, 1, 1]
[2, 2]
[3]
[4, 4]
[5, 5, 5]
итак, из понимания или трансляции мы получаем вектор векторов, каждый вектор является элементом x
повторяющихся y
времен. Теперь все, что остается, это объединить их в один вектор, объединив их по вертикали:
julia> vcat(fill.(x, y)...)
11-element Vector{Int64}:
1
1
1
2
2
3
4
4
5
5
5
Здесь мы используем splatting, чтобы по существу сделать:
z = fill.(x, y)
vcat(z[1], z[2], z[3], z[4], z[5])
Обратите внимание, что splatting может иметь неоптимальную производительность для массивов переменной длины, поэтому лучшим способом является использование reduce
, которое специально предназначено для этого и даст тот же результат:
reduce(vcat, fill.(x, y))
Комментарии:
1. Не могли бы вы подробнее прокомментировать, почему разбиение больших массивов (или, возможно, в более общем смысле, итераций) может иметь плохую производительность? Моим единственным предположением была бы задержка при компиляции метода для каждой длины, но я думал, что Julia не специализировалась автоматически на VarArgs именно по этой причине (упомянуто в советах по производительности).
2. Это справедливый момент, мой ответ был сформулирован там несколько вводящим в заблуждение — это более переменная длина, чем длина как таковая. Конечно, в подобном случае это на самом деле не имеет значения, это будет иметь значение только в том случае, если
vcat(fill.(x, y)...)
произойдет в горячем цикле с изменениемx
иy
длиной.3. Я внимательно изучил его в советах по производительности и форумах и нашел пару вещей. Кто-то утверждает, что специализация переменной длины является проблемой, и в советах по производительности говорится, что VarArg сделает метод специализированным, если он не просто передается другому методу (не уверен, как, поскольку все является вызовом метода). Кто-то также указал, что переменные аргументы (даже если они разделены из ленивой итерации) упаковываются в тип кортежа, зависящий от длины, так что это явная стоимость времени и памяти. В любом случае, правильный способ — это цикл или сокращение с помощью двоичных методов, как в вашем ответе.
Ответ №3:
Если производительность является приоритетом, вы также можете сделать это длинным, ручным способом:
function runlengthdecode(vals::Vector{T}, reps::Vector{<:Integer}) where T
length(vals) == length(reps) || throw(ArgumentError("Same number of values and counts expected"))
result = Vector{T}(undef, sum(reps))
resind = 1
for (valind, numrep) in enumerate(reps)
for i in 1:numrep
@inbounds result[resind] = vals[valind]
resind = 1
end
end
result
end
Это выполняется примерно в 12 раз быстрее, чем vcat
fill
метод на основе / для заданных данных, вероятно, из-за того, что не нужно создавать все промежуточные fill
векторы редактирования.
Вы также можете вместо этого использовать fill!
в предварительно @view
выделенных результатах s, заменив цикл в приведенном выше коде на:
for (val, numrep) in zip(vals, reps)
fill!(@view(result[resind:resind numrep - 1]), val)
resind = numrep
end
который имеет сопоставимую производительность.
Ответ №4:
Кроме того, для полноты понимания для этого может быть весьма удобно. И это быстрее, чем fill
и vcat
.
julia> [x[i] for i=1:length(x) for j=1:y[i]]
11-element Vector{Int64}:
1
1
1
2
2
3
4
4
5
5
5