13주차 · 실용 알고리즘과 데이터 활용
- 카운팅·정렬·탐색 3가지 패턴을 EE 맥락(센서 로그, 측정값, 임계값)에서 사용한다
dict로 빈도를 세는 카운팅 패턴을 작성한다sorted(..., key=함수)로 측정 레코드를 원하는 기준으로 정렬한다for루프와break로 조건을 만족하는 첫 번째 항목을 효율적으로 탐색한다- 알고리즘 결과를 의사결정 문장으로 요약한다
O(n)과O(n²)차이를 루프 횟수로 직관적으로 설명한다
알고리즘 quickstart: 3가지 패턴 뼈대
Section titled “알고리즘 quickstart: 3가지 패턴 뼈대”공학에서 “알고리즘”이라고 하면 복잡한 수학을 떠올리기 쉽지만, 실제 현장 데이터 처리의 대부분은 “이 데이터로 무엇을 결정하려는가”라는 질문에서 출발합니다. 그리고 그 질문은 거의 항상 다음 셋 중 하나로 정리됩니다. 무엇이 얼마나 자주 일어났는지, 무엇이 가장 중요한지, 어디서 처음 문제가 생겼는지입니다. 이 세 질문에 각각 카운팅·정렬·탐색으로 답합니다.
그래서 새로운 문제를 만나면 코드를 먼저 쓰지 말고, 문제 문장을 질문으로 바꾼 뒤 어느 패턴에 속하는지부터 분류하세요. 패턴이 정해지면 자료구조와 루프 구조가 거의 자동으로 따라옵니다.
| 문제 질문 | 공학 의사결정 | 우선 떠올릴 패턴 | 결과 형태 |
|---|---|---|---|
| ”각 상태가 몇 번 나왔나?” | 어떤 고장 모드가 빈번한가 | 카운팅 | dict |
| ”큰 순서대로 보고 싶다” | 어떤 채널/부품을 먼저 볼까 | 정렬 | 정렬된 리스트 |
| ”처음 기준을 넘는 값은?” | 언제 경보가 처음 울렸나 | 탐색 | 값 1개 또는 None |
패턴 1 — 카운팅 (dict)
Section titled “패턴 1 — 카운팅 (dict)”counts = {}for x in items: counts[x] = counts.get(x, 0) + 1패턴 2 — 정렬 (sorted + key 함수)
Section titled “패턴 2 — 정렬 (sorted + key 함수)”def sort_key(item): return item["value"] # 정렬 기준을 함수로 명시한다
ordered = sorted(items, key=sort_key)패턴 3 — 탐색 (for / break)
Section titled “패턴 3 — 탐색 (for / break)”found = Nonefor x in items: if x > threshold: found = x break # 첫 번째 매치에서 즉시 종료처음 쓸 때 체크할 4가지
Section titled “처음 쓸 때 체크할 4가지”- 문제를 먼저 카운팅 / 정렬 / 탐색 중 하나로 분류한다
- 카운팅·조회가 많으면
dict를 쓴다 - 첫 번째 항목만 필요하면
break로 조기 종료한다 - 결과를 의사결정 문장 한 줄로 요약한다
이번 주에 깊게 쓰는 도구
Section titled “이번 주에 깊게 쓰는 도구”세 패턴은 이미 배운 문법을 새로운 의도로 다시 쓰는 것입니다. 아래 4가지 도구의 “왜 쓰는가”를 먼저 익혀 두면 코드가 훨씬 쉽게 읽힙니다.
| 도구 | 하는 일 | 왜 이 패턴에서 쓰나 |
|---|---|---|
dict.get(key, 0) | 키가 없으면 기본값을 돌려줌 | 카운팅에서 “처음 보는 키”를 안전하게 0부터 시작 |
sorted(..., key=함수) | 각 원소에 함수를 적용한 값으로 줄 세움 | 정렬 기준을 코드 밖으로 분리해 읽기 쉽게 |
(-x, y) 튜플 키 | 1차 기준이 같을 때 2차 기준으로 비교 | 정렬에서 동점 처리(tie-break)를 명시 |
break | 루프를 그 자리에서 즉시 종료 | 탐색에서 첫 매치 후 불필요한 반복 제거 |
자료구조 선택 기준
Section titled “자료구조 선택 기준”| 자료구조 | 언제 쓰나 | EE 예시 |
|---|---|---|
list | 순서가 중요하고 인덱스로 접근할 때 | 시간별 측정값 시퀀스 |
dict | ”키 → 값” 조회나 카운팅이 많을 때 | 상태 코드별 발생 횟수 |
set | 중복 제거나 포함 여부 검사가 필요할 때 | 고유 채널 번호 집합 |
시간복잡도 직관 (초급용)
Section titled “시간복잡도 직관 (초급용)”시간복잡도는 데이터가 커질 때 작업량이 어떤 속도로 늘어나는지를 나타냅니다. 정확한 초 단위 시간보다 증가 추세를 봅니다. 측정값이 24개일 때는 어떤 방법을 써도 충분히 빠르지만, 24만 개가 되면 이 추세 차이가 실제 실행 시간 차이로 드러납니다.
O(n): 루프 한 겹 → 데이터 2배면 반복 횟수도 약 2배 (예: 리스트를 한 번 훑기)O(n²): 루프 두 겹 → 데이터 2배면 반복 횟수는 약 4배 (예: 모든 쌍을 비교)
위 그림에서 두 곡선은 작은 n에서는 큰 차이가 없지만, n이 커질수록 O(n²)가 급격히 위로 치솟습니다. 같은 데이터를 두 겹 루프로 비교하는 방식은 데이터가 조금만 커져도 빠르게 느려진다는 뜻입니다. 그래서 “같은 키를 또 찾으려고 리스트를 매번 다시 훑는” 코드는 O(n²)가 되기 쉽고, 이때 dict로 한 번에 조회하면 O(n)으로 줄일 수 있습니다.
n | O(n) 작업량 | O(n²) 작업량 |
|---|---|---|
| 10 | 10 | 100 |
| 100 | 100 | 10,000 |
| 1,000 | 1,000 | 1,000,000 |
루프 횟수로 직접 느끼기
Section titled “루프 횟수로 직접 느끼기”실행 시간이 아니라 반복 횟수를 세어 보면 O(n)과 O(n²) 차이를 더 쉽게 볼 수 있습니다.
아래 예제에서 n을 10에서 100으로 바꾸면, 한 겹 루프는 10배 늘지만 두 겹 루프는 100배 늘어납니다.
1) 카운팅 패턴 — 센서 상태 로그 집계
Section titled “1) 카운팅 패턴 — 센서 상태 로그 집계”카운팅은 긴 리스트를 짧은 빈도표로 압축하는 작업입니다. 수백 줄의 상태 로그를 그대로 읽는 대신, “각 상태가 몇 번 나왔는가”라는 한 장의 요약표로 바꾸면 시스템 상태를 한눈에 파악할 수 있습니다. 이때 표의 키는 상태 이름이고 값은 누적 횟수이므로, 자료구조는 자연스럽게 dict가 됩니다.
핵심은 counts.get(x, 0) + 1 한 줄입니다. get(x, 0)은 “키 x가 이미 있으면 그 값을, 없으면 0을 돌려줘”라는 뜻입니다. 그래서 처음 보는 상태는 0에서 시작해 1이 되고, 이미 본 상태는 기존 값에 1을 더합니다. 만약 dict 대신 list로 “이 상태가 전에 나온 적 있나?”를 매번 다시 훑으면 루프 안에 또 루프가 생겨 O(n²)가 됩니다. 반면 dict 조회는 평균 O(1)이라 전체가 O(n)으로 끝납니다. 카운팅에 dict를 쓰는 이유가 여기 있습니다.
위 그림처럼 입력 리스트를 왼쪽에서 오른쪽으로 한 번만 훑으면서, 같은 키를 만날 때마다 오른쪽 빈도표의 막대를 1칸씩 키웁니다. 마지막에 sum(counts.values())가 원본 길이와 같은지 확인하면, 빠뜨리거나 중복으로 센 항목이 없는지 검증할 수 있습니다.
counts.get(status, 0): 딕셔너리에 키가 없으면 0을 기본값으로 반환합니다max(counts, key=get_count):counts딕셔너리의 키 중에서get_count를 적용했을 때 가장 큰 값을 찾습니다. 여기서key는 “무엇을 기준으로 비교할 것인가”를 알려주는 함수입니다counts.values(): 딕셔너리에서 값만 꺼냅니다. 예를 들어sum(counts.values())는 세어 놓은 전체 횟수가 원본 로그 길이와 같은지 검증할 때 유용합니다- 결과는 단순한 숫자가 아니라 의사결정 문장으로 마무리합니다
한 단계 더: 빈도표를 많이 나온 순서로 정렬하기
Section titled “한 단계 더: 빈도표를 많이 나온 순서로 정렬하기”카운팅 결과를 보고할 때는 가장 많이 나온 상태부터 보여 주는 경우가 많습니다.
딕셔너리의 items()는 ("OK", 4)처럼 키와 값이 한 쌍인 tuple들을 만들어 줍니다.
이때 item[0]은 상태 이름, item[1]은 횟수입니다.
reverse=True와 -item[1] 중 무엇을 쓰나?
Section titled “reverse=True와 -item[1] 중 무엇을 쓰나?”둘 다 내림차순 정렬에 사용할 수 있습니다.
reverse=True는 “정렬 결과 전체를 뒤집는다”는 뜻이고, -item[1]은 “횟수에 마이너스를 붙여 큰 값이 앞에 오도록 기준을 바꾼다”는 뜻입니다.
초급 단계에서는 둘 중 하나만 일관되게 쓰면 충분하지만, 여러 기준을 섞어 정렬할 때는 return (-count, name)처럼 기준 함수를 쓰는 방식이 읽기 쉽습니다.
2) 정렬 패턴 — 측정값 우선순위 정렬
Section titled “2) 정렬 패턴 — 측정값 우선순위 정렬”정렬은 “어떤 기준으로 우선순위를 매길 것인가”를 코드로 표현하는 과정입니다. **“각 레코드에 점수표를 붙인 뒤 그 점수로 줄을 세운다”**고 생각하면 쉽습니다. 이 “점수표를 붙이는 규칙”이 key 함수입니다. 정렬 알고리즘 자체는 신경 쓰지 않아도 되고, 우리가 정하는 것은 비교 기준 하나뿐입니다.
sorted(..., key=함수)에서 key에는 함수 객체를 넘깁니다. lambda도 사용할 수 있지만, 이 수업에서는 가독성을 위해 **이름 있는 함수(def)**를 먼저 권장합니다. 정렬 기준을 함수로 분리하면, 기준이 바뀌어도 정렬 코드는 그대로 두고 key 함수만 고치면 됩니다.
위 그림에서 key 함수는 각 채널에 (-snr, id)라는 비교용 점수를 매깁니다. snr에 마이너스를 붙였으므로 SNR이 큰 채널이 앞으로 옵니다(내림차순). SNR이 같은 CH3·CH4는 2차 기준인 채널 ID 오름차순으로 순서가 정해집니다. 이렇게 튜플로 여러 기준을 한 번에 넘기면 “1차 기준이 같을 때 어떻게 처리할지(tie-break)“까지 명확하게 표현됩니다. 또한 Python의 sorted는 안정 정렬이라, 동점 항목의 원래 등장 순서가 그대로 보존됩니다.
key 파라미터란?
Section titled “key 파라미터란?”sorted(items, key=f)는 각 원소에 함수 f를 적용한 결과를 기준으로 정렬합니다.
함수 이름만 넘기고 괄호는 붙이지 않습니다.
def sort_key(record): return record["snr"] # 함수를 정의하고
ordered = sorted(records, key=sort_key) # 이름만 넘긴다 (sort_key() 가 아님) -ch["snr"]를 쓰는 이유:sorted는 기본 오름차순이므로 내림차순으로 정렬하려면 부호를 반전합니다CH3와CH4는 SNR이 같으므로 두 번째 기준인 채널 ID 오름차순으로 결정됩니다- 정렬 결과의 첫 번째 원소가 최우선 채널입니다
3) 탐색 패턴 — 임계값 초과 항목 찾기
Section titled “3) 탐색 패턴 — 임계값 초과 항목 찾기”탐색은 “조건을 만족하는 항목을 찾는” 패턴입니다. 여기서 배우는 방식은 앞에서부터 하나씩 확인하는 **선형 탐색(linear search)**으로, 데이터가 정렬되어 있지 않아도 항상 동작하는 가장 기본적인 방법입니다. **“앞에서부터 하나씩 확인하다가 원하는 것을 찾는 순간 멈춘다”**는 그림을 떠올리면 됩니다.
핵심은 두 가지입니다. 첫째, 첫 번째 매치만 필요하면 break로 루프를 즉시 종료해 불필요한 반복을 줄입니다. 가장 좋을 때는 첫 항목에서 끝나고, 가장 나쁠 때는 끝까지 가며, 평균적으로는 절반쯤 확인합니다. 둘째, found = None으로 시작해 두면 루프가 끝난 뒤 “찾았다 / 못 찾았다”를 명확히 구분할 수 있습니다. 탐색에서는 “못 찾음”도 똑같이 중요한 결과이기 때문입니다.
위 그림에서 임계값 5.0 V를 넘는 첫 값(5.2 V)을 만나면 break가 실행되어 뒤쪽 값은 확인하지 않습니다. 6개 중 4개만 보고 멈춘 것이 조기 종료의 효과입니다.
break는 첫 번째 매치를 찾은 즉시 루프를 종료합니다 — 이후 데이터를 불필요하게 확인하지 않습니다loop_count를 출력하면 조기 종료 효과를 직접 확인할 수 있습니다first_over is not None조건으로 탐색 실패 케이스를 안전하게 처리합니다
탐색 실패 케이스도 반드시 처리하기
Section titled “탐색 실패 케이스도 반드시 처리하기”탐색에서는 “찾았다”만큼 “못 찾았다”도 중요한 결과입니다.
처음에 found = None으로 시작하면, 루프가 끝난 뒤에도 None인지 확인해서 실패 케이스를 안전하게 처리할 수 있습니다.
4) 작은 통합 예시 — 카운팅 → 정렬 → 탐색
Section titled “4) 작은 통합 예시 — 카운팅 → 정렬 → 탐색”실제 문제는 한 패턴만 쓰고 끝나지 않는 경우가 많습니다. 아래 예시는 같은 채널 로그에서 상태를 세고, 전력 기준으로 정렬하고, 기준보다 강한 첫 채널을 찾습니다.
통합 예시에서 확인할 점
Section titled “통합 예시에서 확인할 점”status_counts는dict카운팅 패턴입니다.ordered는 정렬 결과이므로 원본channel_log와 순서가 다릅니다.- 첫 번째 후보만 필요하므로 탐색 루프에서
break를 사용합니다. - 마지막
assert는 상태 카운트가 원본 데이터 개수와 맞는지 확인합니다.
자주 하는 실수
Section titled “자주 하는 실수”| 실수 | 증상 | 수정 방법 |
|---|---|---|
list로 모든 것을 처리 | 같은 키를 반복 탐색해 코드가 길어짐 | 카운팅·조회는 dict로 분리 |
복잡한 lambda 사용 | 정렬 기준이 한 줄에 숨어 읽기 어려움 | def sort_key(x): return ... 형태로 분리 |
break 없는 탐색 | 첫 매치 후에도 루프가 계속 돔 | 첫 매치 직후 break 추가 |
| 해석 문장 없음 | 숫자만 출력하고 끝남 | 의사결정 문장 한 줄 작성 |
| 이중 루프 남용 | 데이터가 커지면 급격히 느려짐 | dict/set 기반 단일 순회로 재설계 |
교정 워크플로 (이론 → 코드 → 해석)
Section titled “교정 워크플로 (이론 → 코드 → 해석)”- 이론: 문제를 “카운팅 / 정렬 / 탐색” 중 하나로 분류한다.
- 이론: 자료구조를 선택하고 루프 겹 수를 예상한다.
- 코드: 가장 짧은 정답 루프부터 작성한다.
- 코드: 중간 결과(
counts,ordered)를 출력해 검증한다. - 해석: 결과를 근거로 다음 행동 문장을 한 줄 작성한다.
실습 문제 · 패턴 확인하기
Section titled “실습 문제 · 패턴 확인하기”목표: 리스트에 들어 있는 상태값의 발생 횟수를 딕셔너리로 셉니다.
해야 할 일: status_counts를 완성하고 전체 개수가 원본 로그 길이와 같은지 확인하세요.
완료 조건: 상태별 개수와 합계 검증 결과가 출력되어야 합니다.
목표: 수신 전력 데이터를 강한 순서로 정렬합니다.
해야 할 일: 코드의 정렬 방향 버그를 고쳐 상위 2개 채널을 출력하세요.
완료 조건: D, B 순서로 상위 채널이 출력되어야 합니다.
목표: 센서 로그에서 가장 많이 발생한 상태를 찾습니다.
해야 할 일: 빈도 딕셔너리를 만들고, max(…, key=…)로 최빈 상태를 찾으세요.
완료 조건: counts, most_common, 해석 문장이 출력되어야 합니다.
목표: 전압 측정 시계열에서 임계값을 처음 초과하는 시점을 찾습니다.
해야 할 일: 조건을 만족하는 첫 레코드를 first_over에 저장하고 즉시 반복을 멈추세요.
완료 조건: 첫 초과 시점과 해석 문장이 출력되어야 합니다.
목표: 카운팅, 정렬, 탐색을 하나의 데이터 처리 흐름으로 연결합니다.
해야 할 일: 상태 빈도, 전력 강한 순 정렬, 기준을 넘는 첫 채널 탐색을 차례대로 구현하세요.
완료 조건: status_counts, 정렬된 채널 ID 목록, 첫 강신호 채널, 해석 문장이 출력되어야 합니다.
제출 전 자기점검
Section titled “제출 전 자기점검”- 문제를 카운팅/정렬/탐색 중 하나로 먼저 분류했는가?
- 딕셔너리 카운팅에서 없는 key를 안전하게 처리했는가?
- 정렬 방향이 문제 의도와 맞는가?
- 첫 번째 결과만 필요할 때
break를 사용했는가? - 숫자 결과 뒤에 해석 문장을 붙였는가?