이중 연결 목록: C++, Python (코드 예)

이중 연결 목록이란 무엇입니까?

이중 연결 리스트에서는 각 노드가 이전 노드와 다음 노드에 대한 링크를 모두 갖습니다. 각 노드는 세 가지 요소로 구성됩니다. 하나는 데이터를 보유하고 다른 두 개는 다음 및 이전 노드의 포인터입니다. 이 두 포인터는 특정 노드에서 앞으로 또는 뒤로 이동하는 데 도움이 됩니다.

이중 연결 리스트의 기본 구조는 다음과 같습니다.

이중 연결 목록의 구조
이중 연결 리스트의 구조

모든 연결리스트에는 헤드 노드와 테일 노드가 있습니다. 헤드 노드에는 "이전"(이전 포인터) 노드가 없고 테일 노드에는 "다음" 노드가 없습니다.

이중 연결 목록에 대한 몇 가지 중요한 용어는 다음과 같습니다.

  • 이전 : 각 노드는 이전 노드에 연결됩니다. 포인터나 링크로 사용됩니다.
  • 다음 : 각 노드는 다음 노드에 연결됩니다. 포인터나 링크로 사용됩니다.
  • 날짜 : 이는 노드에 데이터를 저장하는 데 사용됩니다. "데이터"는 다른 데이터를 보유할 수 있습니다. 데이터 구조 그 안에. 예를 들어 문자열, 사전, 집합, 해시맵 등이 "데이터"에 저장될 수 있습니다.

이중 연결 리스트의 단일 노드의 기본 구조는 다음과 같습니다.

이중 연결 목록의 노드 구조

이중 연결 목록의 노드 구조

Opera이중 연결 목록의 종류

이중 연결 리스트의 연산에는 노드 추가 및 삭제, 노드 삽입 및 제거, 연결 리스트를 위에서 아래로 탐색하는 작업이 포함됩니다.

이중 연결 리스트를 사용하여 구현할 수 있는 연산 목록은 다음과 같습니다.

  • 앞에 삽입
  • 꼬리 또는 마지막 노드에 삽입
  • 노드 뒤에 삽입
  • 노드 앞에 삽입
  • 앞에서 삭제
  • 꼬리에서 삭제
  • 노드 검색 및 삭제
  • 머리에서 꼬리까지 횡단
  • 꼬리에서 머리로 가로지르기

위의 모든 연산에 대한 구현과 의사코드를 살펴보겠습니다.

이중 연결 목록 앞에 삽입

앞에 삽입한다는 것은 연결된 목록에 노드를 만들고 연결 목록의 시작 부분에 배치해야 함을 의미합니다.

예를 들어, 주어진 노드 "15"가 있습니다. 이것을 헤드 노드에 추가해야 합니다.

이 작업을 수행할 때 중요한 조건은 두 가지입니다.

  1. 이중 연결 목록에 노드가 없으면 새 노드가 헤드 노드가 됩니다.
  2. 이미 헤드 노드가 있는 경우 이전 헤드가 새 노드로 대체됩니다.

이 작업에 대한 의사 코드는 다음과 같습니다.

function insertAtFront (ListHead, value):
  newNode = Node()
  newNode.value = value
  ListHead.prev = NewNode
  NewNode.next = ListHead
  newNode.prev = NULL
  return ListHead
전면 노드에 삽입
프론트 노드에 삽입

이중 연결 목록 끝에 삽입

"끝에 삽입"은 연결 목록에 노드를 만들고 끝에 배치한다는 의미입니다.

이를 수행하기 위해 다음 두 가지 방법을 사용할 수 있습니다.

  • 방법 1 : "다음"이 null이 될 때까지 이중 연결 목록의 선두부터 탐색을 시작합니다. 그런 다음 새 노드를 "다음"과 연결합니다.
  • 방법 2 : 이중 연결 목록의 마지막 노드를 가져옵니다. 그런 다음 마지막 노드의 "다음" 노드가 새 노드에 연결됩니다. 이제 새 노드가 꼬리 노드가 됩니다.

꼬리 노드에 삽입하기 위한 의사 코드는 다음과 같습니다.

function insertAtTail(ListHead, value):
  newNode = Node()
  newNode.value = value
  newNode.next = NULL
  while ListHead.next is not NULL:
	then ListHead = ListHead.next
  newNode.prev = ListHead
  ListHead.next = newNode
  return ListHead
Linked List 끝에 삽입

연결리스트 끝에 삽입

노드 뒤에 삽입

다음과 같은 이중 연결 리스트가 있다고 가정해 보겠습니다.

