자료구조, 시간복잡도 Big-O 표기법

|

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


알고리즘

어떤 문제나 목적을 달성하기 위해 거쳐야하는 여러 과정들을 의미. 상황에 맞게 시간 복잡도가 가장 낮은 알고리즘을 선택해 사용하는 것이 중요하다.

일반적으로 알고리즘의 성능 분석은 실행에 필요한 공간 측면에서 분석하는 공간복잡도, 실행 소요시간 측면에서 분석하는 시간복잡도를 추정해 평가한다.

알고리즘의 실행시간

알고리즘의 실행시간은 컴퓨터가 알고리즘 코드를 실행하는 속도에 의존한다. 이떄 이 속도는 컴퓨터 자체의 처리속도, 사용된 언어의 종류, 컴파일러 속도 등에 달려있다.

알고리즘은 하나의 문제를 풀때에도 여러가지 코드를 사용해 풀 수 있다. 문제 풀이에 대한 방법은 여러가지가 있겠지만, 이중에서도 실행시간이 가장 적은 최적의 코드를 짜는 것이 효율적이다. 따라서 알고리즘을 짤 땐 이 시간복잡도라는 것의 중요성이 굉장히 높아진다.

시간복잡도 = 알고리즘의 실행 속도

이러한 시간 복잡도를 보통 점근적 표기법으로 사용되는데 이러한 점근 표기법에는 3가지 종류가 있다.

  1. Big-O표기법: 알고리즘의 최악의 실행시간을 표기
  2. Ω 표기법: 알고리즘의 최상의 실행시간을 표기
  3. Θ 표기법: 알고리즘의 평균 실행시간을 표기

빅오 표기법

불필요한 연산을 제거하여 알고리즘 분석을 쉽게 할 목적으로 사용되는 시간 복잡도 성능 표기법

  • 가장 왼쪽 O(1)으로 갈수록 실행 횟수가 적은 것 > 시간 복잡도가 낮은 것
  • 가장 오른쪽 O(n!)으로 갈수록 실행 횟수가 많은 것 > 시간 복잡도가 높은 것

O(1)

입력 데이터 크기에 상관없이 언제나 일정한 시간이 걸리는 알고리즘

def constantTime(n):
    print("Constant는 상수다")

O(n)

입력 데이터의 크기에 비례해 처리 시간이 걸리는 알고리즘

  • n이 한번늘어날 때마다 처리시간이 1 증가 > 선형적 증가
  • n 크리만큼 처리시간이 비례하여 증가
def linearTime(n):
    for i in n:  # > n 
        print(i)

O(n^2)

입력 데이터를 n만큼 반복하는데, 그 안에서 또 n만큼 반복할 때의 표기법 > ex: 2중 포문

  • 데이터가 적을때는 문제없지만, 많아질수록 그래프가 수직상승
  • n만큼 반복하면서 그 안에서 또 n만큼 반복하기에 처리 횟수가 가로, 세로 면적만큼 늘어남
  • n 증가 할때마다 가로와 세로 한줄이 증가하기에 데이터가 커질수록 처리속도 부담도 같이 커짐
def quadraticTime(n):
    for i in n:  # > n
        for j in n:  # > n
            print(i + j)

O(log N)

이진탐색. 데이터를 찾을 때마다 반틈씩 줄여나가는 알고리즘

  • 전체리스트레서 중간값을 먼저 확인하고 찾는값이 크면 오른쪽으로 검색, 작으면 왼쪽으로 검색
  • 한번 확일할때마다 절반씩 줄여나가며 검색하는 것이 특징
  • O(n) 순차검색보다 훨씬 빠르다
  • 데이터가 증가해도 성능에 크게 영향을 주지 않는다
def binary_search(n):
    count = 0  # > +1
    i = n  # > +1
    while i > 0:
        count += 1  # > +2
        i = i // 2
    return count

자료구조, 선형(정적, 동적 자료구조) & 비선형 자료구조

|

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


자료구조란

