본문 바로가기

Programming

자료구조 : 쉽고 재미있는 C# Programming 의 기본

 

C# 컬렉션에서 제공하고 있는 자료구조(Data Structure)는 데이터를 효율적으로 저장하고 조직화하는 방법을 제공하는 방법론입니다.

C#은 다양한 자료구조를 지원하며, 이러한 자료구조들은 데이터를 효율적으로 다루고 처리할 수 있도록 도와줍니다.

 

1. 배열(Array)

 

이전 포스팅에서 소개했던 배열(Array)는 간단한 개념만 알아보겠습니다.

  • 배열은 동일한 데이터 타입의 요소가 순서대로 저장된 고정 크기의 자료구조입니다.
  • C#에서 배열은 다음과 같이 선언합니다: int[] myArray = new int[5];
  • 각 요소는 인덱스를 사용하여 접근하며, 인덱스는 0부터 시작합니다: int value = myArray[2];

 

2. 큐(Queue)

 

2.1 개념

 

Queue는 선입선출(FIFO, First-In-First-Out) 원칙을 따르는 자료구조입니다. 새로운 요소는 항상 Queue의 뒤에 추가되고, 맨 앞에서부터 순서대로 제거됩니다.

 

2.2 형식

 

C#에서는 System.Collection.Generic 네임스페이스에 있는 Queue<T> 클래스를 사용하여 Queue를 구현할 수 있습니다.

 

2.2.1 Queue 선언과 초기화

Queue myQueue = new Queue();   // Queue 클래스 사용
Queue<int> myQueue = new Queue<int>();  // Queue<T> 클래스 사용
  • Queue 클래스는 제네릭하지 않으므로 어떤 데이터 형식이든 담을 수 있지만, 컴파일러에서 타입 안전성을 확인할 수 없습니다. 따라서 런타임에 형변환 등의 문제가 발생할 수 있습니다.
  • Queue<T> 클래스는 제네릭하게 설계되어 있어서 컴파일 시에 타입 안전성을 보장합니다. 코드의 가독성과 유지보수성이 높아지며, 잘못된 형변환이나 데이터 형식 관련 오류를 사전에 방지할 수 있습니다.

일반적으로는 제네릭한 Queue<T> 클래스를 사용하는 것이 권장되며, 특별한 이유가 없다면 Queue 클래스보다는 Queue<T>를 사용하는 것이 좋습니다.

 

2.2.2 Enqueue - 요소추가

 

Enqueue 메서드를 사용하여 Queue에 요소를 추가합니다. 추가된 요소는 Queue의 뒤에 위치하게 됩니다.

myQueue.Enqueue(10);
myQueue.Enqueue(20);
myQueue.Enqueue(30);

 

2.2.3 Dequeue - 요소제거

 

Dequeue 메서드를 사용하여 Queue에서 맨 앞의 요소를 제거합니다. 제거된 요소는 반환되며 Queue에서 사라집니다.

int item = myQueue.Dequeue();

 

2.2.4 Peek - 맨 앞의 요소확인

 

Peek 메서드를 사용하여 Queue의 맨 앞에 있는 요소를 확인할 수 있습니다. Queue에서 제거하지 않고 확인만 합니다.

int frontItem = myQueue.Peek();

 

2.2.5 Count - Queue에 있는 요소의 수 확인

 

Count 속성을 사용하여 Queue에 현재 몇 개의 요소가 있는지 확인할 수 있습니다.

int numberOfElements = myQueue.Count;

 

2.2.6 간단한 예제

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 큐 생성
        Queue<string> myQueue = new Queue<string>();

        // 요소 추가
        myQueue.Enqueue("Apple");
        myQueue.Enqueue("Banana");
        myQueue.Enqueue("Cherry");

        // 요소 출력 및 제거
        while (myQueue.Count > 0)
        {
            string item = myQueue.Dequeue();
            Console.WriteLine("Processing: " + item);
        }
    }
}

 

Queue에 요소를 추가하고 하나씩 제거하여 출력합니다.


2.3 활용

 

  • Queue는 주로 순서대로 처리되어야 하는 작업이나 이벤트를 다룰 때 사용됩니다.
  • 예를 들어, 대기열에 들어온 작업을 처리하는 시나리오나 BFS(너비 우선 탐색) 알고리즘에서 사용됩니다.
  • Queue는 일반적으로 Enqueue(추가)와 Dequeue(삭제) 메서드를 사용하여 요소를 추가하고 제거합니다.

2.3.1 작업 대기열 관리

 

Queue는 작업이나 이벤트를 처리하는 대기열로 사용될 수 있습니다. 예를 들어, 다수의 작업을 처리해야 할 때, 작업이 Queue에 추가되고 순차적으로 처리됩니다.

Queue<string> taskQueue = new Queue<string>();

taskQueue.Enqueue("Task 1");
taskQueue.Enqueue("Task 2");
taskQueue.Enqueue("Task 3");

while (taskQueue.Count > 0)
{
    string task = taskQueue.Dequeue();
    Console.WriteLine("Processing: " + task);
}

[코드]

Processing: Task 1
Processing: Task 2
Processing: Task 3

[결과]

 

2.3.2 BFS(너비 우선 탐색) 알고리즘

 

Queue는 그래프의 BFS 알고리즘에서 사용될 수 있습니다. 시작 노드에서 인접한 노드를 모두 방문한 후에 그 인접한 노드들을 Queue에 추가하여 너비 우선으로 탐색합니다.

