3장 배열, 연결 리스트, 재귀(1)

2023. 12. 16. 21:46프로그래밍 공부/Data Structure

3.1 배열 사용

이 섹션에서는 섹션 1.5에서 소개한 배열의 몇 가지 응용 분야에 대해 알아본다

 

3.1.1 배열에 게임 엔트리 저장

우리가 연구하는 첫 번째 응용 프로그램은 엔트리를 배열에 저장하는 것인데, 

특히 비디오 게임을 위한 고득점 엔트리이다. 

배열에 엔트리를 저장하는 것은 일반적인 방법이며, 

병원에 있는 환자들의 기록이나

자료 구조 수업에서 학생들의 이름을 쉽게 저장할 수 있었다. 

대신 고득점 엔트리를 저장하기로 결정했는데, 

이는 이 책에서 다른 구현에 사용할

몇 가지 중요한 자료 구조 개념을 보여주는 간단한 응용 프로그램이다.


우리는 우선 고득점 항목에 무엇을 포함시키기를 원하는지 자문하는 것으로 시작해야 한다. 

score: 점수 자체를 나타내는 정수

name: 점수를 획득한 사람의 이름

여기서부터는 점수를 획득한 날짜를 나타내는 필드 또는

점수로 이어진 게임 통계를 추가할 수 있다. 

그러나 예제를 간단히 유지하고 score와 name의 두 가지 필드를 입력하자. 

코드 조각 3.1에서 게임 항목을 나타내는 자바 클래스인 GameEntry를 보여준다.


코드 조각 3.1: 간단한 GameEntry 클래스에 대한 Java 코드이다. 

게임 엔트리 객체의 이름과 점수를 반환하는 방법과

이 엔트리의 문자열 표현을 반환하는 방법이 있다.

public class GameEntry {
  protected String name;   // name of the person earning this score
  protected int score;     // the score value
  /** Constructor to create a game entry */
  public GameEntry(String n, int s) {
    name = n;
    score = s;
  }
  /** Retrives the name field */
  public int getName() { return name; }
  /** Retrives the score field */
  public int getScore() { return score; }
  /** Returns a string representation of this entry */
  public String toString() {
    return "(" + name + ", " + score + ")";
  }
}

 

높은 점수를 위한 클래스

우리가 엔트리라는 배열에 저장하고 싶은 높은 점수가 있다고 가정해보자. 

저장하고 싶은 점수의 수는 10, 20, 50이 될 수 있으므로 

저장하고 싶은 점수의 수를 나타내는 maxEntries라는 기호 이름을 사용해 보자. 

물론 이 변수를 특정 값으로 설정해야 하지만 필요한 경우 

코드 전체에 걸쳐 이 변수를 사용함으로써 나중에 값을 쉽게 변경할 수 있다. 

그런 다음 배열, 엔트리를 길이 maxEntries의 배열로 정의할 수 있다. 

처음에 이 배열은 null entries만 저장하지만 

사용자가 비디오 게임을 할 때 entries 배열을 엔트리로 채우지만 

사용자가 비디오 게임을 할 때 새로운 GameEntry 객체에 대한 참조로 entries 배열을 채울 것이다. 

따라서 결국 entries 배열의 GameEntry 참조를 업데이트하는 방법을 정의해야 할 것이다.


entries 배열을 구성하는 방법은 간단하다. 

GameEntry 객체 집합을 정수 score 값에서 최고에서 최저로 순서대로 저장한다. 

GameEntry 객체의 수가 maxEntries보다 적으면

entries 배열 끝에 null 참조를 저장하도록 한다. 

이 접근 방식은 인덱스 0부터 게임 엔트리를 저장하는

배열 entries의 연속적인 셀에 빈 셀 또는 "구멍"이 없도록 한다. 

저희는 그림 3.1의 자료 구조 예를 보여주고

코드 조각 3.2에는 이러한 자료 구조에 대한 자바 코드를 제공한다.


그림 3.1: 인덱스 0부터 5까지의 셀에 있는

6개의 GameEntry 객체에 대한 참조를 저장하는

길이 10개의 배열을 보여준다.

나머지는 null 참조이다.


코드 조각 3.2: GameEntry 객체로 점수 집합을 유지하기 위한 클래스이다.

/** Class for storing high scores in an array in non-decreasing order. */
public class Scores {
  public statc final int maxEntries = 10; // number of high scores we keep
  protected int numEntries;          // number of actual entries
  protected GameEntry[] entries;     // array of game entries (names & scores)
  /** Default Constructor */
  public Scores() {
    entries = new GameEntry[maxEntries];
    numEntries = 0;
  }
  /** Returns a string representation of the high scores list */
  public String toString() {
    String s = "[";
    for (int i = 0; i < numEntries; i++) {
      if (i > 0) s += ", "; //seperate entries by commas
      s += entries[i];
    }
    return s + "]";
  }
  // ... methods for updating the set of high scores go here ...
}



 여기에는 entries 배열에서 높은 점수의 문자열 표현을 생성하는 toString()이라는 메소드가 포함되어 있다. 

이 방법은 디버깅 목적으로 상당히 유용하다. 

이 경우 문자열은 entries 배열에 있는 GameEntry 객체를 쉼표로 구분하여 나열한 것이 된다. 

간단한 for문을 사용하여 이 리스트을 생성하고,

첫 번째 항목 뒤에 오는 각 항목 바로 앞에 쉼표를 추가한다. 

이러한 문자열 표현을 사용하면 디버깅하는 동안 

엔트리 배열의 상태를 출력하여 업데이트 전후 상황을 테스트할 수 있다.

 

삽입

높은 점수의 entries 배열에 대해 업데이트하고 싶은 가장 일반적인 것 중 하나는

새로운 게임 엔트리를 추가하는 것이다.

따라서 새로운 GameEntry 객체 e를 삽입하고 싶다고 가정해 보겠다.

특히 Scores 클래스의 인스턴스에 대해

다음 업데이트 작업을 수행하는 방법을 고려해 보겠다:


add(e) : 게임 엔트리 e를 높은 점수들의 모음집에 삽입한다. 

모음집이 가득 차 있을 경우 

집합 내 최저득점 이상일 경우에만 e가 추가되며, 

이 경우 e는 엔트리를 최저 점수로 대체한다.


이 작업을 구현하는 데 있어 주요 과제는

entries 배열에서 어디로 가야 하는지를 파악하고 공간을 확보하는 것이다.


게임 항목 삽입 시각화

