본문 바로가기

단방향 연결 목록

연속 배열 없이 Node를 연결하고 head부터 getNode·add까지 참조 이동 규칙을 구현합니다.

연결 목록은 원소를 연속된 칸에 복사하지 않고 각 Node가 다음 Node의 참조를 가집니다.

그래서 i번째 값을 얻으려면 head부터 i번 next를 따라가야 합니다.

순회 코드는 현재 Node와 next 참조를 분리하며 null을 목록의 끝 표식으로 사용합니다.


null 종단을 지나친 순회가 실패하는 지점

두 노드만 있는 체인에서 세 번 이동합니다.

current가 null이 된 뒤 세 번째 반복에서 current.next를 읽어 NullPointerException이 나며, 마지막 출력문에는 도달하지 않습니다.

lab/BrokenNodeTraversalFailure.java
public final class BrokenNodeTraversalFailure {
    public static void main(String[] args) {
        Node first = new Node("node", new Node("link", null));
        Node current = first;
        for (int i = 0; i <= 2; i++) {
            current = current.next;
        }
        System.out.println(current.value);
    }

    private record Node(String value, Node next) {}
}
관찰 결과
java.lang.NullPointerException

배열의 length처럼 길이를 자동으로 알려 주는 속성이 없습니다.

목록이 size를 관리하고 공개 get(index) 메서드가 유효 범위를 검사한 뒤 getNode(index)를 호출해야 합니다.

순회는 current를 한 칸씩 바꾸되 원래 head 참조를 잃지 않아야 합니다.

null을 지난 세 번째 이동의 실패

next 두 번으로 current=null이 되는 대입은 성공하고 세 번째 역참조에서만 NPE가 납니다. 정상 이동과 실패 위치를 시간순으로 구분할 수 있습니다.

null을 지난 세 번째 이동의 실패

두 노드에서 current가 두 번 이동해 null이 되고, 세 번째 current.next 읽기에서 예외가 나므로 출력문에는 도달하지 않습니다.

실패 경계 · null 다음 이동

null을 지난 세 번째 이동의 실패

두 번째 이동으로 current가 null이 되는 대입은 완료됩니다. 세 번째 반복에서 null의 next를 읽다가 실패합니다.

null 대입 뒤 다시 읽을 때의 실패 지점두 노드 원문은 node에서link, 다음null로current를옮깁니다. null대입자체는정상이지만 다시next를역참조하면NullPointerException이며println을실행하지않습니다. 실선은 원문의 지역변수 대입 순서입니다.
첫 노드를 가리킴current 값은 nodecurrent.next는 link
연결 관계첫 노드를 가리킴 · current 값은 node · current.next는 link → next를 한 번 따름 · current 값은 link · current.next는 null
next를 한 번 따름current 값은 linkcurrent.next는 null
연결 관계next를 한 번 따름 · current 값은 link · current.next는 null → next를 두 번 따름 · current는 null · 이 대입 자체는 정상
next를 두 번 따름current는 null이 대입 자체는 정상
연결 관계next를 두 번 따름 · current는 null · 이 대입 자체는 정상 → 다시 next를 읽음 · null.next에서 실패 · NullPointerException · println은 실행하지 않음
다시 next를 읽음null.next에서 실패NullPointerExceptionprintln은 실행하지 않음

실선: 원문의 current 대입null 대입과 null 역참조 구별

  1. i = 0

    이동 전 current
    value = "node"
    평가 결과
    current.next는 값 "link"의 노드
    current가 그 노드로 이동
  2. i = 1

    이동 전 current
    value = "link"
    평가 결과
    current.next == null
    current = null
  3. i = 2

    이동 전 current
    null
    평가 결과
    current.next 읽기에서 NullPointerException
    대입과 println(current.value)에 도달하지 않음

이 예의 조건

  • println은 실패 뒤 실행되지 않는다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2두 노드 원문은 node에서 link, 다음 null로 current를 옮깁니다. null 대입 자체는 정상이지만 다시 next를 역참조하면 NullPointerException이며 println을 실행하지 않습니다. 실선은 원문의 지역 변수 대입 순서입니다.