// 간단한 그래프 구현
Dictionary<int, List<int>> graph = new Dictionary<int, List<int>>
{
    { 1, new List<int> { 2, 3 } },
    { 2, new List<int> { 4, 5 } },
    { 3, new List<int> { 6 } },
    { 4, new List<int> { } },
    { 5, new List<int> { 7 } },
    { 6, new List<int> { } },
    { 7, new List<int> { } }
};

Queue<int> bfsQueue = new Queue<int>();
HashSet<int> visited = new HashSet<int>();

bfsQueue.Enqueue(1);
visited.Add(1);

while (bfsQueue.Count > 0)
{
    int currentNode = bfsQueue.Dequeue();
    Console.WriteLine("Visiting Node: " + currentNode);

    foreach (int neighbor in graph[currentNode])
    {
        if (!visited.Contains(neighbor))
        {
            bfsQueue.Enqueue(neighbor);
            visited.Add(neighbor);
        }
    }
}

[코드]

Visiting Node: 1
Visiting Node: 2
Visiting Node: 3
Visiting Node: 4
Visiting Node: 5
Visiting Node: 6
Visiting Node: 7

[결과]

2.3.3 캐시(cache) 우선

 

Queue를 사용하여 간단한 캐시 구현이 가능합니다. 최근에 사용된 데이터를 Queue에 추가하고, 일정 크기 이상이 되면 가장 오래된 데이터를 제거합니다.

Queue<int> cacheQueue = new Queue<int>();
int cacheSize = 3;

void AccessData(int data)
{
    if (cacheQueue.Count >= cacheSize)
        cacheQueue.Dequeue(); // 가장 오래된 데이터 제거

    cacheQueue.Enqueue(data); // 새로운 데이터 추가
}

AccessData(1);
AccessData(2);
AccessData(3);
AccessData(4);

Console.WriteLine("Current Cache: " + string.Join(", ", cacheQueue));

[코드]

Current Cache: 2, 3, 4

 

작업대기열, 알고리즘 구현, 데이터 캐싱 등 다양한 분야에서 큐는 유용하게 활용됩니다.

 

2.4 Queue 사용예

 

아래 예제는 음식 주문처리를 모델링한 간단한 프로그램입니다.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Queue<string> orderQueue = new Queue<string>();

        // 음식 주문 추가
        orderQueue.Enqueue("Burger");
        orderQueue.Enqueue("Pizza");
        orderQueue.Enqueue("Pasta");

        Console.WriteLine("Welcome to the Restaurant! Processing orders...\n");

        // 주문 처리 및 출력
        while (orderQueue.Count > 0)
        {
            string currentOrder = orderQueue.Dequeue();
            Console.WriteLine("Preparing and serving: " + currentOrder);
        }

        Console.WriteLine("\nAll orders processed. Thank you for dining with us!");
    }
}

 

  • Queue<string> orderQueue = new Queue<string>();: 주문을 저장하기 위한 큐를 생성합니다.
  • orderQueue.Enqueue("Burger");, orderQueue.Enqueue("Pizza");, orderQueue.Enqueue("Pasta");: 다양한 음식 주문을 큐에 추가합니다.
  • while (orderQueue.Count > 0): 큐에 주문이 있는 동안에 주문을 처리하는 반복문을 실행합니다.
  • string currentOrder = orderQueue.Dequeue();: 큐에서 맨 앞의 주문을 가져와서 처리합니다.
  • Console.WriteLine("Preparing and serving: " + currentOrder);: 현재 주문을 처리하는 메시지를 출력합니다.
  • Console.WriteLine("\nAll orders processed. Thank you for dining with us!");: 모든 주문이 처리되면 마무리 메시지를 출력합니다.
Welcome to the Restaurant! Processing orders...

Preparing and serving: Burger
Preparing and serving: Pizza
Preparing and serving: Pasta

All orders processed. Thank you for dining with us!


Queue는 음식 주문을 처리하는 과정에서 주문의 선입선출 순서를 잘 나타내고 있습니다. 주문이 큐에 추가되고, 주문이 처리되면 큐에서 빠져나가는 형태를 모델링한 간단한 프로그램입니다.


 

3. 스택(Stack)

 

3.1 개념

 

Stack은 Queue의 반대개념으로 후입선출(LIFO, Last-In-Firtst-Out) 원칙을 따르는 자료구조입니다. 가장 최근에 추가된 요소가 가장 먼저 제거되는 특징이 있습니다.

 

3.2 형식

 

C#에서는 System.Collections.Generic 네임스페이스에 있는 Stack<T> 클래스를 사용하여 Stack을 구현할 수 있습니다.

 

3.2.1 Stack 선언과 초기화

Stack<int> myStack = new Stack<int>();

 

3.2.2 Push - 요소추가

 

Push 메서드를 사용하여 Stack에 요소를 추가합니다. 추가된 요소는 Stack의 맨 위에 위치하게 됩니다.

myStack.Push(10);
myStack.Push(20);
myStack.Push(30);

 

3.2.3 Pop - 요소제거

 

Pop 메서드를 사용하여 Stack에서 맨 위의 요소를 제거합니다. 제거된 요소는 반환되며 Stack에서 사라집니다.

int item = myStack.Pop();

 