이 삽입 프로세스를 시각화하려면 

가장 높은 점수를 받은 객체부터 가장 낮은 점수를 받은 객체까지

왼쪽에서 오른쪽으로 나열된 

null이 아닌 GameEntry 객체에 대한 참조를 나타내는

배열 entries 리모컨에 저장한다고 가정한다.


새로운 게임 엔트리 e가 주어졌을 때,

우리는 그것이 어디에 속하는지 알아내야 한다. 

 

1. 우리는 이 탐색을 entries 배열의 끝에서 시작한다. 

만약 이 배열의 마지막 참조가 null이 아니고 

그 참조의 점수가 e의 점수보다 크다면, 

우리는 즉시 중지할 수 있다. 

예를 들어, 이 경우, e는 높은 점수가 아니다. 

그것은 entries 배열에 전혀 속하지 않다. 

그렇지 않으면, 우리는 e가 배열에 속한다는 것을 알고, 

entries 배열의 마지막 참조가 더 이상 거기에 속하지 않는다는 것도 알고 있다.

 

2. 다음으로, 배열의 마지막에서 두번째 참조로 넘어간다.

만약 이 참조가 null이거나

그것이 e보다 낮은 점수를 가진 GameEntry 객체를 가리키면,

이 참조는 entries 배열의 오른쪽으로 한 셀 이동되어야 한다.

 

3. 게다가, 만약 우리가 이 참조를 이동시킨다면,

우리는 entries 배열의 시작에 도달하지 않았다면,

우리는 이를 다음 참조와 비교해야 한다.

 

4. 우리는 entries 배열의 시작에 도달하거나

또는 e의 점수를 더 높은 점수를 가진 게임 엔트리와 비교할 때까지

계속해서 게임 엔트리에 대한 참조를 비교하고 이동한다.

어느 경우든, 우리는 e가 속한 장소를 식별할 것이다.

 

(그림 3.2 참조)
그림 3.2: entries 배열에 새 GameEntry 객체를 추가할 준비를 하고 있다. 

새 참조를 위한 공간을 마련하기 위해 

새 참조보다 점수가 작은 게임 엔트리에 대한 참조를

한 셀 오른쪽으로 이동해야 한다.



entries 배열에서 새로운 게임 엔트리 e가 속한 위치를 확인한 후

이 위치에서 e에 대한 참조를 추가한다. 

즉, 객체 참조를 리모컨으로 시각화하는 작업을 계속하여 

entries 배열의 이 위치에 e에 맞게 특별히 설계된 리모컨을 추가한다(그림 3.3 참조)

 

그림 3.3: 엔트리 배열에 새 GameEntry 객체에 대한 참조를 추가한다. 

새 참조보다 점수가 적은 GameEntry 객체에 대한 참조를

모두 오른쪽으로 이동시켰기 때문에 

이제 인덱스 2에서 참조를 삽입할 수 있다.



새 게임 엔트리 e를 entries 배열에 추가하는 알고리즘에 대한 자세한 내용은

이 비공식적인 설명과 비슷하며, 

코드 조각 3.3의 자바 언어로 제공된다. 

참고로 참조를 방해하지 않도록 반복문을 사용한다. 

이 반복문을 수행하는 횟수는

새 게임 엔트리에 참조할 공간을 확보하기 위해

이동해야 하는 참조 수에 따라 달라진다. 

이동할 참조가 0, 1, 또는 몇 개만 있으면 이 add 메소드는 상당히 빠르다. 

하지만 이동할 참조가 많으면 이 방법은 상당히 느려질 수 있다. 

또한 배열이 꽉 차서 add를 수행하면 

현재 마지막 게임 엔트리에 대한 참조를 제거하거나

새 게임 엔트리에 대한 참조를 추가하지 못한다.
코드 조각 3.3: GameEntry 객체를 삽입하기 위한 자바 코드.

/** Attempt to add a new score to the collection (if it is high enough) */
public void add(GameEntry e) {
  int newScore = e.getScore();
  // is the new entry e really a high score?
  if (numEntries == maxEntries) { // the arrray is full
    if (newScore <= entries[numEntries-1].getScore())
      return; // the new entry e, is not a high score in this case
  }
  else // the array is not full
    numEntries++;
  // Locate the place that new (high score) entry e belongs
  int i = numEntries - 1;
  for( ; (i >=1)&&(newScore > entries[i-1].getScore()); i--)
    entries[i] = entries[i-1];   // move entry i one to the right
  entries[i] = e;                // add the new score to entries
}


객체 제거

어떤 핫샷이 우리 비디오 게임을 플레이하고

우리의 고득점 리스트에 그 혹은 그녀의 이름이 올라왔다고 가정해보자. 

이 경우, 우리는 고득점 리스트에서 게임 엔트리를 제거할 수 있는 방법을 갖고 싶다. 

그러므로 entries 배열에서 GameEntry 객체에 대한 참조를 제거할 수 있는 방법을 고려해 보겠다. 

즉, 우리가 다음 작업을 어떻게 구현할 수 있는지 고려해 보겠다:


remove(i): entries 배열의 인덱스 i에서 게임 엔트리 e를 제거하고 반환한다. 

인덱스 i가 entries 배열의 경계 밖에 있으면 이 메소드는 예외를 던진다. 

그렇지 않으면 엔트리 배열이 업데이트되어 인덱스 i의 객체를 제거하고 

이전에 인덱스 i보다 높은 인덱스에 저장된 모든 객체가 제거된 객체를 채우기 위해 "이동"된다.


remove를 위한 우리의 구현은

객체 추가를 위한 알고리즘을 수행하는 것과 비슷하지만, 그 반대이다. 

다시, 우리는 entries 배열을

GameEntry 객체를 가리키는 리모컨 배열로 시각화할 수 있다. 

인덱스 i에서 객체에 대한 참조를 제거하기 위해, 

우리는 인덱스 i에서 시작하여

인덱스 i보다 높은 하나의 셀의 모든 참조를 왼쪽으로 이동시킨다.

(그림 3.4 참조)


그림 3.4:

GameEntry 객체에 대한 참조를 저장하는

배열의 인덱스 3에서 제거하는 그림.


엔트리 제거에 대한 몇 가지 미묘한 점

1. 배열의 인덱스 i에서 게임 엔트리 e를 제거하고 반환하기 위해서는 

먼저 e를 임시 변수에 저장해야 한다는 것이다. 

제거가 끝나면 이 변수를 사용하여 e를 반환할 것이다. 

 

