자료구조, Queue

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


Queue

선형 자료구조 중에서도 동적 자료구조에 해당하는 Queue는 필요에 따라 메모리에 공간을 유연하게 할당하고 해제해 데이터를 저장할 수 있어 데이터의 추가, 삭제, 크기 변경등이 가능하다. 이때 스택의 가장 중요한 특징은 FIFO 개념이다.

FIFO: First In First Out

  • 먼저 들어온 데이터가 먼저 나가는 자료구조
  • 선착순의 원리 > 먼저 들어온 사람이 먼저 서비스를 받음
  • 스택과 반대되는 개념

주요 연산

  1. Enqueue(인큐): queue의 뒤쪽(rear) 데이터에 삽입
  2. Dequeue(디큐): queue의 앞쪽(front) 데이터 삭제 및 반환

Python으로 구현해보는 queue

class Queue:
    def __init__(self):
        # queue를 담아줄 빈 리스트 생성
        self.items = []
        # 맨 앞의 인덱스 0 지정 
        self.front_index = 0

    def enqueue(self, value):
        # items안에는 일단 차곡차곡 데이터가 쌓인다
        self.items.append(value)

    def dequeue(self):
        try:
            # items이 빈리스트가 아니라면 (값이 있다면)
            return self.items.pop(0)
        except IndexError:
            # items에 아무것도 없다면 더이상 삭제할 아이템이 없으니까 indexError발생
            print('Queue is empty')
            return None
queue = Queue()
queue.enqueue(3)
queue.enqueue(23)
queue.enqueue(5345)
queue.enqueue(234)
queue.enqueue(5456)

queue.items
# 출력: [3, 23, 5345, 234, 5456]
queue.dequeue()
# 출력: 3
stack.items
# 출력: [23, 5345, 234, 5456]

각 연산들의 시간 복잡도: O(1) 인 것도 확인 가능하다. (enqueue, dequeue)

  • Enqueue
    1. 리스트의 append 함수 사용
    2. 항상 리스트의 끝에 데이터가 추가됨
  • Dequeue
    1. 리스트의 pop 함수 사용
    2. 항상 리스트의 맨 앞 데이터가 삭제됨
    3. collections의 deque를 사용해도 됨

큐 예제 > 조세푸스(josephus)

  • 문제 상황
    1. N명이 원형으로 앉아있음
    2. K번째 사람마다 제거
    3. 마지막 남은 1명의 번호 찾기

case1, 그냥 풀어보기

def josephus(n, k):
    queue_list = []

    for i in range(1, n+1):
        # 1부터 n까지 큐에 enqueue
        queue_list.append(i)

    # 2. 반복:
    # - (k-1)개는 dequeue 후 다시 enqueue
    # - k번째는 dequeue만 (제거)   
    # 3. 큐에 1개만 남을 때까지 반복
    while len(queue_list) > 1:
        for i in range(k-1):
            queue_list.append(queue_list.pop(0))
        queue_list.pop(0)

    # 4. 마지막 남은 번호 반환
    return queue_list

josephus(6,2)

case2, deque 활용해서 풀어보기

from collections import deque
# deque 사용해보기
def josephus(n, k):
    dq = deque()

    for i in range(1, n+1):
        # 1부터 n까지 큐에 enqueue
        dq.append(i)

    # 2. 반복:
    # - (k-1)개는 dequeue 후 다시 enqueue > k-1: 앞에서 몇번 건너뛸지
    # - k번째는 dequeue만 (제거)   
    # 3. 큐에 1개만 남을 때까지 반복
    while len(dq) > 1:
        for i in range(k-1):
            dq.append(dq.popleft())
        dq.popleft()

    # 4. 마지막 남은 번호 반환
    return dq

josephus(6,2)

자료구조, Stack

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


Stack

선형 자료구조 중에서도 동적 자료구조에 해당하는 Stack은 필요에 따라 메모리에 공간을 유연하게 할당하고 해제해 데이터를 저장할 수 있어 데이터의 추가, 삭제, 크기 변경등이 가능하다. 이때 스택의 가장 중요한 특징은 LIFO 개념이다.

