리스트, 큐, 스택은 차이점으로 인해서 사용되는 상황이 달라지는 경우가 많습니다. 만약 은행의 번호표를 뽑는 기능을 가진 프로그램을 만든다면 3개중 어떤 것을 사용해야 빠르게 구현이 가능할까요? 정답은 큐입니다. 만약 리스트나 스택으로 번호표 시스템을 만든다면 없던 기능을 만들거나, 최적화에 좋지 못한 코드를 넣는 경우가 생깁니다. 이러한 상황을 방지하기 위해서 이 세가지를 완벽히 이해해 보는 시간을 갖겠습니다.
목차
[리스트(List)의 개념]
C#에서 리스트(List)는 여러 개의 항목을 저장하는 상자와 같은 역할을 합니다. 이것을 컨테이너라고 합니다. 일반적인 배열과는 다르게 크기를 유연하게 조절 할 수 있습니다. 크기를 지정하지 않아도 추가나 제거가 자유롭다는 뜻입니다.
[장점]
- 유연한 크기조절이 가능합니다.
- 게임에서 아이템의 목록을 보여줘야 하는 경우 아이템을 먹거나 버리게 되면 목록의 크기가 바뀌어야 합니다. 이때 리스트를 사용하여 크기조절을 해야 합니다.
- 요소를 간편하게 추가하고 제거가 가능합니다.
- 새로운 적이 나타나거나 아이템이 획득될 때마다 목록에 추가해야 합니다. 리스트를 사용하면 간단하게
Add()
메서드를 호출하여 새로운 요소를 추가할 수 있습니다. 또한, 요소를 제거할 때에도Remove()
메서드를 사용하여 간단하게 처리할 수 있습니다.
- 새로운 적이 나타나거나 아이템이 획득될 때마다 목록에 추가해야 합니다. 리스트를 사용하면 간단하게
[단점]
- 리스트의 추가 제거는 성능의 영향을 줄 수 있습니다. 요소가 10000개이며 중간에 다른 요소를 추가하고 싶다면 5000개를 이동 시켜야 하기때문에 성능에 영향을 미칩니다.
- 요소를 검색하는데 성능에 영향을 줄 수 있습니다.
- 게임에서 적을 관리하기 위해 리스트를 사용한다고 가정해봅시다. 새로운 적이 나타날 때마다 리스트에 추가되고, 적이 제거되면 리스트에서 제거됩니다. 이것은 리스트의 장점 중 하나인 유연한 크기 조절을 잘 보여줍니다. 그러나 만약 많은 적이 나타나고 사라진다면, 성능이 저하될 수 있습니다. 특히 적이 많은 경우 리스트를 순회하는 데 시간이 오래 걸릴 수 있습니다.
[예시코드]
using UnityEngine;
using System.Collections.Generic;
public class EnemyManager : MonoBehaviour
{
// 적(Enemy)를 저장할 List
private List<GameObject> enemies = new List<GameObject>();
// Start 함수에서 적을 추가하고 관리
void Start()
{
// 적을 생성하여 리스트에 추가
AddEnemy("Enemy1");
AddEnemy("Enemy2");
AddEnemy("Enemy3");
// 현재 적 목록 출력
Debug.Log("현재 적 목록:");
PrintEnemies();
}
// 리스트에 새로운 적 추가
void AddEnemy(string enemyName)
{
// 적 프리팹을 로드하거나 생성하여 리스트에 추가
GameObject enemyPrefab = Resources.Load<GameObject>(enemyName);
GameObject newEnemy = Instantiate(enemyPrefab, transform.position, Quaternion.identity);
enemies.Add(newEnemy);
}
// 현재 적 목록을 출력하는 함수
void PrintEnemies()
{
foreach (GameObject enemy in enemies)
{
Debug.Log(enemy.name);
}
}
// Update 함수에서 필요한 로직 추가
void Update()
{
// 적 관련 로직 추가
}
}
리스트는 Add로 요소를 추가하고, Remove로 제거하는 기능을 가집니다.
[언제 사용해야 할까?]
추가 제거가 유연 해야하는 상황이고, 너무 많은 요소가 포함 돼 있지 않고 검색빈도가 적은 상황에서 사용이 가능합니다. 예를 들어서 게임내 아이템 개수가 많지 않고 아이템 검색 빈도가 많지 않다면 사용에 적합할 것 입니다.
[큐(queue)의 개념]
유한한 크기의 컨테이너로, 요소들을 선입선출(FIFO, First-In-First-Out)의 원칙에 따라 저장하는 자료 구조입니다. 이것은 우리가 일상 생활에서 줄을 서는 것과 비슷한 개념입니다. 먼저 줄을 선 사람이 먼저 처리되고, 그 뒤에 줄을 선 사람들이 차례대로 처리됩니다.
[장점]
- 먼저 추가된 요소가 먼저 제거되는 구조를 가지고 있습니다. 이것은 일부 작업을 처리할 때 요소들의 처리 순서가 중요한 경우에 매우 유용합니다.
- 간단한 인터페이스를 가지고 있어 사용하기 쉽습니다. 요소를 추가하는
Enqueue()
메서드와 요소를 제거하는Dequeue()
메서드를 제공합니다. 또한, Queue의 앞쪽에 있는 요소를 확인하는Peek()
메서드도 제공합니다. - 큐는 내부적으로 메모리 관리를 하여 메모리를 효율적으로 사용 합니다.
[단점]
- 선입선출의 원칙을 따르기 때문에, 임의의 위치에 있는 요소에 직접 접근할 수 없습니다. 이는 특정 요소를 검색하거나 수정하는 데 어려움을 초래할 수 있습니다.
- 선입선출의 특성을 가지고 있기 때문에, 특정 요소를 검색하려면 첫 번째부터 순서대로 확인해야 합니다.
[예시코드]
using System.Collections.Generic;
using UnityEngine;
public class QueueExample : MonoBehaviour
{
// 큐를 저장할 리스트
private Queue<int> myQueue = new Queue<int>();
void Start()
{
// 큐에 데이터 삽입
EnqueueData(10);
EnqueueData(20);
EnqueueData(30);
// 큐에서 데이터 삭제 및 출력
DequeueData();
DequeueData();
DequeueData();
}
// 큐에 데이터를 삽입하는 함수
void EnqueueData(int data)
{
myQueue.Enqueue(data);
Debug.Log("Enqueued: " + data);
}
// 큐에서 데이터를 삭제하고 출력하는 함수
void DequeueData()
{
if (myQueue.Count > 0)
{
int data = myQueue.Dequeue();
Debug.Log("Dequeued: " + data);
}
else
{
Debug.Log("Queue is empty!");
}
}
}
Enqueue로 맨 앞에 데이터를 삽입하고 Dequeue로 맨 뒤 데이터를 빼는 예시 입니다. 또한 예시 코드에 적지 않았지만 Peek()라는 코드가 있으며 가장 앞의 데이터를 확인하는 메서드 입니다.
[언제 사용해야 할까?]
플레이어 대기열과 같이 먼저 들어온 명령을 순차적으로 처리 해야하는 경우 적합합니다.
[스택(stack)의 개념]
후입선출(LIFO, Last-In-First-Out)의 원칙에 따라 데이터를 저장하고 접근하는 자료 구조입니다. 이것은 간단히 말해서 “접시 쌓기”와 같은 개념입니다. 마지막에 들어간 접시가 먼저 나오는 것처럼, 스택에 마지막에 추가된 데이터가 먼저 꺼내 지게 됩니다.
[장점]
- 데이터를 추가하거나 제거할 때의 시간 복잡도가 O(1)이므로 빠른 접근이 가능합니다.
- 재귀적 문제 해결
- 함수 호출의 실행 컨텍스트를 스택에 저장하여 재귀적인 문제를 해결할 때 유용합니다.
[단점]
- 정적으로 할당 되기 때문에 사용하지 않는 공간으로 인해 메모리 낭비가 발생할 수 있습니다.
- 가장 최근에 추가된 데이터에만 접근할 수 있고, 중간에 있는 데이터에는 직접적으로 접근할 수 없습니다. 이는 후입선출의 특성을 가지고 있기 때문입니다.
[예시 코드]
using System.Collections.Generic;
using UnityEngine;
public class StackExample : MonoBehaviour
{
// 스택을 저장할 리스트
private Stack<int> myStack = new Stack<int>();
void Start()
{
// 스택에 데이터 추가
PushData(10);
PushData(20);
PushData(30);
// 스택에서 데이터 제거 및 출력
PopData();
PopData();
PopData();
// 스택의 맨 위 데이터 확인
PeekData();
}
// 스택에 데이터를 추가하는 함수
void PushData(int data)
{
myStack.Push(data);
Debug.Log("Pushed: " + data);
}
// 스택에서 데이터를 제거하고 출력하는 함수
void PopData()
{
if (myStack.Count > 0)
{
int data = myStack.Pop();
Debug.Log("Popped: " + data);
}
else
{
Debug.Log("Stack is empty!");
}
}
// 스택의 맨 위 데이터를 확인하는 함수
void PeekData()
{
if (myStack.Count > 0)
{
int data = myStack.Peek();
Debug.Log("Peeked: " + data);
}
else
{
Debug.Log("Stack is empty!");
}
}
}
Push로 데이터를 추가하고 Pop으로 데이터를 제거합니다 이때 후입 선출이라는 원리가 적용 됩니다. Peek은 맨 위 데이터를 확인하는 용도 입니다.
[언제 사용해야 할까?]
- 후입 선출이라는 특징으로 인해 되돌리거나 취소 기능이 필요할 때 사용 합니다.
- AI 행동 관리에 유용합니다.
- AI는 다음에 취할 행동을 스택에 푸시하고, 현재 상황에 따라 스택에서 팝하여 행동을 수행할 수 있습니다. 이를 통해 AI의 행동을 유동적으로 조절할 수 있습니다.
- 경로 탐색
- 게임의 맵이나 레벨에서 경로 탐색이나 상호작용을 구현할 때 스택을 사용할 수 있습니다. 깊이 우선 탐색(DFS)과 같은 알고리즘에서 스택을 사용하여 탐색 경로를 관리할 수 있습니다.
[리스트, 큐, 스택 언제 사용해야 하는지 정리]
- 리스트(List): 데이터의 순서가 중요하고, 데이터를 임의의 위치에서 삽입/삭제해야 할 때 사용합니다.
- 큐(Queue): 선입선출의 원리에 따라 데이터를 관리해야 할 때 사용합니다. 예를 들어, 대기열 관리나 이벤트 처리 등에 사용됩니다.
- 스택(Stack): 후입선출의 원리에 따라 데이터를 관리해야 할 때, 즉 가장 최근에 추가된 데이터가 가장 먼저 처리되어야 할 때 사용합니다. 예를 들어, 되돌리기 기능이나 함수 호출 스택 등에 사용됩니다.