2.  i보다 높은 참조를 왼쪽으로 이동시킬 때

배열의 끝까지 가지 않고 

두 번째 참조에서 마지막 참조로 중단한다는 것이다. 

마지막 참조는 오른쪽 참조가 없으므로 종료 직전에 중단한다. 

entries 배열의 마지막 참조는 단순히 제외하면 된다. 

제거된 엔트리에 대한 참조를 반환하는 것으로 결론을 내릴 수 있다. 

 

코드 조각 3.4를 참조하자.
코드 조각 3.4: remove 작업을 수행하기 위한 자바 코드.

/** Remove and return the high score at index i */
public GameEntry remove(int i) throws IndexOutOfBoundsException{
  if ((i < 0) || (i >= numEntries))
    throw new IndexOutOfBoundsException("Invalid index: " + i);
  GameEntry temp = entries[i];   // temporarily save the object to be removed
  for(int j = i ; j < numEntries - 1; j++)  // count up from i (not down)
    entries[j] = entries[j+1];   // move one cell to the left
  entries[numEntries - 1] = null;    // null out the old last score
  numEntries--;
  return temp;                // return the removed object
}



높은 점수의 배열에서 객체를 추가하고 제거하는 이러한 메소드들은 간단하다. 

그럼에도 불구하고 이들은

반복적으로 더 정교한 자료 구조를 구축하기 위해

사용되는 기법의 기초를 형성한다. 

물론 이러한 다른 구조들은 위의 배열 구조보다 더 일반적일 수 있으며, 

종종 단순한 추가 및 제거보다 훨씬 더 많은 연산을 수행할 수 있다. 

하지만 모든 자료 구조는 구체적인 방법을 사용하여 구현되어야 하기 때문에 

지금처럼 배열 자료 구조를 연구하는 것은

이러한 다른 구조를 이해하는 데 큰 출발점이 된다.


사실 이 책의 후반부에서는 

여기서 공부하고 있는 배열 구조보다 

더 일반적인 자바 컬렉션 클래스인

ArrayList에 대해 공부할 것이다. 

ArrayList에는 배열로 하기를 바라는 많은 작업을 수행하는 동시에 

객체를 전체 배열에 추가할 때 발생하는 오류를 제거하는 메소드이 있다. 

ArrayList는 필요에 따라 객체를 더 큰 배열로 자동 복사함으로써

이러한 오류를 제거한다. 

그러나 여기서 이 과정에 대해 논의하기보다는 

ArrayList에 대해 자세히 설명할 때

어떻게 수행되는지에 대해 자세히 설명하겠다.

 

3.1.2 배열 정렬

앞 절에서 우리는 배열에서 어떤 인덱스 i에 있는 객체들의 이전 순서를 그대로 유지하면서 

어떻게 객체들을 추가하거나 제거할 수 있는지를 보여주기 위해 열심히 노력했다. 

정렬(sorting) 문제: 순서가 맞지 않는 물체들을 배열하는 것부터 시작해서 순서를 정하는 방법

 

간단한 삽입 정렬 알고리즘

우리는 이 책에서 정렬 알고리즘을 공부하는데, 대부분은 11장에 나와 있다. 

이 절에서 정리하자면,

우리는 삽입 정렬(insertion-sort)이라는 멋지고 간단한 정렬 알고리즘을 설명한다. 

이 경우 우리는 입력이 비슷한 요소들의 집합인 알고리즘의 특정한 버전을 설명한다. 

우리는 이 책의 후반부에서 더 일반적인 정렬 알고리즘을 고려한다.

이 간단한 삽입 정렬 알고리즘은 다음과 같다. 

1. 우리는 배열의 첫 번째 문자부터 시작한다. 

하나의 문자는 이미 정렬되어 있다. 

 

2. 그리고 우리는 배열의 다음 문자를 고려한다.

만약 이것이 첫 번째 문자보다 작으면, 우리는 그것들을 바꾼다. 

 

3. 우리는 배열에서 세 번째 문자를 고려한다. 

우리는 그것이 처음 두 문자와 같은 순서가 될 때까지 왼쪽으로 바꾼다. 

 

4. 그리고 나서 우리는 네 번째 문자를 고려하고, 

그것이 처음 세 문자와 같은 순서가 될 때까지 왼쪽으로 바꾼다. 

 

5. 우리는 배열 전체가 정렬될 때까지 계속해서

다섯 번째 정수, 여섯 번째 정수 등을 사용한다. 

 

우리는 이 비공식적인 설명을 프로그래밍 구조와 혼합하여 

코드 조각 3.5와 같은 삽입 정렬 알고리즘을 표현할 수 있다.

 

코드 조각 3.5: 삽입 정렬 알고리즘에 대한 높은 수준의 설명.

Algorithm InsertionSort(A):
  Input: An array A of n comparable elements
  Output: The array A with elements rearranged in non-decreasing order
  for i ←1 to n - 1 do
     Insert A[i] at its proper location A[0], A[1], ..., A[i-1].


이것은 삽입 정렬에 대한 좋은 높은 수준의 설명이다. 

또한 이 알고리즘이 왜 "삽입 정렬"라고 불리는지를 보여주는데, 

메인을 반복할 때마다 배열의 정렬된 부분에 다음 요소가 삽입되기 때문이다. 

그러나 이 설명을 코드화하기 전에

이 삽입 작업을 어떻게 수행하는지에 대한 자세한 내용을 파악해야 한다.


자세한 내용을 좀 더 자세히 살펴보면, 

이제 두 개의 중첩된 루프를 사용하기 위해 설명을 다시 작성해 보겠다. 

외부 루프는 배열의 각 원소를 차례로 고려하고 

내부 루프는 왼쪽에 있는 문자의 부분 배열과 함께 원소를 적절한 위치로 이동시킬 것이다.


삽입 정렬에 대한 세부 정보 다듬기

세부 사항을 정리하면 코드 조각 3.6과 같이 알고리즘을 설명할 수 있다.
코드 조각 3.6: 삽입 정렬 알고리즘의 중간 수준의 설명.

Algorithm InsertionSort(A):
  Input: An array A of n comparable elements
  Output: The array A with elements rearranged in non-decreasing order
  for i ← 1 to n - 1 do
     {Insert A[i] at its proper location A[0], A[1], ..., A[i-1]}
     cur ← A[i]
     j ← i - 1
    while j >= 0 and a[j] > cur do
        A[j+1] ← A[j]
        j ← j - 1
    A[j + 1] ← cur { cur is now in the right place}


 이 설명은 A[i] 원소를 앞에 오는 부분 배열에 삽입하는 방법을 더 잘 설명하기 때문에