LIFO: Last In First Out

  • 가장 마지막에 들어온 데이터가 가장 먼저 나가는 자료구조
  • 데이터는 한쪽 끝에서만 삽입, 삭제가 이루어짐
  • 중간에 대한 접근은 불가능 > 오직 맨 위(top) 데이터에만 접근 가능

주요 연산

  1. push(value): 스택의 맨 위에 값을 추가 > append개념
  2. pop: 스택의 맨 위 값만 제거하고 반환

Python으로 구현해보는 stack

class Stack:
    def __init__(self):
        # stack을 담아줄 빈 리스트 생성
        self.items = []

    def push(self, value):
        # 비어있는 stack에 값을 추가 
        self.items.append(value)

    def pop(self):
        try:
            # items이 빈리스트가 아니라면 (값이 있다면)
            return self.items.pop()
        except IndexError:
            # items에 아무것도 없다면 더이상 삭제할 아이템이 없으니까 indexError발생
            print('Stack is empty')
            return None

    def top(self):
        # 현재 스택 최상단 친구는 누구인지 묻는 함수
        try:
            return self.items[-1]
        except IndexError:
            # items에 아무것도 없다면 더이상 삭제할 아이템이 없으니까 indexError발생
            print('Stack is empty')
            return None

    # 이미 존재하는 함수인 len 접근
    def __len__(self):
        return len(self.items)
stack = Stack()
stack.push(12)
stack.push(3)
stack.push(2)
stack.push(1232)
stack.push(12123124)

stack.items
# 출력: [12, 3, 2, 1232, 12123124]
stack.pop()
# 출력: 12123124
stack.items
# 출력: [12, 3, 2, 1232]
stack.top()
# 출력: 1232
len(stack)
# 출력: 4

각 연산들의 시간 복잡도: O(1)인 것도 확인 가능하다.(push, pop, top, len)

스택 예제 > 괄호 맞추기

  • 문제 상황
    1. 왼쪽 괄호 ‘(‘ → 스택에 push
    2. 오른쪽 괄호 ‘)’ → 스택에서 pop
    3. 모든 괄호 처리 후 스택이 비어있으면 올바른 쌍
def check_parentheses(seq):
    # 위에서 만들어놓은 Stack 클래스를 통해 stack 객체 생성
    stack = Stack()
    
    for s in seq:
        # 들어온 seq에 s가 "(" 라면
        if s == "(":
            # stack에 ( 를 추가 (push)
            stack.push(s)

        # 들어온 seq에 ")"라면
        elif s == ")":
            # 현재 stack의 길이를 확인해서 
            # 길이가 0이면 현재 스택엔 아무것도 없다는 뜻이니까
            if len(stack) == 0:
                # 비어있다고 출력 
                # 근데 원래는 )가 있었어야해 ()이렇게가 쌍이니까! 
                # 그래서 다르게 표현해보면 > return False 이것도 맞다 
                return "empty stack"
            # 만약 stack이 비어있지 않다면? -> 길이가 0이 아니라면
            # (든 )든 둘중 뭐가 있다는 뜻이니까 일단 맨끝에 있는 애를 삭제해! (pop)
            stack.pop()

    # for문을 통해 들어온 seq에 대한 push, pop을 다 진행한 뒤에 stack의 길이를 확인
    # pop을 했는데 stack의 길이가 0보다 많다면 ()의 쌍이 맞지 않았다는 뜻이니까
    if len(stack) > 0:
        # 쌍이 잘 맞지 않았고, stack은 비어있지 않다는 의미!
        return "not empty"
    # len == 0이라는 의미는 쌍이 맞았다는 의미니까
    return "잘 넣으셨군요"

자료구조, 재귀함수

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


재귀함수

자료구조에서 재귀란 함수의 실행 과정에서 자기 자신을 한번 이상 호출하는 함수를 의미한다.

def sum_n(n):
    if n == 1:
        return 1
    return sum_n(n-1) + n

이 함수를 실행시켜보면 sum_n(4) > sum(3) + 4 > sum(2) + 3 > sum(1) + 2 > 1으로 진행될 것이다.

