해시 구조

  • Hash Table: 키(Key) 값(Value)구조로 저장하는 데이터구조
  • Key를 통해 데이터를 받아올 수 있으므로, 속도가 빨라진다.
  • 파이썬 딕셔너리(Dictionary)타입이 해시 테이블의 예
  • 보통 배열로 미리 해시 테이블 사이즈만큼 생성 후에 사용한다.
  • 파이썬에서는 해시를 별도 구현할 이유가 없다.(딕셔너리타입이 존재)

 

시간 복잡도

  • 일반적인 경우(충돌이 없는 경우)는 O(1)
  • 최악의 경우(충돌이 모두 발생하는 경우)는 O(n)

해시테이블의 경우, 일반적인 경우를 기대하고 만들기 때문에 시간복잡도는 O(1)이라고 할 수 있다.

 

용어

  • 해시(Hash): 임의 값을 고정 길이로 변환하는 것
  • 해시 테이블(Hash Table): 키 값의 연산에 의해 직접 접근이 가능한 데이터 구조
  • 해싱 함수(Hashing Function): Key에 대해 산술 연산을 이용해 데이터 위치를 찾을 수 있는 함수
  • 해시 값(Hash Value) 또는 해시 주소(Hash Address): Key를 해싱 함수로 연산해서 해시 값을 알아내고, 이를 기반으로 해시 테이블에서 해당 Key에 대한 데이터 위치를 찾을 수 있다.
  • 슬롯(Slot): 한 개의 데이터를 저장할 수 있는 공간

 

https://ko.wikipedia.org/wiki/%ED%95%B4%EC%8B%9C_%ED%95%A8%EC%88%98

 

해시 테이블의 장단점과 용도

해시 테이블의 장점

  • 데이터 저장/읽기 속도가 빠르다.(검색 속도가 빠름)
  • 해시는 키에 대한 데이터가 있는지(중복) 확인이 쉽다.

 

해시 테이블의 단점

  • 일반적으로 저장공간이 좀 더 많이 필요하다.
  • 여러 키에 해당하는 주소가 동일할 경우 충돌을 해결하기 위한 별도 자료구조가 필요하다.

 

용도

  • 검색이 많이 필요한 경우
  • 저장, 삭제, 읽기가 빈번한 경우
  • Cache 구현(중복 확인이 쉽기 때문이다.)

 

리스트 변수를 이용하여 해시 테이블 구현

hash_table = list([0 for i in range(10)]) # 0부터 9까지의 리스트를 0으로 초기화

# Key를 생성하는 함수
def get_key(data):
    return hash(data)

# Key를 이용하여 Hash address를 생성하는 함수
def hash_function(key):
    return key % 10

# data, value를 해시 테이블에 저장하는 함수
def save_data(data, value):
    hash_address = hash_function(get_key(data))
    hash_table[hash_address] = value

# 데이터값으로 Value값을 가져오는 함수
def read_data(data):
    hash_address = hash_function(get_key(data))
    return hash_table[hash_address]

실행 결과

 

이중 연결 리스트의 구조

 미리 모든 노드가 이전 노드와 다음 노드에 대한 정보를 모두 저장하고 있는 리스트이다.

 양방향으로 연결되어 있어서 노드 탐색이 양쪽으로 모두 가능하다.

출처: wikipedia, https://en.wikipedia.org/wiki/Linked_list

 

Node 클래스 구현

class Node:
    def __init__(self, data, prev=None, next=None):
        self.prev = prev
        self.data = data
        self.next = next

기본적인 연결리스트의 객체선언부이고 Node의 data는 실제 우리가 담고자하는 데이터를 할당하게되고 next는 다음 노드의 주소값을 prev는 이전 노드의 주소값을 가진다.

 

Node Management 클래스 구현

class NodeMgmt:
    def __init__(self, data):
        self.head = Node(data)
        self.tail = self.head

 

단순 연결 리스트와 비슷하지만 이중 연결리스트는 양방향으로 검색이 가능하기 때문에 tail을 선언하여 가장 뒤에있는 노드의 정보를 저장한다.

 