3.2.4 Peek - 맨 위의 요소확인

 

Peek 메서드를 사용하여 Stack의 맨 위에 있는 요소를 확인할 수 있습니다. Stack에서 제거하지 않고 확인만 합니다.

int topItem = myStack.Peek();

 

3.2.5 Count - Stack에 있는 요소의 수 확인

 

Count 속성을 사용하여 스택에 현재 몇 개의 요소가 있는지 확인할 수 있습니다.

int numberOfElements = myStack.Count;

 

3.3 Stack 활용

 

3.3.1 역순 문자열 만들기

 

문자열을 스택에 넣고 Pop 메서드를 사용하여 역순으로 출력할 수 있습니다.

string inputString = "Hello";
Stack<char> charStack = new Stack<char>(inputString);

while (charStack.Count > 0)
{
    char reversedChar = charStack.Pop();
    Console.Write(reversedChar);
}

[코드]

olleH

[결과]

 

3.3.2 괄호 매칭 확인

 

여는 괄호와 닫는 괄호의 매칭을 확인할 때 스택을 사용할 수 있습니다.

string expression = "{[()]}";
Stack<char> bracketStack = new Stack<char>();

foreach (char bracket in expression)
{
    if (bracket == '{' || bracket == '[' || bracket == '(')
        bracketStack.Push(bracket);
    else if (bracket == '}' && bracketStack.Pop() != '{')
        Console.WriteLine("괄호 매칭 오류");
    else if (bracket == ']' && bracketStack.Pop() != '[')
        Console.WriteLine("괄호 매칭 오류");
    else if (bracket == ')' && bracketStack.Pop() != '(')
        Console.WriteLine("괄호 매칭 오류");
}

if (bracketStack.Count == 0)
    Console.WriteLine("괄호 매칭 성공");
else
    Console.WriteLine("괄호 매칭 오류");

이 예제는 입력된 괄호 표현식이 올바른지 확인하고 오류를 출력합니다.

 

3.3.3 함수 호출의 역추적

 

함수가 호출될 때마다 호출된 함수의 정보를 스택에 저장하여 역추적(debugging)에 사용할 수 있습니다.

void FunctionA()
{
    Console.WriteLine("Function A");
}

void FunctionB()
{
    Console.WriteLine("Function B");
}

Stack<string> functionCallStack = new Stack<string>();

functionCallStack.Push("Main");
FunctionA();
functionCallStack.Push("FunctionA");
FunctionB();
functionCallStack.Push("FunctionB");

while (functionCallStack.Count > 0)
{
    string functionName = functionCallStack.Pop();
    Console.WriteLine("Returned from: " + functionName);
}

[코드]

Returned from: FunctionB
Returned from: FunctionA
Returned from: Main

[결과]

 

3.4 Stack 사용 예

 

스택은 후입선출(LIFO) 구조를 가지고 있어, 가장 최근에 추가된 항목이 가장 먼저 제거되는 특징을 가지고 있습니다. 아래는 웹 브라우저의 뒤로 가기 버튼 동작을 모델링한 간단한 프로그램입니다.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Stack<string> webHistory = new Stack<string>();

        // 사용자의 웹 브라우저 동작 모델링
        NavigateToPage(webHistory, "Home");
        NavigateToPage(webHistory, "About");
        NavigateToPage(webHistory, "Contact");

        // 사용자가 뒤로가기 버튼을 누르면 이전 페이지로 이동
        NavigateBack(webHistory);

        // 현재 페이지 출력
        Console.WriteLine("Current Page: " + GetCurrentPage(webHistory));
    }

    static void NavigateToPage(Stack<string> history, string page)
    {
        history.Push(page);
        Console.WriteLine("Navigated to page: " + page);
    }

    static void NavigateBack(Stack<string> history)
    {
        if (history.Count > 1)
        {
            history.Pop();
            Console.WriteLine("Navigated back to previous page.");
        }
        else
        {
            Console.WriteLine("Cannot navigate back. Current page is the home page.");
        }
    }

    static string GetCurrentPage(Stack<string> history)
    {
        return history.Peek();
    }
}

 

  • Stack<string> webHistory = new Stack<string>();: 웹 페이지의 방문 기록을 저장하기 위한 스택을 생성합니다.
  • NavigateToPage 함수: 사용자가 새로운 페이지로 이동할 때마다 해당 페이지를 스택에 추가합니다.
  • NavigateBack 함수: 사용자가 뒤로 가기 버튼을 누를 때 현재 페이지를 스택에서 제거하고 이전 페이지로 이동합니다.
  • GetCurrentPage 함수: 현재 페이지를 스택에서 확인합니다.
Navigated to page: Home
Navigated to page: About
Navigated to page: Contact
Navigated back to previous page.
Current Page: About

 

이 예제에서 스택은 사용자의 웹 페이지 방문 기록을 저장하고, 뒤로가기 버튼을 누를 때 가장 최근에 방문한 페이지로 되돌아가는 동작을 모델링하고 있습니다.


 

4. 해시테이블(Hashtable)  딕셔너리(Dictionary)

 

4.1 개념

  • 해시 테이블은 키(key)와 값(value)으로 이루어진 데이터를 저장하는 자료구조로, 특정 키에 해당하는 값을 빠르게 찾을 수 있는 장점이 있습니다.
  • 해시 함수를 사용하여 키를 해시 값으로 변환하고, 해당 해시 값에 대응되는 인덱스에 데이터를 저장합니다.