원 도표의 설명과 주석
구분내용
설명두 노드 원문은 node에서 link, 다음 null로 current를 옮깁니다. null 대입 자체는 정상이지만 다시 next를 역참조하면 NullPointerException이며 println을 실행하지 않습니다. 실선은 원문의 지역 변수 대입 순서입니다.
전체 읽기 정보null을 지난 세 번째 이동의 실패 null을 지난 세 번째 이동의 실패 두 노드에서 current가 두 번 이동해 null이 되고, 세 번째 current.next 읽기에서 예외가 나므로 출력문에는 도달하지 않습니다. 실패 경계 · null 다음 이동 null을 지난 세 번째 이동의 실패 두 번째 이동으로 current가 null이 되는 대입은 완료됩니다. 세 번째 반복에서 null의 next를 읽다가 실패합니다. i = 0 이동 전 current value = "node" 평가 결과 current.next 는 값 "link" 의 노드 current 가 그 노드로 이동 i = 1 이동 전 current value = "link" 평가 결과 current.next == null current = null i = 2 이동 전 current null 평가 결과 current.next 읽기에서 NullPointerException 대입과 println(current.value) 에 도달하지 않음
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 노드

current가 node를 보유합니다.

  • current: node
  • next: link
  • next 필드를 읽을 대상 객체가 있다.
두 번 이동

link를 거쳐 null을 지역 변수에 저장합니다.

  • 첫 이동: link
  • 둘째 이동: null
  • 대입: 정상 완료
  • null 값을 저장하는 것 자체는 역참조가 아니다.
다음 역참조

null.next를 읽으려다 실패합니다.

  • 결과: NullPointerException
  • println: 미실행
  • 객체 없는 참조를 소비하는 시점이 최초 실패다.

null을 지난 세 번째 이동의 실패

next 두 번으로 current=null이 되는 대입은 성공하고 세 번째 역참조에서만 NPE가 납니다. 정상 이동과 실패 위치를 시간순으로 구분할 수 있습니다.

null을 지난 세 번째 이동의 실패

두 노드에서 current가 두 번 이동해 null이 되고, 세 번째 current.next 읽기에서 예외가 나므로 출력문에는 도달하지 않습니다.

실패 경계 · null 다음 이동

null을 지난 세 번째 이동의 실패

두 번째 이동으로 current가 null이 되는 대입은 완료됩니다. 세 번째 반복에서 null의 next를 읽다가 실패합니다.

null 대입 뒤 다시 읽을 때의 실패 지점두 노드 원문은 node에서link, 다음null로current를옮깁니다. null대입자체는정상이지만 다시next를역참조하면NullPointerException이며println을실행하지않습니다. 실선은 원문의 지역변수 대입 순서입니다.
첫 노드를 가리킴current 값은 nodecurrent.next는 link
연결 관계첫 노드를 가리킴 · current 값은 node · current.next는 link → next를 한 번 따름 · current 값은 link · current.next는 null
next를 한 번 따름current 값은 linkcurrent.next는 null
연결 관계next를 한 번 따름 · current 값은 link · current.next는 null → next를 두 번 따름 · current는 null · 이 대입 자체는 정상
next를 두 번 따름current는 null이 대입 자체는 정상
연결 관계next를 두 번 따름 · current는 null · 이 대입 자체는 정상 → 다시 next를 읽음 · null.next에서 실패 · NullPointerException · println은 실행하지 않음
다시 next를 읽음null.next에서 실패NullPointerExceptionprintln은 실행하지 않음

실선: 원문의 current 대입null 대입과 null 역참조 구별

  1. i = 0

    이동 전 current
    value = "node"
    평가 결과
    current.next는 값 "link"의 노드
    current가 그 노드로 이동
  2. i = 1

    이동 전 current
    value = "link"
    평가 결과
    current.next == null
    current = null
  3. i = 2

    이동 전 current
    null
    평가 결과
    current.next 읽기에서 NullPointerException
    대입과 println(current.value)에 도달하지 않음