Insert 함수 구현

# 차례대로 데이터를 삽입
def insert(self, data):
    if self.head == None:
        self.head = Node(data)
        self.tail = self.head
    else:
        node = self.head
        while node.next:
            node = node.next
        new = Node(data)
        node.next = new
        new.prev = node
        self.tail = new

순차적으로 데이터를 집어넣은 경우에는 새로운 노드가 삽입될 때 기존에 있는 노드의 next는 새로운 노드의 주소값을 가리켜야 되고 새로운 노드의 prev는 기존 노드의 주소값을 가져야 한다.

여기서 새로운 노드는 항상 마지막에 존재하기 때문에 tail에 새로운 노드를 넣어준다.

 

Insert_before 함수 구현

# 특정 데이터앞에 데이터를 삽입
def insert_before(self,data, before_data):
    if self.head == None:
        self.head = Node(data)
        return True
    else:
        node = self.tail
        while node.data != before_data:
            node = node.prev
            if node == None:
                return False
        new = Node(data)
        before_new = node.prev
        before_new.next = new
        new.prev = before_new
        new.next = node
        node.prev = new
        return True
            
# 특정 데이터뒤에 데이터를 삽입
def insert_after(self, data, after_data):
    if self.head == None:
        self.head = Node(data)
        return True
    else:
        node = self.head
        while node.data != after_data:
            node = node.next
            if node == None:
                return False
        new = Node(data)
        after_new = node.next
        after_new.prev = new
        new.next = after_new
            new.prev = node
            node.next = new
            return True      

 

 

Insert_before와 Insert_after는 특정 노드 앞에 데이터를 삽입하냐 아니면 특정 노드 뒤에 데이터를 삽입하냐의 차이라 코드상 큰 차이는 없다.

새로운 노드가 삽입되면 삽입되는 노드 기준으로 앞에 노드의 next 뒤에 노드의 prev가 새로운 노드를 가리켜야하고 새로운 노드의 prev와 next도 마찬가지로 해당되는 노드들을 가리켜야한다.

 

Delete 함수 구현

def delete(self, data):
    node = self.head
    while node.data != data:
        node = node.next
    node.prev.next = node.next
    node.next.prev = node.prev
    return True

반복문을 이용해서 삭제하고 싶은 데이터를 찾아 반복문을 탈출하면 삭제하고싶은 노드의 prev와 next를 서로 연결시켜준다.

 

Search 함수 구현

def search_from_head(self, data):
    if self.head == None:
        return False
    
    node = self.head
    while node:
        if node.data == data:
            return node
        else:
            node = node.next
    return False

def search_from_tail(self, data):
    if self.head == None:
        return False
    
    node = self.tail
    while node:
        if node.data == data:
            return node
        else:
            node = node.prev
    return False

이중 연결 리스트에서는 head(처음)부터 검색을 할 수 도있고 tail(끝)부터 검색이 가능하다.

 

Print 함수 구현

def desc(self):
    node = self.head
    while node:
        print(node.data)
        node = node.next

반복문을 이용하여 링크드 리스트의 헤드부터 순차적으로 데이터를 출력해준다.

 

전체 코드

class Node:
    def __init__(self, data, prev=None, next=None):
        self.prev = prev
        self.data = data
        self.next = next