실제 코드에 훨씬 더 가깝다. 

그것은 여전히 움직이는 원소가 순서가 어긋날 경우에 대한 

비공식적인 설명을 사용하지만, 

이것은 대단히 어려운 일이 아니다.


삽입 정렬에 대한 자바 코드

이제 우리는 삽입 정렬 알고리즘의 단순한 버전에 대한

자바 코드를 부여할 준비가 되었다. 

A가 문자 집합 a일 때 특별한 경우에 대해서

코드 조각 3.7에서 이러한 설명을 한다.
코드 조각 3.7: 문자 배열에 삽입 정렬을 수행하기 위한 자바 코드.

/** Insertion sort an array of characters into non-decreasing order */
public static void insertionSort(char[] a){
  int n = a.length;
  for(int i = 1; i < n; i++) {       // index from the secont character in a
     char cur = a[i];                // the current character to be inserted
     int j = i - 1;                  // start comparing with cell left of i
    while((j >= 0) && (a[j] > cur))  // while a[j] is out of order with cur
        a[j+1] = a[j--];             // move a[j] right and decrement j
    a[j + 1] = cur                   // this is the proper place for cur
  }
}


그림 3.5에서 삽입 정렬 알고리즘의 예제 실행을 보여준다. 

그림 3.5: 8개의 문자 배열에서 삽입 정렬 알고리즘의 실행을 보여준다.
배열의 완성된 부분을 흰색으로 분류하고

배열의 정렬된 부분에 삽입되는 다음 요소를 밝은 파란색으로 칠한다. 

또한 cur 변수에 저장되어 있으므로 왼쪽에 해당 문자를 강조 표시한다. 

각 행은 외부 루프의 반복에 해당하고

행에 있는 배열의 각 복사본은 내부 루프의 반복에 해당한다. 

각 비교는 호로 표시된다. 

또한 비교 결과가 이동했는지 여부를 나타낸다.


배열이 이미 정렬되어 있으면

삽입 정렬 알고리즘에서 흥미로운 일이 발생한다.

이 경우 내부 루프는 한 번의 비교만 수행하여 다음을 결정한다
스왑이 필요하지 않고 다시 외부 루프로 돌아간다. 

즉, 외부 루프를 반복할 때마다 내부 루프를 한 번만 반복한다. 

따라서 이 경우에는 최소한의 비교 작업을 수행한다. 

물론 입력 배열이 극도로 고장나면

이보다 훨씬 더 많은 작업을 수행해야 할 수도 있다. 

사실 입력 배열이 감소하는 순서라면

가장 많은 작업을 수행해야 할 것이다.


3.1.3 배열 및 난수에 대한 java.util 메서드

배열는 매우 중요하기 때문에 

자바는 배열에서 일반적인 작업을 수행하기 위한 여러 가지 내장 메소드를 제공한다. 

이러한 메소드는 java.util.Arrays 클래스에 static 메소드로 나타난다. 

즉, 이 메소드들은 클래스인 java.util.Arrays 자체와 연결되며 

이 클래스의 특정 인스턴스와는 연결되지 않는다. 

그러나 이러한 메소드 중 일부를 설명하는 것은 

이 책의 후반부(이러한 메소드의 기반이 되는 개념에 대해 논의할 때)까지 기다려야 한다.


java.util.Arrays의 몇 가지 간단한 방법

아래는 클래스 java.util.Arrays의 몇 가지 간단한 방법을 나열한다:


equals (A, B): 배열 A와 배열 B가 같은 경우에만 true를 반환한다. 

두 배열의 원소 수가 같고 두 배열의 원소 쌍이 모두 같은 경우에는

두 배열이 동일하다고 간주된다. 

즉, A와 B는 같은 순서로 같은 원소를 갖는다.


fillA,x): 요소 x를 배열 A의 모든 셀에 저장한다. 

 

sort(A): 배열 A의 자연스러운 순서를 사용하여 배열 A의 요소들을 정렬한다.


toString(A): 배열 A의 String 표현을 반환한다.


예를 들어, 다음 문자열은 정수 A = [4,5,2,3,5,7,10]의 배열에서

호출된 toString 메소드에 의해 반환된다:
[4, 5, 2, 3, 5, 7, 10]


위 리스트에서 자바에는 정렬 알고리즘이 내장되어 있음을 주목하자. 

그러나 이것은 우리가 위에서 제시한 삽입 정렬 알고리즘이 아니다. 

이것은 보통 삽입 정렬보다 훨씬 빠르게 실행되는 퀵 정렬 알고리즘이다. 

우리는 11.2절에서 퀵 정렬 알고리즘에 대해 설명한다.

 

의사 난수를 사용한 예제

코드 조각 3.8에서는

위의 방법을 사용하는 짧은(하지만 완전한) 자바 프로그램을 보여준다.
코드 조각 3.8:

Arrays 클래스의 다양한 내장 방법을 사용하는

테스트 프로그램 ArrayTest.

 

import java.util.Arrays;
import java.util.Random;
/** Program showing some array uses. */
public class ArrayTest {
  public static void main(String[] args) {
    int num[] = new int[10];
    Random rand = new Random(); // a pseudo-random number generator
    rand.setSeed(System.currentTimeMillis()); //use current time as seed
    // fill the num array with pseudo-random numbers from 0 to 99, inclusive
    for(int i = 0; i < num.length; i++)
      num[i] = rand.nextInt(100); // the next pseudo-random number
    int[] old = (int[]) num.clone();   // cloning the num array
    System.out.println("arrays equal before sort: " + Arrays.equals(old,num));
    Arrays.sort(num); // sorting the num array (old is unchanged)
    System.out.println("arrays equal after sort: " + Arrays.equals(old,num));
    System.out.println("old = " + Arrays.toString(old));
    System.out.println("num = " + Arrays.toString(num));
  }
}

 

프로그램 ArrayTest는 자바의 또 다른 기능인 의사난수 기능을 사용한다.

의사 난수(pseudo-random number): 실제로 랜덤하지는 않지만 통계적으로 랜덤한 수

프로그램 ArrayTest는

특히 java.util.Random객체를 사용하는데, 이것은 의사난수 생성기이다. 

의사 난수 생성기(pseudo-random number generator): 통계적으로 랜덤한 수들을 계산 또는 "생성"하는 객체

 

시드(seed): 생성기에서 랜덤한 수들의 생성을 시작하는 장소