이 예의 조건

  • println은 실패 뒤 실행되지 않는다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2두 노드 원문은 node에서 link, 다음 null로 current를 옮깁니다. null 대입 자체는 정상이지만 다시 next를 역참조하면 NullPointerException이며 println을 실행하지 않습니다. 실선은 원문의 지역 변수 대입 순서입니다.
원 도표의 설명과 주석
구분내용
설명두 노드 원문은 node에서 link, 다음 null로 current를 옮깁니다. null 대입 자체는 정상이지만 다시 next를 역참조하면 NullPointerException이며 println을 실행하지 않습니다. 실선은 원문의 지역 변수 대입 순서입니다.
전체 읽기 정보null을 지난 세 번째 이동의 실패 null을 지난 세 번째 이동의 실패 두 노드에서 current가 두 번 이동해 null이 되고, 세 번째 current.next 읽기에서 예외가 나므로 출력문에는 도달하지 않습니다. 실패 경계 · null 다음 이동 null을 지난 세 번째 이동의 실패 두 번째 이동으로 current가 null이 되는 대입은 완료됩니다. 세 번째 반복에서 null의 next를 읽다가 실패합니다. i = 0 이동 전 current value = "node" 평가 결과 current.next 는 값 "link" 의 노드 current 가 그 노드로 이동 i = 1 이동 전 current value = "link" 평가 결과 current.next == null current = null i = 2 이동 전 current null 평가 결과 current.next 읽기에서 NullPointerException 대입과 println(current.value) 에 도달하지 않음
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 노드

current가 node를 보유합니다.

  • current: node
  • next: link
  • next 필드를 읽을 대상 객체가 있다.
두 번 이동

link를 거쳐 null을 지역 변수에 저장합니다.

  • 첫 이동: link
  • 둘째 이동: null
  • 대입: 정상 완료
  • null 값을 저장하는 것 자체는 역참조가 아니다.
다음 역참조

null.next를 읽으려다 실패합니다.

  • 결과: NullPointerException
  • println: 미실행
  • 객체 없는 참조를 소비하는 시점이 최초 실패다.

head·current·next의 역할 분리

  • Node는 현재 값과 다음 Node 참조를 묶어 한 연결 단위를 표현합니다.
  • head는 첫 원소를 가리키며 빈 목록에서는 null이라는 상태를 명시합니다.
  • getNode는 head에서 출발해 정확히 index번 next를 따라가므로 O(n)입니다.
  • 비어 있지 않은 목록의 끝 추가는 getNode(size - 1)로 마지막 Node를 찾습니다.
  • 끝 추가는 마지막 Node의 next를 새 Node로 연결하며 size를 한 번만 증가시킵니다.
  • toString 순회는 구조를 변경하지 않고 null에 도달하면 반드시 종료해야 합니다.

getNode·add 기반 단방향 체인

LinkedNodeWalk는 head를 고정하고 current라는 지역 참조만 이동합니다.

add는 빈 목록과 기존 목록을 분리하고, get은 경계 검사를 통과한 뒤 getNode를 호출합니다.

size가 구조와 어긋나지 않도록 연결 성공 뒤 한 번 증가시킵니다.

src/LinkedNodeWalk.java
public final class LinkedNodeWalk {
    public static void main(String[] args) {
        NodeChain chain = new NodeChain();
        chain.add("node");
        chain.add("next");
        chain.add("head");
        System.out.println(chain.get(1));
        System.out.println(chain);
    }

    private static final class NodeChain {
        private Node head;
        private int size;

        void add(String value) {
            Node added = new Node(value);
            if (head == null) head = added;
            else getNode(size - 1).next = added;
            size++;
        }

        String get(int index) {
            check(index);
            return getNode(index).value;
        }

        private Node getNode(int index) {
            Node current = head;
            for (int i = 0; i < index; i++) {
                current = current.next;
            }
            return current;
        }