class NodeMgmt:
    def __init__(self, data):
        self.head = Node(data)
        self.tail = self.head
    
    def insert(self, data):
        if self.head == None:
            self.head = Node(data)
            self.tail = self.head
        else:
            node = self.head
            while node.next:
                node = node.next
            new = Node(data)
            node.next = new
            new.prev = node
            self.tail = new
            
    def insert_before(self,data, before_data):
        if self.head == None:
            self.head = Node(data)
            return True
        else:
            node = self.tail
            while node.data != before_data:
                node = node.prev
                if node == None:
                    return False
            new = Node(data)
            before_new = node.prev
            before_new.next = new
            new.prev = before_new
            new.next = node
            node.prev = new
            return True
        
    def insert_after(self, data, after_data):
        if self.head == None:
            self.head = Node(data)
            return True
        else:
            node = self.head
            while node.data != after_data:
                node = node.next
                if node == None:
                    return False
            new = Node(data)
            after_new = node.next
            after_new.prev = new
            new.next = after_new
            new.prev = node
            node.next = new
            return True      
            
    def delete(self, data):
        node = self.head
        while node.data != data:
            node = node.next
        node.prev.next = node.next
        node.next.prev = node.prev
        return True
        
    def desc(self):
        node = self.head
        while node:
            print(node.data)
            node = node.next

    def search_from_head(self, data):
        if self.head == None:
            return False
        
        node = self.head
        while node:
            if node.data == data:
                return node
            else:
                node = node.next
        return False
    
    def search_from_tail(self, data):
        if self.head == None:
            return False
        
        node = self.tail
        while node:
            if node.data == data:
                return node
            else:
                node = node.prev
        return False

링크드 리스트란?

 자료들을 연속적으로 나열하지않고 임의의 기억공간에 존재하는 데이터를 노드의 포인터 부분을 이용하여 서로 연결시킨 자료 구조이다.

 

링크드 리스트 구조

 링크드 리스트는 연결 리스트라고도 한다.

 배열은 순차적으로 연결된 공간에 데이터를 나열하는 데이터 구조

 링크드 리스트는 떨어진 곳에 존재하는 데이터를 화살표로 연결해서 관리하는 데이터구조

 링크드 리스트 기본 구조와 용어

  - 노드(Node): 데이터 저장 단위(데이터값, 포인터)로 구성되어있다.

  - 포인터(Pointer): 각 노드 안에서, 다음이나 이전의 노드와의 연결 정보를 가지고 있는 공간이다.

 링크드 리스트의 형태

출처: wikipedia, https://en.wikipedia.org/wiki/Linked_list

링크드 리스트의 장단점

링크드 리스트 장점

미리 데이터 공간은 할당하지 않아도 된다.

  - 배열은 미리 데이터 공간을 할당 해야한다.

 

링크드 리스트 단점

연결을 위한 별도 데이터 공간이 필요하므로, 저장공간 효율이 높지 않음

 연결 정보를 찾는 시간이 필요하므로 접근 속도가 느리다.

중간 데이터 삭제시, 앞뒤 데이터의 연결을 재구성해야 하는 부가적인 작업이 필요하다.

 

Node 클래스 구현

class Node:
    def __init__(self, data, next=None):
        self.data = data
        self.next = next

기본적인 연결리스트의 객체선언부이고 Node의 data는 실제 우리가 담고자하는 데이터를 할당하게되고 next는 다음 노드의 주소값을 가진다.

 

Node Management 클래스 구현

class NodeMgmt:
    def __init__(self, data):
        if self.head == None:
            self.head = Node(data)

최초로 링크드리스트가 선언되면 링크드리스트의 정보를 가지는 Management를 만들어준다. 이후 삽입 삭제 검색 출력 함수등이 Management클래스에 속하게된다.

 

데이터 추가(add) 함수 구현

def add(self, data):
        if self.head == None:	# head에 데이터가 존재하지 않으면 바로 추가해주는 예외처리
            self.head = Node(data)
        else:
            node = self.head
            while node.next:
                node = node.next
            node.next = Node(data)

기본적으로 add함수는 연속적으로 데이터를 저장하기 때문에 while문을 이용하여 node의 가장 마지막 위치를 찾는다.

node.next가 존재하지 않으면 반복문을 빠져나온다. 반복문을 빠져나옴과 동시에 node.next에 새로 생성되는 Node의 주소값을 넣어줌으로 기존 링크드리스트와 연결시켜준다.

 

Insert 함수 구현

def insert(self, data, is_data):
        if self.head == None:
            self.head = Node(data)
            return True
        else:
            node = self.head
            while node.data != is_data:
                node = node.next
                if node == None:
                    return False
            new = Node(data)
            new.next = node.next
            node.next = new
            return True

