자료구조 연결 리스트(SLL,DLL,CLL) -(3)CLL

원형연결리스트(Circular Linked List,CLL)

“원형연결리스트(CLL)” 이란,
마지막노드가 첫 노드와 연결된 단순연결리스트이다.

정의에서 알 수 있듯이,
원형연결리스트는 단순연결리스트와 크게 다르지 않지만,
장단점이 존재한다.

첫 번째로,

마지막 노드를 나타내는 “last”가 단순 연결리스트의 ‘head’ 역할을 한다.
따라서 마지막 노드와 첫 노드를 O(1)시간에 방문할 수 있다.
(*SLL과 DLL은 마지막 노드 방문할 시 O(N))

두 번째로,

빈 리스트가 아니라면 어떤 노드도 None(더미 노드)가 아니기 때문에,
소스 코드에서 None 조건을 검사하지 않아도 된다.

그러나,
역방향 탐색이 어렵고,
무한 루프가 발생 할 가능성도 있어 유의해야 한다.

삽입 연산에서 특이사항은,
“원형”연결리스트 이기에, 새 노드를 추가함과 동시에 레퍼런스 연결이 중요하다.

예를 들어,
아무것도 없는 리스트에 노드를 추가한다면
새 노드를 추가함과 동시에 새 노드를 참조해야 원형연결리스트가 구현된다.
last 와 head가 유일한 노드인 새 노드가 되기 때문이다.

추가로,
삭제는 첫 노드부터 삭제가 되는
순차적 삭제연산을 구현했다.

아래는 python으로 작성한 소스 코드이다.

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
class CList:
    class Node:
        def __init__(self, item, link): #노드 생성자
            self.item = item #value
            self.link = link #이을 (레퍼런스)
    
    def __init__(self): #리스트 생성자
        self.last = None #마지막을 가르키는 Node "last", 리스트  , = None
        self.size = 0 #초기 인자값

    def item_count(self): #리스트의 크기 출력
        return self.size
    
    def is_empty(self): # 리스트인지 확인
        return self.size == 0
    
    def insert(self, item): # 리스트  노드 자리에 노드 추가
        n = self.Node(item, None) # 노드 생성  n이 참조함(n이 노드 자체를 가리킨다고 생각)
        if self.is_empty():
            n.next = n  # 노드 자기 자신을 참조
            self.last = n #리스트 인자값이 하나인 리스트,n은 자기 자신을 참조하고, last가  노드인 n을 참조.
        else: #비지 않은 리스트* 과정 이해 필요
            n.next = self.last.next # 노드를  노드 다음으로 참조.
            self.last.next = n #비지 않은 리스트에 노드를 추가하는 , 노드는  노드를 참조하고, last가 참조하는 노드와  노드 연결
        self.size += 1 #요소 추가
    
    def first(self): #가장  노드를 출력해주는 함수
        if self.is_empty(): # 리스트
            raise EmptyError('Underflow')
        f = self.last.next # 노드(last 다음 노드)
        return f.item
    
    def delete(self): #삭제하는 함수
        if self.is_empty(): # 리스트
            raise EmptyError('Underflow')
        x = self.last.next # 노드 = x
        if self.size == 1: #노드가 하나밖에 없는 리스트,  노드를 삭제할  더미 리스트가 .(last == None)
            self.last = None
        else:
            self.last.next = x.next # 노드의 다음노드를 레퍼런스 연결,갱신형식으로 노드 1 순차적 삭제
        self.size -= 1 #요소 1 삭제
        return x.item

    def print_list(self): #리스트 출력 함수
        if self.is_empty():
            print('리스트 비어있음!')
        else:
            f = self.last.next # 노드 = f
            p = f #순차적 접근을 위한 변수 선언
            while p.next != f: #다음 노드가  노드가 되기 전까지~ = 리스트를 탐색, 바퀴 돈다는 
                print(p.item, ' => ' , end ='')
                p = p.next #다음 노드로 이동
            print(p.item)
    
    def search(self, target):
        if self.is_empty():
            print('리스트 비어있음!')
        else:
            f = self.last.next #f를  노드로 설정
            p = f 
            for i in range(self.item_count()): #원형 연결 리스트 전체를 탐색
                if(target == p.item): #target값이 검색이 된다면,
                    return i #index값 출력
                p = p.next #다음 노드로
            return None #탐색 실패