자료구조는 개발자가 데이터를 효율적으로 사용할 수 있도록 정리하는 방법을 말한다. 각각의 자료구조에는 장단점이 있기에 어떤 자료구조가 최선일지는 해결하고자 하는 문제의 종류와 어떤 부분을 우선적으로 최적화할지에 따라 달라질 수 있다. 따라서 다양한 자료구조의 장단점을 살펴보며 어플리케이션의 특징에 따라 어떤 자료구조를 사용하는 것이 최선일지 판단하는 것이 중요하다.

자료구조는 선형비선형 등의 여러 속성을 기반으로 분류 가능하다.

  • 선형 자료구조(Linear Data Structure) > 데이터 요소를 순서대로 정렬
    • 정적 자료구조(Static Data Structure): 크기가 고정되어 있는 자료구조
    • 동적 자료구조(Dynamic Data Structure): 크기가 바뀔 수 있는 자료구조
  • 비선형 자료구조(Nonlinear Data Structure) > 데이터를 비연속적으로 연결

자료구조를 순회한다는 말은 자료구조의 첫번쨰 요소에서 마지막 요소로 이동한다는 것을 의미한다. 선형 자료구조에서는 첫번쨰 요소에서 마지막 요소까지 백트래킹(BackTracking)없이 쉽게 순회가 가능하지만, 비선형 자료구조에서는 종종 되돌아가야 한다.

비선형 자료구조에서는 원하는 요소에 접근하기 위해 백트래킹이나 재귀가 필요한 경우가 많기 때문에 개별요소에 접근하는 작업에는 선형자료구조가 더 효율적이다. 또한 선형 자료구조는 데이터를 쉽게 순회할 수 있어 비선형 자료구조에 비해 요소 전체를 변경하는 작업이 쉽고, 백트래킹 없이 모든 요소에 접근할 수 있어 자료구조를 설계하고 사용하는 것도 더 쉽다. 그치만 비선형 자료구조는 소셜네트워크 연결과 같은 데이터를 저장하기에 선형 자료구조보다 더 효율적이기 때문에 각각의 특징에 따라 알맞은 자료구조를 선택하는 것이 중요하다.

선형 자료구조

데이터의 요소가 순차적 혹은 선형으로 배열되고 일대일 관계에 있으며 각 요소가 이전 및 다음 인접 요소에 연결되는 자료구조

이러한 선형 자료구조에는 정적/동적 자료구조로 구분되어진다.

  1. 정적 자료구조: 크기가 고정되어있는 자료구조. 대표적인 예로 배열이 있다. 선언과 동시에 배열의 크기가 정해지고 이후에는 그 크기가 변경 불가능하다. 미리 할당된 메모리 공간에 데이터를 저장하기 때문에 데이터의 추가, 삭제, 크기 변경등이 제한적인 것이 특징이다.
  2. 동적 자료구조: 크기가 동적으로 저정될 수 있는 자료구조. 대표적인 예로 ArrayList, Linked List, Queue, Stack이 있다. 필요에 따라 메모리에서 유연하게 공간을 할당하고 해제해 데이터를 저장할 수 있어 데이터의 추가, 삭제, 크기 변경등이 가능하다.

배열(Array)

인덱스를 사용해 배열의 각 요소를 쉽게 식별할 수 있는 인덱스 기반 데이터 구조를 사용

동일한 데이터 유형의 여러값을 저장하려는 경우 효율적으로 활용이 가능하다. 각 요소의 인덱스 접근이 용이!

연결 리스트(Linked List)

각 요소가 인접한 메모리 위치에 저장되지 않는 선형 자료구조로 포인터로 연결되는 것이 특징.

자료가 추가될 때마다 메모리를 할당받고, 자료는 링크=주소로 연결된다. 자료의 물리적 위치와 논리적 위치가 다를 수 있다. 첫번째 노드는 Head, 마지막 노드의 다음 포인터는 항상 null이다. 따라서 각 노드는 다음 노드의 주소만 포함하고 있기 떄문에 인덱스를 통한 접근은 불가능하다. (비연속성)

스택(Stack)

작업이 수행되는 특정 순서를 따르는 선형자료구조 > LIFO(Last In First Out)으로 마지막에 쌓인 데이터가 먼저 나오는 후입선출 방식이다. 데이터 입력 및 검색은 한쪽 끝에서만 가능하고 데이터 입력은 push(), 삭제는 pop()으로 이루어진다.