Insert함수는 기본적으로 search의 기능을 가지고있다. 데이터를 중간에 삽입하기위해선 앞이든 뒤든 데이터를 알고있어야 삽입이 가능하기 때문에 반복문을 이용하여 입력받은 is_data가 링크드리스트내에 존재하는지 확인을 하고 존재 한다면 반복문을 빠져나와 데이터를 삽입하게 된다.

출처: wikipedia, https://en.wikipedia.org/wiki/Linked_list

 

그림과 같이 데이터를 삽입하게 되면 기존 노드는 새로운노드의 주소값을 가져야하고 새로운노드는 기존 node가 가지고있던 주소값을 가져야 한다.

 

Delete 함수 구현

def delete(self, data):
        if self.head == None:
            print('노드가 없습니다.')

        if self.head.data == data:
            temp = self.head
            self.head = self.head.next
            del temp
        else:
            node = self.node
            while node.next:
                if node.next.data == data:
                    temp = node.next
                    node.next = node.next.next
                    del temp
                else:
                    node = node.next

삭제할 데이터를 찾으면 빈 공간에 찾은 데이터를 넣어준다.

node.next에 node.next.next의 주소값을 담아주고 node.next가 담겨있던 temp를 지워줌으로 데이터를 삭제한다.

 

 

 

 

출처: wikipedia, https://en.wikipedia.org/wiki/Linked_list

Search 함수 구현

def search(self, data):
        if self.head == '':
            print('노드가 없습니다.')
        if self.head.data == data:
            return self.head.data
        else:
            node = self.head
            while node.next:
                if node.next.data == data:
                    return node.next.data
                else:
                    node = node.next

링크드 리스트의 head가 검색데이터라면 head를 리턴해주고 검색함수를 끝내고 head가 아니라면

반복문을 이용하여 순차적으로 현재 노드의 데이터와 검색할 데이터가 일치하는지 찾아서 결과를 리턴해준다.

 

Print 함수 구현

def desc(self):
        node = self.head
        while node:
            print(node.data)
            node = node.next

반복문을 이용하여 링크드 리스트의 헤드부터 순차적으로 데이터를 출력해준다.

 

전체 코드

class Node:
    def __init__(self, data, next=None):
        self.data = data
        self.next = next
        
class NodeMgmt:
    def __init__(self, data):
        self.head = Node(data)
        
    def add(self, data):
        if self.head == '':
            self.head = Node(data)
        else:
            node = self.head
            while node.next:
                node = node.next
            node.next = Node(data)
            
    def insert(self, data, is_data):
        if self.head == None:
            self.head = Node(data)
            return True
        else:
            node = self.head
            while node.data != is_data:
                node = node.next
                if node == None:
                    return False
            new = Node(data)
            new.next = node.next
            node.next = new
            return True
            
    def desc(self):
        node = self.head
        while node:
            print(node.data)
            node = node.next
            
    def delete(self, data):
        if self.head == '':
            print('노드가 없습니다.')

        if self.head.data == data:
            temp = self.head
            self.head = self.head.next
            del temp
        else:
            node = self.node
            while node.next:
                if node.next.data == data:
                    temp = node.next
                    node.next = node.next.next
                    del temp
                else:
                    node = node.next
    
    def search(self, data):
        search_count = 0
        if self.head == '':
            print('노드가 없습니다.')
        
        if self.head.data == data:
            return self.head.data
        else:
            node = self.head
            while node.next:
                if node.next.data == data:
                    return node.next.data, search_count
                else:
                    node = node.next
                    search_count += 1

스택이란?

스택은 사전적의미로 "쌓다", "더미"라는 의미를 가지고 있다.

메모리의 스택 영역은 함수의 호출과 관계되는 지역번수, 매개변수, 리턴값 등의 임시데이터를 저장한다.

 

스택의 구조

스택은 LIFO(Last In, First Out 후입선출)구조의 데이터 관리 방식을 따른다.

   - LIFO: 마지막에 넣은 데이터를 가장 먼저 추출하는 데이터 관리 정책

   - FILO: 처음에 넣은 데이터를 가장 마지막에 추출하는 데이터 관리 정책

데이터를 제한적으로 접근할 수 있는 구조

   - 한쪽 끝에서만 자료를 넣거나 뺄 수 있는 구조

