Doubly Linked List - template, overloading, Sentinel Node

woongaaaa·2024년 4월 11일
0

자료구조

목록 보기
4/5
post-thumbnail

4번문제 ADT

  • void add(const E& data); 멤버 함수는 string 타입 name과 int 타입 score라는 멤버 변수를 가진 GameEntry라는 클래스와 int, double 등 일반 자료형에 대해서 모두 내림차순 정렬할 수 있어야 한다.
  • GameEntry 클래스는 선언 시 생성자를 통해 멤버 변수를 초기화한다.
  • 이전 3번문제와 마찬가지로 head와 tail을 사용한다. -> 동일하게 데이터를 갖지않는 sentinel nodes를 사용하려고 했었다. 근데 sentinel node의 멤버 변수들에 대해 초기화를 시켜주지 못해서(data가 없어야하는데..) 이와 같은 문제로 MacOS에선 되는데, WSL에선 되지 않았다.....

main 함수(제공)

// 아래와 같이 main함수에서 여러분이 작성한 list.h와 gameentry.h 파일을
// include 하여 주어진 동작을 실행하게 됩니다.
// *** 주의! 아래의 내용을 변경할 경우 오답처리될 수 있습니다. **
#include <iostream>
#include <string>
#include <sstream>
#include "list.h"
#include "gameentry.h"

using namespace std;

void handleInteger()
{
    int maxEntry;
    cin >> maxEntry;
    LinkedList<int> list(maxEntry);
    int score;
    while (true) {
        cin >> score;
        if (score < 0)
            break;
        list.add(score);
    }
    list.printReverse();
    cout << list.length() << endl;
}

void handleGameEntry()
{
    int maxEntry;
    cin >> maxEntry;
    LinkedList<GameEntry> list(maxEntry);
    string name;
    int score;
    while (true) {
        cin >> name;
        if (name == "quit")
        {
            break;
        }
        cin >> score;
        list.add(GameEntry(name, score));
    }
    list.print();
    cout << list.length() << endl;
}

int main(void)
{
    handleInteger();
    handleGameEntry();

    return 0;
}

sentinel node 사용시 해결하지 못한 점

새로운 Node 생성 시 data와 prev, next를 초기화해주는데,
그럼 head와 tail에도 data를 넣어야한다.
-> 그럼 head와 tail을 위한 생성자가 따로 있던가 LinkedList의 생성자에서 head와 tail을 따로 만들어주고 초기화까지 하면?..
-> 하다가 그냥 sentinel node 사용하지 않는 것으로 노선 변경

list.h

head, tail 에도 data 사용

#ifndef ASSIGNMENT_LIST_H
#define ASSIGNMENT_LIST_H

template <typename E>
class LinkedList;

template <typename E>
class Node {
private:
    E data;
    Node<E>* next;
    Node<E>* prev;
    friend class LinkedList<E>;
public:
    Node(const E& _data) : data(_data), next(nullptr), prev(nullptr) {}
};

template <typename E>를 통해 임의의 자료형 E에 대해 모두 처리할 수 있다. GameEntry 클래스의 인스턴스 또한 처리가 가능하다.
Node의 생성자에서 data를 넣어주고, next, prev를 nullptr로 초기화해준다.
이러지 않으면 끊임없이 자기참조가 일어나더라.

template <typename E>
class LinkedList {
public:
    LinkedList(int max) : maxEntry(max) {}
    void add(const E& data);
    int length();
    void print();
    void printReverse();
private:
    int maxEntry;
    int dataCount = 0;
    Node<E>* head;
    Node<E>* tail;
};

int dataCount는 ADT에 없었지만,
length() 멤버 함수 + maxEntry 기능 구현의 편의성을 위해 넣었다.


