Choosing a solution
Before choosing an algorithm, read the constraints. The input size and time limit usually tell you the target complexity, and the access pattern tells you the structure. This lesson turns that into a checklist.
Learning goals
- Estimate an allowed complexity from the input size
- Estimate memory use for common Java structures
- Choose a structure from the dominant operations
- List the edge cases to test before submitting
Prerequisites
Constraints analysis
Constraints are part of the problem statement. “n ≤ 10⁵” and “1 second” already rule out most O(n²) solutions. Reading them first can save hours of designing an approach that cannot possibly be fast enough.
Ask five questions: how large can n be, what is the value range (does it fit in int?), is the input sorted, can there be duplicates, and are there multiple queries over the same data?
Input size
Assume roughly 10⁸ to 10⁹ simple operations per second in Java. Dividing that by the time limit gives a budget, and the input size tells you which growth classes fit within it. Use the calculator to see where each class crosses one second.
These limits are rules of thumb: the constant factor, memory access pattern and JVM warm-up all matter.
| Largest n | Fits within about 1 second | Typical algorithms |
|---|---|---|
| 10-11 | O(n!) | try all permutations |
| 20-25 | O(2ⁿ) | try all subsets, bitmask DP |
| ~500 | O(n³) | Floyd-Warshall, naive matrix product |
| ~5,000 | O(n²) | nested loops, simple DP over pairs |
| ~10⁶ | O(n log n) | sorting, heaps, balanced trees |
| ~10⁷-10⁸ | O(n) | one or a few passes |
| any | O(log n) or O(1) | binary search, hashing |
How fast do the growth classes explode?
Interactive · drag the slidersO(1)
array index, hash lookup
1 ops
instant
O(log n)
binary search
7 ops
instant
O(√n)
jump search, trial division
10 ops
instant
O(n)
one pass
100 ops
instant
O(n log n)
merge sort
664 ops
instant
O(n²)
nested loops, bubble sort
10,000 ops
10 µs
O(n³)
triple loop, Floyd-Warshall
10^6.0 ops
1.0 ms
O(2ⁿ)
all subsets
10^30.1 ops
10^14 years
O(n!)
all permutations
10^158 ops
10^141 years
Bars use a log scale (the bar shows the exponent of 10). Time assumes 10⁹ simple operations per second, a common rule of thumb for competitive and interview limits. Slide n up and watch O(n²) cross one second near n ≈ 30,000 and O(2ⁿ) become impossible before n reaches 60.
Time limit
A time limit is a budget for the whole program: reading input, computing and printing. For large inputs, slow input and output alone can exceed it, which is why Chapter 2 covers fast readers and StringBuilder/PrintWriter.
If your solution is the right complexity but still too slow, look at constant factors: boxing, HashMap instead of arrays, allocation inside loops and repeated string concatenation.
Memory limit
Estimate memory from the sizes of the types: an int[] of n elements takes about 4n bytes plus a 16-byte header, a long[] 8n, and a boolean[] n. Boxed collections are far larger: an ArrayList<Integer> stores a reference per element plus a separate 16-byte Integer object, and a HashMap<Integer, Integer> entry costs roughly 50-80 bytes.
So 10⁷ ints are about 40 MB as int[] but several hundred MB as List<Integer>. If the limit is tight, prefer primitive arrays.
| Structure holding 10⁷ values | Approximate memory |
|---|---|
| int[] | 40 MB |
| long[] | 80 MB |
| boolean[] / BitSet | 10 MB / 1.25 MB |
| ArrayList<Integer> | about 200 MB (references + Integer objects) |
| HashMap<Integer, Integer> | several hundred MB |
Data access patterns
How the data is accessed picks the structure: by position (array), by key (hash map), in sorted order or by range (tree map), always the smallest (heap), first-in first-out (queue), last-in first-out (stack), or by connection (graph).
Write down the three operations your program does most and choose the structure that makes those cheapest.
| You mostly… | Choose |
|---|---|
| read by index | array / ArrayList |
| look up by key | HashMap |
| need keys in sorted order or ranges | TreeMap / TreeSet |
| need the smallest or largest repeatedly | PriorityQueue |
| process in arrival order | ArrayDeque as a queue |
| undo the latest thing | ArrayDeque as a stack |
Operation frequency
Count how often each operation happens, not only how expensive it is. Many reads and few writes favour structures that precompute (sorted array + binary search, prefix sums). Many inserts and few reads favour structures that make inserts cheap (hash set, heap, linked structures).
A query-heavy workload can justify an O(n log n) preprocessing step that makes each of a million queries O(1) or O(log n).
Edge cases
Before submitting, run through the same list every time: empty input, a single element, all elements equal, already sorted and reverse sorted, negative numbers and zero, the maximum and minimum values (overflow), duplicates, and the smallest input that exercises each branch.
Many wrong answers on a first try fail on exactly one of these.
1static int maxOf(int[] a) {2if (a.length == 0) throw new IllegalArgumentException("empty array"); // decide, do not guess3int best = a[0]; // not 0: all values may be negative4for (int x : a) best = Math.max(best, x);5return best;6}
Cost-performance trade-offs
Every choice trades something: time for memory (a hash map speeds up lookups and uses more memory), simplicity for speed (brute force versus a clever structure), and build time for query time (sort once, search many times).
The two-sum pair below shows it. The brute force uses O(1) extra memory and O(n²) time; the hash map version uses O(n) memory and O(n) time. Neither is “better”; the constraints decide.
Brute force: check every pair
Interactive · edit the inputint[] arr · length 6
Try every pair (i, j) with i < j. There are n(n-1)/2 = 15 of them in the worst case.
1public static int[] twoSum(int[] nums, int target) {2for (int i = 0; i < nums.length; i++) {3for (int j = i + 1; j < nums.length; j++) {4if (nums[i] + nums[j] == target) {5return new int[] { i, j };6}7}8}9return new int[] { -1, -1 };10}
Live state
target
13
n
6
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=13 n=6 | Try every pair (i, j) with i < j. There are n(n-1)/2 = 15 of them in the worst case. |
| 2 | L2 | target=13 i=0 | Fix i = 0 (value 7) and pair it with every later element. |
| 3 | L4 | target=13 i=0 j=1 sum=18 | Not the target; try the next j. |
| 4 | L4 | target=13 i=0 j=2 sum=9 | Not the target; try the next j. |
| 5 | L4 | target=13 i=0 j=3 sum=22 | Not the target; try the next j. |
| 6 | L4 | target=13 i=0 j=4 sum=11 | Not the target; try the next j. |
| 7 | L4 | target=13 i=0 j=5 sum=16 | Not the target; try the next j. |
| 8 | L2 | target=13 i=1 | Fix i = 1 (value 11) and pair it with every later element. |
| 9 | L4 | target=13 i=1 j=2 sum=13 | This pair adds up to the target. |
| 10 | L5 | return=[1, 2] | return [1, 2] after 6 pair checks. Simple and obviously correct, but O(n²) in the worst case. |
Optimised: remember what you have seen
Interactive · edit the inputint[] arr · length 6
seen (value:index)
A hash map answers “have I seen this value?” in O(1) on average, so one pass is enough.
1public static int[] twoSum(int[] nums, int target) {2Map<Integer, Integer> seen = new HashMap<>(); // value -> index3for (int i = 0; i < nums.length; i++) {4int need = target - nums[i];5if (seen.containsKey(need)) {6return new int[] { seen.get(need), i };7}8seen.put(nums[i], i);9}10return new int[] { -1, -1 };11}
Live state
target
13
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=13 | A hash map answers “have I seen this value?” in O(1) on average, so one pass is enough. |
| 2 | L5 | i=0 nums[i]=7 need=6 | 13 - 7 = 6, which has not appeared yet. |
| 3 | L8 | i=0 size=1 | Store 7 → 0 for future lookups. |
| 4 | L5 | i=1 nums[i]=11 need=2 | 13 - 11 = 2, which has not appeared yet. |
| 5 | L8 | i=1 size=2 | Store 11 → 1 for future lookups. |
| 6 | L5 | i=2 nums[i]=2 need=11 | 11 is in the map: it was stored at index 1, so that value plus 2 makes 13. |
| 7 | L6 | return=[1, 2] | return [1, 2] after 3 lookups: O(n) time and O(n) extra memory. Brute force needed O(1) memory but O(n²) time. |
Practice ladder
Now try it yourself
Easy
- With n ≤ 2 × 10⁵ and 2 seconds, which complexities are acceptable?
- Estimate the memory of an int[1_000_000] and a List<Integer> of the same size
Medium
- Choose a structure for: 10⁶ lookups by key; the cheapest task first; the last 10 actions
- List the edge cases for “find the second largest value”
Advanced
- Design a solution under 64 MB for 10⁸ booleans (hint: BitSet)
Key takeaways
- Use n and the time limit to pick the target complexity before designing.
- Estimate memory from primitive sizes; boxed collections are several times bigger.
- Choose structures by the dominant operations and test the standard edge cases.