주어진 시드에 대해 생성되는 수들의 시퀀스는 항상 동일할 것이다. 

프로그램에서는 시드를 

1970년 1월 1일 이후(System.CurrentTimeMillis 방법을 사용) 

현재 시간으로 밀리초 단위로 설정했는데, 

이 시간은 우리가 프로그램을 실행할 때마다 다를 것이다.

 

시드를 설정하고 나면 

인수 100과 함께 nextInt 메소드를 호출하여

0과 99 사이의 난수를 반복적으로 얻을 수 있다. 

아래는 이 프로그램의 출력 예를 보여준다:
  arrays equal before sort: true
  arrays equal after sort: false
  old = [41,38,48,12,28,46,33,19,10,58]
  num = [10,12,19,28,33,38,41,46,48,58]

 

그런데 숫자가 분류된 후에도, 

즉 숫자가 복제되기 전에 이미 분류된 경우에도 

기존 배열과 숫자 배열이 동일하게 유지될 가능성은 조금 있다. 

하지만 이런 일이 일어날 확률은 400만 분의 1도 되지 않는다.


3.1.4 문자열 및 문자 배열을 사용한 단순 암호화

배열의 주요 응용 중 하나는 문자열을 문자의 배열로 표현하는 것이다. 

즉 문자열 객체는 보통 내부에 문자의 배열로 저장된다. 

문자열이 다른 방법으로 표현될 수 있더라도 

문자열과 문자 배열 사이에는 자연스러운 관계가 있으며 

둘 다 문자열을 나타내는 인덱스를 사용한다. 

이러한 관계로 인해 Java는 문자열 객체를 문자 배열에서 쉽게 만들 수 있으며, 

문자열 객체를 문자 배열 A에서 쉽게 만들 수 있다. 

특히 String 클래스의 객체를 문자 배열 A에서 만들려면

다음과 같은 식을 간단히 사용한다,
new String(A)

 

즉, String 클래스에 대한 생성자 중 하나는 

문자 배열을 인수로 사용하고 

배열과 같은 순서로 같은 문자를 가진 문자열을 반환한다. 

 

예를 들어, 배열 A = [a, c, a, t]에서 만들 문자열은 acat이다. 

마찬가지로 문자열 S가 주어지면

다음과 같은 식을 사용하여 S의 문자 배열을 만들 수 있다,
S.toCharArray()
즉, String 클래스에는 toCharArray라는 메소드가 있다.

toCharArray 메소드: S와 같은 문자를 가진 배열(char[] 타입의 배열)을 반환한다. 

예를 들어, 문자열 adog에서 CharArray로 호출하면

배열 B = [a, d, o, g]가 나온다.

 

카이사르 암호

문자열에서 문자 배열로, 

그리고 다시 문자 배열로 전환하는 것이 유용한 한 분야는 암호학이다.

암호학(cryptography): 비밀 메시지의 과학과 그 응용에 대한 학문

암호학은 암호화와 복호화를 수행하는 방법을 연구한다.

 

암호화(encryption): 평문 암호문

복호화(decryption): 암호문 평문

평문(plaintext): 일반적인 메시지

암호문(ciphertext): 뒤섞여진 메시지


아마도 가장 초기의 암호화 방법은 카이사르 암호일 것이다.

카이사르 암호(Caesar cipher)

  • 중요한 군사 메시지를 보호하기 위해 이 방법을 이용한 율리어스 카이사르(Julius Caesar)의 이름을 땄다.
  • 카이사르 암호는 알파벳으로 단어를 형성하는 언어로 쓰여진 메시지를 숨기는 간단한 방법이다.
    카이사르 암호는 메시지의 각 문자를 해당 언어의 알파벳 순서대로 세 글자 뒤에 오는 문자로 바꾸는 것을 포함한다. 

       1) 그래서 우리는 영어 메시지에서 각 A를 D로, 각 B를 E로, 각 C를 F로 바꾸는 것이다.

       2) 우리는 이 접근법을 Z로 치환된 W까지 계속한다. 

       3) 그리고 나서 치환 패턴을 둘러싸도록 하여(wrap around) X를 A로, Y를 B로, Z를 C로 치환한다.


문자를 배열 인덱스로 사용

배열 인덱스와 같이 문자의 번호를 매기고, 

A는 0, B는 1, C는 2이라면, 

우리는 카이사르 암호를 간단한 공식으로 쓸 수 있다:
각 문자 i를 문자 (i + 3) mod 26으로 대체한다,

 

mod:  정수 나눗셈을 한 후 나머지를 반환하는 모듈러스(modulus) 연산자

이 연산자는 자바에서 %로 표시되며, 

알파벳 끝에 있는 wrap around를 쉽게 수행하는 데 필요한 연산자가 바로 그것이다. 

예시)

26 mod 26 = 0

27 mod 26 = 1

28 mod 26 = 2

 

카이사르 암호의 암호 해독 알고리즘은 그 반대이다. 

우리는 각 문자를 앞의 세 자리로 바꾸고, A, B, C를 wrap around를 한다.


암호화 및 복호화를 위해

배열을 사용하여 이 대체 규칙을 포착할 수 있다. 

자바의 모든 문자는 실제로 유니코드 값인 숫자로 저장되므로

문자를 배열 인덱스로 사용할 수 있다. 

예를 들어 대문자 c의 경우

유니코드 값인 c를 사용하고 A를 빼면 배열 인덱스로 c를 사용할 수 있다. 

물론 이것은 대문자에 대해서만 작동하므로 비밀 메시지는 대문자여야 한다. 

그런 다음 암호화 대체 규칙을 나타내는 배열 encrypt를 사용하여

encrypt[i]를 암호화하면 문자 번호 i를 대체하는 문자가 된다

(유니코드에서 대문자 c는 A). 

이 사용법은 그림 3.6에 나와 있다. 

마찬가지로 배열인 decrypt는 복호화 대체 규칙을 나타낼 수 있으므로 

decrypt[i]는 문자 번호 i를 대체하는 문자가 된다.


그림 3.6: 카이사르 암호화를 위한 대체 규칙을 수행하기 위해 

대문자를 배열 인덱스로 사용하는 방법을 보여준다.


코드 조각 3.9에서는

카이사르 암호를 수행하기 위한 단순하고 완전한 자바 클래스를 제공하는데, 

위의 접근 방식을 사용하고 문자열과 문자 배열 간의 변환도 사용한다. 