        private void check(int index) {
            if (index < 0 || index >= size) throw new IndexOutOfBoundsException(index);
        }

        public String toString() {
            StringBuilder out = new StringBuilder("[");
            for (Node current = head; current != null; current = current.next) {
                if (out.length() > 1) out.append(", ");
                out.append(current.value);
            }
            return out.append(']').toString();
        }
    }

    private static final class Node {
        String value;
        Node next;

        Node(String value) {
            this.value = value;
        }
    }
}

head 보존과 current 이동

head는 목록의 영구 시작점입니다.

조회를 위해 head 자체를 다음 Node로 바꾸면 앞부분을 다시 찾을 방법이 없습니다.

순회에는 지역 변수 current를 만들고 current = current.next만 반복합니다.

순회가 끝나도 head가 첫 Node를 가리켜야 합니다.

인덱스 0의 Node는 head이고 인덱스 i의 Node는 next를 i번 따라간 결과입니다.

head를 보존한 get(1)의 한 번 이동

getNode(1)는 지역 current만 한 번 이동하고 head를 유지합니다. 세 노드 전체 연결을 고정해 현재 포인터의 실제 이동을 보여 줄 수 있습니다.

head를 보존한 get(1)의 한 번 이동

node, next, head라는 세 값을 저장한 체인에서 head 필드는 첫 노드를 유지하고 getNode(1)의 current만 두 번째 노드로 이동합니다.

연결 목록 · 참조와 값

head를 보존한 get(1)의 한 번 이동

get(1)은 current만 next로 한 번 이동시킵니다. head 필드는 첫 노드를 계속 가리키며 마지막 노드의 값 "head"와 구분됩니다.

head를 보존한 get(1)의 한 번 이동node, next, head라는 세 값을 저장한 체인에서 head 필드는 첫 노드를 유지하고 getNode(1)의 current만 두 번째 노드로 이동합니다.nextnextnextheadgetNode(1)의 currentnext 이동 1회"node"index 0"next"index 1"head"index 2nullsize = 3 · head 필드는 그대로 유지
head와 get1의 지역 current가 가리키는 노드 세 노드는 값node/next/head를보관하며head필드는첫노드를가리킵니다. 실선은head와next참조, 점선은get1의이동후current가두번째노드를가리키는관계입니다. 값문자열head와head필드는다르며조회가head를이동하지않습니다. 목록의 head 첫 Node0을 참조 Node0 · index 0 값 node / next는 Node1 Node1 · index 1 값 next / next는 Node2 Node2 · index 2 값 head / next는 null null get(1)의 current 지역 참조는 Node1 반환 값은 next 실선: head·next 참조 점선: 이동 후 current 참조 head 필드는 그대로 유지
체인 시작
참조와 값: head → 값 "node"의 index 0 노드
각 next 참조
참조와 값:
index 0 "node" → index 1 "next"
index 1 "next" → index 2 "head"
index 2 "head" → null
get(1)의 조회
참조와 값: current가 index 0에서 next 1회 이동하여 index 1을 가리킴. 반환값은 "next".
유지되는 구조
참조와 값: size = 3, head는 index 0 그대로. 값 "head"와 head 필드는 다름.

이 예의 조건

  • getNode(1)의 실제 이동이며 head를 바꾸지 않는다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보head를 보존한 get(1)의 한 번 이동 head를 보존한 get(1)의 한 번 이동 node, next, head라는 세 값을 저장한 체인에서 head 필드는 첫 노드를 유지하고 getNode(1)의 current만 두 번째 노드로 이동합니다. 연결 목록 · 참조와 값 head를 보존한 get(1)의 한 번 이동 get(1)은 current만 next로 한 번 이동시킵니다. head 필드는 첫 노드를 계속 가리키며 마지막 노드의 값 "head"와 구분됩니다. 체인 시작 참조와 값: head → 값 "node" 의 index 0 노드 각 next 참조 참조와 값: index 0 "node" → index 1 "next" index 1 "next" → index 2 "head" index 2 "head" → null get(1)의 조회 참조와 값: current 가 index 0에서 next 1회 이동하여 index 1을 가리킴. 반환값은 "next" . 유지되는 구조 참조와 값: size = 3 , head 는 index 0 그대로. 값 "head" 와 head 필드는 다름.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