큐(Queue)

스택과 마찬가지로 작업이 수행되는 특정 순서를 따르는 선형자료구조 > FIFO(First In First Out)으로 처음 들어온 데이터가 먼저 나오는 선입선출 방식이다. 큐의 마지막 요소를 제거하기 위해서는 큐 안의 모든 요소를 제거해야만 가능하다.

덱, 데크(Deque)

양쪽 끝에서 삽입, 삭제가 모두 가능한 자료구조. Stack + Queue!

비선형 자료구조

Pandas 사용해보기

|

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


Pandas 사용해보기

사용에 앞서 일단 import 해주기!

import pandas as pd

우선 딕셔너리를 통해 데이터프레임 하나를 생성해보도록 하자.

# 데이터프레임에 전달할 딕셔너리 생성
characters = {
    '캐릭터명': ['조조', '유비', '관우', '지혜', '판다'],
    '체력': [120, 80, 100, 90, 110],
    '공격력': [85, 95, 80, 75, 60],
    '방어력': [90, 40, 60, 55, 85],
    '마나': [30, 150, 50, 40, 120]
}

# 일반적
df_character = pd.DataFrame(character)

# 인덱스를 지정해주는 경우 > column을 추가하는데, 어떤것을 인덱스로 설정할지 지정
df_character = pd.DataFrame(character, index = [*])

이렇게 만들어준 딕셔너리(character)를 데이터프레임으로 만들면 아래와 같이 생성이 된다.

DataFrame에서의 copy()힘수는 얕은 복사임에도 깊은 복사 효과를 볼 수 있다고 한다. 즉 copy()함수를 사용하더라도 원본에 영향을 주지는 않는다는 것.이는 pandas만의 특징이라고 하는데, 이를 통해 메모리를 적게 차지하여 메모리 효율성을 높일 수 있다. (깊은 복사하는 방법: .copy(deep=True))

이렇게 만든 데이터프레임으로 무얼 할수 있는지 봐보자

인덱싱과 슬라이싱

수천 수만개의 데이터를 다루다보면 원하는 데이터만 쏙쏙 뽑아 보고싶어질 것이다. 이때 사용하는 것이 인덱싱, 슬라이싱이다.

# 1. column(열)을 직정 지정해 뽑아내는 방법 
cols =['체력', '공격력']
df_character[cols]
# 이때 행에 대한 인덱싱은 불가능하다. > df_character[1] 이런건 불가능
# 아마도 행은 key값이니까 딕셔너리의 key값을 뽑아내는 방식을 떠올려보면 될듯!

# 2. row(행)을 지정해 뽑아내는 방법
df_character[:2]

# 3. 행과 열을 뽑아내는 방법
df_character[cols][:2]

loc, iloc

  1. loc: 라벨 기반의 인덱싱. 즉 행 또는 열의 이름을 이용해 특정 위치의 행과 열에 접근이 가능하다.
  2. iloc: 정수 기반의 인덱싱. 즉 정수 인덱스를 이용해 특정 위치의 행과 열에 접근이 가능하디.

우선 loc 부터 보자

캐릭터명이 ‘지혜’인 데이터 추출

df_characters.loc[df_characters["캐릭터명"] == "지혜"]

각 캐릭터의 ‘마나’ 데이터 추출

df_characters.loc[:,"마나"]

캐릭터명이 ‘지혜’인데, 지혜의 ‘체력’과 ‘마나’데이터만 추출

df_characters.loc[df_characters["캐릭터명"] == "지혜", ["체력", "마나"]]

캐릭터명이 지혜인데, 지혜의 ‘마나’ 데이터만 추출

df_characters.loc[df_characters["캐릭터명"] == "지혜", "마나"]

행의 인덱스 1인 데이터

df_characters.loc[1]

다음은 iloc 예시이다.

행의 인덱스 3인 데이터

df_characters.iloc[3]
# 지혜의 모든 능력치가 나옴 

인덱스 [1,2]인 데이터

df_characters.iloc[1,2]
# 유비의 체력값이 나온다 

각 캐릭터의 체력 능력치

df_characters.iloc[:,1]

행의 0번째 인덱스를 제외한 나머지 캐릭터들의 모든 능력치