template<typename E>
void LinkedList<E>::add(const E &data) {
    Node<E>* newNode = new Node<E>(data);
    if(dataCount == 0) {
        head = tail = newNode;
        newNode->next = tail;
        newNode->prev = head;
        dataCount++;
    } else if (dataCount ==1){
        if(head->data >= newNode->data){
            head->next = newNode;
            tail = newNode;
            tail->prev = head;
            tail->next = nullptr;
        }else{
            newNode->next = tail;
            head = newNode;
            tail->prev = newNode;
            head->prev = nullptr;
        }
        dataCount++;
    }else{

        Node<E> *currentNode = head; // 첫 번째 데이터 노드부터 시작
        while (currentNode != nullptr) {
            if (newNode->data >= currentNode->data) {
                if (currentNode == head) {
                    // 새로운 데이터가 큰 경우
                    newNode->next = head;
                    head->prev = newNode;
                    newNode->prev = nullptr;
                    head = newNode;
                }else {
                    // 중간에 삽입하는 경우
                    newNode->next = currentNode;
                    newNode->prev = currentNode->prev;
                    currentNode->prev->next = newNode;
                    currentNode->prev = newNode;
                }
                dataCount++;
                break;
            }
            //끝까지 감
            if (currentNode == tail) {
                tail -> next = newNode;
                newNode->prev = tail;
                tail = newNode;
                dataCount++;
                break;
            }
            currentNode = currentNode->next;
        }
    }
    if(dataCount > maxEntry) {
        // 리스트가 가득 찬 경우 마지막 노드 제거
        Node<E>* lastNode = tail;
        tail = lastNode->prev;
        tail->next = nullptr;
//        delete lastNode;
        dataCount--;
    }
}

가장 중요하고, 헷갈렸던 add 함수 부분.
일단 template<>를 통해 template specialization을 적용하였다.
노드를 넣는 경우의 수를 잘 생각해봐야한다.
maxEntry 기능을 구현하기 위해 일단 추가한 후 tail을 마지막에 수정해줬다(끊어줬다).


template <typename E>
int LinkedList<E>::length() {
    return dataCount;
}

template <typename E>
void LinkedList<E>::print() {
    Node<E>* currentNode = head;
    while (currentNode != nullptr) {
        std::cout << currentNode->data << " ";
        currentNode = currentNode->next;
    }
    std::cout << std::endl;
}

template <typename E>
void LinkedList<E>::printReverse() {
    Node<E>* currentNode = tail;
    while (currentNode != nullptr) {
        std::cout << currentNode->data << " ";
        currentNode = currentNode->prev;
    }
    std::cout << std::endl;
}

#endif //ASSIGNMENT_LIST_H

gameentry.h

연산자 오버로딩 사용


class GameEntry {
public:
    GameEntry(const std::string &_name, int _score);
    std::string name;
    int score;
    friend std::ostream& operator<<(std::ostream& os, const GameEntry& gameEntry);
    friend bool operator>=(const GameEntry& gameEntry1, const GameEntry& gameEntry2);
};

GameEntry::GameEntry(const std::string &_name, int _score) {
    name = _name;
    score = _score;
}

GameEntry 클래스의 생성자와 friend 키워드
매개변수와 멤버 변수의 이름을 _로 구분했다.
friend 키워드를 사용해야 오버로딩에 대한 준비가 끝난다.

bool operator>=(const GameEntry& gameEntry1,const GameEntry& gameEntry2){
    if(gameEntry1.score == gameEntry2.score){
        if(gameEntry1.name >= gameEntry2.name) {
            return true;
        } else{
            return false;
        }
    }else if(gameEntry1.score >= gameEntry2.score){
            return true;
    }else{
        return false;
    }
}

>= 연산자 오버로딩
>= 연산자의 인수로 GameEntry 클래스의 자료형이 들어왔을 때에만 동작한다.
*int, double 등 다른 자료형에서는 동작하지 않는다.
이름 또한 내림차순으로 정렬해야하는데,
C++에서는 기본 비교 연산자가 문자열 비교도 해준다.


std::ostream& operator<<(std::ostream& os, const GameEntry& gameEntry)
{
    os << "[" << gameEntry.name << "]" << gameEntry.score;
    return os;
}

LinkedList의 멤버함수 print()와 printReverse()에서 정렬된 노드의 data를 하나씩 출력할 때
연산자 <<를 사용한다. 이때 <<를 GameEntry 클래스 자료형에 대해서만
"[이름] 점수" 형태로 출력하도록 오버로딩 해준다.

후기

sentinel 노드를 사용하다가 애를 많이 먹었는데, tail의 next가 순환참조가 일어났었다.
왜 일어났는지 알아보고, sentinel node를 사용해서 못했던 부분을 더 해봐야겠다...
근데 사용하지 않으니 일단 더 쉽고 빠르게 되긴 했다.
교수님 교안에서는 왜 sentinel node를 사용한 예시를 주셨던 걸까?
#sentinel node 사용의 장점?

0개의 댓글