20210617 DataStructure 03 : 연결 리스트(Linked List), 이중 연결 리스트(Double Linked List)
DataStructure 03
Linked List (연결 리스트)
Linked List 구조
- 연결 리스트라고도 함
 - 배열은 순차적으로 연속된 공간에 공간 개수를 예약을 해서 데이터를 나열하는 데이터 구조
    
- 이러한 배열의 단점을 보완한 데이터 구조가 연결 리스트 임
 
 - 연결 리스트는 떨어진 곳에 존재하는 데이터를 화살표로 연결해서 관리하는 데이터 구조
    
- 연결 리스트의 경우 공간이 연속되지 않고, 공간을 미리 예약을 하지 않고 필요할때 마다 공간을 추가 할수 있음(무한정 데이터를 저장 할 수 있음)
 - C언어에서는 주요한 데이터 구조인데, 파이썬은 list type이 연결 리스트 기능을 모두 지원함
 
 
- 배열은 1개의 공간에 1개의 데이터를 관리하지만, 연결리스트의 경우 배열과 달리 특정한 데이터를 저장하는 경우, 해당 데이터 공간 + 뒤에 나올 데이터의 공간을 가르키는 주소값을 가지는 공간(
포인터, Pointer)을 세트로 하여 1개의 데이터(노드(Node))로 관리함 - 용어
    
포인터(Pointer): 각 노드 안에서 이전 또는 이후 노드의 연결 정보(위치한 곳)인 주소값을 가지는 공간노드(Node): 데이터값 + 포인터 를 1 세트로 하는 데이터 저장 단위
 
- 맨 앞에 있는 노드의 Pointer(주소)만 알면, 연결 리스트의 모든 데이터에 접근할 수 있음
 - 마지막에 노드에 pointer가 존재하지 않으면, 그 노드가 끝임
 
배열 vs 연결 리스트
| 항목 | 배열 | 연결 리스트 | 
|---|---|---|
| 공간 연속 | O | X | 
| 공간 예약 | O | X | 
| 공간 추가 | X | O | 
| 단위 | 1공간 1데이터 | 2공간 1노드 (데이터 + 포인터) | 
| 공간효율 | 좋음 | 좋지 않음 | 
| 접근속도 | 빠름 | 느림 | 
| 중간 데이터 수정 | 부가작업 X | 연결 작업 필요 | 
연결 리스트의 장단점
- 장점
    
- 데이터 공간을 미리 할당하지 않아도 됨
        
- 배열은 미리 데이터 공간을 할당 해야 함
 
 
 - 데이터 공간을 미리 할당하지 않아도 됨
        
 - 단점
    
- 연결을 위한 별도 데이터 공간이 필요하므로, 저장공간 효율이 높지 않음
 - 연결 정보를 찾는 시간이 필요하므로 접근 속도가 느림
 - 중간 데이터 삭제시, 앞뒤 데이터의 연결을 재구성해야 하는 부가적인 작업필요
 
 
간단한 연결 리스트 구현하기
- Python으로 연결리스트 구현시, python 클래스를 활용하여 객체지향(OOP) 문법 이해 필요
 - 두 개의 데이터(값 + 주소)를 저장해야 하기 때문에 클래스를 사용
 
연결 리스트 기본 구현
# Node class 정의
# data, next를 인자로 받음 (next 없으면 기본값 None)
class Node:
  def __init__(self, data, next=None):
    self.data = data
    self.next = next
# 데이터 추가
node1 = Node(1) # Node(1, None)
node2 = Node(2) # Node(2, None)
# 포인터 연결
node1.next = node2 # node1 = Node(1, Node(2, None))
# 가장 앞의 노드 주소 명시 (head)
head = node1 # head = node1 = Node(1, Node(2, None))
데이터 추가하면서 포인터 연결하는 함수 만들기
- 객체의 property 가 객체를 값으로 하는 컨셉
 - head의 기준 값이 있어야 시작 할 수 있고, head의 경우 이미 어떤 값을 넣은 객체이고, 아직 pointer가 없는 상태인 Node(Value, None) 임
 - 그리고, 데이터를 추가하는 로직의 경우 head가 pointer 의 값이 pointer가 없지만 원하는 값을 가진 객체인 
Node(value, None)형태를 가지게 해야 한다. - 기준 Node를 head로 지정해서 함수에서 head를 계속 변경해서 가져오고 변경해서 가져오는 로직이여야 함
 
class Node:
  def __init__(self, data, next=None):
    self.data = data
    self.next = next
def add(data):
  # 외부 변수 head값이 add에 의해서 변함
  node = head
  # next가 있으면 next를 가져와서 그 next의 next를 확인해서 next가 없을 때 까지 돌림
  while node.next:
    node = node.next
  # next가 없게 되면, next에 해당 값의 Node 객체를 넣음 (그 객체는 Next는 none 이고)
  node.next = Node(data)