사전과 같은 느낌의 구조입니다. 단어와 뜻이 연결되어 있는 key-Value 자료구조입니다.

 

4.2 형식

  • C#에서는 Hashtable 클래스나 Dictionary<TKey, TValue> 클래스를 사용하여 해시 테이블을 구현할 수 있습니다.
  • Hashtable 클래스는 System.Collections 네임스페이스에, Dictionary<TKey, TValue> 클래스는 System.Collections.Generic 네임스페이스에 속해 있습니다.

4.3 활용

  • 해시 테이블은 특정 키에 대한 값의 검색이 매우 빠르기 때문에 다양한 상황에서 사용됩니다.
  • 자주 사용되는 활용 예시로는 데이터 캐싱, 빠른 검색, 중복 검사, 인덱싱 등이 있습니다.

4.3.1 데이터캐싱

 

해시 테이블은 결과를 빠르게 찾을 수 있는 특성을 활용하여 데이터를 캐싱하는 데에 많이 사용됩니다. 예를 들어, 중복 계산을 피하고 성능을 향상시키기 위해 이전에 계산한 값을 해시 테이블에 저장하고, 필요할 때마다 검색하여 사용합니다.

Hashtable cache = new Hashtable();

int CalculateSquare(int number)
{
    if (cache.ContainsKey(number))
    {
        return (int)cache[number];
    }
    else
    {
        int square = number * number;
        cache.Add(number, square);
        return square;
    }
}

 

4.3.2 빠른 검색 및 인덱싱

 

해시 테이블은 특정 키에 대한 값을 빠르게 검색할 수 있는 장점이 있습니다. 이를 활용하여 데이터를 빠르게 찾거나, 인덱싱할 수 있습니다.

Dictionary<string, string> phoneBook = new Dictionary<string, string>();

// 전화번호부에 이름과 전화번호 추가
phoneBook.Add("Alice", "123-456-7890");
phoneBook.Add("Bob", "456-789-0123");

// 이름으로 전화번호 검색
string bobPhoneNumber = phoneBook["Bob"];
Console.WriteLine("Bob's Phone Number: " + bobPhoneNumber);

 

4.3.3 중복검사

 

해시 테이블을 사용하여 중복된 데이터를 방지하거나 중복을 확인하는 데에도 활용됩니다.

HashSet<string> uniqueNames = new HashSet<string>();

// 중복된 이름 방지
if (!uniqueNames.Contains("John"))
{
    uniqueNames.Add("John");
}

 

4.3.4 캐시 구현

 

해시 테이블을 사용하여 캐시를 구현할 수 있습니다. 이전에 계산한 결과를 해시 테이블에 저장하고, 필요할 때마다 빠르게 검색하여 사용합니다.

Dictionary<string, int> cache = new Dictionary<string, int>();

int CalculateHash(string input)
{
    if (cache.ContainsKey(input))
    {
        return cache[input];
    }
    else
    {
        int hashValue = SomeExpensiveCalculation(input);
        cache.Add(input, hashValue);
        return hashValue;
    }
}

 

해시 테이블은 데이터를 효율적으로 저장하고 검색하는 데에 사용될 수 있으며, 중복 방지나 데이터 캐싱 등 다양한 활용이 가능합니다. 주어진 상황에 따라서 적절한 해시 테이블을 선택하여 사용하면 성능을 향상시킬 수 있습니다.

 

4.3.5 Dictionary<TKey, TValue> 클래스 활용

 

Dictionary<TKey, TValue> 클래스는 제네릭한 해시 테이블을 제공하며, 특정 데이터 형식에 대한 타입 안전성을 보장합니다.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 제네릭 해시 테이블 생성
        Dictionary<string, object> myDictionary = new Dictionary<string, object>();

        // 데이터 추가
        myDictionary.Add("Name", "Jane");
        myDictionary.Add("Age", 30);
        myDictionary.Add("City", "Los Angeles");

        // 데이터 접근
        Console.WriteLine("Name: " + myDictionary["Name"]);
        Console.WriteLine("Age: " + myDictionary["Age"]);
        Console.WriteLine("City: " + myDictionary["City"]);
    }
}

 

 

이 예제에서는 Dictionary<TKey, TValue>를 사용하여 같은 내용을 저장하고 특정 키에 대응하는 값을 출력합니다.

해시 테이블은 데이터의 효율적인 관리와 검색에 활용됩니다. 사용 시에는 특히 키의 해싱 충돌과 관련된 문제에 주의하여 충돌을 최소화하는 해시 함수를 선택해야 합니다.

 

4.4 해시테이블(HashTable)과 딕셔너리(Dictionary) 활용 예

 

4.4.1 해시테이블 (HashTable)

  • 해시 테이블은 키(key)와 값(value)의 쌍을 저장하는 자료구조입니다.
  • 해시 함수를 사용하여 각 키를 해시 값으로 변환하고, 해당 해시 값을 인덱스로 사용하여 데이터를 저장하거나 검색합니다.
  • System.Collections 네임스페이스의 HashTable 클래스를 사용하여 구현할 수 있습니다.
  • 데이터 캐싱, 중복 검사, 빠른 검색 등 다양한 상황에서 활용됩니다.