노드 뒤에 삽입

값이 "12"인 노드 뒤에 연결될 특정 노드를 삽입하려고 합니다.

이에 대한 단계는 다음과 같습니다.

단계 1) 헤드에서 마지막 노드까지 트래버스합니다. 어떤 노드의 값이 "12"인지 확인하십시오.

단계 2) 새 노드를 생성하고 이를 노드 "12"의 다음 포인터로 할당합니다. 새 노드의 "다음" 노드는 15가 됩니다.

이중 연결 리스트의 노드 뒤에 노드를 삽입하는 의사 코드는 다음과 같습니다.

function insertAfter(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.value is not equal searchItem
	then List = ListHead.next
  List = List.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
노드 뒤에 삽입

노드 뒤에 삽입

노드 앞에 삽입

이 작업은 "노드 뒤에 삽입"과 더 비슷합니다. 이를 수행하려면 특정 노드의 값을 검색해야 합니다. 그런 다음 새 노드를 생성하여 검색된 노드 앞에 삽입합니다.

노드 "15" 앞에 주어진 노드 "12"를 삽입하고 싶다고 가정해 보겠습니다. 그런 다음 이를 수행하는 단계는 다음과 같습니다.

단계 1) 헤드 노드에서 테일 노드까지 연결 리스트를 탐색합니다.

단계 2) 현재 노드의 다음 포인터의 값이 "12"인지 확인합니다.

단계 3) 새 노드를 현재 노드의 "다음" 노드로 삽입합니다.

이중 연결 목록의 노드 앞에 노드를 삽입하는 의사 코드는 다음과 같습니다.

function insertBefore(ListHead, searchItem, value):
  List = ListHead
  NewNode = Node()
  NewNode.value = value
  while List.next.value is not equal searchItem
	then List = ListHead.next
  NewNode.next = List.next
  NewNode.prev = List
  List.next = NewNode
노드 앞에 노드 삽입

노드 앞에 노드 삽입

이중 연결 리스트의 선두 삭제

이전 노드가 없는 이중 연결 리스트의 헤드 노드입니다. 따라서 헤드 노드를 삭제하려는 경우 다음 포인터는 새 헤드 노드가 됩니다. 또한 노드를 삭제하는 동안 노드가 차지하는 메모리를 해제해야 합니다.

헤드 노드를 삭제하는 단계는 다음과 같습니다.

단계 1) 현재 헤드 노드에 변수를 할당합니다.

단계 2) 현재 헤드 노드의 "다음" 노드를 방문하고 "이전"(이전 포인터) 노드를 ''NULL'로 만듭니다. 이는 첫 번째 노드에서 두 번째 노드의 연결을 끊는다는 의미입니다.

단계 3) 이전 헤드 노드가 점유한 메모리를 해제합니다.

이중 연결 리스트에서 헤드를 삭제하는 의사 코드는 다음과 같습니다.

function deleteHead(ListHead):
  PrevHead = ListHead
  ListHead = ListHead.next
  ListHead.prev = NULL
  PrevHead.next = NULL
  free memory(PrevHead)
  return ListHead
헤드 노드 삭제

헤드 노드 삭제

모든 종류의 삭제 작업을 수행한 후에는 할당된 메모리를 삭제해야 합니다. 그렇지 않으면 프로그램의 전체 런타임 동안 삭제된 블록의 메모리가 점유된 상태로 유지됩니다. 다른 애플리케이션은 해당 메모리 세그먼트를 사용할 수 없습니다.

이중 연결 리스트의 꼬리를 삭제합니다.

이 작업은 헤드 삭제와 비슷합니다. 여기서는 헤드 대신 테일을 삭제해야 합니다. 노드를 테일 노드로 식별하려면 다음 포인터 또는 다음 노드가 null인지 확인해야 합니다. 테일을 삭제한 후에는 메모리를 해제해야 합니다.

이 작업은 "뒤에서 삭제"라고도 불립니다.

이를 수행하는 단계는 다음과 같습니다.

단계 1) 이중 연결 리스트의 꼬리 노드까지 순회합니다.

단계 2) 꼬리 노드에 변수나 포인터를 할당합니다.

단계 3) "다음" 노드를 NULL로 만들고 꼬리 노드의 메모리를 해제합니다.

꼬리 노드를 삭제하기 위한 의사 코드는 다음과 같습니다.

function DeleteTail( ListHead ):
  head = ListHead
  while ListHead.next is not NULL:
	ListHead = ListHead.next
  Tail = ListHead.next
  ListHead.next = NULL
  free memory( Tail )
  return head