스택의 주요 기능

   - push(): 데이터를 스택에 넣음

   - pop(): 데이터를 스택에서 추출

 

위 그림에서 2를 push하게 되면 스택에 쌓이게 되고 그림의 흐름처럼 3, 4, 5, 6이 차례대로 스택에 쌓이게 된다.

이후 pop을 하면 쌓였던 데이터중에 가장 나중에 쌓인 6이 가장 먼저 추출이 되고 계속 추출을 하면 가장위에 있는 데이터가 추출되게 된다.

 

스택의 장점

구조가 단순해서 구현이 쉽다.

데이터 저장/읽기 속도가 빠르다.

 

스택의 단점

데이터 최대 갯수를 미리 정해야 한다.

저장 공간의 낭비가 발생할 수 있다.(미리 최대 갯수만큼 공간을 확보해야함)

 

파이썬 리스트기능에서 제공하는 메서드로 스택 구현

list_stack = list()

list_stack.append(1) #리시트에 append메소드를 이용해 데이터를 쌓는다.
list_stack.append(2)
list_stack.pop() #가장 마지막에 입력된 2가 추출된다.

 

파이썬으로 push, pop함수 구현하기

list_stack = list() #리스트 선언

def push(data):
	list_stack.append(data) #push에 데이터를 입력받아 스택에 쌓아준다.
    
def pop():
	data = list_stack[-1] #후입선출구조이기 때문에 마지막데이터를 data변수에 입력해준다.
    del list_stack[-1]    #리스트의 마지막데이터를 삭제해준다.
    return data           #마지막으로 저장한 데이터를 리턴해줘서 사용자에게 어떤수가 사라졌는지 보여준다.
   
for i in range(10):
	push(i) #0~9까지 수를 스택에 쌓아준다.
    
pop() #마지막으로 쌓인 9가 추출된다.

큐(Queue)란

 

큐의 구조

줄을 서는 행위와 유사하다.

가장 먼저 넣은 데이터를 가장 먼저 꺼낼 수 있는 구조이다.

    - 버스정류장에서 가장 먼저 줄을 선 사람이 가장 먼저 버스에 타는 것과 동일하다.

    - FIFO(First-In, First-Out) 또는 LILO(Last-In, Last-Out)방식으로 스택과 꺼내는 순서가 반대이다.

출처: http://www.stoimen.com/blog/2012/06/05/computer-algorithms-stack-and-queue-data-structure

 

큐의 용어

Enqueue: 큐에 데이터를 넣는 기능

Dequeue: 큐에서 데이터를 꺼내는 기능

 

파이썬 queue 라이브러리를 활용하여 자료구조 사용하기

queue라이브러리에는 다양한 큐 구조로 Queue(), LifoQueue(), PriorityQueue()를 제공한다.

    - Queue(): 가장 일반적인 큐 자료구조

import queue

d_queue = queue.Queue() # d_queue를 Queue()를 통해 기본큐로 선언
d_queue.put('hello') #put을 이용하여 d_queue에 데이터 추가
d_queue.put(10)
d_queue.get() #get을 이용하여 데이터를 가져온다. 여기서 나오는 출력문은 hello가 된다.

    - LifoQueue(): 나중에 입력된 데이터가 먼저 출력되는 구조(스택구조와 같음)

import queue

d_queue = queue.LifoQueue() #LifoQueue를 통해 LIFO큐를 선언
d_queue.put('hello') #Queue함수로 선언한것과 동일하게 put으로 데이터 삽입
d_queue.put(30)
d_queue.get() #FIFO와 반대로 LIFO는 마지막으로 들어간 데이터가 먼저나오기때문에 출력문은 30

    - PriorityQueue(): 데이터마다 우선순위를 부여하여 우선순위가 높은 순서대로 데이터를 출력한다.

import queue

d_queue = queue.PriorityQueue()
d_queue.put((3, 'hello')) #PriorityQueue는 튜플로입력받고 첫번째에는 우선순위 두번째에 데이터를 입력한다.
d_queue.put((5, 100))
d_queue.put((15, 'world'))
d_queue.get() #우선순위가 가장 낮은수를 출력하기 때문에 출력문은 (3, 'hello')가 된다.

 