이 프로그램을 실행하면 다음과 같은 출력이 나온다:
 Encryption order = DEFGHIJKLMNOPQRSTUVWXYZABC
 Decryption order = XYZABCDEFGHIJKLMNOPQRSTUVW
 WKH HDJOH LV LQ SODB; PHHW DW MRH'V.

 THE EAGLE IS IN PLAY; MEET AT JOE'S.


코드 조각 3.9: 카이사르 암호를 위한 단순하고 완전한 자바 클래스이다.

/** Class for doing encryption and decryption using the Caesar Cipher. */
public class Caesar {
  public static final int ALPHASIZE = 26; // English alphabet (uppercase only)
  public static final char[] alpha = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
  'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
  protected char[] encrypt = new char[ALPHASIZE]; // Encryption array
  protected char[] decrypt = new char[ALPHASIZE]; // Decryption array
  /** Constructor that initializes the encryption and decryption arrays */
  public Caesar() {
    for(int i = 0; i < ALPHASIZE; i++)
      encrypt[i] = alpha[(i + 3) % ALPHASIZE]; // rotate alphabet by 3 places
    for(int i = 0; i < ALPHASIZE; i++)
      decrypt[encrypt[i]-'A'] = alpha[i]; // decrypt is reverse of encrypt
  }
  /** Encryption method */
  public String encrypt(String secret) {
    char[] mess = secret.toCharArray();   // the message array
    for(int i = 0; i < mes.length; i++)   // encryption loop
      if(Character.isUpperCase(mess[i]))  // we have a letter to change
        mess[i] = encrypt[mess[i]-'A'];   // use letter as an index
    return new String(mess);
  }
  /** Decryption method */
  public String decrypt(String secret) {
    char[] mess = secret.toCharArray();   // the message array
    for(int i = 0; i < mes.length; i++)   // decryption loop
      if(Character.isUpperCase(mess[i]))  // we have a letter to change
        mess[i] = decrypt[mess[i]-'A'];   // use letter as an index
    return new String(mess);
  }
  /** Simple main method for testing the Caesar cipher */  
  public static void main(String[] args) {
    Caesar cipher = new Caesar();       // Create a Caesar cipher object
    System.out.println("Encryption order = " + new String(cipher.encrypt));
    System.out.println("Decryption order = " + new String(cipher.decrypt));
    String secret = "THE EAGLE IS IN PLAY; MEET AT JOE'S.";
    secret = cipher.encrypt(secret)     
    System.out.println(secret);         // the cipher text
    secret = cipher.decrypt(secret)     
    System.out.println(secret);         // should be plaintext again
  }
}


3.1.5 2차원 배열 및 위치 게임

전략 게임이든, 시뮬레이션 게임이든, 1인칭 갈등 게임이든

많은 컴퓨터 게임은 2차원 "보드"를 사용한다. 

이러한 위치 게임(positional game)을 다루는 프로그램은 2차원 공간의 물체를 나타내는 방법이 필요하다. 

이를 수행하는 자연스러운 방법은 2차원 배열을 사용하는 것이다.

 

2차원 배열(two-dimensional array)

- 배열에 있는 셀을 나타내기 위해 i와 j와 같은 두 개의 인덱스를 사용한다. 

- 첫 번째 인덱스는 보통 행 번호를 가리킨다.

- 두 번째 인덱스는 보통 열 번호를 가리킨다.

 

그런 배열이 주어진다면 우리는 2차원 게임 보드를 유지할 수 있을 뿐만 아니라 

행과 열에 저장된 데이터와 관련된 다른 종류의 계산을 수행할 수 있다.


자바의 배열은 1차원이며, 우리는 배열의 각 셀에 접근하기 위해 하나의 인덱스를 사용한다. 

그럼에도 불구하고 자바에서 2차원 배열을 정의하는 방법이 있다. 

 

2차원 배열

= 배열의 배열

= 각각의 셀이 다른 배열인 배열

= 행렬(matrix)

 

자바에서 우리는 2차원 배열을 다음과 같이 선언한다:
int[][] Y = new int[8][10];

 

이 문장은 8행 10열로 구성된 8×10인 2차원 "배열의 배열" Y를 만든다. 

즉, Y는 길이 8의 배열이므로 Y의 각 원소는 정수의 길이 10의 배열이다. 

(그림 3.7 참조) 그러면 다음은 Y 배열과 변수 i와 j를 사용할 수 있다:
    Y[i][i+1] = Y[i][i] + 3;
    i = a.length;
    j = Y[4].length;

 

2차원 배열은 수치해석에 많은 응용이 있다. 

그러나 그런 응용에 대해 자세히 살펴보기보다는 

간단한 위치 게임을 구현하기 위한 2차원 배열의 응용에 대해 알아본다.

 

그림 3.7: 8개의 행과 10개의 열로 구성된 2차원 정수 배열 Y의 그림이다. 

Y[3][5] = 100, Y[6][2] = 632

  0 1 2 3 4 5 6 7 8 9
0 22 18 709 5 33 10 4 56 82 440
1 45 32 830 120 750 660 13 77 20 105
2 4 880 45 66 61 28 650 7 510 67
3 940 12 36 3 20 100 306 590 0 500
4 50 65 42 49 88 25 70 126 83 288
5 398 233 5 83 59 232 49 8 365 90
6 33 58 632 87 94 5 59 204 120 829
7 62 394 3 4 102 140 183 390 16 26


틱택토

틱택토의 규칙

  • 3 x 3 보드에서 진행되는 게임이다. 
  • X와 O 두 명의 플레이어가 번갈아 가며 한다.
  • X 플레이어부터 시작하여 보드의 칸에 각각의 마크를 넣는다. 
  • 만약 어느 한 플레이어가 세 개의 마크를 일렬, 열 또는 대각선으로 얻는데 성공하면, 그 플레이어가 이긴다.

 

틱택토의 장점은 2차원 배열이 위치 게임에 어떻게 사용될 수 있는지를 보여주는 간단한 예라는 것이다. 

체커, 체스 또는 인기 있는 시뮬레이션 게임과 같은 더 정교한 위치 게임용 소프트웨어는 

모두 여기에서 틱택토용 2차원 배열을 사용하는 것과 동일한 접근 방식을 기반으로 한다.

 

틱택토 2차원 정수 배열 보드
기본적인 아이디어는 게임 보드를 유지하기 위해

2차원 배열, 즉 board를 사용하는 것이다. 

이 배열의 셀은 셀이 비어 있는지 X 또는 O를 저장하는지를 나타내는 값을 저장한다. 

