I think it's O(1) but some sources online say O(n)
python - Big-O of list slicing - Stack Overflow
algorithm - Python slice complexity? - Stack Overflow
python - Time complexity of string slice - Stack Overflow
List-Slicing
It means the time it takes for something to happen doesn't depend on the size of the input. It deals with the subject of Time Complexity. As far as Python is concerned, list-slicing doesn't take constant time, it's bounded by a some kind of factor depending on the slice operation. You can refer to this page which documents the time complexity for common operations.
Anything with O(1) is something that's completed in constant time. The time complexity for get, set, and del slice operations are affected by either n, k, or some combination of those two. The page itself explains what those variables mean more concisely than I can, so refer to the introduction section for more information on those.
What is string slicing in Python?
Are there any performance considerations when using string slicing?
What is the difference between string slicing and string indexing?
Videos
Getting a slice is O(i_2 - i_1). This is because Python's internal representation of a list is an array, so you can start at i_1 and iterate to i_2.
For more information, see the Python Time Complexity wiki entry
You can also look at the implementation in the CPython source if you want to.
according to http://wiki.python.org/moin/TimeComplexity
it is O(k) where k is the slice size
Short answer: str slices, in general, copy. That means that your function that does a slice for each of your string's n suffixes is doing O(n2) work. That said, you can avoid copies if you can work with bytes-like objects using memoryviews to get zero-copy views of the original bytes data. See How you can do zero copy slicing below for how to make it work.
Long answer: (C)Python str do not slice by referencing a view of a subset of the data. There are exactly three modes of operation for str slicing:
- Complete slice, e.g.
mystr[:]: Returns a reference to the exact samestr(not just shared data, the same actual object,mystr is mystr[:]sincestris immutable so there is no risk to doing so) - The zero length slice and (implementation dependent) cached length 1 slices; the empty string is a singleton (
mystr[1:1] is mystr[2:2] is ''), and low ordinal strings of length one are cached singletons as well (on CPython 3.5.0, it looks like all characters representable in latin-1, that is Unicode ordinals inrange(256), are cached) - All other slices: The sliced
stris copied at creation time, and thereafter unrelated to the originalstr
The reason why #3 is the general rule is to avoid issues with large str being kept in memory by a view of a small portion of it. If you had a 1GB file, read it in and sliced it like so (yes, it's wasteful when you can seek, this is for illustration):
with open(myfile) as f:
data = f.read()[-1024:]
then you'd have 1 GB of data being held in memory to support a view that shows the final 1 KB, a serious waste. Since slices are usually smallish, it's almost always faster to copy on slice instead of create views. It also means str can be simpler; it needs to know its size, but it need not track an offset into the data as well.
How you can do zero copy slicing
There are ways to perform view based slicing in Python, and in Python 2, it will work on str (because str is bytes-like in Python 2, supporting the buffer protocol). With Py2 str and Py3 bytes (as well as many other data types like bytearray, array.array, numpy arrays, mmap.mmaps, etc.), you can create a memoryview that is a zero copy view of the original object, and can be sliced without copying data. So if you can use (or encode) to Py2 str/Py3 bytes, and your function can work with arbitrary bytes-like objects, then you could do:
def do_something_on_all_suffixes(big_string):
# In Py3, may need to encode as latin-1 or the like
remaining_suffix = memoryview(big_string)
# Rather than explicit loop, just replace view with one shorter view
# on each loop
while remaining_suffix: # Stop when we've sliced to empty view
some_constant_time_operation(remaining_suffix)
remaining_suffix = remaining_suffix[1:]
The slices of memoryviews do make new view objects (they're just ultra-lightweight with fixed size unrelated to the amount of data they view), just not any data, so some_constant_time_operation can store a copy if needed and it won't be changed when we slice it down later. Should you need a proper copy as Py2 str/Py3 bytes, you can call .tobytes() to get the raw bytes obj, or (in Py3 only it appears), decode it directly to a str that copies from the buffer, e.g. str(remaining_suffix[10:20], 'latin-1').
It all depends on how big your slices are. I threw together the following two benchmarks. The first slices the entire string and the second only a little bit. Curve fitting with this tool gives
# s[1:-1]
y = 0.09 x^2 + 10.66 x - 3.25
# s[1:1000]
y = -0.15 x + 17.13706461
The first looks quite linear for slices of strings up to 4MB. I guess this really measures the time taken to construct a second string. The second is pretty constant, although it's so fast it's probably not that stable.


import time
def go(n):
start = time.time()
s = "abcd" * n
for j in xrange(50000):
#benchmark one
a = s[1:-1]
#benchmark two
a = s[1:1000]
end = time.time()
return (end - start) * 1000
for n in range(1000, 100000, 5000):
print n/1000.0, go(n)
What is meant by the assumption that "list-slicing takes constant-time"?
It means the time it takes for something to happen doesn't depend on the size of the input. It deals with the subject of Time Complexity. As far as Python is concerned, list-slicing doesn't take constant time, it's bounded by a some kind of factor depending on the slice operation. You can refer to this page which documents the time complexity for common operations.
Anything with O(1) is something that's completed in constant time. The time complexity for get, set, and del slice operations are affected by either n, k, or some combination of those two. The page itself explains what those variables mean more concisely than I can, so refer to the introduction section for more information on those.
Lists are just a set of references to items they hold. Much like a phone book, a contacts list or an ingredient list for a recipe. If you duplicate or slice the list in Python, you're just creating a new copy of the set of references, the list items themselves aren't handled. This means that the amount of work needed to perform the slice is that little (as it's just administrative), it will take practically the same amount of very little time, regardless of the list and slice sizes.
O(n+k) is the average case, which includes having to grow or shrink the list to adjust for the number of elements inserted to replace the original slice.
Your case, where you replace the slice with an equal number of new elements, the implementation only takes O(k) steps. But given all possible combinations of number of elements inserted and deleted, the average case has to move the n remaining elements in the list up or down.
See the list_ass_slice function for the exact implementation.
You're right, if you want to know the exact details it's best to use the source. The CPython implementation of setting a slice is in listobject.c.
If I read it correctly, it will...
- Count how many new elements you're inserting (or deleting!)
- Shift the n existing elements of the list over enough places to make room for the new elements, taking O(n) time in the worst case (when every element of the list has to be shifted).
- Copy over the new elements into the space that was just created, taking O(k) time.
That adds up to O(n+k).
Of course, your case is probably not that worst case: you're changing the last k elements of the list, so there might be no need for shifting at all, reducing the complexity to O(k) you expected. However, that is not true in general.