탐색 시작

current를 첫 노드로 초기화합니다.

  • 체인: node→next→head→null
  • current: node/index0
  • head 필드: 첫 node
  • 탐색용 지역 변수와 목록 소유 참조를 구분한다.
한 칸 이동

current.next를 읽어 둘째 노드로 이동합니다.

  • current: next/index1
  • head 필드: 첫 node 유지
  • 조회가 head를 재대입하지 않는다.
결과 읽기

index 1의 값을 반환합니다.

  • 조회 값: next
  • size: 3
  • 전체 연결: 그대로
  • 현재 위치만 이동하여 목록 구조를 보존한다.

head를 보존한 get(1)의 한 번 이동

getNode(1)는 지역 current만 한 번 이동하고 head를 유지합니다. 세 노드 전체 연결을 고정해 현재 포인터의 실제 이동을 보여 줄 수 있습니다.

head를 보존한 get(1)의 한 번 이동

node, next, head라는 세 값을 저장한 체인에서 head 필드는 첫 노드를 유지하고 getNode(1)의 current만 두 번째 노드로 이동합니다.

연결 목록 · 참조와 값

head를 보존한 get(1)의 한 번 이동

get(1)은 current만 next로 한 번 이동시킵니다. head 필드는 첫 노드를 계속 가리키며 마지막 노드의 값 "head"와 구분됩니다.

head를 보존한 get(1)의 한 번 이동node, next, head라는 세 값을 저장한 체인에서 head 필드는 첫 노드를 유지하고 getNode(1)의 current만 두 번째 노드로 이동합니다.nextnextnextheadgetNode(1)의 currentnext 이동 1회"node"index 0"next"index 1"head"index 2nullsize = 3 · head 필드는 그대로 유지
head와 get1의 지역 current가 가리키는 노드 세 노드는 값node/next/head를보관하며head필드는첫노드를가리킵니다. 실선은head와next참조, 점선은get1의이동후current가두번째노드를가리키는관계입니다. 값문자열head와head필드는다르며조회가head를이동하지않습니다. 목록의 head 첫 Node0을 참조 Node0 · index 0 값 node / next는 Node1 Node1 · index 1 값 next / next는 Node2 Node2 · index 2 값 head / next는 null null get(1)의 current 지역 참조는 Node1 반환 값은 next 실선: head·next 참조 점선: 이동 후 current 참조 head 필드는 그대로 유지
체인 시작
참조와 값: head → 값 "node"의 index 0 노드
각 next 참조
참조와 값:
index 0 "node" → index 1 "next"
index 1 "next" → index 2 "head"
index 2 "head" → null
get(1)의 조회
참조와 값: current가 index 0에서 next 1회 이동하여 index 1을 가리킴. 반환값은 "next".
유지되는 구조
참조와 값: size = 3, head는 index 0 그대로. 값 "head"와 head 필드는 다름.

이 예의 조건

  • getNode(1)의 실제 이동이며 head를 바꾸지 않는다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보head를 보존한 get(1)의 한 번 이동 head를 보존한 get(1)의 한 번 이동 node, next, head라는 세 값을 저장한 체인에서 head 필드는 첫 노드를 유지하고 getNode(1)의 current만 두 번째 노드로 이동합니다. 연결 목록 · 참조와 값 head를 보존한 get(1)의 한 번 이동 get(1)은 current만 next로 한 번 이동시킵니다. head 필드는 첫 노드를 계속 가리키며 마지막 노드의 값 "head"와 구분됩니다. 체인 시작 참조와 값: head → 값 "node" 의 index 0 노드 각 next 참조 참조와 값: index 0 "node" → index 1 "next" index 1 "next" → index 2 "head" index 2 "head" → null get(1)의 조회 참조와 값: current 가 index 0에서 next 1회 이동하여 index 1을 가리킴. 반환값은 "next" . 유지되는 구조 참조와 값: size = 3 , head 는 index 0 그대로. 값 "head" 와 head 필드는 다름.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