즉, 보드는 3 x 3 행렬이고,

가운데 행은 셀 board[1][0], board[1][1], board[1][2]로 구성된다. 

우리의 경우에는 보드 배열의 셀을 정수로 만들기로 했다.

0 = 빈 셀,  1 = X, -1 = O

이 인코딩을 통해 주어진 보드 구성이 X 또는 O의 승리인지, 

즉 행, 열 또는 대각선의 값을 더하면 -3 또는 3이 되는지 간단하게 테스트할 수 있다. 

우리는 그림 3.8에서 이 방법을 설명한다.

 

그림 3.8: 틱택토 보드와 이를 나타내는 2차원 정수 배열 보드의 그림.


저희는 코드 조각 3.10과 3.11에서 

두 명의 플레이어가 틱택토 보드를 유지 관리하기 위한 완전한 자바 클래스를 제공한다. 

저희는 그림 3.9의 샘플 출력을 보여준다. 

이 코드는 틱택토 보드를 유지 관리하고 움직임을 등록하기 위한 것일 뿐이며, 

어떤 전략도 수행하지 않거나 누군가가 컴퓨터를 상대로 틱택토를 하도록 허용하지 않는다. 

이러한 프로그램은 인공지능 수업에서 좋은 프로젝트가 될 것이다.


코드 조각 3.10 & 3.11

두 플레이어 간의 Tic-Tac-Toe 플레이를 위한 단순하고 완전한 자바 클래스. 

/** Simulation of a Tic-Tac-Toe game (does not do strategy). */
public class TicTacToe {
  public static final int X = 1, O = -1; // players
  public static final int EMPTY = 0; // empty cell
  protected int board[][] = new int[3][3]; // game board
  protected int player;                    // current player
  /** Constructor */
  public TicTacToe() { clearBoard(); }
  /** Clears the board */
  public void clearBoard() {
    for(int i = 0; i < 3; i++)
      for(int j = 0; i < 3; j++)
        board[i][j] = EMPTY;              // every cell should by empty
    player = X;                           // the first player is 'X'
  }
  /** Puts an X or O mark at position i,j */
  public void putMark(int i, int j) throws IllegalArgumentException {
    if ((i < 0) || (i > 2) || (j < 0) || (j > 2))
      throw new IllegalArgumentException("Invalid board position");
    if (board[i][j] != EMPTY)
      throw new IllegalArgumentException("Board position occupied");
    board[i][j] = player;          // place the mark for the current player
    player =  - player;            // switch players (uses fact that O = -X)
  }
  /** Checks whether the board configuration is a win for the given player */
  public boolean isWin(int mark) {
    return ((board[0][0] + board[0][1] + board[0][2] == mark*3)  // row 0
            || (board[1][0] + board[1][1] + board[1][2] == mark*3) // row 1
            || (board[2][0] + board[2][1] + board[2][2] == mark*3) // row 2
            || (board[0][0] + board[1][0] + board[2][0] == mark*3) // column 0
            || (board[0][1] + board[1][1] + board[2][1] == mark*3) // column 1
            || (board[0][2] + board[1][2] + board[2][2] == mark*3) // column 2
            || (board[0][0] + board[1][1] + board[2][2] == mark*3) // diagonal
            || (board[2][0] + board[1][1] + board[0][2] == mark*3) // diagonal);
  }
  /** Returns the winning player or 0 to indicate a tie */
  public int winner() {
    if (isWin(X))
      return(X);
    else if (isWin(O))
      return(O);
    else
      return(O);
  }
  /** Returns a simple character string showing the current board */
  public String toString() {
    String s = "";
    for(int i = 0; i < 3; i++)
      for(int j = 0; i < 3; j++)
        switch (board[i][j]) {
        case X: s += "X"; break;
        case O: s += "O"; break;
        case EMPTY: s += " "; break;
        }
        if (j < 2) s += "|";           // column boundary
      }
      if (i < 2) s += "\n-----\n";     // row boundary
    }
    return s;
  }
  /** Test run of a simple game */  
  public static void main(String[] args) {
    TicTacToe game = new TicTacToe();
    /* X moves: */       /* O moves: */
    game.putMark(1,1);   game.putMark(0,2);
    game.putMark(2,2);   game.putMark(0,0);
    game.putMark(0,1);   game.putMark(2,1);
    game.putMark(1,2);   game.putMark(1,0);
    game.putMark(2,0);
    System.out.println(game.toString());
    int winningPlayer = game.winner();
    if (winningPlayer != 0)
      System.out.println(winningPlayer + " wins");
    else
      System.out.println("Tie");
  }
}



그림 3.9: 틱택토(Tic-Tac-Toe) 게임의 샘플 출력.

O|X|O
-----
O|X|X
-----
X|O|X
Tie

3.2 단일 연결 리스트

앞 절에서 배열 자료 구조에 대해 설명하고 몇 가지 응용 분야에 대해 설명했다. 

배열은 배열의 크기 N을 미리 고정해야 하므로 적합성이 높지 않은 단점이 있다.


그러나 이런 단점이 없는 일련의 요소들을 저장하는 다른 방법들도 있다. 

이 절에서는 단일 연결 리스트라는 중요한 대체 구현을 알아본다.


연결 리스트(linked list): 가장 단순한 형태로, 함께 선형 순서를 형성하는 노드들의 집합

각 노드가 요소에 대한 참조와 다른 노드에 대한 참조를 저장하는 객체라는 점에서,

어린이 게임 "Follow the Leader"에서와 같이 순서가 결정된다(그림 3.10 참조)


그림 3.10: 요소가 공항 코드를 나타내는 문자열인 단일 연결 리스트의 예. 

각 노드의 다음 포인터는 화살표로 표시된다. 

null 객체는 로 표시된다.


노드가 다른 노드를 참조하도록 하는 것은 이상해 보일 수 있지만 그러한 방식은 쉽게 작동한다.

노드 내부의 다음 참조 = 다른 노드에 대한 링크(link)  = 다른 노드에 대한 포인터(pointer)

링크 호핑(link hopping) = 포인터 호핑(pointer hopping)

= 다음 참조를 따라 한 노드에서 다른 노드로 이동하는 것

헤드(head): 연결 리스트의 첫 번째 노드

테일(tail): 연결 리스트의 마지막 노드

따라서 우리는 연결 리스트의 헤드에서 시작하여 테일에서 끝나는 연결 리스트를 통해 링크 호핑할 수 있다.

