تعقيد المساحة
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
So far we have counted steps, which is time. Programs also need memory, and memory is limited too. This lesson applies the same Big O idea to the second resource: how much extra memory an algorithm needs as the input grows. This is called space complexity.
You will learn what counts as extra memory, why a recursive method uses memory you cannot see in its variables, and how time and space are often traded against each other: you can frequently make code faster by spending memory.
Why this matters: a server has a fixed amount of RAM. An algorithm that needs memory in proportion to its input can pass quickly on a test and crash a production process on large data.
This lesson builds on Big O (BEG-039-L02, L03), on the stack and heap (BEG-038), on recursion (BEG-014), and on arrays and HashSet (BEG-015, BEG-030).
Learning Objectives
By the end of this lesson you will be able to:
- Define space complexity and say what it counts and what it does not.
- Give the space complexity, in Big O, of simple methods.
- Compare an in-place algorithm with one that builds a copy.
- Explain why recursion uses memory proportional to its depth.
- Describe a time-versus-space trade-off with a concrete example.
- Decide which resource, time or space, is scarcer for a given situation.
Written Explanation
What counts
Space complexity is how the extra memory an algorithm needs grows with the input size n. "Extra" means memory beyond the input itself: new arrays, new collections, and the call stack of nested method calls. The input is already there whatever you do, so it is not counted. (Some texts count it; always check which convention is meant. This course counts the extra memory only.)
We use the same notation as for time:
| Extra memory | Space class | Example |
|---|---|---|
a few variables, whatever n is | O(1) | a loop that tracks the largest value |
| a new array or collection as big as the input | O(n) | a copy of the array, a Set of every item |
| a table of every pair | O(n²) | a grid comparing each item with each other item |
Counting memory in bytes is a separate, lower-level question. Big O ignores the constant, just as it ignored the cost of a single step: a HashSet<Integer> of n numbers uses noticeably more bytes per number than an int[], but both are O(n).
O(1) space: a few variables
static int maxOf(int[] a) {
int m = a[0];
for (int x : a) if (x > m) m = x;
return m;
}The method uses m and the loop variable, no matter how long a is: O(1) space. Its time is O(n). Time and space are separate questions with separate answers.
O(n) space: a copy of the input
Reversing an array has two natural solutions:
static int[] reverseCopy(int[] a) {
int[] r = new int[a.length]; // new array of size n
for (int i = 0; i < a.length; i++) r[a.length - 1 - i] = a[i];
return r;
}
static void reverseInPlace(int[] a) {
for (int i = 0, j = a.length - 1; i < j; i++, j--) {
int t = a[i]; a[i] = a[j]; a[j] = t; // swap the two ends, move inward
}
}With {4, 8, 15, 16, 23, 42} both produced [42, 23, 16, 15, 8, 4]. But reverseCopy left the original unchanged and used O(n) extra space, while reverseInPlace changed the original and used only t, i, and j: O(1) space. Both take O(n) time.
In place means the algorithm rearranges the data where it already is and needs only O(1) extra memory. The price is that the input is changed.
Recursion uses memory for the call stack
Each call to a method creates a stack frame (BEG-038), and a recursive call waits on the stack until the call below it returns. A recursive method with depth n therefore needs O(n) stack space even if it declares no arrays:
static long sumTo(int n) { return n == 0 ? 0 : n + sumTo(n - 1); } // O(n) stack space
static long sumLoop(int n) { long s = 0; for (int i = 1; i <= n; i++) s += i; return s; } // O(1)Both compute the same thing. sumTo(1000) returned 500500 and sumLoop(100_000) returned 5000050000. When I called sumTo(1_000_000) on JDK 21 with default settings, it threw StackOverflowError, the error from BEG-038: a million waiting frames did not fit. (The depth at which this happens depends on the JVM's stack size setting and on the frame size, so I will not quote a limit.) The loop version has no such problem.
Trading space for time
Often one resource can be bought with the other. Duplicate detection shows it:
| Solution | Time | Extra space |
|---|---|---|
| Nested loops comparing every pair | O(n²) | O(1) |
One loop with a HashSet of items already seen | O(n) average | O(n) |
The second solution is much faster for large n, and it pays for the speed with a set that can hold all n items. The first uses almost no memory and is slow. Neither is "correct" in the abstract: you choose by what is scarce in your situation. L06 builds and times both.
Another familiar trade is a cache: you keep results in memory (space) to avoid recomputing them (time).
Examples
Example 1: classify each method's space (simple)
| Method | Extra memory | Space |
|---|---|---|
maxOf(int[] a) | one int | O(1) |
reverseCopy(int[] a) | a new array of length n | O(n) |
reverseInPlace(int[] a) | three int variables | O(1) |
new HashSet<>() filled with all n inputs | n entries | O(n) |
Example 2: a duplicate check with a set (practical)
static boolean dupSet(int[] a) {
Set<Integer> seen = new HashSet<>();
for (int x : a) {
if (!seen.add(x)) return true; // add returns false if x was already there
}
return false;
}dupSet(new int[]{3, 1, 4, 1}) printed true and dupSet(new int[]{3, 1, 4}) printed false. The HashSet documentation says add returns true only if the set did not already contain the element, which is why !seen.add(x) detects a repeat. In the worst case, when there are no duplicates, all n values end up in the set: O(n) space, and the time is O(n) on average because add is constant-time under the documentation's assumption of a good hash function.
Example 3: recursion versus a loop (realistic)
Imagine summing the order totals for the last 1,000,000 orders. The recursive version has one pending call per order: O(n) stack, and as the test above showed, a million frames overflowed the stack. The loop uses O(1) space. In a server, a StackOverflowError caused by deep recursion on large input is a classic production failure: the code was correct on the small data in the tests and wrong on the large data in the field. Prefer a loop when the depth depends on the data size.
Backend Insight
Space matters on a server in three ways:
- Per-request memory. A method that copies a list of
nrecords for each request needs O(n) memory per request. With 200 requests at the same time, that is 200 copies. Streaming or processing in place keeps memory flat. - Caches and sets. A cache that remembers everything is a space-for-time trade. It speeds up repeated work and, unless it has a size limit, grows without bound; BEG-038 showed where
OutOfMemoryErrorcomes from. - Choosing the scarce resource. A batch job on a big machine can happily trade memory for speed. A service packed into a small container may have to accept a slower, lower-memory algorithm. Ask "which is scarcer here?" before choosing.
Common Mistakes
Mistake 1: Counting the input as extra space
An algorithm that only reads an array of 1,000,000 numbers does not "use O(n) space" in the extra-space convention used here. Count only what the algorithm adds.
Mistake 2: Forgetting the call stack
A recursive method with no arrays can still be O(n) space. Look at the depth of the recursion.
Mistake 3: Believing faster always wins
A HashSet solution is faster and needs O(n) memory. If the data does not fit in memory, the O(1)-space solution may be the only one that runs at all.
Mistake 4: Mixing up time and space in one answer
Always state both separately: "O(n) time, O(1) space". The reverse examples have the same time and different space.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. What does space complexity measure, and does it include the input array itself?
أظهر الحل A1
A1. It measures the extra memory the algorithm needs as n grows. In this course it does not include the input itself.
A2. Give the space complexity: (a) a method that counts the vowels in a String with one int counter; (b) a method that builds a List<Integer> of all squares of the input array.
أظهر الحل A2
A2. (a) O(1): one counter. (b) O(n): the list holds n new values.
A3. In one sentence each, explain what "in place" means and what the price of it is.
أظهر الحل A3
A3. In place means the algorithm rearranges the data where it already is, using O(1) extra memory. The price is that the original data is changed.
Level B: Application
B1. State time and space for this method:
static int[] doubled(int[] a) {
int[] r = new int[a.length];
for (int i = 0; i < a.length; i++) r[i] = a[i] * 2;
return r;
}Rewrite it so it is in place, and state the new space.
أظهر الحل B1
B1. O(n) time and O(n) space (a new array of size n). In place:
static void doubleInPlace(int[] a) {
for (int i = 0; i < a.length; i++) a[i] *= 2;
}Time is still O(n); space is O(1), and the original array is modified.
B2. A recursive method countDown(int n) calls countDown(n - 1) until 0 and declares no arrays. What is its space complexity and why?
أظهر الحل B2
B2. O(n): each pending call keeps a stack frame, and n calls are pending at the deepest point. Large n risks StackOverflowError.
B3. A HashSet of every item makes duplicate detection O(n) time. You must process a file of 500 million numbers on a machine whose memory cannot hold them all. Which solution would you consider instead, and what do you give up?
أظهر الحل B3
B3. The nested-loop approach needs O(1) extra memory but O(n²) time, which is impractical at that size. A more realistic route is to sort the data in pieces or use an external method that fits memory limits, or to relax the problem (for example, process the data in chunks). You give up either speed or exactness. (The practical external techniques are beyond this course; the point is that memory can be the deciding resource.)
Level C: Challenge
C1. Write static boolean isPalindrome(String s) that returns true when s reads the same forwards and backwards (assume no spaces and no case issues). Do it with two indexes moving inward, without creating a reversed copy. State time and space. Then say what the version that builds new StringBuilder(s).reverse().toString() would cost in space.
أظهر الحل C1
C1.
static boolean isPalindrome(String s) {
for (int i = 0, j = s.length() - 1; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) return false;
}
return true;
}Time is O(n) and space is O(1). The StringBuilder version creates a reversed copy of n characters, so its space is O(n), with the same O(n) time.
C2. Give an example, other than duplicate detection, of a time-for-space trade you could make in a backend feature, and say what the downside is.
أظهر الحل C2
C2. Example: caching the result of an expensive price calculation by product ID. Time drops from recomputing on each request to a lookup. Downside: the cache uses memory that grows with the number of distinct products, and cached values can become stale when prices change.
Mini Challenge
Take the duplicate detector you will build in L06 (or write two versions now): one with nested loops and one with a HashSet.
- For each version, write time and space in Big O.
- Add a line of comment above each method saying which resource it saves and which it spends.
- Write two sentences stating when you would pick the nested-loop version and when you would pick the
HashSetversion.
Check your work: each claim names both time and space, and your decision names a situation (for example, tiny input, or very tight memory).
Summary & Next Lesson
Key takeaways
- Space complexity is the extra memory used as
ngrows, written in Big O. The input itself is not counted here. - A few variables is O(1); a new collection or array as big as the input is O(n).
- In place algorithms use O(1) extra space but change the input.
- Recursion uses O(depth) stack space and can end in
StackOverflowError. - Time and space often trade: a
HashSetspeeds up duplicate detection and costs O(n) memory. Choose by which resource is scarce.
Next lesson
The next lesson, "Best, average, worst", looks at a question we have mostly skipped: the same algorithm can take different amounts of work on different inputs of the same size, so one Big O needs the case it describes.
Self-check
- What is the space complexity of
maxOfabove? - Why does a recursive
sumTo(n)use O(n) space? - What does
reverseInPlacechange thatreverseCopydoes not? - Name the trade made when a
HashSetreplaces nested loops. - Does a method that only reads an input array of size
nuse O(n) extra space?
أظهر الإجابات
- O(1).
- There are
nmethod calls waiting on the stack at the deepest point, each with its own frame. - It changes the original array, while
reverseCopyleaves it as it was. - Time for space: faster time, but O(n) extra memory.
- No. Reading the input does not add memory beyond the input itself.
References
- Oracle, HashSet (Java SE 21 API). It says
addreturnstrueonly when the set did not already contain the element, and that the basic operations are constant-time given a good hash function. - Oracle, ArrayList (Java SE 21 API). Used for the contrast between constant-time and linear-time operations.
- The definition of space complexity as extra memory (excluding the input) is the convention used in this course; some texts count the input, which I noted in the lesson. I did not fetch a textbook source for this convention, so treat it as the course's own definition. The
StackOverflowErrorwas produced by running the code on JDK 21 with default settings, and the depth at which it appears varies by machine and settings.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.