df_characters.iloc[1:]

연산

덧셈 연산(add)

위 df_character안에 모든 항목에 10을 더해보자

df_character[['체력', "공격력", "방어력", "마나"]].add(10)
df_character

# 혹은
cols = ['체력', "공격력", "방어력", "마나"]
df_character[cols] += 10

이처럼 모든 항목에 10이 더해진 것을 볼 수 있다.

곱셈 연산(mul)

# 전체 데이터에 곱하기 2
df_character[cols].mul(2)

# 혹은
df_character[cols] *= 2

빼기 연산(sub)

# 전체 데이터에 빼기 10
df_character[cols].sub(10)

# 혹은
df_character[cols] -= 10

합 연산(sum)

# 행의 전체 합 > 모든 능력치의 합을 의미 
total = df_character[cols].sum(axis = 1)

나누기(div) & 나머지(mod) 연산

# 나누기
# div(other, axis='columns')
# > 위에서 만든 합에 각 열의 값들을 나눈 값 
df_character[cols].div(total, axis=0)

# 나머지 
# 3으로 나눴을때의 나머지 
df_character[cols].mod(3)

제곱 연산(pow)

# 전체에 2 제곱
df_character[cols].pow(2)

열 추가하기

열을 추가하는 방법은 두가지가 있다.

  1. 맨 뒤에 연결
  2. insert

위에서 만든 총 능력치 데이터를 추가해보도록 하자.

# 위에서 만든 총 능력치 데이터 
total = df_character[cols].sum(axis = 1)
df_character["총 능력치"] = total
# 원하는 인덱스(=0)에 name과 total 데이터 삽입
df_character.insert(0, "총 능력치", total)

그러다 문득 insert에서 마지막 열에 추가하는 방법이 궁금해져서.. python의 마지막 인덱스처럼 -1로 접근해보니 에러가 뜬다….!

df_character.insert(0, "총 능력치", total)
# ValueError: unbounded slice

그래서 바로 강사님께 여쭤보니 판다스에서는 마지막 요소를 가르키는것이 다르게 동작한다 하셨다. 이때는 len(df_character.columns)를 사용해야한다고.. 직접 column의 개수 그 자체를 인덱스값에 넣어주어야 마지막 인덱스에 데이터 삽입이 가능해진다.

df_character.insert(len(df_character.columns), "총 능력치", total)

둘 다 아주 잘 동작한다!

  • max(): 최대값
  • min(): 최소값
  • mean(): 평균값
  • dot(): 행렬곱 연산
  • std()표준편차(분산) 계산
  • sort_values(‘’, ascending=False): 정렬 > 내림차순
total.sort_values('총능력치', ascending=False)
  • date_range(dd, period=, frea=): 날짜모양, 기간, 날짜기준
# 2024-01-01 부터 30 가간을 Day로
pd.date_range('2024-01-01', periods=30, freq='D')

head, tail

  1. head(*): 첫 *만큼의 데이터 확인
  2. tail(*): 끝 *만큼의 데이터 확인
df_character.head(3)
df_character.tail(3)

prod

데이터 프레임 내 값들의 곱을 구함 (내용 설명 필요 > 추후 추가 예정)

fillna

데이터 프레임에서 발생되는 결측치(NaN or None값들) 값을 다른 값으로 채우는데 사용

즉, 결측치를 대체하거나 채울값을 지정해줄 수 있다. 수많은 데이터를 다루다보면 어떤 값에는 NaN or None이라는 값이 반환되는 경우가 존재할텐데 이때 여기에 들어갈 값을 내가 지정해줄 수 있다는 의미이다.

df_character_zero = df.fillna(0)

위와 같은 코드를 보자면, df_character_zero라는 임의의 변수안에는 NaN or None값이 포함된 데이터들이 존재한다고 하자. 그때 NaN or None한 데이터에는 내가 지정한 0이라는 값으로 대체되어 데이터가 채워져 보일 것이다.

아, 번외로 계속 헷갈리게 쓰는것 같다. column은 열이고 row가 행이다

groupby

그룹별로 묶어 연산이 가능

