문제 상황 : 괄호가 바르게 짝지어졌다는 것은 '(' 문자로 열렸으면 반드시 짝지어서 ')' 문자로 닫혀야 한다는 뜻입니다. 예를 들어
"()()" 또는 "(())()" 는 올바른 괄호입니다. ")()(" 또는 "(()(" 는 올바르지 않은 괄호입니다.
'(' 또는 ')' 로만 이루어진 문자열 s가 주어졌을 때, 문자열 s가 올바른 괄호이면 true를 return 하고, 올바르지 않은 괄호이면 false를 return 하는 solution 함수를 완성해 주세요.
1.
먼저 빠른 탈출을 위해 다음 4가지 경우에는 함수를 False로 얼리 리턴하도록 설정해주겠습니다.
주어진 문자열이 ")" 로 시작하는 경우
주어진 문자열이 "(" 로 끝나는 경우
주어진 문자열의 길이가 홀수인 경우
주어진 문자열이 ")" 또는 "(" 한 종류로만 이루어진 경우
def solution(s):
if s[0] == ")" or s[-1] == "(" or len(s) % 2 == 1 or len(set(s)) == 1:
return False
2.
다음으로 stack 리스트를 선언해주도록 하겠습니다.
stack = []
우리는 stack 리스트에 "(" 열기 괄호만 넣어줄 것입니다.
왜 열기 괄호만 넣어줄까요? 예시를 통해 한번 확인해 봅시다.
문자열 s = "(((())"가 있다고 합시다. 딱 봐도 열기 괄호는 4개인데 닫기 괄호는 2개여서 짝이 맞지 않는다는 것을 알 수 있습니다. 우리는 for루프를 통해 0번째 자릿값부터 차례대로 돌면서 해당 자리의 값이 "("이면 stack에 넣어주고, ")"이면 stack에 있는 열기괄호를 하나 빼 줄 것입니다.
for char in s:
if char == '(':
stack.append(char)
elif char == ')':
if not stack:
return False
stack.pop()
s = "(((())"
첫 번째 자리 "(" -> stack = "("
두 번째 자리 "(" -> stack = "(", "("
세 번째 자리 "(" -> stack = "(", "(", "("
네 번째 자리 "(" -> stack = "(", "(", "(", "("
다섯 번째 자리 ")" -> stack이 비어있지 않으므로 stack.pop() -> stack = "(", "(", "("
여섯 번째 자리 ")" -> stack이 비어있지 않으므로 stack.pop() -> stack = "(", "("
if stack:
return False
else:
return True
이터레이션을 다 돌고 났을 때
stack이 비어 있으면 짝이 맞으므로 True
stack에 남은 값이 있다면 짝이 맞지 않는 것이므로 False를 리턴합니다.
결국 "("의 갯수와 ")"의 갯수가 동일한지 확인하는 것과 다름 없습니다. 다른 방법으로도 정확성 문제는 통과하는 데 어려움이 없으나, 효율성에 문제가 생기는 관계로 stack 알고리즘을 통해 효율성을 높여준 것이라고 보시면 되겠습니다.
3.
마지막으로 한 번에 코드를 보겠습니다.
def solution(s):
if s[0] == ")" or s[-1] == "(" or len(s) % 2 == 1 or len(set(s)) == 1:
return False
stack = []
for char in s:
if char == '(':
stack.append(char)
elif char == ')':
if not stack:
return False
stack.pop()
if stack:
return False
else:
return True
클릭하시면 태블로 퍼블릭 웹사이트에서 인터렉티브하게 직접 결과를 조절해 보실 수 있습니다.
<시각화 이후 생각해볼만한 것들>
1. 구별 화장실 수와 구별 면적의 관계는 어떻게 되는가? 2. 구별 화장실 수와 지하철 역의 개수의 상관관계가 있는가? 3. 구별 화장실 수와 구별 인구 수의 관계는 어떻게 되는가? 인구와 구별 화장실 총 수는 비례하는가? 4. 서울특별시 구별 장애인 화장실 데이터를 따로 구할 수 있는가? 구할 수 있다면, 전체 화장실과 장애인 화장실의 비율을 비교해 보자. 5. 상업 단지와 화장실 수의 상관관계가 있는가?
6. 관광 구역과 화장실 수의 상관관계가 있는가?
7. 공공화장실 중에서 지하철 역 화장실의 개수를 특정할 수 있는가? 있다면 그 비율은 어떻게 되는가?
8. 지하철 역 화장실 개수를 구할 수 있다면, 지하철 노선별 유동인구 데이터와 병합하여 화장실의 갯수가 적절하게 비치되어 있는지 비교해보자.
여기까지입니다 :-) 좀더 생각해볼 수 있을것 같지만 본격적인 개인 EDA 프로젝트 준비를 위해서 이번엔 이정도로 간단히만 포스팅을 마치려고 합니다. 쉽고 짧은 작업이었지만 맘에드는 공공데이터를 구하고, 정제하고, 시각화 후 생각해볼거리 도출까지 은근 시간이 걸렸네요. 다음엔 더 능숙하고 멋진 프로젝트를 가져와서 공유해보겠습니다. 감사합니다. :)
(+) 판다스 데이터프레임 CSV파일로 내보내기 한 후 태블로에 불러온 과정
t.to_csv("toilet_df.csv")
먼저 to_csv() 메소드를 통해 간단하게 csv파일로 내보내기를 해주었습니다. 저는 구글 코랩에서 실습을 진행했습니다.
구글 코랩의 왼쪽 파일메뉴에서 간단하게 바로 다운로드를 해서 다운로드 폴더에 넣어 주었는데요. 로컬 환경에 따로 다운로드하지않고 태블로 퍼블릭 환경에서 구글 드라이브를 연동해서 바로 오픈해도 됩니다.
태블로 퍼블릭 프로그램을 실행하고 로컬 환경에 다운로드한 toilet_df.csv 파일을 오픈했습니다.
따로 사용하지 않을 예정인 산지, 부지번 컬럼은 숨기기(Hide) 해주었습니다. 이후 시트 2개에서 작업을 하고 대시보드 1개에서 두개를 합쳐주는 방식으로 간단히 끝을 내주었습니다.
서울시 공중화장실 공공데이터를 가지고 아주 간단한 데이터 시각화, 분석 실습을 해 보려고 합니다. 먼저 이번 포스팅에는 파이썬 pandas 라이브러리를 이용해서 데이터 전처리 작업한 것을 간단히 정리해 보았습니다. 데이터 시각화, 분석은 태블로 프로그램을 이용하여 마친 뒤 다음 포스팅에 이어서 올리도록 하겠습니다.
벌써 4주 차 회고를 작성합니다. 4주차도 매일 1시간 30분 일찍 도착해서 아침공부를 하고, 9-6 수업을 듣고, 6-9 저녁공부를 하고 집에 갔습니다. 뿌듯합니다.
분명 첫주에 패딩을 입고 다녔었는데... 이 회고를 작성하고 있는 오늘(일요일)의 낮 최고 기온이 28도에 육박했습니다. 그래서 반팔 반바지를 입고 나가서 카공을 하고 왔어요. 후텁지근했지만 아직 습하지는 않아서 따뜻하고 공기가 바삭한 게 저는 참 좋았습니다. 매일 하는 게 공부밖에 없는 단조로운 일상을 보내다 보니, 이런 날씨 변화가 주변을 좀 더 돌아보고 한숨 돌리게 하는 것이 반가웠습니다.
이번 주에 처음으로 이력서라는 걸 작성했습니다. 임용고시에 합격한 교사는 교육청에서 알아서 발령을 내 주기 때문에 이력서나 자기소개서 같은 걸 작성할 필요가 없습니다. 그래서 경험이 전무했어요. 다행히 지난번에 들었던 이력서와 자기소개서 작성 특강 내용이 큰 도움이 되었습니다. 특강 오신 강사님께서 공유해주신 기본 템플릿을 참고해서 기본 이력서를 국문과 영문으로 하나씩 작성하고, 혹시 몰라 노션 페이지도 간단하게 제작을 했습니다. 생각보다 오랜 시간과 많은 노력을 들여야 했습니다.
항상 학생들이 쓴 글을 평가하고 학부모들이 제출한 신청서를 심사하고 분류하는 일을 하던 제가 이제는 저좀 뽑아주십사 서류를 써서 제출하는 입장이 된 것이 재밌습니다. 사람 일이 어떻게 될지 모른다는 걸 매일 체감합니다. 과연 내가 인사 담당자라면 나의 이력서를 보고 마음이 동할까? 하는 한편의 메타인지를 발동해 가며 최대한 나를 잘 표현하고자 노력했어요. 힘들었지만 재밌기도 했습니다. 저의 성장 과정에 대해 돌아보고 스스로의 강점과 단점에 대해 숙고해 보는 경험을 통해 앞으로 원하는 기업에 지원하기까지 무엇을 더 보충하고 발전시켜야 할 것인지 플랜을 세울 수 있었습니다.
아무래도 아직 완성한 프로젝트가 하나도 없다 보니 프로젝트 칸은 비워뒀어요. 생각은 하고 있었지만, 텅 빈 프로젝트란을 보니 제대로 된 프로젝트 뭐든 하나라도 있어야한다는 생각이 확 들더라고요. 지금까지 저는 데이터분석과 머신러닝에 필요한 각종 프레임워크에 익숙해지고 기술적인 능력을 키우는 데 집중을 하고 있었거든요. 공부는 계속하되 가능한 빨리 개인 프로젝트를 구체화기로 마음먹었습니다. 다만 프로젝트 주제를 정하는 일이 생각보다 어려운 게 걱정입니다. 동기들과 이야기해 보면 다들 관심 있는 분야 하나정도 가지고 있더라고요. 금융이면 금융, 게임이면 게임, 유통이면 유통, 그런 거 말이죠. 저는 딱히 하나의 분야에 꽂혀 있지는 않고, 데이터 사이언티스트로서 머신러닝과 딥러닝을 아우를 수 있는 멀티플레이어로 성장하고 싶다.. 뭐 그런 거시적인 목표만 있는 상태입니다. 그래서 주제 구체화가 더욱 막막한데요. 앉아서 머릿속으로 고민만 하면 답이 나오지 않는다는 걸 인생 경험으로 알고 있기 때문에 먼저 데이터 수집부터 이것저것 시작해보려고 합니다.
이번주에 수업에서 웹 크롤링 기법 여러가지를 배웠는데, 크롤링을 해본 적이 없던 저에게 정말 큰 도움이 되었습니다. 소위 노가다성 작업이었음에도 불구하고 생각보다 훨씬 더 재밌었어요. 어떤 식으로 블로그에 정리를 할까 하다가, 수업 내용을 그대로 정리해서 올리면 편하긴 하겠지만 남는 게 없을 것 같아서, 간단한 토이 플젝을 진행해 봤습니다. 연금복권 720+ 당첨 결과를 크롤링 후 정제해서 데이터프레임화하고 간단한 분석을 해보는 실습을 하고 과정을 정리해 포스팅했어요. 정말 재밌었고 복습이 쏙쏙 됐습니다. 역시 스스로 생각하고 작업한 내용을 글로 출력하면 배운 내용을 체화하고 내 것으로 만드는 데 정말 큰 도움이 되네요. 다음 주에는 셀레니움 프레임워크 사용하는 법을 배우는데 큰 기대가 됩니다.
다음 주 회고에서는 주제를 좀더 구체화해서 가지고올 수 있기를 기대하며, 다들 일교차와 냉방병 조심하시고 건강하세요.
파이썬 requests, BeautifulSoup 라이브러리를 이용한 웹크롤링 후 데이터분석 실습을 해보았습니다 :-)
연금복권720+은 제가 한달에 2-3회정도 꾸준하게 구매하는 최애 복권인데요. 슬프게도 지금까지 제대로 당첨된 적은 단 한번도 없지만, 앞으로도 저는 꾸준히 구매를 할 예정인 아주아주 매력적인 복권입니다. 1등에 당첨이 되면 (세전) 700만원을 매월 20년동안 수령할 수 있어요. 동행복권 온라인 사이트에서 간단히 온라인 구매를 할 수도 있구요. 1등 번호는 온라인 1명, 오프라인 1명 총 2명이 당첨될 수 있습니다. 자세한 복권 구조는 동행복권 홈페이지를 참고해 보시구요.
복권의 경우 통계를 공부해보신 분들께는 아주 친숙한 소재이실텐데요. (저는 아닙니다.ㅋㅋㅋ) 동행복권 사이트에서는 복권 당첨번호를 엑셀파일로도 제공하고 통계 자료를 따로 분석해서 메뉴도로 제공하고 있습니다. 다만 저는 철저히 requests 라이브러리를 이용한 웹크롤링에 익숙해지기 위해서 엑셀 파일이나 통계자료를 건드리지 않고 처음부터 끝까지 혼자 힘으로 본 실습을 했습니다!
[참고] 본 포스팅은 수리링 본인의 공부 기록을 목적으로 작성하였습니다. 해당 라이브러리에 대해 전혀 모르시는 분께서 보면서 따라하시기엔 많이 불친절하게 느껴질 수 있습니다. 참고하시고 봐 주시면 감사드리겠습니다 :-)
[참고] 본 포스팅은 책, 강의, 다른 사람의 포스팅을 참고하지 않은 스스로의 창작물입니다! 참고하여 포스팅 하시는 경우 출처 밝혀주심 감사드리겠습니다!
[실습 목차]
206회차로 모의 실습
원하는 회차 구간을 입력하면 모든 정보를 담아 데이터프레임으로 리턴하는 함수 작성
데이터프레임으로 간단한 데이터분석 (은근 재밌으니 귀찮으시면 이것만 보고 가세요...^^)
그런데 기본 URL에 회차 정보가 드러나지 않고 숨어 있어요. 206회를 조회해도, 200회를 조회해도 계속 같은 URL이 유지됩니다. 따라서 회차를 특정하여 정보를 뽑아낼 수가 없는 상황입니다. 우리는 회차를 조회할 수 있는 상세URL을 알아내야 해요.
문제상황을 해결하기 위해 크롬 웹브라우저의 inspection(개발자 도구) 메뉴의 Network 탭을 확인해 봅시다.
위와 같이 네트워크 탭을 켜둔 상태로 조회 버튼을 눌러봅니다. Name 탭의 맨 첫 번째 gameResult 어쩌구를 클릭한 다음 Payload를 확인합니다. (누가 봐도 수상한) Round: 206 이라는 정보를 확인했습니다. 기존 url 뒤에 &Round=206을 붙여 주면 될 것 같다는 합리적 의심을 해봅니다.
206회차를 가지고 적당히 연습을 해 봤으니, 원하는 회차를 입력하면 하나씩 모두 조회해서 딕셔너리로 담아 리턴하는 함수를 작성해 보았습니다.
def win720(round):
# 입력받은 회차 번호로 url을 만들고 정보를 받아냅니다.
url = f"https://dhlottery.co.kr/gameResult.do?method=win720&Round={round}"
res = requests.get(url)
soup = BS(res.text, "html.parser")
rows = soup.find("tbody").find_all("tr")
# data_dict에 앞으로 하나씩 정보를 추가할 겁니다. 먼저 라운드 값을 첫 번째로 넣어줬습니다.
data_dict = {"round":round}
nums = rows[0].find_all("span", {"class":"num"})
# 1등 조, 당첨번호
group = int(nums[0].find("span").text)
n_1 = int(nums[1].find("span").text)
n_2 = int(nums[2].find("span").text)
n_3 = int(nums[3].find("span").text)
n_4 = int(nums[4].find("span").text)
n_5 = int(nums[5].find("span").text)
n_6 = int(nums[6].find("span").text)
data_dict["group"] = group
data_dict["n_1"] = n_1
data_dict["n_2"] = n_2
data_dict["n_3"] = n_3
data_dict["n_4"] = n_4
data_dict["n_5"] = n_5
data_dict["n_6"] = n_6
# 1-7등 당첨자수
for i in range(7):
rank_counts = rows[i].find_all("td", {"class":"ta_right"})[-1].text.strip()
rank_counts = re.sub(",","", rank_counts)
rank_counts = int(rank_counts)
column_name = f"rank{i+1}"
data_dict[column_name] = rank_counts
# 보너스 당첨번호 6개
for i in range(6):
bonus_num = rows[7].find_all("span", {"class" : "num"})[i].find("span").text
column_name = f"bonus_{int(i)+1}"
data_dict[column_name] = bonus_num
# 보너스 당첨자수
bonus_counts = int(rows[7].find_all("td", {"class":"ta_right"})[-1].text.strip())
data_dict["bonus"] = bonus_counts
return data_dict
더럽게 길지만 그래도 잘 작동했습다^_^;;;; 너무 길어져서 쓰면서 불길했는데 그래도 오류 수정 2-3번만에 원하는 대로 값이 나와서 다행이였어요
205회차로 테스트를 해 봤는데요. 조, 1등 넘버 6자리, 등수별 당첨매수, 보너스 번호 6자리, 보너스 당첨매수가 딕셔너리로 제대로 들어온 것을 확인했습니다 :) 이게 뭐라고 너무 재밌었어요 (ㅋㅋㅋㅋ)
2-2. 회차 구간을 설정하고 데이터프레임을 리턴하는 함수 작성하기
위에서 작성한 win720()함수를 가지고 원하는 회차 구간의 모든 정보를 담은 데이터프레임을 반환하는 함수를 작성해 주었습니다.
def lucky_chart(start, end):
lucky_results = []
for i in range(start, end+1):
win = win720(i)
values = list(win.values())
print(values)
print(len(values))
lucky_results.append(values)
columns = list(win.keys())
print(columns)
print(len(columns))
import pandas as pd
df = pd.DataFrame(lucky_results, columns = columns)
return df.set_index("round")
중간 중간에 있는 print 함수들은 제가 함수를 작성하면서 중간 과정을 시각화하기 위해 굳이 넣어줬구요, 깔끔하게 없애줘도 됩니다.
history = lucky_chart(190, 206)
190회부터 206회차까지 럭키차트 함수를 돌려보았습니다.
요런식으로 진행상황을 시각화 하기 위해 print 함수를 넣어줬습니다. (중간에 오류가 있었어서 저런식으로 시각화 하면서 수정해줬어요!)
history
알흠다운 판다스 데이터프레임이 완성되었어요 ❤️
3. 간단 데이터분석
마지막으로 데이터분석은 1회차부터 206회차까지로 구간을 늘려서 진행했습니다! (1등은 제껍니다.)
1등 조 비율이 어떻게 될까?
# 조별 value_counts() 구하기
group_counts = history['group'].value_counts()
# matplotlib 임포트
import matplotlib.pyplot as plt
# 차트 작성
plt.pie(group_counts,
labels = group_counts.index,
shadow = True,
autopct = '%1.2f%%')
plt.title("Rank 1 groups ratio")
plt.legend()
plt.show()
아주.. 흥미로운.. 결과입니댜... 연금복권720+ 1회차부터 206회차까지 모든 데이터들을 살펴본 결과... 지금까지 가장 많은 1등을 배출한 조는 4조였습니다. 4조 > 1조 > 3조 > 5조 > 2조 순이네요.
연금복권을 아는 분들께서는 이해를 하실텐데, 저는 혹시라도 번호 6개를 다 맞췄지만 조가 다를 때 2등이라도 당첨되도록 + 혹시라도 1등이 되면 2등도 동시 당첨되도록(ㅋㅋㅋ) 번호 6개를 고르고 나면 모든 조(1~5조)로 총 5줄(5,000원)을 구매해버립니다. 솔직히 이게 더 확률이 낮을 것 같기는 한데.......... 만약 특정 조를 골라서 구매해야 한다면 앞으로 저는 1조 또는 4조를 고르겠습니댜.
지금까지 역대 2등은 몇명이 나왔을까?
import seaborn as sns
sns.set_style("darkgrid")
sns.set_palette("bright")
sns.barplot(history["rank2"].value_counts())
history['rank2'].agg(['min', 'max', 'mean'])
# min 0.0
# max 8.0
# mean 4.5
1등에 가려 2등의 당첨자 수는 사실 잘 확인해 본 적이 없는데요. 2등에 당첨되면 매달 100만원씩 10년간 수령할 수 있거든요. 2등이라도 당첨시켜 주신면 제가 굉장히 감사할텐데요. 몇 명이나 당첨되나 보니, 역대 2등 당첨자 수는 최대 8명, 최소 0명(ㅋㅋㅋㅋㅋㅋ), 평균 4.5명이 나왔다고 합니다. 그래프로 확인해 보니 2등이 한 명도 나오지 않은 회차가 20번이 넘네요? 실화냐?
오랜만에 피보나치 수열 문제 풀이를 했습니다. 처음에 간단한 재귀함수를 적용해서 풀이를 했더니 테스트는 모두 통과하는데 효율성 테스트에서 시간 초과로 계속 실패를 하는 문제가 발생했습니다. 그래서 효율성을 올리기 위해 메모화를 적용해서 풀이를 했더니 효율성 테스트도 모두 통과할 수가 있었습니다 :-)
간단하게 풀이방법을 설명해 보겠습니다!
피보나치 수열
1. 메모화 없이 기본 피보나치 수열 함수 작성하기
def basic_fibonacci(n):
if n in [1, 2]:
return 1
if n == 3:
return 2
return basic_fibonacci(n-2) + basic_fibonacci(n-1)
만약 주어진 정수가 1 또는 2라면 바로 1을 리턴,
주어진 정수가 3이라면 바로 2를 리턴하도록 초기 설정을 해 줍니다.
4 이상의 정수 n이 주어졌을 때 재귀함수 형식으로 n-2와 n-1에 basic_fibonacci 함수를 적용한 값을 구해 더한 값을 리턴합니다.
위 함수는 제대로 작동하지만, n의 값이 커질수록 효율성이 급격하게 떨어진다는 단점이 있어요. 메모화를 이용해서 함수의 효율을 높여보겠습니다.
2. 메모화를 적용한 피보나치 수열 함수 작성하기
def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
if n in [2, 3, 4, 5, 6]:
return memo[n]
if n > 6:
for i in range(7, n+1):
if i in memo: pass
else:
memo[i] = memo[i-2] + memo[i-1]
return memo[n]
가장 먼저 함수를 선언할 때 파라미터 값으로 memo라는 이름으로 딕셔너리를 포함해 주었습니다. 저는 간단하게 n이 1부터 6번째일때 피보나치 수열 값을 딕셔너리에 초기값으로 미리 메모를 해 두었습니다.
def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
if n in [2, 3, 4, 5, 6]:
return memo[n]
내가 구하고자 하는 것은 피보나치 수열의 n번째 값입니다. 만약 n이 2, 3, 4, 5, 6인 경우 (문제 조건에서 n은 2 이상의 정수라고 했으므로 1은 제외합니다) memo 딕셔너리의 해당하는 value 값을 바로 리턴하고 함수를 종료하게 됩니다.
if n > 6:
for i in range(7, n+1):
if i in memo: pass
else:
memo[i] = memo[i-2] + memo[i-1]
return memo[n]
만약 n이 6보다 크면 어떻게 해야 할까요?
주어진 n의 값이 50일 때를 예시로 생각해 봅시다. 우리는 50번째 값을 구하기 위해 48, 49번째 값이 필요합니다.
49번째 값을 구하기 위해서는 47, 48번째가 필요하고
48번째 값을 구하기 위해서는 46, 47번째가 필요합니다.
결국 50번째 값을 구하기 위해서는 1부터 49번째까지 피보나치 수열 값을 모두 알아야 합니다.
그럼 메모화는 어떻게 작동할까요?
제가 만약 50번째 피보나치 수열 값을 구하기 전에,
40번째 피보나치 수열 값을 먼저 구했다고 생각해 보겠습니다.
fibonacci(40)
# 102334155
우리는 40번째 피보나치 수열 값을 구하기 위해 지금 1번째~39번째 피보나치 수열의 값을 열심히 구했습니다.
이제 다음으로 50번째 피보나치 수열 값을 구하려고 합니다.
먼저 메모화를 해놓지 않은 경우를 생각해 봅시다.
우리는 이미 1번부터 40번째까지의 값을 이전에 미리 구한 전적이 있지만
따로 기록을 해 두지 않았기 때문에 같은 계산을 또 반복해야 합니다.
1번째~49번째 피보나치 수열 값을 열심히 또 구하는 거죠.
하지만 제가 기록을(메모화를) 해 두었다면 얘기가 달라집니다.
즉 처음에 fibonacci(40)을 계산했을 때
memo 딕셔너리에 1번부터 40번까지 피보나치 수열 값을
key(n) : value(값) 형식으로 미리 저장을 해 두었다면,
우리는 fibonacci(50)을 구할 때
1. 1번부터 40번까지는 간단히 딕셔너리에서 값을 찾아 가져올 수 있습니다.
2. 41번부터 49번째 값은 새로 구하고 딕셔너리에 값을 추가해주는 방식으로 계속해서 메모를 해나갈 수 있습니다.
+ 참고로 함수를 여러번 작동하면서 메모장에 추가하는 key와 value 값은 매번 초기화되지 않고 계속해서 누적 기록을 보관합니다.
def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
# 주어진 정수가 2-6중 하나일 경우 바로 메모장에서 수열값을 찾아 리턴, 함수종료
if n in [2, 3, 4, 5, 6]:
return memo[n]
# 주어진 정수가 6보다 큰 경우(7 이상)
if n > 6:
# 7부터 n까지의 정수 i에 대해서
for i in range(7, n+1):
# 만약 메모장에 i값이 이미 메모되어 있다면 그냥 넘어가고
if i in memo:
pass
# 만약 메모장에 i값이 없다면 피보나치 수를 구해서 추가해주세요
else:
memo[i] = memo[i-2] + memo[i-1]
# 메모장에서 n번째 피보나치 수열 값을 찾아 리턴해주세요
return memo[n]
최종 피보나치 함수입니다 :)
내가 구하고자 하는 n의 피보나치 값을 찾기 위해 7부터 n-1번째의 수열값을 계속해서 차례대로 메모장에 누적 메모해나가는 방식입니다. 재귀함수랑 약간 비슷한듯 다른 느낌이네요!
3. 최종 문제풀이
마지막으로 문제 해결을 위해 피보나치 수열을 활용해서 soluton()함수를 작성했습니다. 저는 코딩테스트 문제를 풀 때 이렇게 함수를 나누어 작성하는것을 좋아합니다. 함수 안에 함수 작성하는거 시러요.....(개취)
def fibonacci(n, memo = {1:1, 2:1, 3:2, 4:3, 5:5, 6:8}):
if n in [2, 3, 4, 5, 6]:
return memo[n]
if n > 6:
for i in range(7, n+1):
if i in memo:
pass
else:
memo[i] = memo[i-2] + memo[i-1]
return memo[n]
def solution(n):
fibo_num = fibonacci(n)
# fibonacci(30)< 1234567 < fibonacci(31)
# 만약 n이 30 이하라면 바로 fibo_num을 리턴하고(몫이 0이므로 나머지와 동일)
if n <= 30 :
return fibo_num
# 만약 n이 30보다 크다면 fibo_num을 1234567으로 나눈 나머지를 리턴
else :
return fibo_num % 1234567
FutureWarning: use_inf_as_na option is deprecated and will be removed in a future version. Convert inf values to NaN before operating instead. with pd.option_context('mode.use_inf_as_na', True):
주피터 노트북이나 코랩에서 pandas, seaborn, matplotlib 등 라이브러리를 사용할 때 위와 같이 퓨처 워닝 어쩌구 하면서 경고 메세지가 나타나서 꼴보기 싫은 경우가 있습니다.
프로그래머스 JadenCase 문자열 만들기 문제를 가지고 방법을 설명해 보겠습니다. (다른 다양한 문제에서도 활용할 수 있습니다!)
문제 : JadenCase란 모든 단어의 첫 문자가 대문자이고, 그 외의 알파벳은 소문자인 문자열입니다. 단, 첫 문자가 알파벳이 아닐 때에는 이어지는 알파벳은 소문자로 쓰면 됩니다. 문자열 s가 주어졌을 때, s를 JadenCase로 바꾼 문자열을 리턴하는 함수, solution을 완성해주세요.
이렇게 공백문자가 연속해서 나올 수 있다는 조건이 있는 경우 split() 함수를 사용하면 공백 여러개가 모두 없어져버리므로 조심해야 합니다.
저는 먼저 단어 1개를 JadenCase로 바꾸어 리턴하는 함수 jaden을 작성했습니다.
def jaden(w):
try:
# 첫 번째 글자가 정수일 때
int(w[0])
return w[0] + w[1:].lower()
except:
# 첫 번째 글자가 정수가 아닐 때
return w[0].upper() + w[1:].lower()
이렇게 작성한 jaden 함수를 가지고 최종 solution 함수를 작성해 볼게요.
import re
먼저 정규식 패키지를 불러옵니다.
word = "Hello World Bye"
위의 예시를 단어와 공백문자로 구분한 리스트를 어떻게 만들 수 있을까요?
word = "Hello World Bye"
re.findall("\S+|\s+",word)
# ['Hello', ' ', 'World', ' ', 'Bye']
re.findall() : RE가 일치하는 모든 부분 문자열을 찾아 리스트로 반환합니다. 활용해서 원하는 구성요소를 찾아 리스트로 만들어줍니다.
\s+ : 일치하는 모든 공백 문자를 찾아줍니다. (여러 개의 공백도 하나로 평탄화하지 않고 유지한 채로 찾아줍니다.)
\S+ : 일치하는 모든 비공백 문자를 찾아줍니다.
결과를 보면 여러 개의 공백 문자가 잘 보존되어 있는 것을 볼 수 있습니다. 이제 최종 솔루션 함수를 작성해 볼게요.
def jaden(w):
try:
# 첫 번째 글자가 정수일 때
int(w[0])
return w[0] + w[1:].lower()
except:
# 첫 번째 글자가 정수가 아닐 때
return w[0].upper() + w[1:].lower()
import re
def solution(s):
# 입력 받은 s를 whitespace와 nonwhitespace로 구분합니다
words = re.findall("\S+|\s+", s)
answer = ""
for word in words:
# 만약 공백문자가 아니라면 jaden 함수를 적용해서 answer에 추가합니다.
if word.isalnum():
answer += jaden(word)
# 만약 공백문자라면 그냥 바로 answer에 추가합니다.
else:
answer += word
return answer
정확성: 100.0
합계: 100.0 / 100.0
끝입니다! 정규식이 기억에 잘 남지는 않지만,
잘만 숙지해 두면 연속 공백문자 처리를 해야하는 문제에서 요긴하게 잘 쓸 수 있답니다 :-)
문제 : 2번 형질을 보유하지 않으면서 1번이나 3번 형질을 보유하고 있는 대장균 개체의 수(COUNT)를 출력하는 SQL 문을 작성해주세요. 1번과 3번 형질을 모두 보유하고 있는 경우도 1번이나 3번 형질을 보유하고 있는 경우에 포함합니다.
[1] 2진법으로 계산해 보기
먼저 각 열의 GENOTYPE의 수를 2진법으로 바꾸어 출력해 보겠습니다. 주어진 정수 데이터에 BIN() 함수를 사용하면 정수를 2진법 수로 변환한 결과를 출력합니다.
SELECT BIN(GENOTYPE) AS GENOTYPE_BIN
FROM ECOLI_DATA
그럼 이제 출력된 결과를 CAST함수를 이용해서 CHAR 데이터타입으로 바꾼 뒤, WHERE문을 사용해서 필터링을 해 보겠습니다.
SELECT COUNT(*) AS `COUNT`
FROM (SELECT ID,
CAST(BIN(GENOTYPE) AS CHAR) AS GENOTYPE_BIN
FROM ECOLI_DATA) A
WHERE A.GENOTYPE_BIN LIKE '1' OR
A.GENOTYPE_BIN LIKE '%10_' OR
A.GENOTYPE_BIN LIKE '%01'
WHERE문의 각 줄은 다음과 같은 결과를 필터링합니다.
서브쿼리문의 수가 1인 경우
서브쿼리가 101 또는 100인 경우
서브쿼리가 01로 끝나는 경우
마지막으로 SELECT COUNT(*) AS `COUNT`를 통해 필터링된 데이터 열의 갯수를 COUNT라는 컬럼명으로 출력하도록 해 주었습니다. 정답 통과입니다.
[2] 비트 연산으로 풀어보기
MySQL에는 비트(Bit) 단위로 논리 연산을 수행하는 연산자가 있습니다. 챗지피티한테 비트 연산자의 종류에 뭐가 있는지 테이블을 만들어 달라고 했는데요.
비트 AND (&)
비트별 AND 연산을 수행합니다. 두 비트가 모두 1이면 결과는 1이 되고, 그렇지 않으면 결과는 0이 됩니다.
비트 OR ( | )
비트별 OR 연산을 수행합니다. 두 비트 중 하나라도 1이면 결과는 1이 되고, 둘 다 0이면 결과는 0이 됩니다.
비트 XOR (^)
비트별 XOR 연산을 수행합니다. 두 비트가 같으면 결과는 0이 되고, 다르면 결과는 1이 됩니다.
비트 NOT (~)
비트를 반전시킵니다. 0은 1로, 1은 0으로 변환됩니다.
비트 왼쪽 시프트 (<<)
모든 비트를 왼쪽으로 이동시킵니다. 오른쪽에 0으로 채워집니다.
비트 오른쪽 시프트 (>>)
모든 비트를 오른쪽으로 이동시킵니다. 왼쪽에 부호 비트와 같은 값으로 채워집니다.
네. 이런게 있다네요. (^_^;;) 저는 여기서 &연산자와 NOT 연산자를 사용해서 위의 문제를 풀어보겠습니다.
SELECT * FROM ECOLI_DATA
WHERE
GENOTYPE & 5
AND NOT GENOTYPE & 2
GENOTYPE & 5
정수 5를 2진법 비트로 변환하면 101입니다. 따라서 &5는 값의 첫 번째, 세 번째 비트가 1인지 여부를 확인합니다.
NOT GENOTYPE & 2
비트 NOT 연산은 비트를 반전시키는 역할을 합니다. 여기서 2의 이진 표현은 10입니다. 따라서 이 비트 연산은 GENOTYPE 열의 값에서 2번째 비트를 확인하고, 그 값을 반전시키므로, 2번째 비트가 0인지 여부를 확인합니다.
SELECT COUNT(ID) AS `COUNT`
FROM ECOLI_DATA
WHERE
GENOTYPE & 5
AND NOT GENOTYPE & 2
마지막으로 카운트한 값을 출력해 주면 정답 통과입니다.
비트 연산자는 간단하게 풀 수 있지만 까먹기 쉽다는 단점이 있는 것 같습니다.
그래도 이런 게 있다는 걸 알아두고 필요할 때마다 열심히 꺼내 보면서 익숙해 져야겠습니다.
관련 문제로 프로그래머스 MySQL 코딩테스트 3단계 문제 오랜 기간 보호한 동물(1) 풀이를 함께 첨부해 보겠습니다.
문제 상황 : 아직 입양을 못 간 동물 중, 가장 오래 보호소에 있었던 동물 3마리의 이름과 보호 시작일을 조회하는 SQL문을 작성해주세요. 이때 결과는 보호 시작일 순으로 조회해야 합니다.
SQL문을 실행하면 다음과 같이 나와야 합니다.
먼저 ANIMAL_INS 테이블에 ANIMAL_OUTS 테이블을 LEFT JOIN한 결과를 보겠습니다.
SELECT I.NAME, I.DATETIME
FROM ANIMAL_INS I
LEFT JOIN ANIMAL_OUTS O
ON I.ANIMAL_ID = O.ANIMAL_ID
아직 아무 조건을 걸어주지 않은 관계로 위 코드가 출력하는 결과는
SELECT NAME, DATETIME
FROM ANIMAL_INS와 동일합니다.
저는 여기서 A-B를 구하기 위해
A와 B의 교집합을 제거해 주려고 합니다.
어떻게 해볼 수 있을까요?
SELECT I.NAME, I.DATETIME
FROM ANIMAL_INS I
LEFT JOIN ANIMAL_OUTS O
ON I.ANIMAL_ID = O.ANIMAL_ID
WHERE O.ANIMAL_ID IS NULL
OUT 테이블의 아이디가 없는 데이터만 출력하도록 WHERE O.ANIMAL_ID IS NULL을 추가해 주었습니다.
이제 마지막으로 이 결과에서 가장 오래 보호소에 있었던 동물 3마리의 이름을 고르고, 결과는 보호 시작일 순으로 조회하도록 조건을 추가하겠습니다.
SELECT I.NAME, I.DATETIME
FROM ANIMAL_INS I
LEFT JOIN ANIMAL_OUTS O
ON I.ANIMAL_ID = O.ANIMAL_ID
WHERE O.ANIMAL_ID IS NULL
ORDER BY I.DATETIME ASC LIMIT 3;
정답 통과 입니다.
참고한 다이어그램
출처: 구글 이미지
비슷한 문제로 프로그래머스 3단계 '없어진 기록 찾기' 문제도 풀어보시면 좋을 것 같습니다 :-)
집에서 편하게 쉬엄쉬엄 공부를 하다가 매일 강의실에 물리적으로 출퇴근을 하는 일이 쉬운 일은 아니었습니다만, 이쯤 되니 다행히도 몸이 잘 적응을 한 것 같습니다. 아무래도 지난 7년간 학교에 출퇴근하던 일에 비교하면 (비교할 수 없을 만큼) 훨씬 살만하네요. 학교에서 학생을 대하고 가르치는 일은 체력적, 정신적으로 굉장히 큰 에너지를 소모하는데요. 그에 비하면 지금은 그냥 방학 같아요. 내가 하고 싶은 공부를 원 없이 할 수 있는 환경과 자원이 지원된다는 게 감사할 따름입니다.
과정을 시작하면서 제 자신과 약속한 것이 한 가지 있어요. 바로 출퇴근하는 월요일부터 금요일까지는 반드시 아침에 일찍 일어나자는 것이었는데요. 저는 저에 대해서 꽤 잘 알거든요. 내가 원하는 시각에 망설임 없이 일어나서 하루를 시작할 수 있느냐 없느냐가 저의 그날 하루 전체 생산성을 결정합니다. 지금까지 쉬엄쉬엄 편하게 공부해 왔으니, ASAC 과정에 참여하는 6개월 동안만큼은 죽었다 생각하고 일찍 일어나기로 했어요. 그래서 저는 매일 아침 5시 40분에 기상합니다. 간단히 씻고 준비를 마친 다음 6시 15분-20분에 집을 나서 6시 28분 지하철을 탑니다. 다행히도 3주간 잘 지켜왔습니다. 앞으로도 잘 지킬 거예요.
집이 꽤 먼 편이라 목적지까지 환승 없이 약 45분 정도가 걸리는데요. 출퇴근 길에는 책을 읽습니다. 책 읽는 걸 너무 좋아합니다. 더 나아가 책 읽는 제 자신이 좋습니다. 책을 읽을수록 생각이 확장되고 더 나은 사람이 되는 것을 느껴서 꾸준히 읽고 기록하려고 노력하고 있어요. 데이터를 분석하고 통찰하고 관철할 수 있는 사람이 되는 데 독서도 분명 큰 도움이 될 것이라고 생각합니다. 그런데 요즘 같은 시기에 따로 시간을 내어 책을 읽기는 또 어렵다 보니, 대중교통에서 보내는 시간을 활용하는 거예요. 저도 책을 멀리하던 시절에는 지하철이나 버스에서 책 꺼내서 읽는 사람 보면 괴물 같고 그랬는데 말이죠. 막상 직접 해 보니 오히려 숏폼 영상보다도 더 즐겁고 시간도 더 잘 갑니다.
강의실에 도착하고 나면 7시 20-30분 사이가 되는데 그때부터 8시 50분까지 약 한 시간 조금 넘는 시간 동안 개인 공부를 합니다. 잠깐 쉬고 9시 10분부터 수업을 듣습니다. 6시에 수업이 끝나면 저녁을 먹고 8시까지 개인 공부를 합니다. 가끔 컨디션이 안 좋으면 7시 30분쯤 일찍 가고, 컨디션이 유독 좋으면 9시까지 공부를 합니다. 이렇게 말하고 보니 정말 공부에 미친 광인 같네요. 그래도 저도 사람이기 때문에 주말엔 늦잠도 좀 자고 맛있는 것도 먹고 맥주 한 잔 하기도 하면서 편히 지내고 있어요. 요즘 주말 공부 시간은 하루에 3-6시간 사이 정도인데 조금씩 늘려가려고 합니다.
수업에서 3주 차 초반까지는 파이썬 기본부터 심화 문법까지 리뷰를 하고 주요 알고리즘을 몇 가지 공부했습니다. 특히 저는 알고리즘 자체를 따로 배워 본 적이 따로 없었기 때문에 구현과 탐색 알고리즘을 배웠던 시간이 큰 도움이 되었어요. 강사님께서 여러 가지 실전 코딩테스트 문제들을 가지고 하나하나 설명을 해주시면서 알고리즘을 코테 문제에 어떻게 적용하는지 보여 주신 것이 참 좋았습니다. 덕분에 저도 자극을 받아서 블로그에 코딩 테스트 문제 풀이 방법을 작성해서 올리기 시작했는데요. 무언가 배웠을 때 그걸 남에게 설명할 수 있어야 진짜로 아는 것이라고 학생들한테 누누이 말하곤 했던 걸 이제는 스스로에게 되새고 있는 저를 보면서, 기분이 묘했습니다.
파이썬 과정이 끝나고 간단한 테스트가 있었습니다. 다행히 쉽게 느껴지는 문제들로 구성되어 있어 10분-15분 만에 빠르게 제출을 할 수 있었습니다. 다른 동기들에 비해 문제를 빠르게 해결한 편이었는데, 제가 특별히 잘나고 똑똑해서는 아닙니다. 블로그에 직접 정리하면서 반복하고 복습했던 스니펫들을 활용할 수 있는 문제가 정말 많이 나왔어요. 운이 좋았던 거죠. 공부한 것을 남에게 설명하기를 저는 블로그에 글을 쓰는 걸로 대신하고 있는데, 이게 정말 큰 의미가 있다는 걸 실감할 수 있었던 좋은 기회였습니다.
파이썬 과정이 끝나고는 짧고 콤팩트하게 MySQL을 배웠는데요. 저는 MySQL도 유데미 강의를 통해 한 번 훑고 왔기 때문에 강의 속도를 따라가는 데 큰 어려움이 없었습니다. 프로그래머스에서 MySQL 코딩테스트 문제도 제공하고 있거든요. 그걸 같이 풀면서 수업을 들으니 체화하는 데 훨씬 더 좋더라고요. MySQL이 무슨 대단한 알고리즘을 요구하는 언어는 아니기 때문에 엄청 어렵고 복잡하진 않더라도 이게 손 놓고 있으면 잊어버리기 십상이거든요. 꾸준히 놓지 않는 게 가장 중요하다고 생각되어서, 앞으로 프로그래머스 문제를 조금씩 야금야금 꾸준히 풀면서 리듬감을 유지해보려고 합니다.
만약 저의 회고를 참고하시고 다음 기수의 ASAC과정을 고려하고 계신 분이 계시다면, 반드시 파이썬과 MySQL의 기초 문법에 대한 (최소) 1회독을 끝낸 다음 지원을 하시라고 말씀드리고 싶습니다. 그게 정신건강에 좋습니다. 교육과정 자체가 노베이스 비기너를 위한 과정이 아닙니다. 굉장히 타이트한 교육과정이에요. 진도가 빠를 뿐만 아니라 난이도도 있습니다. 저는 비록 비전공자이기는 하지만 전공자에 뒤쳐지고 싶지 않아서 여러 가지 공부를 마친 다음 본 과정을 시작했기 때문에 아직은 수업을 따라가는 데 어려움이 없습니다. KDT 등록하면 알아서 어떻게든 되겠지- 와 같은 해이한 마음으로 시작하시면 크게 낭패를 보실 수 있다는 점을 반드시 숙지하시면 좋겠습니다.
그럼 4주 차도 맑은 눈의 광인 모드를 탑재하고 열심히 공부하고 돌아오겠습니다. 회고 읽어주셔서 감사합니다. :)