Hashtable myHashTable = new Hashtable();
myHashTable.Add("Name", "John");
myHashTable.Add("Age", 25);
myHashTable.Add("City", "New York");

 

4.4.2 딕셔너리 (Dictionary)

 

  • 딕셔너리는 키(key)와 값(value)의 쌍을 저장하는 자료구조입니다.
  • 제네릭한 구조로, System.Collections.Generic 네임스페이스의 Dictionary<TKey, TValue> 클래스를 사용하여 구현할 수 있습니다.
  • 특히 제네릭한 형태로 되어 있어 타입 안전성을 보장하며, 빠른 검색, 중복 검사 등에 사용됩니다.
Dictionary<string, object> myDictionary = new Dictionary<string, object>();
myDictionary.Add("Name", "Jane");
myDictionary.Add("Age", 30);
myDictionary.Add("City", "Los Angeles");

 

4.4.3 사용 예

 

딕셔너리(Dictionary)를 사용하여 간단한 주소록 관리 프로그램을 모델링합니다. 각 연락처는 이름을 키로 하고, 전화번호를 값으로 가지는 구조로 저장됩니다.

using System;
using System.Collections.Generic;

class AddressBook
{
    static void Main()
    {
        Dictionary<string, string> contacts = new Dictionary<string, string>();

        // 주소록에 연락처 추가
        AddContact(contacts, "John", "123-456-7890");
        AddContact(contacts, "Jane", "987-654-3210");
        AddContact(contacts, "Bob", "456-789-0123");

        // 주소록 출력
        DisplayAddressBook(contacts);

        // 특정 연락처 검색
        SearchContact(contacts, "Jane");
    }

    static void AddContact(Dictionary<string, string> addressBook, string name, string phoneNumber)
    {
        // 연락처 추가
        addressBook.Add(name, phoneNumber);
        Console.WriteLine("Added contact: " + name + " (" + phoneNumber + ")");
    }

    static void DisplayAddressBook(Dictionary<string, string> addressBook)
    {
        // 주소록 출력
        Console.WriteLine("\nAddress Book:");
        foreach (var contact in addressBook)
        {
            Console.WriteLine(contact.Key + ": " + contact.Value);
        }
        Console.WriteLine();
    }

    static void SearchContact(Dictionary<string, string> addressBook, string name)
    {
        // 특정 연락처 검색
        if (addressBook.ContainsKey(name))
        {
            Console.WriteLine("Contact found: " + name + " (" + addressBook[name] + ")");
        }
        else
        {
            Console.WriteLine("Contact not found: " + name);
        }
    }
}
Added contact: John (123-456-7890)
Added contact: Jane (987-654-3210)
Added contact: Bob (456-789-0123)

Address Book:
John: 123-456-7890
Jane: 987-654-3210
Bob: 456-789-0123

Contact found: Jane (987-654-3210)

 

  • Dictionary<string, string> contacts: 이름을 키로 하고, 전화번호를 값으로 가지는 딕셔너리를 생성합니다.
  • AddContact 함수: 연락처를 주소록에 추가합니다.
  • DisplayAddressBook 함수: 주소록의 모든 연락처를 출력합니다.
  • SearchContact 함수: 특정 이름으로 연락처를 검색하고 결과를 출력합니다.

이 프로그램은 주소록에 연락처를 추가하고, 주소록 전체를 출력한 뒤에 특정 연락처를 검색하는 간단한 예제입니다. 실제로는 데이터베이스 또는 파일로 데이터를 관리하는 것이 일반적이지만, 이 예제를 통해 딕셔너리를 활용한 간단한 데이터 관리의 원리를 이해할 수 있습니다.


5. 리스트(List):

 

5.1 개념

 

리스트는 동일한 데이터 형식의 요소들을 순서대로 저장하는 동적 배열 형태의 자료구조입니다.

동적 배열이라 함은 배열의 크기가 자동으로 조절되며, 요소를 중간에 삽입하거나 삭제할 수 있는 특징을 가지고 있습니다.

List의 사용법은 ArrayList 와 비슷하지만, 타입을 미리 선언해야 하는 점이 다릅니다.

 

5.2 형식

 

C#에서 리스트는 System.Collections.Generic 네임스페이스에 속한 List<T> 클래스를 사용하여 생성됩니다. 여기서 T는 리스트에 저장되는 요소의 데이터 형식을 나타냅니다.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 정수형 리스트 생성
        List<int> myIntList = new List<int>();

        // 문자열 리스트 생성
        List<string> myStringList = new List<string>();
    }
}

 

5.3 주요 메서드와 속성

 

  • Add(T item): 리스트에 요소를 추가합니다.
  • Remove(T item): 리스트에서 특정 요소를 삭제합니다.
  • Count: 리스트에 포함된 요소의 개수를 반환합니다.
  • Clear(): 리스트의 모든 요소를 삭제합니다.
  • Contains(T item): 리스트에 특정 요소가 포함되어 있는지 여부를 확인합니다.

5.4 List 활용

 

리스트는 동적 배열의 특성을 활용하여 데이터를 효율적으로 관리할 수 있는 자료구조입니다.

 

5.4.1 데이터 관리

 

리스트는 동적으로 크기가 조절되므로, 데이터의 추가, 삭제, 수정이 용이합니다.

List<int> numbers = new List<int>();

numbers.Add(10);      // 추가
numbers.Add(20);
numbers.Add(30);

numbers.Remove(20);   // 삭제

