Table of Contents
An array is a low-level data structure where elements are identified by integer indices. Arrays are…
An array organizes items sequentially, one after another in memory.
Each position in the array has an index , starting at 0.
数组中的每个位置都有一个索引，从 0 开始。
- Fast lookups. Retrieving the element at a given index takes O(1) time, regardless of the length of the array.
- Fast appends. Adding a new element at the end of the array takes O(1) time.
- 快速查找 – 无论数组的长度如何，以给定索引检索元素都只需要 O（1）时间；
- 快速追加 – 在数组末尾添加新元素需要 O（1）时间。
- Fixed size. You need to specify how many elements you're going to store in your array ahead of time. (Unless you're using a funcy dynamic array.)
- Costly inserts and deletes. You have to "scoot over" the other elements to fill in or close gaps, which takes worst-case O(n) time.
- 固定大小 – 您需要预先指定要在数组中存储多少元素(除非您使用的是函数动态数组)；
- “昂贵的”插入和删除 – 你必须“移动”其他元素来填补或填补空白，这需要最坏情况 O(n) 的时间。
In Python 2.7
Some languages (including Python) don't have these bare-bones arrays.
有些语言(包括 Python )没有这些基本的数组。
Here's what arrays look like in Java.
// instantiate an array that holds 10 integers int gasPrices = new int; gasPrices = 346; gasPrices = 360; gasPrices = 354;
If we want to insert something into an array, first we have to make space by "scooting over" everything starting at the index we're inserting into:
In the worst case we're inserting into the 0th index in the array (prepending), so we have to "scoot over" everything in the array. That's O(n) time.
在最坏的情况下，我们将插入到数组的第 0 个索引(前置)，因此我们必须“移动”数组中的所有内容，这是 O (n) 时间。
Array elements are stored adjacent to each other. So when we remove an element, we have to fill in the gap – "scooting over" all the elements that were after it:
数组元素彼此相邻存储。因此，当我们删除一个元素时，我们必须填补空白 – “移动”它之后的所有元素：
In the worst case we're deleting the 0th item in the array, so we have to "scoot over" everything else in the array. That's O(n) time.
在最坏的情况下，我们正在删除数组中的第 0 项，因此我们必须“移动”数组中的其他所有内容，这是 O (n) 时间。
Why not just leave the gap? Because the quick lookup power of arrays depends on everything being sequential and uninterrupted. This lets us predict exactly how far from the start of the array the 138th or 9,203rd item is. If there are gaps, we can no longer predict exactly where each array item will be.
为什么不留下空白呢?因为数组的快速查找能力依赖于一切都是连续的、不间断的，这让我们可以准确地预测第 138 项或第 9,203 项与数组开始的距离。如果存在空白，我们就不能再准确地预测每个数组项的位置。
Data structures built on arrays
Arrays are the building blocks for lots of other, more complex data structures.
数组是许多其他更复杂的数据结构的 构建块 。
Don't want to specify the size of your array ahead of time? One option: use a dynamic array.
Want to look up items by something other than an index? Use a dictionary.
This is a common tool, but you need to know what it does under the hood!
Array slicing involves taking a subset from an array and allocating a new array with those elements .
In Python 2.7 you can create a new list of the elements in
end_index (exclusize), like this:
You can also get everything from
start_index onwards by just omitting
Careful: there's a hidden time and space cost here! It's tempting to think of slicing as just "getting elements," but in reality you are:
- allocating a new list;
- copying the elements from the original list to the new list.
This takes O(n) time and O(n) space, where
n is the number of elements in the resulting list.
这需要 O(n) 时间和 O(n) 空间，其中 n 是结果数组中的元素数量。
That's a bit easier to see when you save the result of the slice to a variable:
tail_of_list = my_list[1:]
But a bit harder to see when you don't save the result of the slice to a variable:
return my_list[1:] # Whoops, I just spent O(n) time and space!
for item in my_list[1:]: # Whoops, I just spent O(n) time and space! pass
So keep an eye out. Slice wisely.
ANCHOR In-Place Algorithms
- State "ANCHOR" from "DONE"
- State "DONE" from "ANCHOR"
An in-place algorithm operates directly on its input and changes it, instead of creating an return…
An in-place function modifies data structures or objects outside of its own stack frame ↓ (i.e.:stored on the process heap or in the stack frame of a calling function). Because of this, the changes made by the function remain after the call completes.
In-place algorithms are sometimes called destructive, since the original input is "destroyed" (or modified) during the function call.
Careful: "In-place" does not mean "without creating any additional variables!" Rather, it means "without creating a new copy of the input." In general, an in-place function will only create additional variables that are O(1) space.
An out-of-place function doesn't make any changes that are visible to other functions. Usually, those functions copy any data structures or objects before manipulating and changing them.
In many languages, primitive values (integers, floating point numbers, or characters) are copied when passed as arguments, and more complex data structures (lists, heaps, or has tables) are passed by reference. This is waht Python does.
Here are two funcions that do the same operation on a list, except one is in-place and the other is out-of-place:
def square_list_in_place(int_list): for index, element in enumerate(int_list): int_list[index] *= element # NOTE: no need to return anything -- we modified # int_list in place def square_list_out_of_place(int_list): # We allocate a new list with the length of the iniput list square_list = [None] * len(int_list) for index, element in enumerate(int_list): square_list[index] = element ** 2 return square_list
Working in-place is a good way to save time and space. An in-place algorithm avoids the cost of initializing or copying data structures, and it usually has an O(1) space cost.
But be careful: an in-place algorithm can cause side effects. Your input is "destroyed" or "altered," which can affect code outside of your function. For example:
original_list = [2, 3, 4, 5] square_list_in_place(original_list) print("original list: %s" % original_list) # Prints: original list: [4, 9, 16, 25], confusingly!
Generally, out-of-place algorithms are considered safer because they avoid side effects. You should only use an in-place algorithm if you're space constrained or you're positive you don't need the original input anymore, even for debugging.
The call stack is what a program uses to keep track of function calls. The call stack is made up of stack frames – one for each function call.
For instance, say we called a function that rolled two dice and printed the sum.
def roll_die(): return random.randint(1, 6) def roll_two_and_sum(): total = 0 total += roll_die() total += roll_die() print(total) roll_two_and_sum()
First, our program calls
roll_two_and_sum() . It goes on the call stack:
That function calls
roll_die() , which gets pushed on to the top of the call stack:
roll_die() , we call
random.randint() . Here's what our call stack looks like then:
random.randint() roll_die() roll_two_and_sum()
random.randint() finishes, we return back to
roll_die() by removing ("popping")
random.randint()'s stack frame.
Same thing when
We're not done yet!
random.randint() roll_die() roll_two_and_sum()
random.randint() returns, then
roll_die() returns, putting us back in
What's stored in a stack frame?
What actually goes in a function's stack frame?
A stack frame usually stores:
- Local variables
- Arguments passed into the function
- Information about the caller's stack frame
- The return address – what the program should do after the function returns (i.e.: where it should "return to"). This is usually somewhere in the middle of the caller's code.
Some of the specifics vary between processor architectures. For instance, AMD64 (64-bit x86) processors pass some arguments in registers and some on the call stack. And, ARM processors (common in phones) store the return address in a special register instead of putting it on the call stack.
For example, if we wanted to multiply all the numbers between
n, we could use this recursive approache:
def product_1_to_n(n): return 1 if n <= 1 else n * product_1_to_n(n - 1)
What would the call stack look like when
n = 10 ?
product_1_to_n() gets called with
n = 10 :
product_1_to_n() # n = 10
n = 8 .
product_1_to_n() # n = 8 product_1_to_n() # n = 9 product_1_to_n() # n = 10
And so on until we get to
n = 1 .
product_1_to_n() # n = 1 product_1_to_n() # n = 2 product_1_to_n() # n = 3 product_1_to_n() # n = 4 product_1_to_n() # n = 5 product_1_to_n() # n = 6 product_1_to_n() # n = 7 product_1_to_n() # n = 8 product_1_to_n() # n = 9 product_1_to_n() # n = 10
Look at the size of all those stack frames! The entire call stack takes up O(n) space. That's right – we have O(n) space cost even though our function itself doesn't create any data structures!
What if we'd used an iterative approach instead of a recursive one?
def product_1_to_n(n): # We assume n >= 1 result = 1 for num in range(1, n + 1): result *= num return result
This version takes a constant amount of space. At the beginning of the loop, the call stack looks like this:
product_1_to_n() # n = 10, result = 1, num = 1
As we iterate through the loop, the local variables change, but we stay in the same stack frame because we don't call any other functions.
product_1_to_n() # n = 10, result = 2, num = 2
product_1_to_n() # n = 10, result = 6, num = 3
product_1_to_n() # n = 10, result = 24, num = 4
In general, even though the compiler or interpreter will take care of managing the call stack for you, it's important to consider the depth of the call stack when analyzing the space complexity of an algorithm.
Be especially careful with recursive functions! They can end up building huge call stacks.
What happens if we run out of space? It's a stack overflow ! In Python 3.6, you'll get a RecursionError.
If the very last thing a function does is call another function, then its stack frame might not be needed any more. The function could free up its stack frame before doing its final call, saving space.
This is called tail call optimization (TCO). If a recursive function is optimized with TCO, then it may not end up with a big call stack.
A dynamic array automatically doubles its size when you try to make an insertion and there's no mor…
A dynamic array is an array with a big improvement: automatic resizing.
One limitation of arrays is that they're fixed size , meaning you need to specify the number of elements your array will hold ahead of time.
A dynamic array expands as you add more elements. So you don't need to determine the size ahead of time.
- Fast lookups. Just like arrays, retrieving the element at a given index takes O(1) time.
- Variable size. You can add as many items as you want, and the dynamic array will expand to hold them.
- Cache-friendly. Just like arrays, dynamic arrays place items right next to each other in memory, making efficient use of caches.
- Slow worst-case appends. Usually, adding a new element at the end of the dynamic array takes O(1) time. But if the dynamic array doesn't have any room for the new item, it'll need to expand, which takes O(n) time.
- Costly inserts and deletes. Just like arrays, elements are stored adjacent to each other. So adding or removing an item in the middle of the array requires "scooting over" other elements, which takes O(n) time.
In Python 3.6
In Python, dynamic arrays are called lists.
Here's what they look like:
gas_prices =  gas_prices.append(346) gas_prices.append(360) gas_prices.append(354)
Size vs. Capacity
When you allocate a dynamic array, your dynamic array implementation makes an underlying fixed-size array . The starting size depends on the implementation – let's say our implementation uses 10 indices. Now say we append 4 items to our dynamic array. At this point, our dynamic array has a length of 4. But the underlying array has a length of 10.
We'd say this dynamic array's size is 4 and its capacity is 10. The dynamic array stores an
end_index to keep track of where the dynamic array ends and the extra capacity begins.
What if we try to append an item but our array's capacity is already full?
To make room, dynamic arrays automatically make a new, bigger underlying array. Usually twice as big.
Why not just extend the existing array? Because taht memory might already be taken by another program.
Each item has to be individually copied into the new array.
Copying each item over costs O(n) time! So whenever appending an item to our dynamic array forces us to make a new double-size underlying array, that append takes O(n) time.
That's the worst case. But in the best case (and the average case), appends are just O(1) time.
Amortized cost of appending
- The time cost of each special O(n) "doubling append" doubles each time.
- At the same time, the number of O(1) appends you get until the next doubling append also doubles.
These two things sort of "cancel out," and we can say each append has an average cost or amortized cost of O(1).
Given this, in industry we usually wave our hands and say dynamic arrays have a time cost of O(1) for appends, even though strictly speaking that's only true for the average case or the amortized cost.