class EmptyError(Exception):
    pass

비지않은 CLL에 요소를 추가할 때,
과정을 이해해보자.

Example)
=> A => B => C => 라는 CLL이 존재하고,
last가 A일때,
insert(D)를 실행한다.
self.last.next 의 의미는, 첫 노드 자리를 의미하며,
이 첫 노드 자리를 D의 item을 가진 n(참조노드)의 다음으로 참조하고,
첫노드를 n으로 참조하여
첫 노드 자리에 D를 삽입한다.

1
2
3
4
5
6
7
8
9
    def insert(self, item): #순차적으로 리스트에 노드 추가
        n = self.Node(item, None) # 노드 생성  n이 참조함(n이 노드 자체를 가리킨다고 생각)
        if self.is_empty():
            n.next = n  # 노드 자기 자신을 참조
            self.last = n #리스트 인자값이 하나인 리스트,n은 자기 자신을 참조하고, last가  노드인 n을 참조.
        else: #비지 않은 리스트* 과정 이해 필요
            n.next = self.last.next # 노드를  노드 다음으로 참조.
            self.last.next = n #비지 않은 리스트에 노드를 추가하는 , 노드는  노드를 참조하고, last가 참조하는 노드와  노드 연결
        self.size += 1 #요소 추가

아래는 실행코드와 결과이다.

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
from Circular_Linked_List import CList
if __name__ == '__main__':
    c = CList()
    c.insert('pear')
    c.insert('cherry')
    c.insert('orange')
    c.insert('apple')
    c.print_list() #apple => orange => cherry => pear

    print('c의 길이 =', c.item_count()) # 4
    print('c의 첫 항목 :', c.first()) #apple

    print('apple은 이 리스트의 %d번째에 존재합니다.' % c.search('apple'))
    print('pear는 이 리스트의 %d번째에 존재합니다.' % c.search('pear'))
    
    c.delete() #apple 삭제
    print('첫 노드 삭제 후: ', end='')
    c.print_list() #orange => cherry => pear
    print('c의 길이 =', c.item_count()) #3
    print('c의 첫 항목: ', c.first()) #orange

    c.delete() #orange 삭제
    print('첫 노드 삭제 후: ', end='')
    c.print_list() #cherry => pear

    c.delete() #cherry 삭제
    print('첫 노드 삭제 후: ', end='')
    c.print_list() #pear

    c.delete() #pear 삭제
    print('첫 노드 삭제 후: ', end='')
    c.print_list() #리스트 비어있음!

실행 결과 =

1
2
3
4
5
6
7
8
9
c의  항목 : apple
apple은  리스트의 0 번째에 존재합니다.
pear는  리스트의 3번째에 존재합니다.
 노드 삭제 : orange  => cherry  => pear
c의 길이 = 3
c의  항목:  orange
 노드 삭제 : cherry  => pear
 노드 삭제 : pear
 노드 삭제 : 리스트 비어있음!

#plus 1.CLL의 삽입,삭제 연산은 O(1) 시간이 걸리며,
탐색 시간도 O(N)으로 SLL과 같다.

2.그러나 마지막 노드를 O(1)이라는 시간에 접근할 수 있는 장점이 존재한다.
또한 빈리스트가 아닐때 더미 노드가 존재하지 않는다!(None 조건 이용 x)

3.CLL은 여러사람이 차례로 돌아가며 플레이하는 게임을 구현하는데 적합한 자료구조이다.
운영체제에도 이용되며,이항힙,피보나치힙같은 우선순위큐를 구현하는데도 부분적으로 사용된다.