numbers[0] = 15;      // 수정

 

5.4.2 순회 및 출력

 

foreach문을 사용하여 리스트의 모든 요소를 순회하고 출력할 수 있습니다.

List<string> colors = new List<string> { "Red", "Green", "Blue" };

foreach (string color in colors)
{
    Console.WriteLine(color);
}

 

5.4.3 검색 및 확인

 

Contains, IndexOf 메서드 등을 사용하여 특정 요소의 존재 여부를 확인하거나 인덱스를 검색할 수 있습니다.

List<int> scores = new List<int> { 80, 90, 75, 95, 85 };

bool contains85 = scores.Contains(85);  // true
int index95 = scores.IndexOf(95);       // 3

 

5.4.4 정렬 및 역순 정력

 

Sort 메서드를 사용하여 리스트의 요소를 정렬할 수 있습니다.

List<int> data = new List<int> { 30, 10, 20, 50, 40 };

data.Sort();            // 오름차순 정렬
data.Reverse();         // 내림차순 정렬

 

5.4.5 서브리스트 추출

 

GetRange 메서드를 사용하여 리스트의 일부를 추출할 수 있습니다.

List<string> names = new List<string> { "Alice", "Bob", "Charlie", "David", "Eve" };

List<string> selectedNames = names.GetRange(1, 3);  // "Bob", "Charlie", "David"

 

5.4.6 LINQ 사용

 

Language Integrated Query(LINQ)를 사용하여 리스트에서 데이터를 쿼리할 수 있습니다.

List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };

var result = numbers.Where(n => n > 30).ToList();  // 40, 50

 

5.4.7 복사 및 연결

 

CopyTo 메서드를 사용하여 리스트를 배열로 복사하거나, AddRange 메서드를 사용하여 다른 리스트와 연결할 수 있습니다.

List<int> source = new List<int> { 1, 2, 3 };
int[] array = new int[3];

source.CopyTo(array);           // 리스트를 배열로 복사
source.AddRange(new List<int> { 4, 5, 6 });  // 두 리스트 연결

 

리스트는 데이터의 동적 관리와 다양한 연산을 편리하게 수행할 수 있는 강력한 자료구조입니다. 활용에 따라서 데이터를 효율적으로 처리할 수 있으며, LINQ와의 조합으로 강력한 쿼리 기능도 제공합니다.

 

5.5 List 사용 예 - ToDoList

 

할 일 목록을 관리하는 프로그램을 만들어 보겠습니다. 리스트를 사용하여 할 일을 추가, 삭제, 출력하는 간단한 프로그램입니다.

using System;
using System.Collections.Generic;

class ToDoList
{
    static void Main()
    {
        List<string> tasks = new List<string>();

        Console.WriteLine("To-Do List Application\n");

        while (true)
        {
            Console.WriteLine("1. Add Task");
            Console.WriteLine("2. Remove Task");
            Console.WriteLine("3. Show Tasks");
            Console.WriteLine("4. Exit");

            Console.Write("Enter your choice (1-4): ");
            string choice = Console.ReadLine();

            switch (choice)
            {
                case "1":
                    AddTask(tasks);
                    break;
                case "2":
                    RemoveTask(tasks);
                    break;
                case "3":
                    ShowTasks(tasks);
                    break;
                case "4":
                    Console.WriteLine("Exiting the application.");
                    return;
                default:
                    Console.WriteLine("Invalid choice. Please enter a valid option.");
                    break;
            }
        }
    }

    static void AddTask(List<string> taskList)
    {
        Console.Write("Enter the task: ");
        string task = Console.ReadLine();
        taskList.Add(task);
        Console.WriteLine("Task added successfully!\n");
    }

    static void RemoveTask(List<string> taskList)
    {
        Console.Write("Enter the task to remove: ");
        string task = Console.ReadLine();

        if (taskList.Contains(task))
        {
            taskList.Remove(task);
            Console.WriteLine("Task removed successfully!\n");
        }
        else
        {
            Console.WriteLine("Task not found.\n");
        }
    }

    static void ShowTasks(List<string> taskList)
    {
        Console.WriteLine("\nTask List:");
        for (int i = 0; i < taskList.Count; i++)
        {
            Console.WriteLine($"{i + 1}. {taskList[i]}");
        }
        Console.WriteLine();
    }
}
To-Do List Application

1. Add Task
2. Remove Task
3. Show Tasks
4. Exit
Enter your choice (1-4): 1
Enter the task: Complete project
Task added successfully!

1. Add Task
2. Remove Task
3. Show Tasks
4. Exit
Enter your choice (1-4): 1
Enter the task: Go to the gym
Task added successfully!

1. Add Task
2. Remove Task
3. Show Tasks
4. Exit
Enter your choice (1-4): 3

Task List:
1. Complete project
2. Go to the gym

1. Add Task
2. Remove Task
3. Show Tasks
4. Exit
Enter your choice (1-4): 2
Enter the task to remove: Go to the gym
Task removed successfully!

1. Add Task
2. Remove Task
3. Show Tasks
4. Exit
Enter your choice (1-4): 3

Task List:
1. Complete project

