자료구조 - 연결 리스트, 스택, 큐
컴퓨터 사이언스 부트캠프 with 파이썬을 보고 정리한 내용입니다.
자료구조란
- 자료구조(data structure)
- 데이터를 효율적으로 검색/변경/삭제할 수 있도록 저장/관리하는 방법
- 상황에 따라 적절한 자료구조가 달라질 수 있음
- 데이터 검색은 빈번하게 일어나는데 반해 새로운 데이터 삽입이 없다면 → 배열을 쓰는 것이 합리적
- 배열: 메모리에 순서대로 할당되므로 캐시 히트가 일어날 확률이 높고, 배열 안에 있는 변수 위치(인덱스)를 통해 변수에 매우 빠르게 접근할 수 있음
- 검색에 비해 삽입이나 기존 데이터 삭제가 자주 일어난다면 연결 리스트를 사용하는 것이 효율적
- 데이터 검색은 빈번하게 일어나는데 반해 새로운 데이터 삽입이 없다면 → 배열을 쓰는 것이 합리적
- 추상 자료형
- ADT (Abstract Data Type)
- 자료 구조에서 삽입/탐색/삭제를 담당하는 함수들의 사용 설명서
- 추상화한다는 것은 인터페이스와 구현을 분리한다는 것
- 인터페이스란?
L.append(object) → None
: 함수의 이름, 인자, 반환형을 알 수 있음 - 하지만 append 라는 함수가 어떻게 구현되어 있는지는 알 수 없고 알 필요도 없음
- 인터페이스란?
- ADT (Abstract Data Type)
연결 리스트(linked list)
- 데이터와 참조로 구성된 노드가 한 방향 혹은 양방향으로 쭉 이어져 있는 자료 구조
- 참조는 다음 노드 또는 이전 노드를 가리킴
- 양방향으로 이어져 있는 것은 이중 연결 리스트라고 하며, 힙 세그먼트를 구현하는 큰 틀을 제공함
- 단일 연결 리스트의 노드 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Node:
def __init__(self, data = None):
#노드의 구조: 데이터 부분과 참조 부분
self.__data = data
self.__next = None
def __del__(self):
print("data of {} is deleted".format(self.data))
@property
def data(self):
return self.__data
@data.setter
def data(self, data):
self.__data = data
@property
def next(self):
return self.__next
@next.setter
def next(self, n):
self.__next = n
- 정보 은닉 기법(언더바 및 프로퍼티 기법) 사용했음
- 단일 연결 리스트 클래스의 멤버
- head: 첫번째 데이터
- tail: 마지막 데이터
- d_size: 데이터의 개수
- 단일 연결 리스트의 ADT
S.append(data) -> None
- 데이터를 삽입
S.search_target(target, start = 0) -> (data, pos)
- 데이터를 검색 (순회하면서 대상 데이터를 찾아 위치와 함께 반환)
pos
: 리스트에서 몇번째 데이터인지, 인덱스처럼 0처럼 시작하지만 인덱스처럼 인덱싱이 불가능하다는 것이 차이점 (원하는 데이터를 찾으려면 처음부터 모두 순회해야 함)- 찾는 데이터가 없으면
(None, None)
반환 start
는 검색을 시작하는 위치로, 그 전까지는 단순히 순회를 통해 이동만
S.search_pos(pos) -> data
- 데이터를 검색 (위치를 인자로 받아서 해당 위치의 데이터를 반환)
- 이것도 마찬가지로 처음부터 모두 순회가 필요
S.remove(target) -> data
- 데이터를 삭제
- 데이터가 있으면, 첫번째 데이터를 지움
- 대상 데이터가 없으면 None 반환
- 데이터를 삭제
S.empty() -> bool
- 비어있으면 참, 비어있지 않다면 거짓 반환
S.size() -> integer
- 데이터의 개수를 반환
- 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class Linked_list:
#생성자
def __init__(self):
self.head = None #첫번째 노드
self.tail = None #마지막 노드
self.d_size = 0 #데이터의 개수
def empty(self):
if self.d_size == 0:
return True
else:
return False
def size(self):
return self.d_size
#삽입
def append(self, data):
new_node = Node(data)
#비어있을 때
if self.empty():
self.head = new_node
self.tail = new_node
self.d_size += 1
#이미 데이터가 있을 때
else:
self.tail.next = new_node
self.tail = new_node #tail을 새로운 노드로 옮기는 과정
self.d_size +=1
def search_target(self, target, start = 0):
if self.empty():
return None
#첫번째 노드에서 시작
pos = 0
cur = self.head
#start 위치 이상이어야만 탐색을 하고 아니면 그냥 지나감
if pos >= start and target == cur.data:
return cur.data, pos
while cur.next:
pos += 1
cur = cur.next
if pos >= start and target == cur.data:
return cur.data, pos
#끝까지 못찾으면
return None, None
def search_pos(self, pos):
#범위를 벗어나면 None
if pos > self.size() -1:
return None
cnt = 0
cur = self.head
if cnt ==pos:
return cur.data
while cnt < pos:
cur = cur.next
cnt += 1
return cur.data
# del을 이용해 명시적으로 삭제하지 않고, 레퍼런스 카운팅을 이용해 0으로 만듦
def remove(self, target):
if self.empty():
return None
bef = self.head
cur = self.head
#삭제 노드가 첫번째 노드일 때
if target == cur.data:
#데이터가 하나일 때, head 와 tail 을 없애줌
if self.size() ==1:
self.head = None
self.tail = None
#데이터가 하나 이상일 때
else:
self.head = self.head.next #head를 옮겨줌
self.d_size -= 1 #데이터 크기를 하나 줄여줌
return cur.data
while cur.next:
bef = cur
cur = cur.next
#삭제 노드가 첫번째 노드가 아닐 때
if target == cur.data:
#마지막 노드일 때
if cur == self.tail:
self.tail = bef #tail을 옮겨줌
bef.next = cur.next #없애줌
self.d_size -= 1 #데이터 크기를 하나 줄여줌
return cur.data
#끝까지 찾지 못하면 None
return None
스택
- 데이터를 차곡차곡 쌓아올림
- LIFO(Last In, First Out) / 후입선출
- 스택의 ADT
S.push(data) -> None
- 맨 위에 새로운 데이터를 삽입
S.pop() -> data
- 맨 위의 데이터를 삭제하면서 반환
S.empty() -> bool
- 스택이 비었으면 참, 비어 있지 않다면 거짓
S.peek() - > data
- 맨 위의 데이터를 반환하되 삭제하지 않음
- 구현
- 파이썬 리스트의 append와 pop을 사용하지만, 객체지향 관점에서 정의할 때 상속이 아닌 합성을 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Stack:
def __init__(self):
self.container = list()
def push(self, data):
self.container.append(data)
def pop(self):
return self.container.pop()
def empty(self):
if not self.container:
return True
else:
return False
def peek(self):
return self.container[-1]
큐
- 스택이 접시 쌓기라면 큐는 줄서기
- FIFO(First In, First Out) / 선입선출
- 큐의 ADT
Q.enqueue(data) -> None
- 마지막에 새로운 데이터를 삽입
Q.d(equeue) -> data
- 가장 먼저 들어온 데이터를 삭제하면서 반환
Q.empty() -> bool
- 큐가 비었으면 참, 비어 있지 않다면 거짓
Q.peek() - > data
- 가장 먼저 들어온 데이터를 반환하되 삭제하지 않음
- 구현
- 파이썬 리스트의 append와 pop을 사용하지만, 객체지향 관점에서 정의할 때 상속이 아닌 합성을 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Queue:
def __init__(self):
self.container = list()
def enqueue(self, data):
self.container.append(data)
def dequeue(self):
return self.container.pop(0)
def empty(self):
if not self.container:
return True
else:
return False
def peek(self):
return self.container[0]
This post is licensed under CC BY 4.0 by the author.