이중 연결의 꼬리 삭제

이중 연결 목록에서 노드 검색 및 삭제

이 연산을 통해 특정 노드 데이터를 검색하고 해당 노드를 삭제할 수 있습니다. 연결 리스트는 선형 데이터 구조이므로 선형 검색을 수행해야 합니다. 삭제한 후에는 메모리를 해제해야 합니다.

이중 연결 리스트에서 노드를 검색하고 삭제하는 단계는 다음과 같습니다.

단계 1) 노드의 값이 검색 항목과 같아질 때까지 헤드에서 연결된 목록을 탐색합니다.

단계 2) 일치하는 노드에 변수 “deleteNode”를 할당합니다.

단계 3) "deleteNode"의 이전 노드를 다음 노드에 할당합니다.

단계 4) "deleteNode"의 메모리를 해제합니다.

연결 리스트에서 노드를 검색하고 삭제하기 위한 의사코드는 다음과 같습니다.

function SearchAndDelete(ListHead, searchItem):
  head = ListHead
  while ListHead.next.value not equals searchItem:
	head = head.next
  deleteNode = head.next
  head.next = head.next.next
  head.next.prev = head
  deleteNode.next, deleteNode.next = NULL
  free memory(deleteNode)
  return ListHead
검색 및 삭제 Opera기

검색 및 삭제 작업

이중 연결 리스트를 앞으로부터 순회

헤드 노드에서 순회하고 "NULL"을 찾을 때까지 다음 노드를 반복합니다. 각 노드를 순회하는 동안 노드의 값을 인쇄할 수 있습니다. 다음은 전면(전방향)에서 횡단하는 단계입니다.

단계 1) 현재 헤드 노드에 포인터나 변수를 할당합니다.

단계 2) "NULL"이 나올 때까지 헤드의 다음 노드를 반복합니다.

단계 3) 각 반복마다 노드의 데이터를 인쇄합니다.

단계 4) 헤드 노드를 반환합니다.

이중 연결 목록을 앞에서 순회하는 의사 코드는 다음과 같습니다.

function traverseFromFront(ListHead):
  head = ListHead
  while head not equals to NULL:
	print head.data
	head = head.next
  return ListHead

여기서 반환은 필수는 아닙니다. 하지만 작업 후 헤드 노드를 반환하는 것이 좋은 관행입니다.

이중 연결 리스트를 뒤에서부터 순회

이 작업은 전면에서 횡단하는 것과 반대입니다. 접근 방식은 약간 다르지만 동일합니다. 끝 노드까지 횡단한 다음 끝 노드에서 전면 노드까지 횡단해야 합니다.

이중 연결 리스트를 뒤에서 순회하는 단계는 다음과 같습니다.

단계 1) 꼬리 노드에 도달할 때까지 횡단합니다.

단계 2) 꼬리 노드에서 이전 노드가 "NULL"이 될 때까지 순회합니다. "prev"(이전 포인터)는 헤드 노드에 대해 null입니다.

단계 3) 각 반복마다 노드 데이터를 인쇄합니다.

뒤에서 탐색하기 위한 의사 코드는 다음과 같습니다.

function traverseFromBack(ListHead):
  head = ListHead
  while head not equals NULL:
	head = head.next
  tail = head
  while tail not equal to NULL:
	print tail.value
	tail = tail.prev
  return ListHead

단일 연결 목록과 이중 연결 목록의 차이점

단일 연결 목록과 이중 연결 목록의 주요 차이점은 링크 수입니다.

단일 연결 목록과 이중 연결 목록의 차이점

단일 연결 목록의 노드와 이중 연결 목록의 노드 구조 간의 차이점은 다음과 같습니다.

분야 단일 연결 목록 이중 연결 목록
Structure 단일 연결 목록 하나의 데이터 필드와 다음 노드에 대한 하나의 링크가 있습니다. 이중 연결 목록에는 하나의 데이터 필드와 두 개의 링크가 있습니다. 하나는 이전 노드용이고 다른 하나는 다음 노드용입니다.
순회 머리에서 꼬리까지만 횡단할 수 있습니다. 앞뒤로 모두 이동할 수 있습니다.
메모리 메모리를 덜 차지합니다. 단일 연결 리스트보다 더 많은 메모리를 차지합니다.
접근 용이성 단일 연결 목록은 다음 노드에 대한 링크를 하나만 사용하므로 효율성이 떨어집니다. 이전 노드에 대한 링크가 없습니다. 이중 연결 목록은 단일 연결 목록보다 효율적입니다.

이중 연결리스트 C++