탐색 시작

current를 첫 노드로 초기화합니다.

  • 체인: node→next→head→null
  • current: node/index0
  • head 필드: 첫 node
  • 탐색용 지역 변수와 목록 소유 참조를 구분한다.
한 칸 이동

current.next를 읽어 둘째 노드로 이동합니다.

  • current: next/index1
  • head 필드: 첫 node 유지
  • 조회가 head를 재대입하지 않는다.
결과 읽기

index 1의 값을 반환합니다.

  • 조회 값: next
  • size: 3
  • 전체 연결: 그대로
  • 현재 위치만 이동하여 목록 구조를 보존한다.

원문의 세 값은 "node", "next", "head"입니다. 마지막 값의 문자열 "head"와 첫 노드를 가리키는 필드 head는 서로 다른 뜻입니다. get(1)에서 이동하는 지역 변수 current는 첫 노드의 next를 한 번 따라 두 번째 값 "next"를 반환합니다. 이때 목록의 head 필드에 대입하는 문장은 없어 다음 조회도 첫 노드에서 시작합니다.

순회 위치 예측하기

같은 목록에서 get(2)를 이어 호출하면 next 이동은 몇 번이고, 결과와 head가 가리키는 값은 각각 무엇인가요? get(3)도 정상 결과를 주는지 범위 조건으로 판단하세요.

지역 커서와 목록 시작점 확인

새 current를 head로 초기화한 뒤 두 번 이동해 문자열 "head"를 반환합니다. 목록의 head는 여전히 값 "node"인 첫 노드를 가리킵니다. 크기 3에서 인덱스 3은 존재하지 않으므로 범위 검사에서 거부해야 합니다. null까지 이동한 뒤 next를 읽어 발견하는 것보다 공개 메서드의 경계에서 실패 원인을 분명히 보여 줍니다.

따라서 연결 목록 get의 비용은 위치에 비례합니다.

마지막 원소 조회를 반복하는 코드가 있다면 각 호출마다 전체 체인을 걷는다는 점을 놓치지 않습니다.


null 종단 표식

빈 목록의 head는 null이며, 비어 있지 않은 목록의 마지막 Node의 next도 null입니다.

두 의미를 문맥으로 구분합니다.

toString은 current가 null이 되면 종료합니다. 공개 get은 범위를 검사한 뒤 getNode를 호출하고, 비어 있지 않은 목록의 add는 size - 1로 끝 노드를 찾습니다.

size와 실제 Node 수가 어긋나면 경계 검사를 통과했는데 current가 null이 되는 내부 결함이 발생합니다.

이 구현의 add는 연결이 성공한 뒤 size를 늘립니다. 다음 절에서 remove를 추가할 때도 연결 변경 뒤 size를 줄여야 합니다.


끝 노드 탐색과 끝 추가의 비용

꼬리 노드 필드가 없는 이 구현은 끝 추가 때 getNode(size - 1)로 head부터 마지막 Node까지 이동합니다.

끝 추가 한 번은 O(n)이며 n개를 연달아 넣으면 전체 순회가 커집니다.

이 버전은 연결 원리를 드러내는 데는 좋지만 대량 끝 추가의 최종 설계는 아닙니다.

순환 참조가 생기면 null에 도달하지 못하므로 toString도 끝나지 않습니다.

직접 구현을 확장할 때는 방문 Node 수가 size를 넘는지 진단하거나 Floyd 순환 탐지 같은 별도 검사를 고려할 수 있습니다.


연결 순서 출력

CLI 결과는 여전히 입력 순서와 합계를 보여 줍니다.

연결 저장소로 바꾸더라도 사용자는 next나 head를 알 필요가 없습니다.

이 분리는 자료 구조 내부 참조와 애플리케이션 명령을 섞지 않게 합니다.