수행시간은 T(n) = T(n-1) + C (C는 상수) > T(n-2) + C + C > T(n-3) + C + C + C ... > C*N(=T1~Tn)으로 시간복잡도는 O(n)임을 알 수 있다.

재귀함수의 주의점

  1. 기본케이스(base case)를 반드시 만들어야한다.
  2. recursive call이 반드시 있어야한다.

기본 케이스란 재귀 호출을 수행하지 않는 반환값을 의미한다. 위 코드에서 기본케이스는 if n == 1: return 1를 의미한다. 즉, 함수가 반복적으로 호출되었을때 제일 마지막 호출에서는 기본 케이스가 돌아야한다. 그렇지 않으면 코드는 무한반복으로 자기 자신을 호출하기 때문에 함수가 끝나지 않게된다. 파이썬에서의 while True를 떠올리면 이해가 쉽다.

그리고 재귀함수는 반드시 자기 자신을 한번 이상 호출해야한다. 위 코드에서는 return sum_n(n-1) + n에 해당한다.

이러한 재귀함수는 for,while문을 호출하지 않기에 좀 더 코드가 간결해진다는 장점은 존재하지만, 이러한 재귀호출이 많아지면 질수록 메모리의 효율은 떨어진다는 단점 또한 존재한다.

Pandas 데이터프레임 합치기(concat, merge)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


데이터프레임 합치기

다수의 데이터프레임(DataFrame) 혹은 시리즈(Series)를 결합하는 방법에 대해 정리해본다.

pd.concat() > 데이터프레임 붙이기

데이터프레임을 말그대로 물리적으로 이어붙여주는 함수 > pd.concat(데이터프레임 리스트)로 사용

import pandas as pd

# df1, df2 데이터 프레임을 각각 만들어 주면서 각각의 index를 지정해줌
df1 = pd.DataFrame({'a':['a0','a1'],
                   'b':['b0','b1'], 
                   'c':['c0','c1']},
                   index = [0,1])

df2 = pd.DataFrame({'a':['a1','a2','a3'],
                   'b':['b1','b2','b3'],
                   'c':['c1','c2','c3'],
                   'd':['d1','d2','d3']},
                   index = [1,2,3])

df1, df2는 각각 이렇게 출력될 것이다.

[Output]
    a   b   c
0  a0  b0  c0
1  a1  b1  c1

    a   b   c   d
1  a1  b1  c1  d1
2  a2  b2  c2  d2
3  a3  b3  c3  d3

이제 이 두 데이터프레임을 concat을 통해 합쳐보자.

result = pd.concat([df1, df2])
result
[Output]
    a   b   c   d
0  a0  b0  c0  NaN
1  a1  b1  c1  NaN
1  a1  b1  c1  d1  
2  a2  b2  c2  d2
3  a3  b3  c3  d3

pd.concat()의 axis는 디폴트 0로 지정되어있기 때문에 둘의 합쳐진 방향이 아래로 이어붙여짐을 알 수 있다.(axis=0은 열기준, 위아래) 그리고 df1의 c는열은 존재하지 않기 때문에 c에 해당하는 데이터가 NaN으로 채워진것 또한 볼 수 있다. 그리고 더 나아가 데이터프레임을 붙일때 그냥 이어붙이다보니 인덱스 번호도 그대로 붙여진 것을 볼 수 있다. 이때는 ignore_index=True를 하면 인덱스 재배열이 가능하다.

  • pd.concat(axis=0) 이 defalut
  • 비워진 값은 NaN으로 채워짐
  • 인덱스 재배열은 ignore_index=True

pd.concat(axis=1)을 하는 경우

axis=1은 행기준, 좌우방향!

result = pd.concat([df1, df2], axis=1)
result
[Output]
    a    b    c    a    b   c   d
0  a0   b0   c0   NaN  NaN  NaN  NaN
1  a1   b1   b1   a1   b1   c1   d1
2  NaN  NaN  a2   a2   b2   c2   d2
3  NaN  NaN  a3   a3   b3   c3   d3

이때 보면 느꼈겠지만 pd.concat()함수는 default로 outer를 가진다.

  • outer: 합집합
  • inner: 교집합

이때 그러면 inner옵션을 줘보도록 하자.

pd.concat(join=’inner’)을 하는 경우

