301_Moved_Permanently's answer is great and changes the performance from \$O(nk\log(n))\$ to \$O(n\log(n))\$.
The change results in a measurable impact on performance.
To satiate both my curiosity and the site's need for at least one IO I have explored two micro-optimisations.
Firstly I want to note micro-optimisations are not typically a good approach only use micro-optimisations for when you need improvements in the micro level -- \$10^{-6}\$s. Which is pretty rare.
sorted
builds a new list where list.sort
mutates an existing list.
list.sort
is typically faster because we remove the copy operation.
def test_peil__sort(items: Iterable[int]) -> list[int]:
items = [i for i in items if i != 0]
items.sort()
non_zeroes = iter(items)
return [next(non_zeroes) if i != 0 else 0 for i in items]
Python is typically slower than C, so offloading to C is a common micro-optimisation technique. Here we can offload to filter
's C loop.
Thanks @duckboycool for reminding me of filter
over itertools.filterfalse
.
def test_peil__filter(items: Iterable[int]) -> list[int]:
non_zeroes = iter(sorted(filter(None, items)))
return [next(non_zeroes) if i != 0 else 0 for i in items]
Assuming 1 & 2 are faster, combining the two together may result in faster code.
def test_peil__filter_sort(items: Iterable[int]) -> list[int]:
items = list(filter(None, items))
items.sort()
non_zeroes = iter(items)
return [next(non_zeroes) if i != 0 else 0 for i in items]
Note: To test the performance of the functions we need to build sample data. I'm using the following function to do so:
random.seed(42401)
@functools.cache
def args_conv(size: int) -> list[int]:
arr = list(range(int(size)))
random.shuffle(arr)
return arr
We can see:
test_mathias
is faster than the test_orig
.
test_peil__sort
's micro-optimisation is faster than test_mathias
in the micro section, getting quite close otherwise.
test_peil__filter
's micro-optimisation is marginally better than test_mathias
throughout.
test_peil__filter_sort
seems virtually indistinguishable from test_peil__filter
.
test_cris
shows NumPy is much better than Python when working with numbers.
test_cris__py
shows converting between Python and NumPy can greatly diminish the benefit of using NumPy.
We can also see all Python based algorithms are tending towards the same worst case.
To make comparing user's suggestions easier, here is the graph again with only one algorithm per user.
I said at the beginning of the answer 301_Moved_Permanently's answer "changed the performance from \$O(nk\log(n))\$ to \$O(n\log(n))\$." However we can't see the improvement in the graph. In args_conv
I used a sampling method which only has one 0, so \$k = 1\$. If we change the sampling method to have \$n / 2\$ 0s we can show test_orig
pulling away from test_mathias
with a curve.
Full code:
final/__init__.py
from collections.abc import Iterable
import numpy
from numpy.typing import NDArray
def test_orig(items: list[int]) -> Iterable[int]:
without_z = [i for i in items if i != 0]
place_z = [i[0] for i in enumerate([i if i == 0 else 1 for i in items]) if i[1] == 0]
without_z = sorted(without_z)
for i in place_z: without_z.insert(i, 0)
return without_z
# Derived https://codereview.stackexchange.com/a/287025
# By 301_Moved_Permanently
def test_mathias(items: Iterable[int]) -> list[int]:
non_zeroes = iter(sorted(i for i in items if i != 0))
return [next(non_zeroes) if i != 0 else 0 for i in items]
# Derived https://codereview.stackexchange.com/a/287029
# By Cris Luengo
def test_cris(items: NDArray[numpy.int_]) -> NDArray[numpy.int_]:
items = items.copy() # can leave this out if you want to work in-place
nonzeros = items != 0
items[nonzeros] = numpy.sort(items[nonzeros])
return items
# Derived https://codereview.stackexchange.com/a/287029
# By Cris Luengo
def test_cris__py(items: Iterable[int]) -> list[int]:
items = numpy.array(items)
nonzeros = items != 0
items[nonzeros] = numpy.sort(items[nonzeros])
return list(items)
# Derived https://codereview.stackexchange.com/a/287025
# By 301_Moved_Permanently
def test_peil__sort(items: Iterable[int]) -> list[int]:
items = [i for i in items if i != 0]
items.sort()
non_zeroes = iter(items)
return [next(non_zeroes) if i != 0 else 0 for i in items]
# Derived https://codereview.stackexchange.com/a/287025
# By 301_Moved_Permanently
def test_peil__filter(items: Iterable[int]) -> list[int]:
non_zeroes = iter(sorted(filter(None, items)))
return [next(non_zeroes) if i != 0 else 0 for i in items]
# Derived https://codereview.stackexchange.com/a/287025
# By 301_Moved_Permanently
def test_peil__filter_sort(items: Iterable[int]) -> list[int]:
_items = list(filter(None, items))
_items.sort()
non_zeroes = iter(_items)
return [next(non_zeroes) if i != 0 else 0 for i in items]
plot.py
import functools
import random
from typing import Any
import matplotlib.pyplot
import numpy
from numpy.typing import NDArray
import graphtimer
from . import final
random.seed(42401)
@functools.cache
def args_conv(size: int) -> list[int]:
arr = list(range(int(size)))
random.shuffle(arr)
return arr
def args_conv__np(size: int) -> NDArray[numpy.int_]:
return numpy.array(args_conv(size))
@functools.cache
def args_conv__k(size: int) -> list[int]:
arr = list(range(int(size))) + [0] * int(size / 2)
random.shuffle(arr)
return arr
def args_conv__k__np(size: int) -> NDArray[numpy.int_]:
return numpy.array(args_conv__k(size))
def join_plots(
base: graphtimer.plotter.PlotValues,
*values: graphtimer.plotter.PlotValues,
) -> graphtimer.plotter.PlotValues:
for value in values:
if (base.kwargs["domain"] != value.kwargs["domain"]).any():
raise ValueError("Can only merge PlotValues with the same domain.")
data: list[list[graphtimer.plotter._DataValues]] = list(base.data)
kwargs: dict[str, Any] = {
"functions": list(base.kwargs["functions"]),
"domain": base.kwargs["domain"],
}
for value in values:
data += value.data
kwargs["functions"] += value.kwargs["functions"]
return graphtimer.plotter.PlotValues(data, kwargs)
def main():
fig, axs = matplotlib.pyplot.subplots()
axs.set_yscale('log')
axs.set_xscale('log')
join_plots(
(
graphtimer.Plotter(graphtimer.MultiTimer([final.test_orig, final.test_mathias, final.test_peil__filter, final.test_cris__py, final.test_peil__sort, final.test_peil__filter_sort]))
.repeat(10, 10, numpy.logspace(0, 4, num=50), args_conv=args_conv)
.min()
),
(
graphtimer.Plotter(graphtimer.MultiTimer([final.test_cris]))
.repeat(10, 10, numpy.logspace(0, 4, num=50), args_conv=args_conv__np)
.min()
),
).plot(axs, x_label='len(nums)')
fig.show()
fig, axs = matplotlib.pyplot.subplots()
axs.set_yscale('log')
axs.set_xscale('log')
join_plots(
(
graphtimer.Plotter(graphtimer.MultiTimer([final.test_orig, final.test_mathias, final.test_peil__filter]))
.repeat(10, 10, numpy.logspace(0, 4, num=50), args_conv=args_conv)
.min()
),
(
graphtimer.Plotter(graphtimer.MultiTimer([final.test_cris]))
.repeat(10, 10, numpy.logspace(0, 4, num=50), args_conv=args_conv__np)
.min()
),
).plot(axs, x_label='len(nums)')
fig.show()
fig, axs = matplotlib.pyplot.subplots()
axs.set_yscale('log')
axs.set_xscale('log')
join_plots(
(
graphtimer.Plotter(graphtimer.MultiTimer([final.test_orig, final.test_mathias, final.test_peil__filter]))
.repeat(10, 10, numpy.logspace(0, 4, num=50), args_conv=args_conv__k)
.min()
),
(
graphtimer.Plotter(graphtimer.MultiTimer([final.test_cris]))
.repeat(10, 10, numpy.logspace(0, 4, num=50), args_conv=args_conv__k__np)
.min()
),
).plot(axs, x_label='len(nums)')
fig.show()
input()
if __name__ == '__main__':
main()