app/PostRepositoryCliCH144.java
public final class PostRepositoryCliCH144 {
    public static void main(String[] args) {
        PostRepository repository = new PostRepository();
        repository.add("node-chain", 38);
        repository.add("linear-walk", 47);
        repository.print();
        System.out.println("total=" + repository.totalViewCount());
    }

    private static final class PostRepository {
        private Entry[] entries = new Entry[2];
        private int size;

        void add(String title, int viewCount) {
            if (title == null || title.isBlank()) throw new IllegalArgumentException("title");
            if (viewCount <= 0) throw new IllegalArgumentException("viewCount");
            ensureCapacity(size + 1);
            entries[size++] = new Entry(title, viewCount);
        }

        private void ensureCapacity(int required) {
            if (required <= entries.length) return;
            Entry[] grown = new Entry[Math.max(required, entries.length * 2)];
            System.arraycopy(entries, 0, grown, 0, size);
            entries = grown;
        }

        int totalViewCount() {
            int total = 0;
            for (int i = 0; i < size; i++) {
                total += entries[i].viewCount();
            }
            return total;
        }

        void print() {
            for (int i = 0; i < size; i++) {
                Entry entry = entries[i];
                System.out.println(i + ":" + entry.title() + "=" + entry.viewCount());
            }
        }
    }

    private record Entry(String title, int viewCount) {}
}

이 CLI는 배열을 사용하지만 출력 규칙은 연결 목록에서도 그대로 적용됩니다.

연결 저장소를 실험할 때 head부터 방문한 Entry 수가 size와 같고 마지막 next가 null인지 먼저 검사합니다.

이 CLI는 배열의 사용 구간에 있는 조회수 38과 47을 합해 85를 냅니다. 연결 저장소로 바꾸는 실험에서는 각 Node를 한 번씩 방문하는지 별도로 확인합니다.


단방향 연결이 유리한 연산

질문관찰할 값선택 또는 조치
임의 조회가 많은가평균 index배열 목록 우선
끝 참조를 저장하는가끝 추가 횟수꼬리 노드 필드 검토
노드 오버헤드가 허용되는가원소당 참조 수메모리 측정
순회 중 변경하는가구조 수정 횟수반복자 정책 필요

연결 목록은 “삽입 O(1)”로만 선택할 수 없습니다.

삽입 위치의 Node를 찾는 시간이 포함되면 전체가 O(n)입니다.

이미 Node 참조를 가진 알고리즘인지, 임의 조회가 필요한지 먼저 묻습니다.


연습 문제

세 Node를 직접 연결하고 head부터 순회해 개수와 마지막 값을 출력하세요.

head가 null인 입력도 처리할 수 있는 메서드로 분리합니다.

정답과 해설

current를 이동하기 전에 last를 갱신하면 반복 종료 뒤 마지막 방문 값을 보존할 수 있습니다.

빈 체인의 last는 null로 남습니다.

exercise/NodeChainSummarySolution.java
public final class NodeChainSummarySolution {
    public static void main(String[] args) {
        Node head = new Node("node", new Node("next", new Node("end", null)));
        Summary result = summarize(head);
        System.out.println("count=" + result.count() + ", last=" + result.last());
    }

    private static Summary summarize(Node head) {
        int count = 0;
        String last = null;
        for (Node current = head; current != null; current = current.next()) {
            count++;
            last = current.value();
        }
        return new Summary(count, last);
    }

    private record Node(String value, Node next) {}

    private record Summary(int count, String last) {}
}

결과는 count=3, last=end입니다.

summarize는 head를 재대입하지 않아 호출자의 체인을 손상하지 않습니다.


단방향 순회 종료 기준

빈 목록의 head는 null, 비어 있지 않은 목록의 마지막 next도 null입니다.

getNode(i)는 i번만 이동하고 공개 get이 먼저 범위를 검사합니다.

구조를 읽는 메서드가 head나 next를 변경하지 않는지 확인하면 단방향 순회의 기본 규칙이 완성됩니다.