result = pd.concat([df1, df2], axis=1, join='inner')
result
[Output]
    a    b    c    a    b   c   d
1  a1   b2   c3   a1   b1   c1  d1

pd.merge() > 데이터프레임 병합

두 데이터프레임을 각 데이터에 존재하는 고유값(key)을 기준으로 병합할 때 사용(공통된 열 혹은 인덱스를 기준으로 두 테이블을 합침)

디폴트는 pd.merge(df_left, df_right, how=’inner’, on=None)이다. 예시로 위에 만들었던 데이터프레임을 다시 사용할 것이다. (이떄 on은 기준이 되는 열을 지정해주는 매개변수를 의미)

  • left, right: 병합하려는 두 데이터프레임
  • on: 기준이 되는 열 이름
  • how(->join): 병합 방식, 기본은 inner(교집합) outer(합집합) left(왼쪽값 기준) right(오른쪽값 기준)
  • suffixes: 중복된 열 이름에 접미사 추가 (기본값: ‘_x’, ‘_y’)

좀 더 보기 편한 데이터를 가져와 예시를 들어본다.

fruit = pd.DataFrame({'Num':[123, 456, 789, 1011, 1112], 
                      'Fruit':['Apple', 'Banana', 'Cherry', 'Lemon', 'Peach'], 
                      'Grade':['A', 'B', 'A', 'D', 'E']})

grade = pd.DataFrame({'Num':[123, 789, 1314, 1011], 
                      'Grade':['A', 'B', 'C', 'D']})

pd.merge() 그냥 해보는 경우

아무 옵션을 적용하지 않으면, on=None이기에 두 데이터의 공통된 열인 NumGrade를 기준으로 교집합인 데이터가 반환된다.

result = pd.merge(fruit, grade)
result

여기서 pd.merge(fruit, grade)는 pd.merge(fruit, grade, on=[‘Num’,’Grade’])와 동일하다.

pd.merge(fruit, grade, how=’outer’)의 경우

merge()의 how는 inner(교집합)가 디폴트이기때문에 outer(합집합)인 경우를 보겠다.

result = pd.merge(fruit, grade, how='outer')
result

Grade에 존재하지 않았던 Fruit 값은 NaN으로 들어가있음을 볼 수 있고, 공통적으로 있던 값은 하나로 합쳐져 출력된 것을 볼 수 있다.

pd.merge(fruit, grade, how=’left or right’)

  • how=’left’
result = pd.merge(fruit, grade, how='left')
result

fruit에 관한것만 나오는것을 볼 수 있다. 그 이유는 pd.merge(fruit, grade, how=’left’)에서 데이터프레임의 순서가 fruit, grade 순서로 병합해준 것이기 때문이다. 따라서 왼쪽에 위치만 fruit에 관련된 데이터만 출력된 것을 볼 수 있다.

  • how=’right’
result = pd.merge(fruit, grade, how='right')
result

여기서도 마찬가지로 grade에 관련된 데이터만 나왔고, 비어있는 값에 대해서는 NaN으로 출력되어있다.

열을 지정해주는 경우

result = pd.merge(fruit, grade, on='Num')
result  

공통된 열 중에 Num 하나만 지정해주는 경우 재밌는 결과가 나온다.

Num을 기준으로 Fruit 데이터가 나오고 그에 대한 fruit, grade의 Grade 데이터가 나왔다.

열을 지정해주고 합집합으로 지정한 경우

result = pd.merge(fruit, grade, on='Num', how='outer')
result

공통된 열이 아닌 열을 기준으로 지정할때

result = pd.merge(fruit, grade, on='Fruit')
result 

당연히 에러가 뜬다


데이터프레임을 병합하는데에 있어서 흥미로웠던 점은 inner join을 했음에도, 공통되지 않은 열이 같이 결합되어 나왔다는 점이었다.

import pandas as pd
df1 = pd.DataFrame({'a':['a0','a1'],
                   'b':['b0','b1'],
                   'c':['c0','c1']},
                   index = [0,1])

df2 = pd.DataFrame({'a':['a1','a2','a3'],
                   'b':['b1','b2','b3'],
                   'c':['c1','c2','c3'],
                   'd':['d1','d2','d3']},
                   index = [1,2,3])

