[Python] 리스트(List)
동빈나 채널의 파이썬 문법 부수기 유튜브 강의를 참고하여 정리한 내용이다.
파이썬에서 가장 많이 쓰는 자료구조라고 할 수 있다.
배열처럼 사용하고, 크기를 동적으로 늘릴 수 있고, 다양한 내장 메서드를 제공한다.
리스트 개념
리스트는 순서가 있는 데이터의 묶음이다.
배열처럼 인덱스로 접근하고, 다른 언어와 달리 파이썬 리스트는 여러 타입의 데이터를 함께 저장할 수 있다.
# 기본 생성
a = [1, 2, 3, 4, 5]
b = [] # 빈 리스트
c = list() # 빈 리스트 - b와 동일
# 혼합 타입도 가능 (코딩테스트에선 잘 안 씀)
mixed = [1, "hello", 3.14, True]
print(a) # [1, 2, 3, 4, 5]
print(len(a)) # 5 - 길이
리스트 초기화
직접 초기화
# 값을 미리 알 때
scores = [90, 85, 92, 78, 95]
names = ["alice", "bob", "charlie"]
반복으로 초기화
# n개의 0으로 초기화
n = 5
a = [0] * n
print(a) # [0, 0, 0, 0, 0]
# n개의 특정 값으로
b = [1] * 10
print(b) # [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
[0] * n 패턴은 코딩테스트에서 배열을 초기화할 때 매우 자주 쓴다. DP 테이블이나 방문 체크 배열을 초기화할 때 기본이다.
인덱싱
양수 인덱스
a = [1, 2, 3, 4, 5]
# 0 1 2 3 4 (양수 인덱스)
print(a[0]) # 1 - 첫 번째
print(a[2]) # 3 - 세 번째
print(a[4]) # 5 - 마지막
음수 인덱스
파이썬은 뒤에서부터 접근할 때 음수 인덱스를 사용한다. 이게 편하다.
a = [1, 2, 3, 4, 5]
# -5 -4 -3 -2 -1 (음수 인덱스)
print(a[-1]) # 5 - 마지막
print(a[-2]) # 4 - 뒤에서 두 번째
print(a[-5]) # 1 - 첫 번째 (a[0]과 동일)
마지막 원소를 가져올 때 a[len(a)-1] 대신 a[-1]을 쓴다. 훨씬 깔끔하다.
인덱스 범위 오류
a = [1, 2, 3, 4, 5]
print(a[5])
# IndexError: list index out of range
print(a[-6])
# IndexError: list index out of range
인덱스는 0부터 len(a)-1까지, 음수는 -len(a)부터 -1까지만 유효하다. 이 범위를 넘으면 IndexError가 발생한다.
값 변경
a = [1, 2, 3, 4, 5]
a[2] = 10
print(a) # [1, 2, 10, 4, 5]
a[-1] = 99
print(a) # [1, 2, 10, 4, 99]
슬라이싱
리스트의 일부를 잘라내는 기능이다. a[시작:끝] 형태이며, 끝 인덱스는 포함하지 않는다.
a = [1, 2, 3, 4, 5]
print(a[1:3]) # [2, 3] - 인덱스 1, 2 (3은 미포함)
print(a[0:2]) # [1, 2]
print(a[2:]) # [3, 4, 5] - 2부터 끝까지
print(a[:3]) # [1, 2, 3] - 처음부터 2까지
print(a[:]) # [1, 2, 3, 4, 5] - 전체 복사
스텝 사용
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(a[::2]) # [0, 2, 4, 6, 8] - 2칸씩 건너뜀
print(a[1::2]) # [1, 3, 5, 7, 9] - 홀수 인덱스
print(a[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - 역순
print(a[5:1:-1]) # [5, 4, 3, 2] - 5부터 2까지 역순
a[::-1]은 리스트를 뒤집는 관용 표현이다. a.reverse()와 달리 원본은 그대로고 새 리스트를 반환한다.
슬라이싱으로 값 수정
a = [1, 2, 3, 4, 5]
a[1:3] = [20, 30]
print(a) # [1, 20, 30, 4, 5]
# 다른 길이로 교체도 가능
a[1:3] = [200, 300, 400]
print(a) # [1, 200, 300, 400, 4, 5]
리스트 컴프리헨션
반복문으로 리스트를 만드는 파이썬스러운 방법이다. 간결하고 일반적으로 for 루프보다 빠르다.
기본 형태
# 기본 형태
result = [표현식 for 변수 in 이터러블]
# 0~4의 제곱 리스트
squares = [i ** 2 for i in range(5)]
print(squares) # [0, 1, 4, 9, 16]
# 동일한 for 루프
squares = []
for i in range(5):
squares.append(i ** 2)
조건부 컴프리헨션
# 조건이 있는 경우
evens = [i for i in range(10) if i % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
# 1~30 중 3의 배수
multiples_of_3 = [i for i in range(1, 31) if i % 3 == 0]
print(multiples_of_3) # [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
# if-else는 표현식 앞에
abs_vals = [i if i >= 0 else -i for i in [-3, -1, 2, 4, -5]]
print(abs_vals) # [3, 1, 2, 4, 5]
2차원 리스트 초기화
코딩테스트에서 2차원 배열을 초기화할 때 반드시 컴프리헨션 방식을 써야 한다.
n, m = 3, 4
# 올바른 방법 - 컴프리헨션
matrix = [[0] * m for _ in range(n)]
print(matrix)
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
matrix[0][0] = 1
print(matrix)
# [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] - 의도한 대로
# 잘못된 방법 - 같은 리스트 객체가 공유됨
matrix_wrong = [[0] * m] * n
print(matrix_wrong)
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
matrix_wrong[0][0] = 1
print(matrix_wrong)
# [[1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]] - 모든 행이 같이 바뀜!
[[0] * m] * n은 내부에서 동일한 리스트 객체를 n번 참조하는 구조다.
하나를 바꾸면 전부 바뀐다. 코딩테스트에서 이 함정에 빠지면 디버깅하는 데 시간을 많이 쓰게 된다.
[[0] * m for _ in range(n)]이 방식만 사용한다.[[0] * m] * n은 쓰지 않는다.
언더바(_) 활용
반복 변수를 실제로 사용하지 않을 때 _를 쓴다. 관례적으로 “이 변수는 필요 없음”을 표현한다.
# _ 는 값을 버리는 관례
for _ in range(5):
print("hello")
# 컴프리헨션에서도 동일
zeros = [0 for _ in range(10)]
print(zeros) # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# 다중 언패킹에서 필요 없는 값 버리기
a, _, c = (1, 2, 3)
print(a, c) # 1 3
리스트 메서드
주요 메서드와 시간복잡도
| 메서드 | 기능 | 시간복잡도 |
|---|---|---|
append(x) |
끝에 원소 추가 | O(1) |
insert(i, x) |
i번 인덱스에 x 삽입 | O(N) |
remove(x) |
첫 번째로 나오는 x 제거 | O(N) |
pop() |
마지막 원소 제거 후 반환 | O(1) |
pop(i) |
i번 인덱스 원소 제거 후 반환 | O(N) |
sort() |
정렬 (오름차순) | O(N log N) |
reverse() |
역순 정렬 | O(N) |
count(x) |
x의 개수 반환 | O(N) |
index(x) |
x의 첫 번째 인덱스 반환 | O(N) |
clear() |
모든 원소 삭제 | O(N) |
copy() |
얕은 복사 반환 | O(N) |
append와 insert
a = [1, 2, 3]
# append: 끝에 추가
a.append(4)
print(a) # [1, 2, 3, 4]
# insert: 특정 위치에 삽입
a.insert(1, 10)
print(a) # [1, 10, 2, 3, 4]
# insert는 O(N) 이므로 끝에 추가할 때는 append 사용
sort와 sorted
a = [3, 1, 4, 1, 5, 9, 2, 6]
# sort(): 원본을 정렬 (반환값 없음)
a.sort()
print(a) # [1, 1, 2, 3, 4, 5, 6, 9]
# 내림차순
a.sort(reverse=True)
print(a) # [9, 6, 5, 4, 3, 2, 1, 1]
# sorted(): 원본 유지, 새 리스트 반환
b = [3, 1, 4, 1, 5]
c = sorted(b)
print(b) # [3, 1, 4, 1, 5] - 원본 유지
print(c) # [1, 1, 3, 4, 5]
# key 함수 사용
words = ["banana", "apple", "cherry", "date"]
words.sort(key=len) # 길이 기준 정렬
print(words) # ['date', 'apple', 'banana', 'cherry']
# 절댓값 기준 정렬
nums = [-5, 3, -1, 4, -2]
nums.sort(key=abs)
print(nums) # [-1, 3, -2, 4, -5]
# 자주 하는 실수: sort()의 반환값을 변수에 저장
a = [3, 1, 2]
b = a.sort() # sort()는 None을 반환
print(b) # None
# 올바른 방법
a.sort()
print(a) # [1, 2, 3]
# 또는 sorted() 사용
a = [3, 1, 2]
b = sorted(a)
print(b) # [1, 2, 3]
remove와 pop
a = [1, 2, 3, 2, 4]
# remove(): 첫 번째로 나오는 값 제거
a.remove(2)
print(a) # [1, 3, 2, 4] - 첫 번째 2만 제거
# pop(): 마지막 원소 제거 후 반환
last = a.pop()
print(last) # 4
print(a) # [1, 3, 2]
# pop(i): i번 인덱스 제거 후 반환
item = a.pop(1)
print(item) # 3
print(a) # [1, 2]
# 없는 값을 remove하면 에러
a = [1, 2, 3]
a.remove(5)
# ValueError: list.remove(x): x not in list
# 제거 전에 확인
if 5 in a:
a.remove(5)
count와 index
a = [1, 2, 3, 2, 2, 4]
print(a.count(2)) # 3 - 2가 3개
print(a.count(5)) # 0 - 없으면 0
print(a.index(3)) # 2 - 3은 인덱스 2에 있음
print(a.index(2)) # 1 - 첫 번째 2의 인덱스
# 없는 값의 index는 에러
a.index(5)
# ValueError: 5 is not in list
특정 값 원소 모두 제거
remove()는 한 번에 하나만 지운다. 특정 값을 모두 제거하려면 컴프리헨션을 쓴다.
a = [1, 2, 3, 4, 5, 2, 3, 2]
# 2를 모두 제거
result = [x for x in a if x != 2]
print(result) # [1, 3, 4, 5, 3]
# 원본 변경
a = [x for x in a if x != 2]
print(a) # [1, 3, 4, 5, 3]
집합을 이용한 방법도 있지만, 순서가 유지되지 않으니 주의한다.
a = [1, 2, 3, 4, 5, 2, 3, 2]
remove_set = {2, 3}
# 여러 값을 한 번에 제거
result = [x for x in a if x not in remove_set]
print(result) # [1, 4, 5]
x not in remove_set에서 remove_set이 집합이면 in 연산이 O(1)이고,
리스트면 O(N)이다. 제거할 값이 많을 때는 집합으로 만들어 두는 게 효율적이다.
리스트 연산
a = [1, 2, 3]
b = [4, 5, 6]
# 합치기
c = a + b
print(c) # [1, 2, 3, 4, 5, 6]
# 반복
d = a * 3
print(d) # [1, 2, 3, 1, 2, 3, 1, 2, 3]
# in 연산자
print(2 in a) # True
print(5 in a) # False
print(5 not in a) # True
리스트 복사 주의사항
# 얕은 복사 - 같은 객체를 참조
a = [1, 2, 3]
b = a # 같은 리스트를 가리킴
b[0] = 100
print(a) # [100, 2, 3] - a도 바뀜!
# 진짜 복사
c = a.copy() # 방법 1
d = a[:] # 방법 2
e = list(a) # 방법 3
c[0] = 999
print(a) # [100, 2, 3] - 원본 유지
print(c) # [999, 2, 3]
# 2차원 리스트는 얕은 복사로 안 됨
import copy
matrix = [[1, 2], [3, 4]]
shallow = matrix.copy()
shallow[0][0] = 999
print(matrix) # [[999, 2], [3, 4]] - 내부 리스트는 공유됨!
deep = copy.deepcopy(matrix)
deep[0][0] = 100
print(matrix) # [[999, 2], [3, 4]] - 영향 없음
2차원 이상의 리스트를 복사할 때는 copy.deepcopy()를 사용한다.
정리
- 2차원 리스트 초기화는 반드시
[[0] * m for _ in range(n)] a[::-1]로 역순 리스트 생성sort()는 원본 변경,sorted()는 새 리스트 반환remove()는 첫 번째 값만 제거, 없으면ValueErrorinsert()와pop(i)는 O(N),append()와pop()은 O(1)- 리스트 복사는
=가 아닌a[:]또는a.copy()사용