node1 = Node(1) # node1 = Node(1 , None)
head = node1 # head = Node(1, None)
add(2) # -> head = Node(1, Node(2, None))
add(3) # -> head = Node(1, Node(2, Node(3, None)))
add(4) # -> head = Node(1, Node(2, Node(3, Node(4, None))))
add(5) # -> head = Node(1, Node(2, Node(3, Node(4, Node(5, None)))))
# 객체를 타고 값이 연결되는 것!
# 여러 값 추가하기
for i in range(6, 10):
  add(i)
연결 리스트 데이터 출력하기 (검색하기)
# Node 객체 겉에서 부터 안쪽으로 파고듬
node = head
while node.next:
  print(node.data)
  node = node.next
print(node.data) # 마지막 next가 None인 값의 data 출력을 위함
연결 리스트의 복잡합 기능 01: 중간에 데이터를 추가하는 경우
- 연결 리스트는 유지 관리에 부가적인 구현이 필요함
    
- 새로운 C노드를 A노드(앞)와 B노드(뒤) 사이에 넣어 연결하려고 하는 경우
 - A노드의 주소는 C노드를, C노드의 주소는 B노드를 가르키게 해야함
 - A노드 Pointer = C노드, C노드 Pointer = B노드
 Node('A' Node('B' , None))->Node('A' Node('C' Node('B' None)))
 
# Node Class 정의
class Node:
  def __init__(self, data, next=None):
    self.data = data
    self.next = next
# Add Funciton 정의
def add(data):
  node = head
  while node.next:
    node = node.next
  node.next = Node(data)
# data 추가
head = Node(1)
for i in range(2, 10):
  add(i)
# 1 2 3 4 5 6 7 8 9
# new Data 정하기
newNode = Node(1.5)
# ---- 데이터 1과 2사이에 1.5데이터를 넣고 싶음 ----
# 현재 상태의 Node 객체 가져오기
node = head
# 수정할 부분 찾기 (data가 1을 가지는 Node 찾기)
search = False
while not search:
  if node.data == 1:
    search = True # node = Node(1, Node(2, ... ))
  else:
    node = node.next
# 수정할 부분의 뒷부분 기억
node_next = node.next # node_next = Node(2, Node(3, ...))
# new Data 연결 작업
node.next = newNode # Node(1, Node(1.5, None))
newNode.next = node_next # Node(1, Node(1.5, Node(2, Node(3 , ...))))
# 값 확인하기
while node.next:
  print(node.data)
  node = node.next
print(node.data)
# 1 1.5 2 3 4 5 6 7 8 9
Python OOP으로 연결 리스트 구현하기 (시작, 추가, 보기)
# 연결 리스트 구현을 위한 재료인 Node Class
class Node:
  def __init__(self, data, next=None):
    self.data = data
    self.next = next
# Node 관리 Class (시작, 연결&값추가, 보기)
class NodeMgmt:
  def __init__(self, data):
    self.head = Node(data) # 시작시 head를 지정
  def add(self, data):
    if self.head == "": # head가 없으면 head를 만듦 (방어 코드)
      self.head = Node(data)
    else: # head가 있으면 받은 데이터를 head에 계속 연결 시킴
      node = head
      while node.next:
        node = node.next
      node.next = Node(data)
  def desc(self):
    node = self.head # 현재 상태의 head를 가져옴
    while node: # node.next가 None이 되어 node로 할당 될때 까지 해당 node 값을 print
      print(node.data)
      node = node.next
# 연결 리스트 생성
linked_list1 = NodeMgmt(0)
# 연결 리스트 보기
linked_list1.desc() # 0
# 연결 리스트 값 추가
for i in range(1, 10):
  linked_list1.add(i)
# 연결 리스트 보기
linked_list1.desc()
# 0 1 2 3 4 5 6 7 8 9
연결 리스트의 복잡합 기능 02: 특정 노드를 삭제하는 경우, 노드 찾기
- 
    
노드를 삭제하는 3가지 경우
- head의 값을 삭제 -> head를 head 다음 노드로 바꾸고, 기존의 head를 삭제
 - 마지막 노드의 값을 삭제 -> 마지막 노드 삭제, 그전 노드의 주소값 null 변경
 - 중간 노드의 값을 삭제 -> 중간 노드 삭제, 앞 노드의 주소값 과 뒷노드의 값을 연결
        
- 공통적으로 마지막, 중간 노드의 값 삭제의 경우 둘다, 삭제하는 노드의 pointer를 앞 노드 pointer에 붙이면 됨
 
 
 
- 값을 기준으로 해당 노드를 찾는 함수 만들기
    