우리는 테일을 연결 리스트의 끝을 나타내는 null next 참조를 갖는 노드로 식별할 수 있다.

이렇게 정의된 연결 리스트을 단일 연결 리스트(singly linked list)라고 한다.

 

단일 연결 리스트의 특징
단일 연결 리스트는 배열과 마찬가지로 원소들의 순서를 일정하게 유지한다.

이 순서는 각 노드에서 리스트의 next 노드로 이어지는 다음 링크들의 연쇄에 의해 결정된다.

단일 연결 리스트는 배열과 달리 정해진 크기가 없고, 원소들의 수에 비례하는 공간을 사용한다.

마찬가지로 우리는 연결 리스트의 노드들에 대한 어떤 인덱스 번호도 추적하지 않는다.

그래서 우리는 어떤 노드가 리스트에서 두 번째, 다섯 번째, 혹은 스무 번째 노드인지를

조사하는 것만으로는 구별할 수 없다.

 

단일 연결 리스트 구현

단일 연결 리스트를 구현하기 위해 코드 조각 3.12와 같이 Node 클래스를 정의한다.

여기서 요소는 문자열이라고 가정한다.

5장에서는 임의의 유형의 요소를 저장할 수 있는 노드를 정의하는 방법을 설명한다.

노드 클래스가 주어지면 코드 조각 3.13과 같이

실제 연결 리스트를 정의하는 클래스인 SLinkedList를 정의할 수 있다.

이 클래스는 헤드 노드에 대한 참조와 전체 노드 수를 계산하는 변수를 유지한다.


코드 조각 3.12: 단일 연결 리스트의 노드 구현.

/** Node of a singly linked list of strings. */
public class Node {
  private String element;     // we assume elements are character strings
  private Node next;
  /** Creates a node with the given element and next node. */
  public Node(String s, Node n) {
    element = s;
    next = n;
  }
  /** Returns the element of this node. */
  public String getElement() { return element; }
  /** Returns the next node of this node. */
  public Node getNext() { return next; }
  // Modifier methods:
  /** Sets the element of this node. */
  public void setElement(String newElem) { element = newElem; }
  /** Sets the next node of this node. */
  public void setNext(Node newNext) { next = newNext; }
}

 

코드 조각 3.13: 단일 연결 리스트를 위한 클래스의 부분 구현.

/** Singly linked list. */
public class SLinkedList {
  protected Node head;     // head node of the list
  protected long size;     // number of nodes in the list
  /** Default constructor that creates an empty list.*/
  public SLinkedList() {
    head = null;
    size = 0;
  }
  // ... update and search methods would go here ...
}

3.2.1 단일 연결 리스트의 삽입

단일 연결 리스트을 사용할 때, 

우리는 그림 3.11과 코드 조각 3.14와 같이 리스트의 맨 앞에 요소를 쉽게 삽입할 수 있다.

주요 아이디어는 새로운 노드를 만들고

next 링크를 head와 같은 객체를 참조하도록 설정한 다음

head가 새로운 노드를 가리키도록 설정하는 것이다.
그림 3.11: 단일 연결 리스트의 헤드에 요소 삽입: 

(a) 삽입 전; (b) 새로운 노드 생성; (c) 삽입 후.

 


코드 조각 3.14: 단일 연결 리스트의 처음에 새로운 노드 v를 삽입한다.

이 방법은 리스트가 비어 있어도 작동한다. 

변수 head가 v를 가리키도록 하기 전에

새로운 노드 v에 대한 next 포인터를 설정한다.

Algorithm addFirst(v):

    v.setNext(head)       {make v point to the old head node}

    head ← v            {make variable head point to new node}

    size ← size + 1        {increment the node count}


단일 연결 리스트의 맨 뒤에 요소 삽입

또한 그림 3.12와 같이

테일 노드에 대한 참조를 유지하는 경우

리스트의 테일에 요소를 쉽게 삽입할 수 있다. 

이 경우 새로운 노드를 생성하여 

null 객체를 가리키도록 다음 참조를 할당하고 

이 새로운 객체를 가리키도록 tail의 다음 참조를 설정한 다음

이 새 노드에 tail 참조 자체를 할당한다. 

코드 조각 3.15에 자세한 내용을 설명한다.

그림 3.12:

(a) 삽입 전, (b) 새 노드 생성, (c) 삽입 후.

(c)의 새 노드를 가리키도록 tail 변수를 지정하기 전에

(b)에 tail에 대한 next 링크를 설정한다.


코드 조각 3.15:

단일 연결 리스트의 끝에 새로운 노드를 삽입한다. 

이 방법은 리스트가 비어 있는 경우에도 적용된다. 

변수 tail이  새 노드를 가리키도록 하기 전에

오래된 테일 노드에 대한 next 포인터를 설정한다.

Algorithm addLast(v):

    v.setNext(null)       {make new node v point to null object}

    v.setNext(tail)       {make old tail node point to new node}

    tail ← v            {make variable tail point to new node}

    size ← size + 1        {increment the node count}


3.2.2 단일 연결 리스트의 요소 제거

연결 리스트의 맨 앞에 새로운 요소를 삽입하는 역연산은

맨 앞에 있는 요소를 제거하는 것이다.

이 작업은 그림 3.13에 나타나 있으며 코드 조각 3.16에 자세히 나와 있다.
그림 3.13: 단일 연결 리스트의 맨 위에 있는 요소 제거: 

(a) 제거 전; (b) 이전 새 노드 "연결" 후; (c) 제거 후.

 

코드 조각 3.16: 단일 연결 리스트의 시작 부분에서 노드를 제거한다.

Algorithm removeFirst():

    if head = null then

        Indicate an error: the list is empty.

    t head

    head head.getNext()  {make head point to next node (or null)}

    t.setNext(null)       {null out the next pointer of the removed node}

    size ← size - 1        {decrement the node count}

 

불행히도 우리는 단일 연결 리스트에서 테일 노드를 쉽게 삭제할 수 없다. 

리스트의 마지막 노드에 대한 tail 참조가 있다고 하더라도 

마지막 노드를 제거하기 위해서는 마지막 노드보다 먼저 노드에 접근할 수 있어야 한다.

하지만 테일에서 다음 연결을 따라가면 테일 앞에 있는 마디에 도달할 수 없다. 

이 노드에 접근하는 유일한 방법은

리스트의 맨 앞에서 시작해서 리스트 전체를 탐색하는 것이다.

하지만 그런 일련의 링크 호핑 작업은 오랜 시간이 걸릴 수 있다.