#include<iostream>
using namespace std;
  struct node{
  int data;
  struct node *next;
  struct node *prev;
  };
void insertFront(node* &listHead, int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  if(listHead!=NULL)
  {
	listHead->prev = newNode;
  	newNode->next = listHead;
  }
  
  listHead = newNode;
  cout<<"Added "<<value<<" at the front"<<endl;
  }
void insertEnd(node * &listHead, int value){
  if(listHead==NULL)
  {
  	insertFront(listHead,value);
  }
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;
  newNode->next = NULL;
  node *head = listHead;
  while(head->next!=NULL){
  head = head->next;
  }
  head->next = newNode;
  newNode->prev = head;
  cout<<"Added "<<value<<" at the end"<<endl;
  }
void insertAfter(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;
  node *head = listHead;	
  while(head->next!=NULL &&  head->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" after node "<<searchValue<<endl;
  }
void insertBefore(node * &listHead, int searchValue,int value){
  node* newNode = new node();
  newNode->data = value;
  newNode->prev = NULL;	
  newNode->next = NULL;	
  node *head = listHead;	
  while(head->next!=NULL &&  head->next->data!=searchValue){	
  head = head->next;	
  }	
  newNode->next = head->next;	
  head->next = newNode;	
  newNode->prev = head;	
  if(newNode->next !=NULL){	
  newNode->next->prev = newNode;	
  }	
  cout<<"Inserted "<<value<<" before node "<<searchValue<<endl;	
  }
void traverseFromFront(node *listHead){
  node* head = new node();
  head = listHead;
  cout<<"Traversal from head:\t";	
  while(head!=NULL){	
  cout<<head->data<<"\t ";	
  head = head->next;	
  }	
  cout<<endl;
  }	
void traverseFromEnd(node *listHead){
  node* head = new node();
  head = listHead;	
  cout<<"Traversal from head:\t";	
  while(head->next!=NULL){	
  head = head->next;	
  }	
  node *tail = head;	
  while(tail!=NULL){	
  cout<<tail->data<<"\t";	
  tail = tail->prev;	
  }	
  cout<<endl;	
  }
void searchAndDelete(node **listHead, int searchItem){
  node* head = new node();
  head = (*listHead);
  while(head->next!=NULL &&  head->data!=searchItem){
  head = head->next;
  }
  if(*listHead == NULL || head == NULL) return;
  if((*listHead)->data == head->data){
  	*listHead = head->next;
  }
  if(head->next != NULL){
  	head->next->prev = head->prev;
  }
  
  if(head->prev != NULL){
  	head->prev->next = head->next;
  }
  free(head);
  cout<<"Deleted Node\t"<<searchItem<<endl;
  return;
  }
int main(){
  node *head = NULL;
  insertFront(head,5);
  insertFront(head,6);
  insertFront(head,7);
  insertEnd(head,9);
  insertEnd(head,10);
  insertAfter(head,5,11);
  insertBefore(head,5,20);
  traverseFromFront(head);
  traverseFromEnd(head);
  searchAndDelete(&head,7);
  traverseFromFront(head);
  traverseFromEnd(head);
}

산출

Added 5 at the front
Added 6 at the front
Added 7 at the front
Aded 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversal from head:    7        6       20      5       11      9       10
Traversal from head:    10      9       11      5       20      6       7
Deleted Node    7
Traversal from head:    6        20      5       11      9       10
Traversal from head:    10      9       11      5       20      6

이중 연결리스트 Python

class node:
  def __init__(self,data = None, prev=None, next = None):
    self.data = data
    self.next = next
    self.prev = prev
class DoublyLinkedList:
  def __init__(self):
    self.head = None

  def insertFront(self, val):
    newNode = node(data=val)
    newNode.next = self.head
    if self.head is not None:
      self.head.prev = newNode
    self.head = newNode
    print("Added {} at the front".format(val))

  def insertEnd(self,val):
    newNode = node(data=val)
    if self.head is None:
      self.insertFront(val)
    
    temp = self.head
    while temp.next is not None:
      temp = temp.next
    temp.next = newNode
    newNode.prev = temp
    print("Added {} at the end".format(val))

  def traverseFromFront(self):
    temp = self.head
    print("Traversing from head:\t",end="")
    while temp:
      print("{}\t".format(temp.data),end="")
      temp = temp.next
    print()

  def traverseFromEnd(self):
    temp = self.head
    print("Traversing from Tail:\t",end="")
    while temp.next is not None:
      temp = temp.next
    tail = temp
    while tail is not None:
      print("{}\t".format(tail.data),end="")
      tail = tail.prev
    print()
  
  def insertAfter(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} after node {}".format(value,searchItem))

  def insertBefore(self,searchItem, value):
    newNode = node(data=value)

    temp = self.head
    while temp.next is not None and temp.next.data is not searchItem:
      temp = temp.next
    
    newNode.next = temp.next
    temp.next = newNode
    newNode.prev = temp

    if newNode.next is not None:
      newNode.next.prev = newNode
    print("Inserted {} before node {}".format(value,searchItem))

  def searchAndDelete(self,searchItem):
    temp = self.head

    while temp.next is not None and temp.data is not searchItem:
      temp = temp.next
    
    if self.head is None or temp is None:
      return

    if self.head.data is temp.data:
      self.head = temp.next

    if temp.next is not None:
      temp.next.prev = temp.prev
    
    if temp.prev is not None:
      temp.prev.next = temp.next
    
    print("Deleted Node\t{}".format(searchItem))
    return

doublyLinkedList = DoublyLinkedList()
doublyLinkedList.insertFront(5)
doublyLinkedList.insertFront(6)
doublyLinkedList.insertFront(7)
doublyLinkedList.insertEnd(9)
doublyLinkedList.insertEnd(10)
doublyLinkedList.insertAfter(5, 11)
doublyLinkedList.insertBefore(5, 20)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()
doublyLinkedList.searchAndDelete(7)
doublyLinkedList.traverseFromFront()
doublyLinkedList.traverseFromEnd()

산출

Added 5 at the front
Added 6 at the front
Added 7 at the front
Added 9 at the end
Added 10 at the end
Inserted 11 after node 5
Inserted 20 before node 5
Traversing from head:   7       6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6       7
Deleted Node    7
Traversing from head:   6       20      5       11      9       10
Traversing from Tail:   10      9       11      5       20      6

이중 연결 리스트의 복잡성

일반적으로 시간 복잡도는 세 가지 유형으로 나뉜다.

그들은 :

  1. 최상의 사례
  2. 평균 케이스
  3. 최악의 경우

이중 연결 리스트의 최상의 경우 시간 복잡도:

  1. head 또는 tail에 삽입하면 O(1)이 듭니다. 연결 리스트 내부를 탐색할 필요가 없기 때문입니다. head와 tail 포인터를 통해 O(1) 시간 복잡도로 head와 tail 노드에 접근할 수 있습니다.
  2. 머리 또는 꼬리 부분을 삭제하면 O(1)의 비용이 발생합니다.
  3. 노드 검색은 O(1)의 시간 복잡도를 갖습니다. 왜냐하면 대상 노드가 헤드 노드가 될 수 있기 때문입니다.

이중 연결 리스트의 평균적 경우의 시간 복잡도:

  1. 머리 또는 꼬리에 삽입하면 비용 O(1)의 시간 복잡도가 발생합니다.
  2. 머리 또는 꼬리 부분에서 삭제하면 비용 O(1)의 시간 복잡도가 발생합니다.
  3. 노드를 검색하는 데는 O(n)의 시간 복잡도가 있습니다. 검색 항목은 연결 리스트 사이의 어디에나 있을 수 있기 때문입니다. 여기서 "n"은 연결 리스트에 있는 전체 노드입니다.

이중 연결 리스트의 최악의 경우 시간 복잡도는 평균적인 경우와 동일합니다.

이중 연결 리스트의 메모리 복잡도

메모리 복잡도는 O(n)이며, 여기서 "n"은 노드의 총 개수입니다. 연결 리스트를 구현하는 동안 사용한 메모리를 해제해야 합니다. 그렇지 않으면 더 큰 연결 리스트의 경우 메모리 누수가 발생합니다.

요약

  • 연결된 목록은 일종의 선형 데이터 구조로, 선형 방식으로 표현되는 데이터 모음입니다.
  • 이중 연결 리스트(Double Linked List)는 한 노드가 이전 노드와 다음 노드 모두와 연결되어 있는 일종의 연결 리스트입니다.
  • 이중 연결 리스트에는 노드 추가, 노드 삭제, 노드 뒤나 앞에 노드 삽입, 연결 리스트를 머리부터 꼬리까지 탐색하는 등의 모든 작업이 포함되어 있습니다.
  • 이중 연결 목록에는 하나의 데이터 필드와 두 개의 링크가 있습니다. 하나는 이전 노드용이고 다른 하나는 다음 노드용입니다.
  • 이중 연결 리스트의 복잡성 최상의 경우, 평균적 경우
  • 그리고 최악의 경우.