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 you 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).