- node를 돌면서, 해당 data가 맞으면 해당 node를 return 하면 됨
 
 
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 = head
      while node.next:
        node = node.next
      node.next = Node(data)
  def desc(self):
    node = self.head
    while node:
      print(node.data)
      node = node.next
  def delete(self, data):
    if self.head == "": # 방어 코드
      print("생성된 노드가 없습니다.")
      return
    if self.head.data == data: # head 삭제의 경우
      temp = self.head
      self.head = self.head.next
      del temp
    else: # 중간. 마지막 삭제의 경우
      node = self.head # Node(1, Node(2, Node(3, ...)))      # Node(1, Node(2, None))
      while node.next: # Node(2, Node(3, ...))               # Node(2, None)
        if node.next.data == data: # 2 == data (중간노드)     # 2 == data (끝 노드)
          temp = node.next # temp = Node(2, Node(3, ...))    # temp = Node(2, None)
          node.next = node.next.next # Node(1, Node(3, ...)) # Node(1, None)
          del temp # 제거
          return # 나가기
        else:
          node = node.next # 다음 노드 data 확인
    # 특정값의 노드 찾아 가져오기
    def search_node(self, data):
      node = self.head
      while node:
        if node.data == data:
          return node
        else:
          node = node.next
### 공통적으로 중간, 마지막 삭제의 경우 삭제하는 노드의 pointer를 앞 노드의 pointer에 연결하면 됨
linkedList1 = NodeMgmt(0)
linkedList1.head # <__main__.Node at 0x1099fc6a0> 즉, head 노드인 Node(0)을 표시
linkedList1.delete(0) # head 삭제
linkedList1.head # 아무것도 출력 되지 않음 -> 이때를 위해서 head 방어코드 만든 것
for i in range(1, 10): # 데이터 추가
  linkedList1.add(i)
linkedList1.desc() # 0 1 2 3 4 5 6 7 8 9
linkedList1.delete(4) # 중간 노드 삭제
linkedList1.desc() # 0 1 2 3 5 6 7 8 9
linkedList1.delete(9) # 끝 노드 삭제
linkedList1.desc() # 0 1 2 3 5 6 7 8
search_node3 = linkedList1.search_node(3) # 노드 찾기
print(search_node3.data) # 3
다양한 연결 리스트 구조 : Doubly linked list(이중 연결 리스트)
이중 연결 리스트 기본 구조
- 일반 연결 리스트의 경우 무수히 많은 데이터 중에서 엄청 뒤에 있는 데이터를 찾고자 한다면, head에서 부터 찾아야 해서 시간이 오래 걸리게 됨
 - 그래서 원하는 데이터가 앞에서 가까운지, 뒤에서 가까운지만 대략적으로 안다면 시간을 조금 더 절약 할 수 있음
 - 그래서, 이중 연결 리스트를 활용함
 - 하나의 노드가 앞 노드, 뒷 노드의 주소 모두를 가지고 있는 형태 -> Pointer가 2개
 
class Node:
  def __init__(self, data, prev=None, next=None):
    self.data = data
    self.prev = prev
    self.next = next
class NodeMgmt:
  def __init__(self, data): # head -> Node(1 ,None, None) <- tail
    self.head = Node(data) # head는 다음 노드를 가르키고
    self.tail = self.head # tail은 이전 노드를 가르킴
  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 # 결국엔 node가 tail 전 노드로 마지막 노드를 가르킴
      new = Node(data) # 새로운 노드를 new에 기억함
      node.next = new # 해당 노드의 next에 새로운 노드를 가르키게 함
      new.prev = node # 새로운 노드의 prev의 기준 노드를 가르키게 함
      self.tail = new # tail 새로운 노드를 가르키게 함
  # prev, next는 노드 전체를 가르킨다! 다음 또는 전 노드의 prev, next가 아니라
  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.tail == None: # 방어 코드
      return False
    node = self.tail
    while node:
      if node.data == data:
        return node
      else:
        node = node.prev
    return False
# 첫번째 매개변수가 새로운 Node 만들때 쓰는 값
# 두번째 매개변수가 노드 찾을 때 쓰는 값
  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
      if before_new != None: # 제일 첫번째 노드를 기준으로 추가하는 경우
        before_new.next = new
        new.prev = before_new
      else:
        self.head = new
        new.prev = self.head
      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
      new.next = after_new
      new.prev = node
      node.next = new
      if new.next == None: # 마지막 노드를 기준으로 추가하는 경우
        self.tail = new
      return True
# 반복문을 사용할 때는 해당 노드를 찾는 용도로 사용하고, 수정하는 작업은 반복문 나와서 다음에 작업 하는 걸로 해야 변함
Test
double_linked_list = NodeMgmt(0)
for data in range(1, 10):
  double_linked_list.insert(data)
double_linked_list.desc() # 1 2 3 4 5 6 7 8 9
search3_from_head = double_linked_list.search_from_head(3)
print(search3_from_head) # 3
search7_from_tail = double_linked_list.search_from_tail(7)
print(search7_from_tail) # 7
double_linked.insert_before("insertBefore0", 0)
double_linked.desc()
# insertBefore0 0 1 2 3 4 5 6 7 8 9
double_linked.insert_after("insertAfter9", 9)
double_linked.desc()
# insertBefore0 0 1 2 3 4 5 6 7 8 9 insertAfter9