Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
You can return the answer in any order.
Example 1:
1 | Input: nums = [2,7,11,15], target = 9 |
Example 2:
1 | Input: nums = [3,2,4], target = 6 |
Example 3:
1 | Input: nums = [3,3], target = 6 |
Constraints:
- \(2 \leq \text{nums.length} \leq 10^4\)
- \(-10^9 \leq \text{nums[i]} \leq 10^9\)
- \(-10^9 \leq \text{target} \leq 10^9\)
- Only one valid answer exists.
Follow-up: Can you come up with an algorithm that is less than \(O(n^2)\) time complexity?
Approach 1: Brute Force
Algorithm
The brute force approach is simple. Loop through each element \(x\) and find if there is another value that equals to target−\(x\)
Implementation
1 | from __future__ import annotations |
Complexity Analysis
- Time complexity: \(O(n^2)\). For each element, we try to find its complement by looping through the rest of the array which takes \(O(n)\) time. Therefore, the time complexity is \(O(n^2)\).
- Space complexity: \(O(1)\). The space required does not depend on the size of the input array, so only constant space is used.
explanation
1 | from typing import List |
from typing import List imports the List type for type hints. (on Python 3.9+, we can write list[int] and skip this import.)
class Solution: calls code as Solution().twoSum(...). So the function to live inside a class named Solution.
def twoSum(self, nums: List[int], target: int) -> List[int]: defines a method named twoSum.
selfis the instance ofSolution. We don’t use it here; it’s just the method signature.nums: List[int]is a type hint: “numsshould be a list of integers”.target: intmeans “targetshould be an integer”.-> List[int]means “this function returns a list of integers” (the two indices).- Type hints don’t change runtime behavior; they help readers and tools.
1 | return [i,j] |
return [i, j]exits the function immediately when a pair is found.The trailing
return []only runs if the loops finish without finding any pair.
1 | if __name__ == "__main__": |
__ are called “dunder” (double-underscore) names, e.g. __init__, __str__, __name__. These are special identifiers reserved by Python for language features. We generally don’t invent new dunder names ourself—use only the ones Python defines.
__name__ vs "__main__" vs main
__name__— a built-in variable that Python sets for every module (file)."__main__"— a string literal. Python sets__name__to this string only for the file that’s being executed directly (e.g.,python app.py).main— just a normal function name we chose. It has nothing to do with"__main__"except by convention.
So this snippet means: “If this file is the entry script, then call the function main().” If the file is imported by another module, __name__ will be the module’s name (like "app"), the condition is False, and main() will not run.
Tiny demo (what happens when run vs import)
app.py
1 | def main() -> None: |
Run
python app.py→ prints:1
2In app.py, __name__ is: __main__
Running as a script
other.py
1 | import app |
Run
python other.py→ prints:1
2In app.py, __name__ is: app
Imported app; not running its main().
Supplements
Type annotations && method
Here, we provide a detailed explanation of type annotations and methods.
[[leetcode-1-2-two-sum-type-annotation]]
instance
As for the instance, it doesn’t have to be named self, but we should name it self (and cls for class methods). That’s the Python convention (PEP 8), and most linters/IDEs expect it. e.g.
1 | from __future__ import annotations |
Notes
§a. from dataclasses import dataclass
dataclassesis a standard library module (a Python file bundled with Python, introduced in 3.7).dataclassis not a class; it’s a function (a decorator) defined inside thedataclassesmodule. We import that function with this line so we can write@dataclassabove a class.
So:
dataclasses→ module (a namespace of things)dataclass→ a decorator function provided by that module
§b. What does @dataclass do? What does @ mean?
@something above a function or class is decorator syntax sugar. This:
1 |
|
is equivalent to:
1 | def f(): ... |
Same for classes:
1 |
|
§c. What @dataclass specifically does
@dataclass takes our class definition and returns a modified class with a bunch of boilerplate auto-generated based on our annotated attributes:
__init__(constructor)__repr__(nice string representation)__eq__(equality by fields)- optional ordering methods (
__lt__, …) if we passorder=True - optional immutability if we pass
frozen=True - slots if we pass
slots=True(Py3.10+)
Example:
1 | from dataclasses import dataclass |
Without @dataclass, we’d have to manually write __init__, __repr__, __eq__, etc.
A slightly fancier variant:
1 | from dataclasses import dataclass |
§d. What does @classmethod do?
Same decorator idea: @classmethod transforms a function defined inside a class into a class method.
- An instance method (normal
def method(self, ...)) receives the instance as its first argument (self). - A class method receives the class as its first argument (
cls) instead—so it can operate on class-level data, construct new instances, or behave polymorphically with subclasses.
Example: alternate constructor
1 | class User: |
Example: class-level state
1 | class Counter: |
§e. When to use which?
- Use
@dataclasswhen we want Python to generate boilerplate methods for a data-holding class. - Use
@classmethodwhen a method should work with the class itself (cls) rather than a specific instance (self)—e.g., alternate constructors or class-wide configuration. - The
@just means “apply this decorator to the thing below.”
decorators
A decorator is just a callable that takes a function and returns a new one, often a wrapper, letting us extend behavior while keeping functions clean and following the Open–Closed Principle.
[[leetcode-1-1-two-sum-type-decorators]]
range
How range works?
range can be called in three standard forms:
range(stop)→ start defaults to0, step defaults to1.range(start, stop)→ step defaults to1.range(start, stop, step)→ full control (step ≠ 0; can be negative).
return
Quick contrasts:
continue: skip to the next iteration of the current (innermost) loop.break: exit the innermost loop only (outer loop keeps running).return: exit the function (all loops stop).
Once a return runs, the function exits immediately and any code after that return in the same function will not execute.
Approach 2: Two-pass Hash Table
Idea: First build a dictionary value -> index for all elements. Second pass: for each i, compute complement = target - nums[i]; if complement is in the dict at some j != i, return [i, j].
- Time: O(n) average (dict lookups are amortized O(1)).
- Space: O(n).
- Logic note: Storing the last index for each value still works; the
j != icheck prevents reusing the same element.
1 | from __future__ import annotations |
Syntax
is and is not are identity operators. They check whether two variables refer to the same object, not just equal values. This is why we write:
x is None/x is not None✅ (recommended by PEP 8)- rather than
x == None❌ (works but not idiomatic)
Why this matters for j in Two Sum:
0is a valid index but is falsy (if j:would skip it).Nonemeans “missing.”So we must test specifically for
None:1
2if j is not None and j != i:
...
Other “English-like” operators Python uses
and,or,notin,not inis,is not
They’re real operators, not sugar—Python is deliberately designed to read close to English for clarity.
Rule of thumb:
- Use
is/is notonly for identity checks, especiallyNone(and singletons). - Use
==/!=for value comparisons.
Supplements
enumerate
enumerate is a built-in that lets us loop over both the index and the value of any iterable (list, tuple, string, file, generator) without writing range(len(...)).
What it does
1 | for index, value in enumerate(iterable, start=0): |
iterable: anything we can loop over (list[int],str, file, etc.).start: the first index to use (default0; setstart=1if we want 1-based numbering).- It yields pairs:
(index, value)lazily (no copying of the data).
vs range(len(seq))
- Clearer: for
enumeratewe see bothindexandvaluedirectly. - Safer: no repeated indexing like
seq[i]. - Idiomatic: this is the Python way.
What enumerate returns
It returns an iterator that yields (index, value) pairs. It’s lazy and memory-efficient:
1 | it = enumerate(["a", "b", "c"]) # type: enumerate[str] |
Notes
next() is a built-in function that retrieves the next item from an iterator.
§a. How it works:
enumerate(["a", "b", "c"])creates an iterator object- Each call to
next(it)advances the iterator and returns the next value - The iterator keeps track of its current position internally
§b. What happens step by step:
1 | it = enumerate(["a", "b", "c"]) # Creates iterator |
§c. Equivalent using a for loop:
1 | # This does the same thing automatically |
§d. Key points:
next()is a built-in system function, not something user defined- It's commonly used when we want manual control over iteration
- If we call
next()when no items remain, it raises aStopIterationexception - Most Python loops use
next()internally behind the scenes
Property start
1 | numlist = [4, 5, 6] |
enumerate(..., start=1) only changes the counter it yields; it does not change how the list is indexed.
- The list
numlist = [4, 5, 6]still has real indices0→4,1→5,2→6. - With
start=1,enumeratewill report pairs(1, 4), (2, 5), (3, 6), but the underlying list is still 0-based.
dict
1 | index_of: dict[int,int]={} |
Python dictvs list(as Array) - Core Concepts & Structure
| Feature | Dictionary (dict) |
List (used as an "Array") |
|---|---|---|
| Basic Structure | Key-Value pairs (key: value) |
Ordered sequence of elements |
| Indexing / Access | By key (any hashable type: string, number, tuple) | By integer index (position, starting from 0) |
| Element Requirements | Keys must be immutable and unique. Values can be any type. | Elements can be of any type. |
| Mutability | Mutable (can add, remove, modify key-value pairs) | Mutable (can add, remove, modify elements) |
| Order (Python 3.6+) | Insertion order is preserved | Insertion order is preserved |
| Typical Use Case | Representing a collection of related attributes (like a record). Fast lookups based on a key. | Representing a collection of ordered items (e.g., a sequence of names). |
Key Characteristics of dict
- Key-Value Pairs: Stores data as unique keys mapped to values. Think of it like a phone book (key=name, value=number).
- Efficiency: Optimized for fast lookups by key. Underlying implementation uses a hash table.
- Mutability: we can change, add, or remove key-value pairs after creation.
- Key Uniqueness: If a key is used again, the previous value is overwritten.
- Syntax: Created with curly braces
{}.person = {"name": "Alice", "age": 30, "city": "London"}
Key Characteristics of list(as a String Array)
- In Python, the
listis the primary data structure used as a flexible "array" that can hold items of any type, including strings. - Ordered Collection: Elements are stored in a specific sequence.
- Index-Based Access: The first element is at index
0, the second at1, and so on. - Mutability: we can change, add, or remove elements after creation.
- Syntax: Created with square brackets
[].fruits_list = ["apple", "banana", "orange"] # A list of strings
When to Use Which?
- Use a
dictwhen we need to label and associate data. Each item has a unique name (key) that holds a specific value. Perfect for structured data like a user profile ('name','id','email'). - Use a
listwhen we have a simple, ordered collection of items of the same kind, and we care about their sequence. Ideal for lists of names, tasks, or any sequence where position matters.
dict.get is a safe dictionary lookup. It returns the value for a key if present; otherwise it returns a default (which is None if we don’t supply one). It also avoids raising KeyError.
dict.get
1 | value = mapping.get(key, default=None) |
- If
keyexists → returnsmapping[key]. - If
keyis missing → returnsdefault(orNoneif we didn’t pass one).
In this snippet:
1 | j = index_of.get(complement) |
index_ofisdict[int, int].jwill beint | None.- We then check
if j is not None and j != i:to confirm we found a distinct index.
Why use get here?
- It’s one operation (hash lookup) that either gives we the value or
None. - If we used
if complement in index_of: j = index_of[complement], we’d typically do two lookups (membership test, then indexing).
Important pitfall
Do not write if j: — index 0 is a valid result but is “falsy” in Python. Use is not None (or test membership first).
Approach 3: one-pass hash table
One-pass Two Sum: scan once with a dict mapping value → index. For each x at i, compute y = target - x; if y is in the dict, return [index_of[y], i], else store index_of[x] = i. Use enumerate for (i, x). Time: O(n) (amortized), Space: O(n). A thin Solution.twoSum method just delegates to the typed, PEP-8 two_sum function.
1 | from __future__ import annotations |
Function inside vs outside the class -> self
1 | def two_sum(nums: list[int], target: int) -> list[int]: |
Why do we put two_sum outside the Solution
- Stateless logic ⇒ module-level function. Google’s Python style (and most “serious” codebases) prefer a plain function when code doesn’t need object state. Don’t create classes/methods unless we need them.
- Testability & reuse. A top-level
two_sum(...)is easy to import, unit-test, and reuse anywhere. TheSolutionclass is just a LeetCode quirk. - PEP 8 naming. At module level we can use proper
snake_case. The LeetCode method nametwoSumis camelCase because the platform requires it.
With slef vs without self
1 | def two_sum(nums: list[int], target: int) -> list[int]: |
- Top-level function (defined outside a class) has no implicit object, so no
self. - Instance method (defined inside a class, without decorators) is called on an instance; Python automatically passes that instance as the first argument. By convention we name it
self.
Explanation
When we write:
1 | class Accumulator: |
and call:
1 | acc = Accumulator(10) |
Python rewrites acc.add(5) to:
1 | Accumulator.add(acc, 5) |
So self is just the instance (acc) that’s implicitly supplied. That’s why an instance method signature must accept a first parameter—by convention named self.
When we need (or don’t need) self
We need
selfwhen the method uses instance state or calls other instance methods:1
2
3
4
5
6
7class Counter:
def __init__(self, start: int = 0) -> None:
self.count = start
def inc(self, step: int = 1) -> int:
self.count += step
return self.countWe don’t need
selfif the logic doesn’t touch instance or class state. Use a top-level function or a@staticmethod:1
2
3
4
5
6
7
8
9
10
11
12
13def two_sum(nums: list[int], target: int) -> list[int]:
index_of: dict[int, int] = {}
for i, x in enumerate(nums):
y = target - x
if y in index_of:
return [index_of[y], i]
index_of[x] = i
return []
class Solution:
def twoSum(nums: list[int], target: int) -> list[int]:
return two_sum(nums, target)If the method needs the class (not an instance)—e.g., alternate constructors or class-wide config—use
@classmethodand name the first parametercls.
Why the outside function doesn’t need self
A top-level function like:
1 | def two_sum(nums: list[int], target: int) -> list[int]: ... |
isn’t bound to any object. There’s no instance to pass, so no self parameter.
Rule of thumb:
- Put pure, stateless logic at module level (no
self). - Use instance methods (with
self) when we need per-object state. - Use
@staticmethodor@classmethodwhen appropriate.
dict used in if
1 | if y in index_of: |
This checks membership of the key (not values) in the dict.
True if we have already seen
yearlier and stored it as a key.Dict membership is amortized O(1).
If found, return the pair of indices. (Either
[index_of[y], i]or[i, index_of[y]]is fine for LeetCode.)
others
1 | return [] |
- Defensive fallback. LeetCode guarantees a solution; outside LeetCode, this makes the function total (never implicitly returns
None).
1 | def main() -> None: ... |
- Standard script entry-point guard. Runs
main()only when this file is executed directly, not when imported.
Complexity
- Time:
O(n)average (each step does O(1) dict operations). - Space:
O(n)(worst case store almost all elements before finding the pair).
Common gotchas
index_of.get vs index_of[]
statement
Between the two idioms, the single-lookup .get() pattern is safest and usually a bit faster. Direct indexing (d[key]) can raise KeyError when the key is missing; combining key in d and d[key] is safe but does two lookups. All variants are O(n) overall; differences are constant-factor only.
Given two one-pass Two Sum snippets, is there a speed/behavior difference?
1 | # A) single lookup (recommended) |
Why does index_of[nums_complement] is not None sometimes error?
Analysis
- Direct indexing
index_of[nums_complement]:- If the key is absent, Python immediately raises KeyError—we never reach the
is not Nonecheck.
- If the key is absent, Python immediately raises KeyError—we never reach the
.get()(index_of.get(nums_complement)):- Returns the value or
Nonewhen missing (no exception). - Lets we safely write
if j is not None:. (Useis not Nonebecause index0is valid but falsy.) - One lookup per iteration.
- Returns the value or
in+ indexing (key in d and d[key]):- Safe due to short-circuiting, but performs two lookups (membership, then indexing).
- Performance:
- All approaches keep the algorithm O(n).
.get()typically has the best constant factor (one hash probe).in+[]doubles lookups; using exceptions for flow control is slower.
Case explanation
Mini examples show behavior:
1 | d: dict[int, int] = {7: 1} |
Applied to Two Sum (concise, typed):
1 | from __future__ import annotations |
Takeaway: Prefer the .get() single-lookup pattern for clarity, safety (no KeyError), and slightly better performance.
Addendum: why and works here, but .get() is still preferable
Snippet
1 | if nums_complement in index_of and index_of[nums_complement] != i: |
is safe because Python’s and short-circuits:
- It first evaluates
nums_complement in index_of(membership on keys). - Only if that is True does it evaluate
index_of[nums_complement] != i. This prevents aKeyErrorwhen the key is missing.
So it runs normally. The tradeoff is performance: when the key is present, we’ve done two hash lookups (one for in, one for []). The single-lookup pattern avoids that extra work:
1 | j = index_of.get(nums_complement) # one lookup |
Use .get() when we can—it’s clearer, avoids KeyError, handles index 0 correctly with is not None, and typically has a slightly better constant factor.
Conventions
- If function inside a class then the name of it should like
twoSumif outside a class it will betwo_sum