1. Add Task
2. Remove Task
3. Show Tasks
4. Exit
Enter your choice (1-4): 4
Exiting the application.
  • 사용자는 메뉴에서 1을 선택하여 할 일을 추가하거나, 2를 선택하여 할 일을 삭제하거나, 3을 선택하여 현재 할 일 목록을 출력할 수 있습니다.
  • 할 일 목록은 List<string>을 사용하여 동적으로 관리됩니다.
  • 사용자가 4를 선택하면 프로그램이 종료됩니다.

이러한 형태의 프로그램은 일상적인 작업 관리나 목록 관리에 활용될 수 있습니다.

 

5.6 ArrayList 와 List<T> 비교

 

ArrayListList<T>는 둘 다 동적 배열을 나타내는 컬렉션 클래스입니다. 하지만 ArrayList는 비제네릭(non-generic)이며, List<T>는 제네릭(generic)입니다. 이 두 클래스의 주요 차이점을 살펴보겠습니다.

 

5.6.1 ArrayList

  • ArrayList는 .NET Framework 초기 버전에서 사용되었으며, 비제네릭 컬렉션 클래스입니다.
  • 요소를 추가할 때 Add 메서드를 사용하고, 요소에 접근할 때 형변환을 해야 합니다.
  • 타입 안정성(type safety)이 부족하며, 런타임에 형변환 오류가 발생할 수 있습니다.
  • 성능 면에서 더 많은 오버헤드가 발생할 수 있습니다.
ArrayList myArrayList = new ArrayList();
myArrayList.Add(10);
myArrayList.Add("Hello");
int intValue = (int)myArrayList[0]; // 형변환 필요

 

5.6.2 List<T>

  • List<T>는 제네릭 컬렉션 클래스로, .NET Framework 2.0 이후에 도입되었습니다.
  • 요소의 데이터 형식을 컬렉션 생성 시점에 지정하므로 타입 안정성이 제공됩니다.
  • Add, Remove, Contains 등의 메서드를 사용할 때 형변환이 필요하지 않습니다.
  • 컬렉션 내부에서 데이터를 저장할 때 형변환 없이 타입 안전하게 처리합니다.
List<int> myIntList = new List<int>();
myIntList.Add(10);
int intValue = myIntList[0]; // 형변환 불필요

 

5.6.3 비교

  • 타입 안정성:
    • ArrayList는 비제네릭이므로 타입 안정성이 떨어집니다.
    • List<T>는 제네릭이므로 컴파일 시점에 타입 안정성이 보장됩니다.
  • 성능:
    • ArrayList는 내부적으로 object 배열을 사용하며, 박싱/언박싱이 자주 발생하여 성능 저하가 있을 수 있습니다.
    • List<T>는 제네릭이므로 내부 배열은 특정 형식으로 지정되어 있어 박싱/언박싱 없이 데이터를 저장하고 접근할 수 있습니다.
  • 용량 동적 조절:
    • ArrayList는 요소를 추가할 때 동적으로 크기를 조절합니다.
    • List<T>도 크기를 동적으로 조절하지만, 제네릭 형식의 특성상 비제네릭 컬렉션보다 효율적으로 동작합니다.

5.6.4 어떤 것을 사용해야 하나?

using System;
using System.Collections;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // ArrayList 예제
        ArrayList arrayList = new ArrayList();
        arrayList.Add("Hello");
        arrayList.Add("World");

        // ToUpper() 메소드를 사용할 때 형변환 필요 (형변환을 하지 않으면 컴파일 오류발생)
        string element1 = (string)arrayList[0];
        string element2 = (string)arrayList[1];

        // 에러가 발생하지 않지만, 코드가 복잡해짐
        string upperCase1 = element1.ToUpper();
        string upperCase2 = element2.ToUpper();

        Console.WriteLine("ArrayList:");
        Console.WriteLine(upperCase1);
        Console.WriteLine(upperCase2);

        // List<T> 예제
        List<string> stringList = new List<string>();
        stringList.Add("Hello");
        stringList.Add("World");

        // 바로 ToUpper() 메소드를 사용 가능
        string upperCase3 = stringList[0].ToUpper();
        string upperCase4 = stringList[1].ToUpper();

        Console.WriteLine("\nList<T>:");
        Console.WriteLine(upperCase3);
        Console.WriteLine(upperCase4);
    }
}

 

예를 들어 ArrayList을 사용할 때 ToUpper() 메소드를 사용하려면 요소를 string으로 형변환해야 합니다.

이는 ArrayList가 object 타입을 다루기 때문에 컴파일러가 형식을 알지 못하기 때문입니다.

반면에 List<T>에서는 제네릭으로 타입이 명시되어 있기 때문에 바로 ToUpper() 메소드를 사용할 수 있습니다.

 

  • 최신 .NET 버전에서는 보편적으로 List<T>를 사용하는 것이 권장됩니다.
  • List<T>는 제네릭 형식을 사용하기 때문에 타입 안정성이 보장되며, 박싱/언박싱의 오버헤드를 피할 수 있습니다.
  • 만약 .NET Framework 1.1 등 오래된 버전을 사용하는 경우에만 ArrayList를 사용해야 합니다.

▶ 제네릭(Generic)에 대해

 

제네릭은 C#에서 도입된 강력한 기능으로, 코드의 재사용성과 타입 안정성을 높이기 위해 사용됩니다. 제네릭을 사용하면 클래스, 인터페이스, 메서드 등을 일반화하여 다양한 데이터 형식에 대응할 수 있습니다.

제네릭을 사용하기 위해서는 "System.Collections.Generic" 을 using 절에 추가해야 합니다.

 

