포스트

Linked-List

Linked-List

@TODO


Linked-List 연결 리스트
(혹은 List 리스트)

원소를 저장할 때 그 다음 원소가 있는 위치를 포함시키는 방식으로 저장하는 자료구조
원소들은 이곳 저곳에 흩어져있다.

흩어져있기에 원하는 원소가 어디에 있는지 알 수 없음
이를테면 마지막 원소가 뭔지 바로 알 수 없음
모든 원소를 한 번 씩 읽어야 한다면 연결 리스트가 좋지만,
특정한 원소 하나만 알고 싶다면 연결 리스트는 최악

순차 접근 Sequential Access

극장에서 서로 흩어져서 영화보기
서로 어디있는지는 앎

_

  • 성질
    • k번째 원소를 확인/변경하기 위해 O(k)가 필요함
    • 임의의 위치에 원소를 추가/임의 위치의 원소 제거는 O(1)
      • 추가, 제거할 위치를 알고 있다는 (이미 접근해 있다는) 것을 전제로
      • 아니라면 read하는 만큼의 시간 복잡도가 더 들겠죠 (+O(k))
    • 원소들이 메모리 상에 연속해있지 않아 Cache hit rate가 낮지만 할당이 다소 쉬움
  • 종류
    • 단일 연결 리스트 Singly Linked List
    • 이중 연결 리스트 Doubly Linked List (STL list)
    • 원형 연결 리스트 Circular Linked List
      • 환형 “
      • 순환 “

첫 번째 요소, 두 번째 요소를 식별할 수 있음
배열과 마찬가지로 선형 자료구조
트리, 그래프, 해쉬 등은 비선형 자료구조

 배열연결 리스트
k번째 원소에 접근O(1)O(k)
임의 위치에 원소 추가/제거O(N)O(1) (추가/제거할 위치를 알고 있는 경우)
메모리 상의 배치연속불연속
추가적으로 필요한 공간 (Overhead)- (없음, 굳이 만든다면 배열 길이)O(N) (주소값, 32bit 4N바이트, 64bit 8N바이트, N에 비례하는 만큼의 메모리)
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
// 야매 연결 리스트
// 메모리 누수 문제로 실무에서는 절대 쓸 수 없는 방식이지만,
// 코테에서는 구현 난이도가 일반적인 구현보다 낮은데 시간복잡도는 동일해서 애용할 수 있다

const int MX = 1000005;
int dat[MX], pre[MX], nxt[MX];
int unused = 1; // top?

// 배열 순서대로 저장하는 것이 아님, pre nxt에 원소 index를 저장

// 단, 0번지는 고정적인 단순 시작지로써, 데이터를 저장하지 않음, 맨 처음 원소 앞에 0번지 원소가 있다고 생각
// 이런 Dummy node를 두지 않으면, 삽입삭제 등을 구현할 때 원소가 아예 없는 경우에 대한 예외처리를 해야 하는 번거로움이

// @ stl list의 경우 dummy node는 맨 마지막에 있음

fill(pre, pre + MX, -1);
fill(nxt, nxt + MX, -1);

// traverse
// 0번지에서 출발해 nxt를 따라가는
void traverse()
{
    int cur = nxt[0];
    while(cur != -1)
    {
        cout << dat[cur] << ' ';
        cur = nxt[cur];
    }
    cout << "\n\n";
}

// insert (int addr추가할위치주소, int num)
// 새로운 원소 생성
// 새로운 원소 pre, nxt 대입
// 삽입할 위치의 nxt 값과 삽입할 위치의 다음 원소의 pre 값을 새 원소로 변경
// unused 1 증가
void insert(int addr, int num)
{
    dat[unused] = num;
    pre[unused] = addr;
    nxt[unused] = nxt[addr];
    if (nxt[addr] != -1) pre[nxt[addr]] = unused;
    nxt[addr] = unused;
    unused++;
}

// erase
// 이전 위치의 nxt를 삭제할 위치의 nxt로 변경
// 다음 위치의 pre를 삭제할 위치의 pre로 변경

// 야매 방법으로는 제거를 해도 프로그램이 종료될 때 까지 메모리를 점유하고 있음
// 그래서 실무에서는 못씀
void erase(int addr)
{
    // dummy node의 존재로 인해 그 어떤 노드를 지우더라도 pre[addr]은 -1이 아님이 보장됨
    nxt[pre[addr]] = nxt[addr];
    if (nxt[addr] != -1) pre[nxt[addr]] = pre[addr];

    // 남은 데이터를 일부러 지울 필요는 없음
    // 어차피 접근할 일도 없고, 새로 값이 들어올 때 덮어씌워질 것이기 때문
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// STL list
#include <bits/stdc++.h>
using namespace std;

// push_back, pop_back, push_front, pop_front는 모두 O(1)
int main(void)
{
    list<int> L = { 1, 2 }; // 1 2
    list<int>::iterator t = L.begin(); // t는 1을 가리키는 중
    // c++ 11 이상 일때 auto t = L.begin() 가능
    L.push_front(10); // 10 1 2
    cout << *t << '\n'; // t가 가리키는 값 = 1을 출력
    L.push_back(5); // 10 1 2 5
    L.insert(t, 6); // t가 가리키는 곳 앞에 6을 삽입, 10 6 1 2 5
    t++; // t를 1칸 앞으로 전진, 현재 t가 가리키는 값은 2
    t = L.erase(t); // t가 가리키는 값을 제거, 그 다음 원소인 5의 위치를 반환
                    // 10 6 1 5, t가 가리키는 값은 5
    cout << *t << '\n'; // 5
    for(auto i: L) cout << i << ' '; // C++ 11 이상
    cout << '\n';
    for(list<int>::iterator it = L.begin(); it != L.end(); it++)
    cout << *it << ' ';
}// remove unique merge reverse sort splice
// []를 지원하지 않음, 따라서 임의 접근 반복자를 필요로 하는 binary_search 알고리듬 적용할 수 없다

중간에 만나는 두 연결 리스트의 시작점들이 주어졌을 때 만나는 지점을 구하는 방법?

둘 다 끝까지 진행시킨 후에 그 길이를 기록
다시 시작점들로 돌아와서 짧은 쪽을 길이 차이만큼 먼저 진행
이후 동시에 하나씩 전진해나가며 만나는 지점 찾기

공간 복잡도 O(1), 시간 복잡도 O(A + B)


주어진 연결 리스트 안에 사이클이 있는지 판단하라

Floyd’s cycle-finding algorithm, 공간 복잡도 O(1), 시간 복잡도 O(N)


  • 운영 체제에서 동적 메모리를 관리할 때
  • 희소 행렬 Sparse Matrix을 표현할때
  • 덱이나 스택, 큐와 같은 다른 자료구조를 표현할 때 기초 자료 구조로
  • 텍스트 에디터도 내부적으로 컨텐츠를 저장할 때 연결리스트를 사용. 텍스트 파일의 중간에서 문자가 입력될 수 있기 때문
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.