# 와인의 종류별로(type) 퀄리티(quality) 컬럼의 평균 구하기
wine.groupby('type')['quality'].mean()

외에도 describe(), std() ,count() 등 다양한 연산이 가능하다. 이때 각 연산들을 한꺼번에 보는 방법도 있다.

wine.groupby('type')['quality'].agg(['mean', 'std'])

위처럼 agg함수를 통해 가능하다.

Pandas란? (Series와 DataFrame)

|

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


Pandas란?

데이터프레임을 처리하기 위해 이용하는 python의 라이브러리 중 하나.

일반적으로 데이터 처리 시 필요한 데이터 세트는 2차원 데이터로 구성되며, (데이터 정리 분석, 모델링, 분석결과 또는) 표 형식으로 표시하기에 적합한 형식으로 구성된다. 2차원 데이터는 행렬로 이루어져 있기 때문에 이해하기 쉬운 구조이며, 효과적으로 데이터를 담을 수 있기 때문이다.

Series와 DataFrame

우선 Series와 DataFrame 모두 pandas의 데이터 오브젝트이다. 아래 사진을 보면 이해가 더욱 쉬워진다.

우선 Series는 인덱스와 값으로 이루어진 이 하나인 자료형이다. 그렇다면 DataFrame은 어떻게 구성되어있을까? 칼럼 단위의 시리즈 모음인덱스로 구성이 되어있다.

즉, DataFrame은 행과 열의 인덱스가 존재하고 이 인덱스에 맞게 데이터들이 존재하는 데이터구조를 의미한다. 이를 직접 만들수도 있고, 엑셀이나 csv 파일을 읽어와 만들기도 한다.


번외로 데이터프레임을 만드는 형식은 늘 딕셔너리인가? 궁금했었는데..

딕셔너리가 아닌 리스트, 넘파이 등등 다양한 방법으로도 데이터프레임을 만든다고는 한다. 하지만 이는 실무에서 너무 복잡한 형태여서 거의 사용하지는 않는다고..! 대체로 데이터프레임은 딕셔너리로 생성하고 이때의 key값이 컬럼명, value값이 각 데이터 값(열)에 들어가는 것!

NumPy sort 와 argsort

|

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


np.sort 와 np.argsort

  • np.sort(배열명): 원본 데이터 유지한 채 정렬된 행렬 반환
  • np.sort(): 행렬 자체를 정렬해서 반환 > 반환되는 값은 None
  • np.argsort(): 정렬된 행렬의 인덱스를 반환

우선 np.sort(배열명)과 np.sort()의 차이를 먼저 보자.

arr1 = np.array([3,1,7,5])
# [3,1,7,5]
arr_sort1 = np.sort(arr1)
# [1,3,5,7]
arr1 
# [3,1,7,5]
arr1 = np.array([3,1,7,5])
# [3,1,7,5]
arr_sort2 = arr1.sort()
# None
arr1
# [1,3,5,7]

이처럼 np.sort(배열명)의 경우에는 복사한 값에 새롭게 정렬된 배열이 할당되지만(arr_sort1) 기존 arr1의 배열에는 아무런 변화가 없음을 알 수 있다. 그러나 np.sort()의 경우에는 기존 arr1의 값이 변하는 것을 볼 수 있다. 다만 arr_sort2에서 반환되는 값은 None이다.

내림차순 하는 방법은 아래와 같다.

arr_re_sort = np.sort(arr1)[::-1]
# [7,5,3,1]

그러면 argsort()는 어떻게 쓸까?

arr1 = np.argsort([3,1,7,5])
arr1
# > ([1, 0, 3, 2])

이렇듯 정렬된 값의 기존 인덱스 값을 배열에 반환하는 것을 볼 수 있다.

arr1 = np.argsort([3,1,7,5])
#[1,3,5,7] 로 소팅된 후 기존 [3,1,7,5]일때의 인덱스 값을 반환 
arr1
# > ([1, 0, 3, 2])

그렇다면 이것도 내림차순이 가능할까? 가능함!

arr1 = np.argsort([3,1,7,5])[::-1]
# [7,5,3,1]로 역순 소팅 후 기존 [3,1,7,5]일때의 인덱스 값 반환 
arr1
# > ([2, 3, 0, 1])