이렇게 데이터프레임 df1, df2를 각각 만들고 이 둘을 merge해본다고 하자.

result = pd.merge(df1,df2)
result

기본 how=inner'이기 떄문에 나는 당연하게 공통된 열 a,b,c에 대한 데이터만 나올 것이라고 생각했다. 혹여 d열이 나오더라도 안의 값은 NaN이 나오지 않을까? 했었다. 근데 이 merge를 함에 있어서 일정한 규칙이 있다고 한다.

즉, 이론상으로는 교집합만 합쳐지는 것이 맞지만 파이썬에서는 조금 다르게 설계 되어있다고 한다.

  1. 컬럼(열)정렬을 우선 > 각 데이터프레임이 가지고 있는 모든 열이 합해진다. 이때 공통된 컬럼이 기준으로 정렬되는 것. 중요한 것은 일단 모든 열이 다 합해진다는 것(d열이 나오는 이유)
  2. 로우(행) 정렬 > 값이 있으면(값이 올수있다면) 일단 다 가져온다. 이때 없는 경우 NaN이 채워진다.(d1이 나오는 이유)

다시 교집합을 하는 방법을 정리해보면,

  1. 데이터프레임이 가진 모든 열이 합쳐지고
  2. 교집합에 부합하는 행과 열이 있다면
  3. 교집합에 해당하지 않은 행 값은 탈락되지 않고
  4. 공통되는 행과 열에 남아 들어온다

이러한 이유는 SQL과 Pandas의 디자인 철학 때문이라고 한다.

  1. 기본적으로 join조건은 행을 매칭하는 기준으로 사용
  2. 데이터분석을 하는 경우, 더 많은 경우에 관련된 전체 정보가 필요함

따라서, SQL과 Pandas는 데이터분석을 위한 도구로서 데이터분석을 하기에 가장 좋은 방식으로 디자인되어 이렇게 설계된것이라고 이해하면 쉬울 것 같다!

궁금증 완료 !

NumPy 논리연산자

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


논리연산

Numpy에서 논리연산은 배열에 참, 거짓으로 이뤄진 배열을 입력해 True, False값을 받을 수 있도록 한다.

기본적인 논리연산자

>, <, >=, <=, ==, !=

집계 함수

데이터를 종합해 특정 연산을 적용하는 함수

  • sum(): 합
  • min(): 최소값
  • max(): 최대값
  • mean(): 평균값
  • median(): 중앙값
  • std(): 표준편차 …
  • all(): 모든 원소가 True인지 > 하나라도 False면 False
  • any(): 하나라도 True인지 > 하나라도 True면 True

논리 함수

기본적으로 True, False 값으로 구성된 결과가 반환됨

  • logical_and() > AND 연산(&)
  • logical_or() > OR 연산(|)
  • logical_not() > NOT 연산(~)
  • logical_xor() > XOR 연산

NumPy에서는 Python의 키워드 and, or, not을 사용할 수 없다.(배열에서 사용 불가능) 따라서 배열에서의 논리연산자는 &, |, ~를 사용해야한다.

logical_and()

a = np.array([True, False, True])
b = np.array([True, True, True])
result = np.logical_and(a, b)  
# 출력값 > [True, False, True] : and연산에서는 하나라도 false면 false가 출력된다.

logical_or()

a = np.array([True, False, True])
b = np.array([True, True, True])
result1 = np.logical_or(a, b)  
# 출력값 > [True, True, True] : or연산에서는 하나라도 True면 True가 출력된다. 

NOT = 논리부정

True면 False / False면 True

a = np.array([True, False, True])
b = np.array([True, True, True])
result2 = np.logical_not(a, b) 
# 출력값 > [False, True, False] : True면 False, False면 True가 출력된다.

XOR = 배타적 OR(eXclusive OR)

한쪽만 True면 True / 둘다 True이거나 둘다 False면 False

a = np.array([True, False, True])
b = np.array([True, True, True])
result2 = np.logical_xor(a, b) 
# 출력값 > [False, True, False] : 둘다 True면 False, 한쪽만 True면 True가 출력된다.