1. 타입 매개변수

 

제네릭을 사용할 때에는 타입 매개변수를 도입합니다. 이는 실제 데이터 형식으로 대체될 수 있는 변수입니다. 보통 <T>와 같은 형태로 표현하며, T는 타입을 의미합니다.

// 예: List<T>에서 T는 리스트가 저장할 요소의 데이터 형식을 나타냄
List<int> intList = new List<int>();
List<string> stringList = new List<string>();

 

2. 클래스와 메서드의 일반화

 

클래스나 메서드를 일반화하기 위해 제네릭을 사용할 수 있습니다.

// 제네릭 클래스
public class Box<T>
{
    private T value;

    public void SetValue(T newValue)
    {
        value = newValue;
    }

    public T GetValue()
    {
        return value;
    }
}

// 제네릭 메서드
public T Add<T>(T a, T b)
{
    return (dynamic)a + (dynamic)b;
}

 

3. 타입안정성

 

제네릭을 사용하면 컴파일 시점에 타입 안전성을 보장할 수 있습니다. 즉, 컴파일러가 잘못된 데이터 형식으로의 접근을 방지합니다.

List<int> numbers = new List<int>();
numbers.Add(42);

// 컴파일 시점에 오류: numbers 리스트는 int 형식만 허용
numbers.Add("Hello"); // Error

 

4. 코드 재사용과 가독성 향상

 

제네릭을 사용하면 동일한 코드를 다양한 데이터 형식에 대해 재사용할 수 있으며, 가독성을 향상시킵니다.

// 제네릭을 사용하지 않은 경우
public int AddInt(int a, int b)
{
    return a + b;
}

public double AddDouble(double a, double b)
{
    return a + b;
}

// 제네릭을 사용한 경우
public T Add<T>(T a, T b)
{
    return (dynamic)a + (dynamic)b;
}

 

제네릭을 사용하면 타입에 관계없이 일반적인 알고리즘을 작성할 수 있으며, 코드의 유연성과 유지보수성을 향상시킬 수 있습니다.


박싱(Boxing)과 언박싱(Unboxing)

 

1. 박싱(Boxing)

 

박싱은 값 형식(예: 정수, 부동 소수점)을 참조 형식(예: object)으로 변환하는 프로세스를 의미합니다. 즉, 주소 값만을 가지게 됩니다.

값 형식은 스택 메모리에 저장되지만, 박싱을 통해 해당 값을 힙 메모리에 있는 참조 형식으로 변환합니다.

 

박싱은 값 타입의 변수를 '객체화'하기 위해서 메모리를 힙 영역에 생성합니다. 힙 영역에 생성된다면, ArrayList와 같은 컬렉션들은 데이터에 대한 성능 저하가 생기게 됩니다.

 

string 형태로 혹은 int 형태로 저장했을 뿐인데, 컬렉션의 자료구조는 값과 타입을 모두 object 로 변환시키는 작업을 하게 되면서 불필요한 작업을 하게 되는 것과 마찬가지입니다.

int myInt = 42;
object boxedInt = myInt; // 박싱
  • myInt는 스택에 정수로 저장되어 있습니다.
  • boxedInt에 할당될 때 박싱이 발생하여 값 형식이 참조 형식으로 변환됩니다.

2. 언박싱(Unboxing)

 

언박싱은 박싱의 반대되는 개념으로, 참조 형식을 값 형식으로 변환하는 프로세스를 의미합니다. 박싱된 값을 다시 원래의 값 형식으로 되돌리는 것입니다.

 

즉, 주소 값으로 받은 데이터를 Value 타입으로 바꿔주는 것을 말합니다.

int unboxedInt = (int)boxedInt; // 언박싱
  • boxedInt는 박싱된 값 형식입니다.
  • (int)를 사용하여 언박싱을 수행하고, 이를 unboxedInt에 할당합니다.

3. 주의사항

  • 박싱과 언박싱은 성능 면에서 비용이 큽니다. 값 형식을 참조 형식으로 변환하고 다시 값 형식으로 변환하는 작업은 메모리 할당과 복사를 포함하므로 피하는 것이 좋습니다.
  • 값 형식과 참조 형식 간 변환 시 형식 안전성에 주의해야 합니다. 박싱된 값이 원래 값 형식과 호환되지 않으면 런타임 에러가 발생합니다.
// 주의: 박싱된 값이 정수가 아니라 다른 형식일 경우 런타임 에러 발생
double myDouble = 3.14;
object boxedDouble = myDouble; // 박싱
int unboxedInt = (int)boxedDouble; // 런타임 에러 (InvalidCastException)

 

4. 실제 활용 예제

 

박싱과 언박싱은 주로 제네릭이나 컬렉션과 같이 값 형식을 사용할 수 없는 상황에서 발생합니다. 하지만 성능상의 이슈를 고려하여 최대한 박싱과 언박싱을 피하는 것이 좋습니다.

List<object> myList = new List<object>();
int myInt = 42;

myList.Add(myInt); // 박싱

object boxedInt = myList[0];
int unboxedInt = (int)boxedInt; // 언박싱

 

위의 예제에서는 List<object>를 사용하고 있기 때문에 값 형식인 intobject로 박싱하여 리스트에 추가하고, 다시 언박싱하여 int로 사용하는 과정이 포함되어 있습니다.