파이썬 라이브러리를 사용하지않고 enqueue, dequeue구현

list_q = [] #리스트 선언

def enqueue(data):

    list_q.append(data) #추가하고싶은 데이터를 입력하면 리스트에 추가

def dequeue():

    data = list_q[0] #리스트의 0번째 데이터를 data에 대입
    del list_q[0]    #리스트의 0번째 데이터를 삭제
    return data      #data를 리턴해줌으로 삭제된 데이터를 보여준다.

배열(Array)이란

데이터를 나열하고, 각 데이터를 인덱스에 대응하도록 구성한 데이터 구조이다.

파이썬에서는 리스트 타입이 배열 기능을 제공하고 있다.

 

배열이 필요한 이유

같은 종류의 데이터를 효율적으로 관리하기 위해 사용한다.

같은 종류의 데이터를 순차적으로 저장한다.

 

배열의 장점

빠른 접근 가능(인덱스를 이용하여 순차적 접근이 아닌 찾고자하는 데이터의 인덱스만 알면 바로 찾을 수 있다.)

 

배열의 단점

추가/삭제가 쉽지않다.

미리 최대 길이를 지정해야 한다.

 

파이썬과 C언어 배열 예제

C언어

#include <stdio.h>

int main(int argc, char* argv[])
{
    char country[3] = "US";
    printf("%c%c\n", country[0], country[1]);
    printf("%s \n", country);
    return 0;
}

Python

country = 'US'
print(country)

country = country + 'A'
print(country)

파이썬은 C언어와 다르게 배열의 최대길이를 설정해주지 않는데 이것은 파이썬에서 리스트를 이용해 배열을 만들기 때문이다.

 

파이썬 리스트를 활용한 배열

# 1차원 배열: 리스트로 구현시
data = [1, 2, 3, 4, 5]

# 2차원 배열: 리스트로 구현시
data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

 

 

 

자료구조(데이터구조, data structure)란

자료(Data)란 현실 세계로부터 수집한 사실이나 개념의 값 또는 이들의 집합을 의미한다.

자료구조(Data Structure)란 자료의 집합을 의미하며 각 원소들 사이의 관계가 논리적으로 정의된 일정한 규칙에 의하여 나열되며 자료에 대한 처리를 효율적으로 수행할 수 있도록 자료를 조직적으로 구분하여 표한한 것을 말한다.

즉 대량의 데이터를 효율적으로 관리할 수 있는 데이터의 구조를 의미한다.

 

자료구조의 선택 기준

작업의 효율성, 추상화, 재사용성을 증가시키기 위해 상황에 맞는 적절한 자료구조를 사용해야한다.

자료 처리를 효율적으로 하기위한 고려사항

 자료의 처리시간

 자료의 크기

 자료의 활용 빈도

 자료의 갱신 정도

 프로그램의 용이성

 

효율적으로 데이터를 관리하는 예

우편번호: 5자리 우편번호로 국가의 기초구역을 제공

학생관리: 학년, 반 번호를 학생에게 부여해 학생부를 관리

 

대표적인 자료구조

배열, 스택, 큐, 링크드 리스트, 해쉬 테이블, 힙 등

 

 

알고리즘이란

어떤 문제를 풀기 위한 절차 / 방법

어떤 문제에 대해, 특정한 입력을 넣으면 원하는 출력을 얻을 수 있도록 만드는 프로그래밍

 

알고리즘의 판단 기준

 처리 시간

 저장공간활용

 

 

어떠한 자료구조와 알고리즘을 쓰느냐에 따라, 성능의 차이가 심하기 때문에 자료구조와 알고리즘은 중요하다.

 

 

ps, 자료구조와 알고리즘은 어떤 언어로든 익힐 수 있다.

      이전에는 무조건 C또는 C++ 로만 작성하도록 하는경우가 많았지만 최근에는 언어로 인한 제약/평가가 없어짐

+ Recent posts