원형 연결 리스트는 마지막 노드가 첫 번째 노드를 가리키고 닫힌 원을 형성하는 데이터를 구성하고 저장하는 방법입니다. 삽입 및 삭제와 같은 작업을 실행할 수 있기 때문에 다양한 애플리케이션에서 메모리를 관리하는 것과 같은 작업에 효과적입니다. 원형 연결 리스트는 상수 시간 삽입 및 삭제(O(1))를 허용하고 null 참조를 처리하지 않고도 효율적인 반복을 가능하게 하여 코드를 최적화합니다. 순환 데이터 처리가 필요한 실시간 시스템, 게임 및 네트워킹과 같은 애플리케이션에서 특히 유용합니다. 이 블로그에서는 원형 연결 리스트, 유형, 수행되는 작업 및 애플리케이션에 대해 알아봅니다.
이 매혹적인 YouTube 동영상으로 C 프로그래밍의 세계를 탐험해 보세요. 포괄적인 학습 경험을 위한 관문입니다!
원형 연결 리스트란 무엇인가요?
원형 연결 리스트는 마지막 노드가 첫 번째 노드에 다시 연결되어 원을 형성하는 일반 연결 리스트와 같습니다. 마지막 링크가 첫 번째 링크에 연결되어 연속 루프를 만드는 체인을 상상해 보세요. 이 구조를 사용하면 리스트의 모든 지점에서 데이터 요소를 살펴볼 수 있습니다.
위의 이미지는 4개의 노드가 있는 원형 연결 리스트를 나타냅니다. 이 이미지에서 첫 번째 노드는 두 가지 구성 요소로 구성됩니다. 하나는 데이터를 보유하고 다른 하나는 다음 노드를 가리키는 주소(포인터/ptr)를 포함합니다. 리스트의 첫 번째 노드는 일반적으로 “헤드”라고 합니다. 첫 번째 노드가 삭제되면 두 번째 노드(이제 첫 번째 노드)가 “헤드” 역할을 합니다.
목록의 노드는 서로 연결되어 있습니다. 예를 들어, 노드 1은 노드 2에 연결되고, 노드 2는 노드 3에 연결됩니다. 마지막 노드(이 경우 노드 4)는 초기 노드에 다시 연결되어 원형 연결 목록을 만듭니다.
통사론:
구조체 노드 {
int 데이터;
구조체 노드 *next;
};
C 프로그래밍에 대해 더 알고 싶다면 이것을 살펴보세요. C 프로그래밍 인증 과정!
원형 연결 리스트의 종류
원형 연결 리스트는 두 가지 유형으로 구성됩니다.
- 원형 단일 연결 리스트
- 원형 이중 연결 리스트
원형 단일 연결 리스트
순환 단일 연결 리스트는 단일 연결 리스트의 변형으로 리스트의 마지막 노드가 첫 번째 노드를 가리키며 순환 구조를 형성합니다. 여기에는 두 가지 구성 요소가 있습니다.
- 데이터: 노드가 저장하고 있는 실제 데이터나 값을 보관합니다.
- 다음 포인터: 이는 시퀀스의 다음 노드를 가리킵니다.
위의 이미지에서 첫 번째 노드는 두 번째 노드를 가리키고, 두 번째 노드는 세 번째 노드를 가리키고, 마지막 노드는 첫 번째 노드를 다시 가리키고 있어 원을 형성합니다. 다음 노드로 이동하면 이전 노드로 돌아갈 수 없습니다.
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; } } class CircularSinglyLinkedList { Node head; public CircularSinglyLinkedList() { this.head = null; } public void append(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; head.next = head; } else { Node current = head; while (current.next != head) { current = current.next; } current.next = newNode; newNode.next = head; } } public void printList() { if (head == null) { System.out.println("List is empty."); return; } Node current = head; do { System.out.print(current.data + " -> "); current = current.next; } while (current != head); System.out.println("HEAD"); } public static void main(String[] args) { CircularSinglyLinkedList cSLL = new CircularSinglyLinkedList(); cSLL.append(1); cSLL.append(2); cSLL.append(3); cSLL.printList(); } }
원형 이중 연결 리스트
원형 이중 연결 리스트는 각 노드가 다음 노드와 이전 노드에 대한 참조를 갖는 리스트입니다. 이중 연결이라는 것 외에도 마지막 노드의 포인터는 첫 번째 노드를 가리키고 첫 번째 노드의 포인터는 마지막 노드를 가리키며 원형 구조를 형성합니다.
이는 세 가지 구성요소로 구성되어 있습니다.
- 데이터: 노드가 저장하고 있는 실제 데이터 또는 값을 보유합니다.
- 다음 포인터: 시퀀스의 다음 노드를 가리킴
- 이전 포인터: 시퀀스의 이전 노드를 가리킴
위의 이미지에서 첫 번째 노드는 다음 노드를 가리키고 마지막 노드를 향해 뒤로 가리키고 있습니다. 마찬가지로 두 번째 노드는 세 번째 노드를 가리키고 이전 노드를 향해 뒤로 가리키고 있습니다. 이는 각 노드가 다음 노드와 이전 노드에 모두 연결되어 있음을 보여줍니다.
class Node { int data; Node next; Node prev; public Node(int data) { this.data = data; this.next = null; this.prev = null; } } class CircularDoublyLinkedList { Node head; public CircularDoublyLinkedList() { this.head = null; } public void append(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; head.next = head; head.prev = head; } else { Node tail = head.prev; tail.next = newNode; newNode.prev = tail; newNode.next = head; head.prev = newNode; } } public void printList() { if (head == null) { System.out.println("List is empty."); return; } Node current = head; do { System.out.print(current.data + " <-> "); current = current.next; } while (current != head); System.out.println("HEAD"); } public static void main(String[] args) { CircularDoublyLinkedList cDLL = new CircularDoublyLinkedList(); cDLL.append(1); cDLL.append(2); cDLL.append(3); cDLL.printList(); } }
원형 연결 리스트의 연산
원형 연결 리스트에서 수행할 수 있는 세 가지 주요 작업은 다음과 같습니다.
횡단
원형 연결 리스트를 탐색하려면 다음 단계를 따라야 합니다.
포인터를 초기화합니다: 원형 연결 리스트의 초기 노드(헤드)로 시작합니다. 원하는 조건이 충족될 때까지 루프합니다. 끝에 도달하거나 특정 조건을 충족할 때까지(예: 특정 노드에 도달할 때까지) 리스트를 계속 탐색합니다.
포인터 업데이트: 노드를 방문한 후 포인터를 업데이트하여 목록의 다음 노드로 이동합니다. 원형 목록이므로 포인터가 올바르게 래핑되어 끝에 도달한 후 다시 처음부터 시작하도록 합니다.
다음은 원형 연결 리스트를 탐색하는 방법을 보여주는 간단한 Python 예제입니다.
class Node: def __init__(self, data=None): self.data = data self.next = None class CircularLinkedList: def __init__(self): self.head = None # Method to add nodes at the end of the list def append(self, data): if not self.head: self.head = Node(data) self.head.next = self.head else: new_node = Node(data) cur = self.head while cur.next != self.head: cur = cur.next cur.next = new_node new_node.next = self.head # Method to print the list def display(self): elements = [] cur_node = self.head while True: elements.append(cur_node.data) cur_node = cur_node.next if cur_node == self.head: break return elements # Example usage cll = CircularLinkedList() cll.append('A') cll.append('B') cll.append('C') print("Circular Linked List:", cll.display())
삽입
원형 연결 리스트에서 삽입은 기존 리스트에 새 노드를 추가하는 것을 포함합니다. 이 프로세스에는 다음이 포함됩니다.
- 새로운 노드 생성
- 데이터 설정
- 새로운 노드를 원형 구조에 넣기 위해 포인터 조정
시작에서
여기서는 목록의 시작 부분에 요소를 추가합니다.
위 이미지에서 우리의 목록은 이미 두 개의 요소, 즉 7과 9로 구성되어 있으며, 우리는 처음에 2를 삽입하고 있습니다.
결과적으로 우리는 위 그림과 같이 목록의 시작 부분에 2를 성공적으로 삽입했습니다.
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; } } public class CircularLinkedList { private Node head; private Node tail; public void addNode(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; } else { tail.next = newNode; newNode.next = head; tail = newNode; } } public void insertAtStart(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; } else { newNode.next = head.next; head.next = newNode; if (newNode.next == head) { tail = newNode; } } } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node current = head; do { sb.append(current.data).append(" -> "); current = current.next; } while (current!= head); sb.append("HEAD"); return sb.toString(); } public static void main(String[] args) { CircularLinkedList clist = new CircularLinkedList(); clist.addNode(5); clist.addNode(4); clist.addNode(3); clist.addNode(2); clist.addNode(1); System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> 1 -> HEAD clist.insertAtStart(0); // Inserts 0 at the start System.out.println(clist); // Expected output: 0 -> 5 -> 4 -> 3 -> 2 -> 1 -> HEAD } }
중간에
여기서는 목록의 중간에 요소를 추가해 보겠습니다.
위 이미지에서 우리는 요소 1과 8을 가지고 있으며, 이 두 노드 사이에 요소 3을 삽입하려고 합니다.
결과적으로, 요소 3이 원형 연결 리스트에 성공적으로 삽입되었습니다. 이제 첫 번째 노드, 즉 요소 1은 두 번째 노드, 즉 요소 3을 가리킬 것입니다.
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; } } public class CircularLinkedList { private Node head; private Node tail; public void addNode(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; } else { tail.next = newNode; newNode.next = head; tail = newNode; } } public void insertInMiddle(int data) { if (head == null) { head = new Node(data); tail = head; head.next = head; } else { Node newNode = new Node(data); Node slowPtr = head; Node fastPtr = head; do { fastPtr = fastPtr.next; } while (fastPtr!= head && fastPtr.next!= head); newNode.next = fastPtr.next; fastPtr.next = newNode; if (newNode.next == head) { tail = newNode; } } } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node current = head; do { sb.append(current.data).append(" -> "); current = current.next; } while (current!= head); sb.append("HEAD"); return sb.toString(); } public static void main(String[] args) { CircularLinkedList clist = new CircularLinkedList(); clist.addNode(5); clist.addNode(4); clist.addNode(3); clist.addNode(2); clist.addNode(1); System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> 1 -> HEAD clist.insertInMiddle(6); // Inserts 6 in the middle System.out.println(clist); // Expected output: 5 -> 4 -> 6 -> 3 -> 2 -> 1 -> HEAD } }
끝에서
여기서는 목록의 끝에 요소를 추가합니다.
위 이미지에서, 우리는 목록에 세 개의 요소가 있고, 네 번째 요소를 추가하려고 합니다.
결과적으로 우리는 리스트의 끝에 네 번째 요소를 성공적으로 삽입했습니다. 이제 새 노드는 첫 번째 노드를 가리킬 것입니다.
이것들로 고소득 프로그래밍 일자리를 준비하세요 최고의 C & 데이터 구조 인터뷰 질문과 답변!
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; } } public class CircularLinkedList { private Node head; private Node tail; public void addNode(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; } else { tail.next = newNode; newNode.next = head; tail = newNode; } } public void insertAtEnd(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; } else { tail.next = newNode; newNode.next = head; tail = newNode; } } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node current = head; do { sb.append(current.data).append(" -> "); current = current.next; } while (current!= head); sb.append("HEAD"); return sb.toString(); } public static void main(String[] args) { CircularLinkedList clist = new CircularLinkedList(); clist.addNode(5); clist.addNode(4); clist.addNode(3); clist.addNode(2); clist.addNode(1); System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> 1 -> HEAD clist.insertAtEnd(6); // Inserts 6 at the end System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> 1 -> 6 -> HEAD } }
삭제
원형 연결 리스트에서 삭제는 리스트에서 특정 노드를 제거하는 프로세스를 말합니다. 삭제 프로세스는 리스트 내에서 제거할 노드의 위치에 따라 다릅니다. 세 가지 주요 시나리오를 고려합니다.
- 헤드 노드 삭제
- 리스트 중간에서 노드 삭제
- 마지막 노드 삭제
시작에서
여기서는 목록의 시작 부분에 있는 요소를 제거합니다.
원래 목록에는 5개의 요소가 있으므로 목록에서 첫 번째 요소를 삭제하고 싶습니다.
결과적으로 목록의 첫 번째 요소가 성공적으로 삭제되었습니다.
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; // For simplicity, assuming singly linked list } } public class CircularLinkedList { private Node head; private Node tail; public void addNode(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; // Make it circular } else { tail.next = newNode; newNode.next = head; tail = newNode; } } public void deleteFromBeginning() { if (head == null) return; // List is empty // Find the new head node Node current = head; do { current = current.next; } while (current!= head); // Update the head node head = current.next; tail.next = head; // Ensure the list remains circular // Delete the old head node current.next = null; } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node current = head; do { sb.append(current.data).append(" -> "); current = current.next; } while (current!= head); sb.append("HEAD"); return sb.toString(); } public static void main(String[] args) { CircularLinkedList clist = new CircularLinkedList(); clist.addNode(5); clist.addNode(4); clist.addNode(3); clist.addNode(2); clist.addNode(1); System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> 1 -> HEAD // Deleting from the beginning clist.deleteFromBeginning(); System.out.println(clist); // Expected output: 4 -> 3 -> 2 -> 1 -> HEAD } }
중간에
여기서는 목록 사이의 요소를 제거합니다.
우리는 목록에 1, 2, 3, 4, 5의 5개 요소를 가지고 있습니다. 우리는 목록 중간에서 삭제를 원합니다. 즉, 가운데 요소인 “3”을 삭제하고 싶습니다.
결과적으로, 원형 연결 리스트에서 “3”이 제거되었습니다.
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; } } public class CircularLinkedList { private Node head; private Node tail; public void addNode(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; } else { tail.next = newNode; newNode.next = head; tail = newNode; } } public void deleteMiddleElement() { if (head == null || head.next == head) return; Node slowPtr = head; Node fastPtr = head; boolean found = false; do { fastPtr = fastPtr.next; if (fastPtr!= head) { found = true; } fastPtr = fastPtr.next; } while (fastPtr!= head &&!found); if (!found) { slowPtr = slowPtr.next; } slowPtr.next = slowPtr.next.next; tail.next = head; } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node current = head; do { sb.append(current.data).append(" -> "); current = current.next; } while (current!= head); sb.append("HEAD"); return sb.toString(); } public static void main(String[] args) { CircularLinkedList clist = new CircularLinkedList(); clist.addNode(5); clist.addNode(4); clist.addNode(3); clist.addNode(2); clist.addNode(1); System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> 1 -> HEAD clist.deleteMiddleElement(); // Deletes the middle element System.out.println(clist); // Expected output: 5 -> 3 -> 2 -> 1 -> HEAD } }
끝에서
여기서는 목록의 끝에 있는 요소를 제거합니다.
여기서 우리는 다시 1, 2, 3, 4, 5라는 5개의 요소를 가지고 있습니다. 우리는 원형 연결 리스트에서 마지막 요소를 삭제하고 싶습니다.
결과적으로, 우리는 원형 연결 리스트의 마지막 노드를 성공적으로 삭제했습니다.
class Node { int data; Node next; public Node(int data) { this.data = data; this.next = null; } } public class CircularLinkedList { private Node head; private Node tail; public void addNode(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; head.next = head; } else { tail.next = newNode; newNode.next = head; tail = newNode; } } public void removeLastElement() { if (head == null) return; if (head.next == head) { // Only one node exists head = null; tail = null; } else { Node current = head; do { current = current.next; } while (current.next!= head); tail = current; tail.next = head.prev; // Set the new tail's next to the previous head head.prev.next = head; // Connect the previous head's next to the new head head.prev = null; // Reset the previous head's prev to null head = head.prev; // Move the head to the new head } } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node current = head; do { sb.append(current.data).append(" -> "); current = current.next; } while (current!= head); sb.append("HEAD"); return sb.toString(); } public static void main(String[] args) { CircularLinkedList list = new CircularLinkedList(); clist.addNode(5); clist.addNode(4); clist.addNode(3); clist.addNode(2); clist.addNode(1); System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> 1 -> HEAD clist.removeLastElement(); // Removes the last element System.out.println(clist); // Expected output: 5 -> 4 -> 3 -> 2 -> HEAD } }
원형 연결 리스트의 장단점
단일 및 이중 원형 연결 리스트는 각자의 장단점을 가지고 있습니다. 개요는 다음과 같습니다.
장점
- 선형 연결 리스트에 비해 리스트의 시작과 끝에 노드를 삽입하거나 삭제하는 작업이 더 효율적으로 수행됩니다.
- 끝에 삽입이나 삭제를 할 때 마지막 노드의 다음 포인터를 업데이트하기 위해 탐색이 필요하지 않습니다.
- 원형 구조 덕분에 어떤 노드에서 시작해서 전체 목록을 순회하기 쉽습니다. 이는 원형 순회가 필요한 특정 애플리케이션에서 유용할 수 있습니다.
- 특정 시나리오에서는 원형 연결 리스트가 마지막 노드의 “다음” 포인터를 재사용하여 첫 번째 노드에 다시 연결할 수 있기 때문에 선형 연결 리스트보다 메모리 효율성이 더 높을 수 있습니다.
단점:
- 적절하게 구현되거나 관리되지 않으면 원형 연결 리스트는 탐색 중에 무한 루프로 이어질 수 있습니다.
- 순환 구조는 연결 리스트의 구현과 작업에 복잡성을 더합니다.
- 단일 원형 연결 리스트의 경우 탐색하는 동안 이전 노드에 접근하려면 헤드에서부터 추가로 탐색해야 하며, 이는 이중 연결 리스트보다 효율성이 떨어질 수 있습니다.
- 원형 연결 리스트의 각 노드는 다음(그리고 이중 연결 리스트의 경우 이전) 포인터를 위한 추가 공간이 필요합니다. 이는 특히 큰 리스트의 경우 메모리 오버헤드를 증가시킬 수 있습니다.
원형 연결 리스트의 응용
원형 연결 리스트는 고유한 속성으로 인해 다양한 시나리오에서 응용 프로그램을 찾습니다. 일반적인 응용 프로그램 중 일부는 다음과 같습니다.
- 라운드 로빈 스케줄링: 순환 연결 리스트는 운영 체제의 라운드 로빈과 같은 스케줄링 알고리즘에서 사용됩니다. 각 노드는 프로세스를 나타내며, 순환 구조는 각 프로세스가 CPU 시간을 동등하게 공유하도록 보장합니다.
- 음악 재생 목록: 순환 연결 리스트는 음악 플레이어에서 재생 목록을 구현하는 데 사용할 수 있습니다. 마지막 노래의 다음 포인터는 첫 번째 노래를 가리키며, 재생할 노래의 연속 루프를 만듭니다.
- 멀티플레이어 게임: 멀티플레이어 게임에서 원형 연결 리스트는 플레이어 간의 턴이나 액션 순서를 나타낼 수 있습니다. 마지막 플레이어가 턴을 마친 후, 다음 턴은 첫 번째 플레이어에게 돌아갑니다.
- 파일 시스템 관리: 파일 시스템에서 순환 연결 리스트는 여유 디스크 블록을 관리하는 데 활용될 수 있습니다. 마지막 블록의 다음 포인터는 첫 번째 여유 블록으로 돌아갈 수 있습니다.
- 시뮬레이션 시스템: 시뮬레이션 시스템에서 원형 연결 리스트는 엔티티가 서로 순환적으로 상호 작용하는 시나리오를 모델링하는 데 사용됩니다. 리스트의 각 노드는 엔티티를 나타내고 시뮬레이션은 원형 시퀀스로 진행됩니다. 이 설정은 엔티티 간의 지속적인 상호 작용을 허용하여 동적이고 반복적인 시뮬레이션 환경을 만듭니다.
결론적인 생각
결론적으로, 원형 연결 리스트는 메모리 관리, 작업 스케줄링, 효율적인 탐색에 응용할 수 있는 다재다능한 데이터 구조를 제공합니다. 폐쇄 루프를 형성하는 능력은 상수 시간 삽입 및 삭제를 용이하게 하여 동적 시나리오에 적합합니다. 미래의 범위에는 특정 사용 사례에 대한 최적화를 탐구하고, 원형 리스트 조작을 위한 알고리즘을 개선하고, 고급 기능을 다양한 도메인에 통합하여 컴퓨터 과학에서 효율적이고 확장 가능한 데이터 구조의 진화에 기여하는 것이 포함됩니다.
아직도 C 프로그래밍에 대해 의심이 있으신가요? 저희 전문가와 함께 의심과 질문을 해소하세요. C 프로그래밍 커뮤니티!
자주 묻는 질문
실제 응용프로그램에서 원형 연결 리스트는 어디에 사용됩니까?
원형 연결 리스트는 라운드 로빈 스케줄링, 음악 재생 목록, 멀티플레이어 게임 턴 관리, 리소스 할당, 실시간 시스템의 작업 스케줄링, 파일 시스템 관리, 시뮬레이션 시스템, 버퍼 관리, 실행 취소/다시 실행 기능과 같은 시나리오에서 사용됩니다.
원형 연결 리스트에 노드를 삽입하려면 어떻게 해야 하나요?
원형 연결 리스트에 노드를 삽입하려면 마지막 노드로 이동하여 새 노드를 만들고 마지막 노드의 다음 포인터를 업데이트하여 새 노드를 가리키도록 할 수 있습니다. 원형 구조를 유지하려면 새 노드의 다음 포인터가 첫 번째 노드로 설정되어 있는지 확인합니다.
원형 연결 리스트에서 노드를 삭제하려면 어떻게 해야 하나요?
원형 연결 리스트에서 노드를 삭제하려면 이전 노드의 다음 포인터를 업데이트하여 삭제할 노드를 건너뛰어야 합니다. 원형 구조를 유지하려면 마지막 노드의 다음 포인터를 적절히 처리해야 합니다.
원형 연결 리스트를 어떻게 탐색하나요?
트래버설은 임의의 노드에서 시작하여 다음 노드로 이동하여 다시 시작 노드에 도달하는 것을 포함합니다. do-while 루프는 트래버설에 일반적으로 사용되어 루프가 최소한 한 번 실행되도록 합니다.
원형 연결 리스트도 이중 연결될 수 있나요?
네, 원형 연결 리스트는 이중 연결될 수 있습니다. 즉, 각 노드에 다음 포인터와 이전 포인터가 모두 있습니다. 이를 통해 앞뒤 방향 모두에서 더 효율적인 순회가 가능합니다.