※ 정렬 (Sorting)

정렬은 오름차순, 내림차순과 같은 방식으로 특정 필드값인 key를 기준으로 value간의 정렬을 진행해 주며 다음 2가지로 나뉜다.

단순, 비효율적 방법: 삽입, 선택, 버블 정렬      ==> 대개 자료의 개수가 적을 때 사용
복잡, 효율적인 방법: 힙, 합병, 퀵 정렬             ==> 반드시 자료의 개수가 많다면 효율적 알고리즘을 사용해야 함.

 

🧐 간단하지만, 비효율적인 정렬 방법 

👉 삽입 정렬 (insertion Sort)

삽입정렬은 마치 카드놀이 새 카드를 받는 것 처럼 정렬한 카드들 사이의 올바른 자리에 넣는 것이다.

삽입정렬 방식은 다음과 같이 진행된다.

 

🤨 insertion sort Algorithm

insertionSort(A, n)

for i <- 1 to n-1 do
    key <- A[i];
    j <- i-1;
    while j ≥ 0 and A[j] > key do
        A[j+1] <- A[j];
        j <- j-1;
    A[j+1] <- key

void insertionSort (int A[], int n) {
    for (int i = 0; i < n; i++) {
        int key = A[i];
        int j;
        for (j = i-1; j >= 0 && A[j] > key; j--) 
            A[j+1] = A[j];
        A[j+1] = key;
    }
}

 

 

 

 

 

 

 

 

👉 선택 정렬 (Selection Sort)

가장 이해하기 쉬운 정렬방법으로 오른쪽 리스트가 공백상태가 될 때까지
오른쪽 리스트에서 왼쪽리스트로 가장 작은 숫자를 선택해 이동시킨다.

이렇게 입력배열 이외에 다른 추가 메모리를 요구하지 않는 정렬방법을 제자리 정렬이라 한다.

🤨 selection sort Algorithm

selectionSort (A, n)

for i <- 0 to n-2 do
    least <- A[i], A[i+1], . . . , A[n-1] 중 가장 작은 값의 idx;
    A[i]와 A[least]의 교환;
    i++;
void selectionSort (int A[], int n) {
    for (int i = 0; i < n-1; i++) {
        int least = i;
        for (int j = i+1; j < n; j++) 
            if (A[j] < A[least]) 
                least = j;
        swap(A[i], A[least]);
    }
}

 

 

 

 

 

 

 

 

👉 버블 정렬 (Bubble Sort)

마치 거품이 일어나듯 인접한 2개의 인덱스 값들을 서로 비교하여 큰 값을 오른쪽에 두도록 서로 swapping 하는 것으로 과정은 아래와 같다.

https://commons.wikimedia.org/wiki/File:Bubble-sort-example-300px.gif

🤨 bubble sort Algorithm

BubbleSort(A, n)

for i <- n-1 to 1 do
    for j <- 0 to i-1 do
        j와 j+1번째의 요소가 크기순이 아니면 swap
        j++;
    i--;
void bubbleSort (int A[], int n) {
    for (int i = n-1; i >= 0; i--) {
        for (int j = 0; j < i; j++) 
            if (A[j] > A[j+1])
                swap(A[j], A[j+1]);
    }
}
 

 

 

 

 

※ 정렬 (Sorting)

정렬은 오름차순, 내림차순과 같은 방식으로 특정 필드값인 key를 기준으로 value간의 정렬을 진행해 주며 다음 2가지로 나뉜다.

단순, 비효율적 방법: 삽입, 선택, 버블 정렬      ==> 대개 자료의 개수가 적을 때 사용
복잡, 효율적인 방법: 힙, 합병, 퀵 정렬             ==> 반드시 자료의 개수가 많다면 효율적 알고리즘을 사용해야 함.

 

🧐 복잡하지만, 효율적인 정렬 방법 

👉 힙 정렬 (Heap Sort)

힙 (heap)은 완전 이진트리(complete binary tree)의 일종으로 우선순위 큐를 위해 만들어진 자료구조다.

힙(heap)은 요소들이 완전히 정렬된 것은 아니지만 일종의 반 정렬상태를 유지한다.
이로 인해 삽입 삭제의 시간복잡도가 O(log n)으로 다른 방법보다 훨씬 유리하다.

이런 힙은 여러 개의 값들 중 최댓값과 최솟값을 빠르게 찾아내기 위해 만들어진 자료구조이다.
이를 위해 부모 노드의 key 값  >  자식 노드의 key 값 을 항상 만족하도록 만들어졌다. (보통 최대힙으로 가정하는 편)
단, BST와는 다르게 Heap tree는 중복된 값을 허용한다.

- 위는 힙에 대한 설명으로 힙정렬은 다음과 같은 방식으로 진행한다.

1. 정렬할 배열을 먼저 최소 힙으로 변환한다.
2. 가장 작은 원소부터 차례대로 추출한다.
#include <queue>
#include <functional>  //최소 힙을 사용하기에 써줘야 함
using namespace std;

// 최대 힙을 사용한 내림차순 정렬
void heapSortDec(int arr[], int n) {
    priority_queue<int> maxHeap;
    for (int i = 0; i < n; i++) {
        maxHeap.push(arr[i]);
    }
    // maxHeap을 이용해 내림차순으로 정렬
    for (int i = 0; i < n; i++) {
        arr[i] = maxHeap.top();
        maxHeap.pop();
    }
}

// 최소 힙을 사용한 오름차순 정렬
void heapSortInc(int arr[], int n) {
    priority_queue<int, vector<int>, greater<int> > minHeap;
    for (int i = 0; i < n; i++) {
        minHeap.push(arr[i]);
    }
    // minHeap을 이용해 오름차순으로 정렬
    for (int i = 0; i < n; i++) {
        arr[i] = minHeap.top();
        minHeap.pop();
    }
}

 

 

 

👉 합병 정렬 (Merge Sort)

합병정렬은 분할 정복(divide and conquer)기법을 바탕으로 두고 있다.

🤨 Divide and Conquer란?

1. 하나의 리스트를 2개의 균등한 크기로 분할 (점점 작은 sub problem을 만드는 방법)
2. 분할을 계속 지속해서 1개의 원소로 나눈 후 정렬 진행
3. 두 원소씩 다시 합쳐 전체가 정렬된 리스트를 만드는 방법

🤨 merge Algorithm

merge(A, left, mid, last)
b1 <- left;
e1 <- mid;
b2 <- mid + 1;
e2 <- right;
sorted 배열을 생성;
index <- 0;
while b1 ≤ e1 and b2 ≤ e2 do
    if (A[b1] < A[b2])
        then
            sorted[idx] <- A[b1];
            b1++;
            idx++;
         else
             sorted[idx] <- A[b2];
             b2++;
             idx++;
요소가 남아있는 부분배열을 sorted로 복사;
sorted를 A로 복사;

🤨 merge sort Algorithm

mergeSort (A, left, right)
if left < right
    mid = (left + right) / 2;
    mergeSort(A, left, mid);
    mergeSort(A, mid + 1, right)
    merge(A, left, mid, right)
#define Max_Size 1024
static void merge(int A[], int left, int mid, int right) {
    int i, j, k = left, l;
    static int sorted[Max_Size];
    for (i = left, j = mid + 1; i <= mid && j <= right; )
        sorted[k++] = (A[i] <= A[j]) ? A[i++] : A[j++];
    if (i > mid)
        for (l = j; l <= right; l++, k++)
            sorted[k] = A[l];
    else
        for (l = i; l <= mid; l++, k++)
            sorted[k] = A[l];
        for (l = left; l <= right; l++)
            A[l] = sorted[l];
}

void mergeSort (int A[], int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2;
        mergeSort(A, left, mid);
        mergeSort(A, mid + 1, right);
        merge(A, left, mid, right);
    }
}

 

 

 

 

👉 퀵 정렬 (Quick Sort)

평균적으로 매우 빠른 속도를 자랑하는 정렬방법으로 위의 합병정렬같은 분할 정복(divide and conquer)기법을 바탕으로 두고 있다.

다만 합병정렬과는 달리 리스트를 균등하게 분할하지 않고 리스트 안의 한 요소를 pivot으로 선택한다.

퀵정렬의 과정은 아래와 같이 진행된다.

🤨 퀵 정렬 과정

1. 리스트에서 하나의 원소, pivot을 고른다.
2. pivot앞에는 보다 값이 작은 모든 원소들이,  pivot 뒤에는 보다 값이 큰 모든 원소들이 오도록  pivot을 기준으로 리스트를 둘로 나눈다(분할).  분할을 마친 후  pivot은 더 이상 움직이지 않는다.
3. 분할된 두 개의 작은 리스트에 대해 재귀(Recursion)적으로 이 과정을 반복한다. 재귀는 리스트의 크기가 0이나 1이 될 때까지 반복된다.

이를 위해 중요한 3개의 함수가 사용된다.
- quickSort: 퀵정렬 알고리즘을 이용해 배열의 left ~ right를 오름차순으로 정렬.
- partition: 퀵정렬에서 가장 중요한 함수로 pivot을 설정, 데이터를 left, right로 크기에 따라 이동시킴.
- swap: 조건에 따라 pivot기준으로 왼쪽과 오른쪽의 값들을 서로 바꿔주기 위한 함수

 

https://coding-factory.tistory.com/615

#include <iostream>
using namespace std;

static int partition(int A[], int left, int right) {
    int low = left;
    int high = right + 1;
    int pivot = A[low];

    do {
        do {
            low++;
        } while (low <= high && A[low] < pivot);
        do {
            high--;
        } while (high >= left && A[high] > pivot);
        if (low < high)
            swap(A[low], A[high]);
    } while (low < high);
    swap(A[left], A[high]);
    return high;
}

void quickSort (int A[], int left, int right) {
    if (left < right){
        int q = partition(A, left, right);
        quickSort(A, left, q - 1);
        quickSort(A, q + 1, right);
    }
}

 

 

 

 

※ 우선순위 큐 (Priority Queue

우선순위 개념을 큐에 대입한 자료구조로 컴퓨터 분야에서 다양하게 이용된다.

우선순위 큐는 다양한 방법(배열, 연결리스트, ...)으로 구현할 수 있지만 가장 효율적인 구조는 힙(Heap)이다.

이런 우선순위 큐는 어떤 요소가 먼저 삭제될 것인지에 따라 다음 2가지로 나뉜다.

최대 우선순위 큐: 우선순위가 가장 높은 것 먼저 삭제
최소 우선순위 큐:
우선순위가 가장 낮은 것 먼저 삭제

 

 

※ 힙 (Heap)

힙 (heap)은 완전 이진트리(complete binary tree)의 일종으로 우선순위 큐를 위해 만들어진 자료구조다.

힙(heap)은 요소들이 완전히 정렬된 것은 아니지만 일종의 반 정렬상태를 유지한다.
이로 인해 삽입 삭제의 시간복잡도가 O(log n)으로 다른 방법보다 훨씬 유리하다.

이런 힙은 여러 개의 값들 중 최댓값과 최솟값을 빠르게 찾아내기 위해 만들어진 자료구조이다.
이를 위해 부모 노드의 key 값  >  자식 노드의 key 값 을 항상 만족하도록 만들어졌다. (보통 최대힙으로 가정하는 편)
단, BST와는 다르게 Heap tree는 중복된 값을 허용한다.

- 최대 힙 (max heap)

key(부모) ≥ key(자식)

 

 

 

 

 

 

 

 

- 최소 힙 (min heap)

key(부모) key(자식)

 

 

 

 

 

 

✯ 힙의 기본 틀

왼쪽 자식의 index = 부모 index * 2
오른쪽 자식의 index = 부모 index* 2 + 1

∴ 부모의 index = 자식의 index / 2

 

 

 

 

※ MaxHeap의 삽입연산 (insert)

힙에 새로운 요소가 들어오면 
1. 힙의 마지막 노드에 이어서 새 노드를 삽입
2. 삽입 후 새 노드를 부모노드와 교환하여 힙의 성질을 만족시켜준다.

 

✯ BST의 insert Algorithm

insert (key)

heapSize <- heapSize + 1;
i <- heapSize;
node[i] <- key;
while i != 1 and node[i] > node[PARENT(i)] do
	node[i] ↔ node[PARENT(i)];	// 노드를 서로 교환
    i <- PARENT(i);

 

 

 

MaxHeap의 삭제연산 (delete)

1. 먼저 root노드 삭제 후, 빈 루트에 힙의 마지막 노드를 가져 온다. (레벨순회 순 마지막 노드)
2. 순차적으로 강등 진행 (자식들 중 더 큰 값과 교환 진행)

✯ BST의 delete Algorithm

remove()

item <- A[1];
A[1] <- A[HeapSize];
i <- 2;

while i ≤ heapSize do
	if i <heapSize nad A[LEFT(i)] > A[RIGHT(i)]
    	then largest <- LEFT(i);
    	else largest <- RIGHT(i);
    if A[PARENT(largest)] > A[largest]
    	then break;
    A[PARENT(largest)] ↔ A[largest];
    i <- LEFT(largest);
return item;

 

 

 

 

 

 

※ MaxHeap 구현 

- HeapNode.h

#include <iostream>
#define MAX_ELEMENT 200      // heap array size
using namespace std;

template <typename T>
class Node{
private:
    int key;
    T data;
public:
    Node(){ key = 0; }
    Node(int key, T _data){
        this->key = key;
        this->data = data;
    }
    ~Node(){}

    // getter/setter
    int getKey(){ return key; }
    void setKey(int key){ this->key = key; }
    T getData(){ return data; }
    void setData(T data){ this->data = data; }
};

template <typename T>
class MaxHeap{
private:
    Node<T> node[MAX_ELEMENT];
    int size;   // heap 요소 개수
public:
    MaxHeap(){ size = 0; }
    ~MaxHeap(){}

    // search node
    Node<T>& getParent(int index){
        return node[index/2];
    }
    Node<T>& getLeftChild(int index){
        return node[index*2];
    }
    Node<T>& getRightChild(int index){
        return node[index*2+1];
    }

    // 삽입
    void insert(int key, T data){
        if(isFull()){
            cout << "Heap is Full" << endl;
            return;
        }

        int index = ++size;     // 힙트리 마지막 노드의 다음 위치에서 시작

        // 힙트리를 거슬러 올라가며 부모 노드와 비교
        while(index != 1 && key > getParent(index).getKey()){
            node[index] = getParent(index);
            index /= 2;
        }

        // 최종 위치에 데이터 삽입
        node[index].setKey(key);
        node[index].setData(data);
    }

    // 삭제
    T remove(){
        if(isEmpty()){
            cout << "Heap is Empty" << endl;
            exit(EXIT_FAILURE);
        }

        Node<T> itemNode = node[1];         // root node (삭제 대상)
        Node<T> lastNode = node[size--];    // 힙트리의 마지막 노드
        int index = 1;                      // 마지막 노드의 index (root 부터 시작)

        // root 부터 내려가며 자식 노드와 비교
        while(index <= size){
            if(index*2 > size){             // leaf node인 경우 (자식 노드가 없는 경우)
                break;
            }
            else if(index*2 == size){      // 자식노드가 하나인 경우
                if(lastNode.getKey() < getLeftChild(index).getKey()) {
                    node[index] = getLeftChild(index);
                    index *= 2;
                }
                else
                    break;

            }
            else{                          // 자식노드가 두개인 경우
                int leftChildKey = getLeftChild(index).getKey();
                int rightChildKey = getRightChild(index).getKey();

                // 둘 중 key가 더 큰 자식노드와 교환
                if(lastNode.getKey() < leftChildKey || lastNode.getKey() < rightChildKey){
                    node[index] = leftChildKey > rightChildKey ? getLeftChild(index) : getRightChild(index);
                    index = leftChildKey > rightChildKey ? index*2 : index*2+1;
                }
                else
                    break;

            }
        }
        node[index] = lastNode;     // 마지막 노드를 최종 위치에 저장
        return itemNode.getData();  // 삭제 노드의 data 반환
    }

    // 출력
    void display(){
        int level = 1;
        for(int i=1; i<= size; i++){
            if(i == level){
                cout << endl;
                level *= 2;
            }
            cout << node[i].getData() << "(" << node[i].getKey() << ")  ";
        }
        cout << '\n' << "-------------------------" << endl;
    }

    bool isEmpty(){ return size == 0; }
    bool isFull(){ return size == MAX_ELEMENT - 1; }
};

- main.cpp

#include "HeapNode.h"

int main(){
    MaxHeap<int> priorityQueue;

    // 삽입
    priorityQueue.insert(10, 10);
    priorityQueue.insert(5, 5);
    priorityQueue.insert(30, 30);
    priorityQueue.insert(8, 8);
    priorityQueue.insert(9, 9);
    priorityQueue.insert(3, 3);
    priorityQueue.insert(7, 7);
    priorityQueue.display();

    // 삭제
    priorityQueue.remove();
    priorityQueue.display();
    priorityQueue.remove();
    priorityQueue.display();

    return 0;
}

 

 

 

 

※ 이진 탐색 트리 (Binary Search Tree

이진 탐색 트리는 효율적인 탐색작업(search)을 위한 자료구조로 다음과 같은 특징을 갖는다.

모든 node는 유일한 key를 갖는다.
왼쪽 sub tree의 key값 < root의 key 값 < 오른쪽 sub tree의 key값
모든 sub tree는 BST를 만족한다.

즉, 삽입 삭제가 다음 그림과 같이 진행된다.

선형 자료구조와 달리 이전 시간의 트리에서 삽입과 삭제, 탐색을 위해 node의 위치를 지정하는 것이 쉽지 않다.
하지만 BST에서는 이 연산들을 구체화 할 수 있고 또한 key값을 기준으로 정렬이 가능하다.

 

 

 

※ BST 기본 틀 설계

☠︎ skeleton code 

 

- BinaryNode.h

#include <iostream>

class BinaryNode{
protected:
    int data;
    BinaryNode* left;
    BinaryNode* right;
public:
    BinaryNode(int val = 0, BinaryNode *l = nullptr, BinaryNode *r = nullptr) : data(val), left(l), right(r) { }
    void setData(int val) { data = val; }
    void setLeft(BinaryNode* l) { left = l; }
    void setRight(BinaryNode* r) { right = r; }
    int getData() const { return data; }
    BinaryNode* getLeft() const { return left; }
    BinaryNode* getRight() const { return right; }
    bool isLeaf() const { return left == nullptr && right == nullptr; }
};

 

- BinaryTree.h

#include "BinaryNode.h"
using namespace std;

class BinaryTree {
private:
    BinaryNode *root;
public:
    BinaryTree() : root(nullptr) { };
    void setRoot(BinaryNode * node) { root = node; }
    BinaryNode * getRoot() { return root; }
    bool isEmpty() { return root == nullptr; }

    // 이진트리의 순회연산 (전위, 중위, 후위, 레벨 순회)
    void inorder() { cout << endl << "inorder: "; inorder(root); }
    void inorder(BinaryNode *node) {
        if (node != nullptr) {
            inorder(node->getLeft());
            cout << node->getData() << " ";
            inorder(node->getRight());
        }
    }
    void preorder() { cout << endl << "preorder: "; preorder(root); }
    void preorder(BinaryNode *node) {
        if (node!= nullptr) {
            cout << node->getData() << " ";
            preorder(node->getLeft());
            preorder(node->getRight());
        }
    }
    void postorder() { cout << endl << "postorder: "; postorder(root); }
    void postorder(BinaryNode *node) {
        if (node!= nullptr) {
            postorder(node->getLeft());
            postorder(node->getRight());
            cout << node->getData() << " ";
        }
    }

    // 이진트리의 추가 연산 (이진트리 노드의 개수 구하기 ,단말노드개수 구하기, 높이 구하기)
    int getCount() { return isEmpty() ? 0 : getCount(root); }
    int getCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        return 1 + getCount(node->getLeft()) + getCount(node->getRight());
    }
    int getLeafCount() { return isEmpty() ? 0 : getLeafCount(root); }
    int getLeafCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        if (node->getLeft()) return 1;
        else
            return getLeafCount(node->getLeft()) + getLeafCount(node->getRight());
    }
    int getHeight() { return isEmpty() ? 0 : getHeight(root); }
    int getHeight(BinaryNode *node) {
        if (node == nullptr) return 0;
        int hLeft = getHeight(node->getLeft());
        int hRight = getHeight(node->getRight());
        return (hLeft > hRight) ? hLeft + 1 : hRight + 1;
    }
};

- BST.h

#include "BinaryTree.h"

class BST : public BinaryTree {
public:
    BST(){}
    ~BST(){}

    BinaryNode *search(int key) {}
    BinaryNode *search(BinaryNode *n, int key){}

    void insert(BinaryNode *n){}
    void insert(BinaryNode *r, BinaryNode *n){}

    void remove(int data){}
    void remove(BinaryNode *parent, BinaryNode *node){}
};

 

 

 

※ BST의 탐색연산 (search)

✯ BST의 search Algorithm

search (root, key)

if root == null
	then return null;
    
if key = KEY(root)
	then return root;
    else if key < KEY(root)
    	then return search(LEFT(root), key);
        else return search(RIGHT(root), key);

key값으로 node를 탐색하는 함수로 2가지 방식으로 구현이 가능하다. 

- 순환적인 방법

BinaryNode *search(BinaryNode *n, int key){
    if (n == NULL) return NULL;
    if (key == n->getData()) return n;	// key == 현재 node의 data
    else if (key < n->getData()) return search(n->getLeft(), key);	// key < 현재 node의 data
    else return search(n->getRight(), key);	// key > 현재 node의 data
}

- 반복적인 방법 (효율성의 측면에서 훨씬 우수)

BinaryNode *search(BinaryNode *n, int key){
    while(n != nullptr){
        if(key == n->getData())
            return n;
        else if(key < n->getData())
            n = n->getLeft();
        else
            n = n->getRight();
    }
    return nullptr;
}

 

 

 

※ BST의 삽입연산 (insert)

BST에서 삽입을 위해 탐색이 선행되어야 한다! 

( ∵ BST에서는 같은 key값을 갖는 node가 없어야 하기 때문! )

( ∵ BST에서는 탐색에 실패한 위치 = NULL값 = new node를 insert하는 위치 )

 

✯ BST의 insert Algorithm

insert (root, n)

if KEY(n) == KEY(root)
	then return;
else if KEY(n) < KEY(root) then
	if LEFT(root) == null
    	then LEFT(root) <- n;
        else insert(LEFT(root), n);
else
	if RIGHT(root) == null

 

key값으로 node를 탐색하는 함수로 2가지 방식으로 구현이 가능하다. 

- 순환적인 방법

void insert(BinaryNode *r, BinaryNode *n){
    if (n->getData() == r->getData())
        return;
    else if (n->getData() < r->getData()){
        if (r->getLeft() == nullptr)
            r->setLeft(n);
        else
            insert(r->getLeft(), n);    // recursive insertion
    }
    else {
        if (r->getRight() == nullptr)
            r->setRight(n);
        else    
            insert(r->getRight(), n);  // recursive insertion
    }
}

key값으로 node를 탐색하는 함수로 2가지 방식으로 구현이 가능하다. 

- 반복적인 방법 (위와 동일한 방법으로 변환 가능)

 

 

 

 

※ BST의 삭제연산 (delete)

앞의 두 연산보다 더욱 중요한 연산으로 BST에서 가장 복잡한 연산이다. 다음 3가지의 경우를 고려해야 하기 때문이다.

case1. 삭제하려는 node가 leaf node인 경우.   -->  그냥 삭제하면 되서 가장 간단!
case2. 삭제하려는 node가 왼쪽이나 오른쪽 sub tree중 하나만 갖는 경우   -->  해당 sub tree를 부모로!
case3. 삭제하려는 node가 양쪽 sub tree 모두 갖는 경우  --> 가장 복잡!

 

➣ case 1. 삭제 node가 leaf node

i) 부모 노드를 찾아서
ii) 부모 노드의 link 영역을 nullptr로 만든다.
iii) leaf node를 최종적으로 동적으로 해제한다.
즉, 부모 노드를 반.드.시! 알아야 한다!

 

➣ case 2. 삭제 node가 sub tree 하나만 갖고 있음

i) 해당 노드는 삭제하고
ii) 유일한 자식을 부모 노드에 붙여준다.
즉, 부모와 자식노드를 둘 다! 알아야 한다!

 

 

 

➣ case 3. 삭제 node가 sub tree 모두 갖고 있음

가장 핵심은 "삭제되는 노드와 가장 값이 비슷한 노드"가 핵심!
i) left sub tree의 최하단우측 node와  right sub tree의 최하단좌측 node 중 선택 (위 그림에서 30과 50중 선택)
(오른쪽 서브트리의 제일 작은 값을 선택했다 가정)
이때, 여러 부모 및 자식 노드의 정보가 필요해서 매우 구현이 복잡해진다.

 

 

 

※ BST 구현 

- BinaryNode.h

#include <iostream>

class BinaryNode{
protected:
    int data;
    BinaryNode* left;
    BinaryNode* right;
public:
    BinaryNode(int val = 0, BinaryNode *l = nullptr, BinaryNode *r = nullptr) : data(val), left(l), right(r) { }
    void setData(int val) { data = val; }
    void setLeft(BinaryNode* l) { left = l; }
    void setRight(BinaryNode* r) { right = r; }
    int getData() const { return data; }
    BinaryNode* getLeft() const { return left; }
    BinaryNode* getRight() const { return right; }
    bool isLeaf() const { return left == nullptr && right == nullptr; }
};

- BinaryTree.h

#include "BinaryNode.h"
using namespace std;

class BinaryTree {
public:
    BinaryNode *root;
    BinaryTree() : root(nullptr) { };
    void setRoot(BinaryNode * node) { root = node; }
    BinaryNode * getRoot() { return root; }
    bool isEmpty() { return root == nullptr; }

    // 이진트리의 순회연산 (전위, 중위, 후위, 레벨 순회)
    void inorder() { cout << endl << "inorder: "; inorder(root); }
    void inorder(BinaryNode *node) {
        if (node != nullptr) {
            inorder(node->getLeft());
            cout << node->getData() << " ";
            inorder(node->getRight());
        }
    }
    void preorder() { cout << endl << "preorder: "; preorder(root); }
    void preorder(BinaryNode *node) {
        if (node!= nullptr) {
            cout << node->getData() << " ";
            preorder(node->getLeft());
            preorder(node->getRight());
        }
    }
    void postorder() { cout << endl << "postorder: "; postorder(root); }
    void postorder(BinaryNode *node) {
        if (node!= nullptr) {
            postorder(node->getLeft());
            postorder(node->getRight());
            cout << node->getData() << " ";
        }
    }

    // 이진트리의 추가 연산 (이진트리 노드의 개수 구하기 ,단말노드개수 구하기, 높이 구하기)
    int getCount() { return isEmpty() ? 0 : getCount(root); }
    int getCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        return 1 + getCount(node->getLeft()) + getCount(node->getRight());
    }
    int getLeafCount() { return isEmpty() ? 0 : getLeafCount(root); }
    int getLeafCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        if (node->getLeft()) return 1;
        else
            return getLeafCount(node->getLeft()) + getLeafCount(node->getRight());
    }
    int getHeight() { return isEmpty() ? 0 : getHeight(root); }
    int getHeight(BinaryNode *node) {
        if (node == nullptr) return 0;
        int hLeft = getHeight(node->getLeft());
        int hRight = getHeight(node->getRight());
        return (hLeft > hRight) ? hLeft + 1 : hRight + 1;
    }
};

- BST.h

#include "BinaryTree.h"

class BST : public BinaryTree {
public:
    BST(){}
    ~BST(){}

    BinaryNode *search(int key) {
        BinaryNode *node = search(root, key);
        if (node != NULL) {
            cout << "탐색 성공: key값이 " << node->getData() << "인 노드 = " << node << endl;
        }
        else
            cout << "탐색 실패: key값이 " << key << "인 노드 없음" << endl;
        return node;
    }
    BinaryNode *search(BinaryNode *n, int key){
        while(n != nullptr){
            if(key == n->getData())
                return n;
            else if(key < n->getData())
                n = n->getLeft();
            else
                n = n->getRight();
        }
        return nullptr;
    }

    void insert(BinaryNode *n){
        if ( n == nullptr ) return;
        if (isEmpty()) root = n;
        else insert(root, n);
    }
    void insert(BinaryNode *r, BinaryNode *n){
        if (n->getData() == r->getData())
            return;
        else if (n->getData() < r->getData()){
            if (r->getLeft() == nullptr)
                r->setLeft(n);
            else
                insert(r->getLeft(), n);    // recursive insertion
        }
        else {
            if (r->getRight() == nullptr)
                r->setRight(n);
            else
                insert(r->getRight(), n);  // recursive insertion
        }
    }

    void remove(int key){
        if (isEmpty()) return;

        BinaryNode *parent = nullptr;
        BinaryNode *node = root;
        while(node != nullptr && node->getData() != key){
            parent = node;
            node = (key < node->getData()) ? node->getLeft() : node->getRight();
        }
        if (node == nullptr){
            cout << "Error: key is not in the tree!" << endl;
            return;
        }

        else
            remove(parent, node);
    }
    void remove(BinaryNode *parent, BinaryNode *node){
        // case 1: leaf node 삭제
        if (node->isLeaf()) {
            if (parent == nullptr)
                root = nullptr;
            else {
                if (parent->getLeft() == node)
                    parent->setLeft(nullptr);
                else
                    parent->setRight(nullptr);
            }
        }

        // case 2: 1개 자식만 갖는 node 삭제
        else if (node->getLeft() == nullptr || node->getRight() == nullptr) {
            BinaryNode *child = (node->getLeft() != nullptr) ? node->getLeft() : node->getRight();
            if (node == root)
                root = child;
            else {
                if (parent->getLeft() == node)
                    parent->setLeft(child);
                else
                    parent->setRight(child);
            }
        }

        // case 3: 2개 자식 모두를 갖는 node를 삭제
        else {
            BinaryNode *newnode = node;
            BinaryNode *newnode_parent = node->getRight();
            while (newnode->getLeft() != nullptr) {
                newnode_parent = newnode;
                newnode = newnode->getLeft();
            }
            if (newnode_parent->getLeft() == newnode)
                newnode_parent->setLeft(newnode->getRight());
            else
                newnode_parent->setRight(newnode->getRight());
            node->setData(newnode->getData());
            node = newnode;
        }
        delete node;    // 메모리 동적 해제
    }
};

- main.cpp

#include "BST.h"
#include <iostream>
using namespace std;

int main() {
    BST tree;
    tree.insert(new BinaryNode(35));
    tree.insert(new BinaryNode(18));
    tree.insert(new BinaryNode(7 ));
    tree.insert(new BinaryNode(26));
    tree.insert(new BinaryNode(12));
    tree.insert(new BinaryNode(3 ));
    tree.insert(new BinaryNode(68));
    tree.insert(new BinaryNode(22));
    tree.insert(new BinaryNode(30));
    tree.insert(new BinaryNode(99));

    cout << "노드 개수: " << tree.getCount() << endl;
    cout << "leaf 개수: " << tree.getLeafCount() << endl;
    cout << "height: " << tree.getHeight() << endl;

    tree.preorder();
    tree.inorder();
    tree.postorder();
    cout << endl;

    tree.search(26);
    tree.search(25);
    cout << endl;

    cout << "case 1 ==> 노드 3 삭제" << endl;
    tree.remove(3);
    cout << "case 2 ==> 노드 68 삭제" << endl;
    tree.remove(68);
    cout << "case 3 ==> 노드 18 삭제" << endl;
    tree.remove(18);

    tree.preorder();
    cout << endl;

    cout << "case 3 ==> root 삭제" << endl;
    tree.remove(35);

    tree.preorder();
    cout << endl;

    cout << "노드 개수: " << tree.getCount() << endl;
    cout << "leaf 개수: " << tree.getLeafCount() << endl;
    cout << "height: " << tree.getHeight() << endl;

    /********************************************************************/
    // 노드 개수: 10
    // leaf 개수: 1
    // height: 4
    //
    // preorder: 35 18 7 3 12 26 22 30 68 99
    // inorder: 3 7 12 18 22 26 30 35 68 99
    // postorder: 3 12 7 22 30 26 18 99 68 35
    // 탐색 성공: key값이 26인 노드 = 0x6000030c9200
    // 탐색 실패: key값이 25인 노드 없음
    //
    // case 1 ==> 노드 3 삭제
    // case 2 ==> 노드 68 삭제
    // case 3 ==> 노드 18 삭제
    //
    // preorder: 35 7 12 26 22 30 99
    // case 3 ==> root 삭제
    //
    // preorder: 12 7 26 22 30 99
    // 노드 개수: 6
    // leaf 개수: 1
    // height: 4
}

 

 

※ BST의 성능분석 

트리의 높이를 h라 할 때, 일반적인 선형자료구조의 경우 O(h)가 된다.
이진트리의 경우, 보통 트리의 높이가 log h가 되므로 평균적인 시간복잡도는 O(log n)으로 매우 탁월함을 보여준다.

다만 이런 이진탐색트리의 경우 트리가 간혹 좌우로 불균형해져 트리의 높이가 log n이 만들어 지지 않는 경우가 발생할 수 있다.
이를 보완하는 방법으로 트리의 높이를 항상 log n으로 한정시키는 AVL 트리가 있다.
Adelson Velskii Landis tree이진탐색트리의 노드의 불균형 문제를 해결한 트리로 밑에서 설명하겠다.

 

 

 

 

 

※ AVL 트리 (Adelson Velskii Landis   Tree) 

AVL 트리는 왼쪽과 오른쪽 sub tree간의 높이차가 1 이하인 BST이다. 이를 통해  O(log n)의 시간을 항상 보장한다!

탐색연산은 BST와 동일하지만 삽입, 삭제의 경우가 균형이 깨지기에 AVL 트리는 삽입과 삭제에 추가적 연산을 진행한다.

이런 균형이 깨진 트리를 다시 균형있게 만드는 방법으로 다음 4가지 방법이 있다.

새로 삽입된 노드 NN에서 가장 가까운 조상 노드 A에 대해
LL 타입: NA의 왼쪽 sub tree의 왼쪽 sub tree로 삽입
LR 타입: NA의 왼쪽 sub tree의 오른쪽 sub tree로 삽입 
RR 타입: NA의 오른쪽 sub tree의 오른쪽 sub tree로 삽입
‣ RL 타입: NA의 오른쪽 sub tree의 왼쪽 sub tree로 삽입 

‣ LL 회전: 오른쪽으로 회전
‣ LR 회전: 왼쪽-오른쪽으로 회전
 RR회전: 왼쪽으로 회전
RL 회전: 오른쪽-왼쪽으로 회전

 

✯ AVL Tree의 Rotate Algorithm

☞ 단순 회전 알고리즘(LL, RR)
rotate_LL(A)
	B <- A의 왼쪽 자식
    B의 오른쪽 자식을 A의 왼쪽 자식으로 만든다.
    A를 B의 오른쪽 자식 노드로 만든다.

rotate_RR(A)
	B <- A의 오른쪽 자식
    B의 왼쪽 자식을 A의 오른쪽 자식으로 만든다.
    A를 B의 왼쪽 자식 노드로 만든다.


☞ 이중 회전 알고리즘 (RL, LR)
rotate_RL(A)
	B <- A의 오른쪽 자식
    rotate_LL(B)가 반환하는 노드를 A의 오른쪽 자식으로 만든다.

rotate_LR(A)
	B <- A의 왼쪽 자식
    rotate_RR(B)가 반환하는 노드를 A의 왼쪽 자식으로 만든다.
    rotate_LL(A)

 

 

※ AVL Tree 구현 

- AVLTree.h

#include "BST.h"

class AVLTree : public BST {
public:
    int getHeightDiff(BinaryNode *node) {
        if (node == nullptr) return 0;
        int hLeft = getHeight(node->getLeft());
        int hRight = getHeight(node->getRight());
        return hRight - hLeft;
    }

    // LL rotation
    BinaryNode *LL (BinaryNode *parent){
        BinaryNode *child = parent->getLeft();
        parent->setLeft(child->getRight());
        child->setRight(parent);
        return child;
    }

    // RR rotation
    BinaryNode *RR (BinaryNode *parent){
        BinaryNode *child = parent->getRight();
        parent->setRight(child->getLeft());
        child->setLeft(parent);
        return child;
    }

    // LR rotation
    BinaryNode *LR (BinaryNode *parent){
        BinaryNode *child = parent->getLeft();
        parent->setLeft(RR(child));
        return LL(parent);
    }

    // RL rotation
    BinaryNode *RL (BinaryNode *parent){
        BinaryNode *child = parent->getRight();
        parent->setRight(LL(child));
        return RR(parent);
    }

    // AVL Tree
    BinaryNode *reBalance (BinaryNode *parent) {
        int hDiff = getHeightDiff(parent);
        if (hDiff > 1) {
            if (getHeightDiff(parent->getLeft()) > 0)
                parent = LL(parent);
            else
                parent = RR(parent);
        }
        else if (hDiff < -1) {
            if (getHeightDiff(parent->getRight()) < 0)
                parent = RR(parent);
            else
                parent = RL(parent);
        }
        return parent;
    }

    // AVL Tree insertion
    void insert (int data) {
        if (isEmpty())
            root = new BinaryNode(data);
        else
            root = insertAVL(root, data);
    }
    BinaryNode *insertAVL (BinaryNode *parent, int data) {
        if (data < parent->getData()) {
            if (parent->getLeft() != nullptr)
                parent->setLeft(insertAVL(parent->getLeft(), data));
            else
                parent->setLeft(new BinaryNode(data));
            return reBalance(parent);
        }
        else if (data > parent->getData()) {
            if (parent->getRight()!= nullptr)
                parent->setRight(insertAVL(parent->getRight(), data));
            else
                parent->setRight(new BinaryNode(data));
            return reBalance(parent);
        }
        else
            cout << "Error! 중복된 key" << endl;
            return nullptr;
    }
};

※ 선형 자료구조

앞서 우리가 배운 것들은 모두 선형 자료구조에 속하는데 대표적으로 다음과 같다.

- stack, queue, deque, list . . . 

이와 달리 선형이 아닌 "계층적 구조 (hierarchical)"를 표현하기 위해 사용되는 자료구조인 트리(tree)에 대해 알아보고자 한다.

 

컴퓨터 저장장치의 디렉토리 구조도 계층적인 자료구조로 많은 상황에서 사용되기에 자료구조를 배우는 이유중 하나라 할 수 있다.

자료구조의 양대산맥이라 생각될 정도로 그래프만큼 많이 사용되며 자주 사용되는 자료구조이기에 잘 익혀두자!

 

 

※ 트리 용어 

트리의 구성요소로는 루트(root), 서브트리(subtree), 간선(edge), 노드(node) 등이 있다.

- 차수(degree): 어떤 node가 갖는 자식노드의 개수

- 레벨(level): 트리의 층 수로 root를 level 0이나 1로 매긴다.(책마다 조금씩 다름)

- 높이(height): 트리의 각 층에 번호를 매기는 것으로 트리의 높이는 최대 레벨을 통해 결정할 수 있다.

 

 

※ 이진트리 (Binary Tree)

모든 노드의 차수가 2 이하이며 왼쪽 서브트리와 오른쪽 서브트리가 반드시 구별되어야 한다.

n개의 노드는 n-1개의 간선(edge)를 가지며 높이가 h인 이진트리의 최대 노드 개수는 2^h - 1이다.
n개의 노드를 갖는 이진트리의 높이는 다음과 같다. ⎡log(n+1)⎤ ≤ h ≤ n
포화이진트리(full binary tree): 모든 레벨의 노드가 꽉 차있는 트리, 높이가 h라면 총 2^h-1개의 노드를 갖는다.
완전이진트리(complete binary tree): 레벨 1~h-1까지는 모두 노드가 채워져 있고 마지막 레벨 h는 왼쪽~>오른쪽으로 순서대로 채워진 트리이다. (즉, 마지막 레벨의 노드가 꽉 차있지 않아도 됨)

대표적인 완전이진트리 예로는 힙(heap)이 있다.

 

 

 

※ 이진트리 순회 알고리즘

✯ 전위 순회 (preorder)

preorder(x)

if x != null
	then print data(x);	// 루트(x)노드 처리 
    	 preorder(LEFT(x));  // 왼쪽 subtree 방문
         preorder(RIGHT(x)); // 오른쪽 subtree 방문

 

✯ 중위 순회 (inorder)

inorder(x)

if x != null
	then inorder(LEFT(x));	// 왼쪽 subtree 방문
         print data(x);     // 루트(x)노드 처리
    	 inorder(RIGHT(x)); // 오른쪽 subtree 방문

 

✯ 후위 순회 (postorder)

postorder(x)

if x != null
	then postorder(LEFT(x)); // 왼쪽 subtree 방문
         postorder(RIGHT(x)); // 오른쪽 subtree 방문
         print data(x);     // 루트(x)노드 처리

 

 

 

 

 

※ 이진트리 노드 개수 및 높이 구하는 알고리즘

✯ node 개수 구하기

getCount(x)

if x == null
	then return 0;
    else return 1 + getCount(LEFT(x)) + getCount(RIGHT(x));

 

✯ leaf node 개수 구하기

getLeafCount(x)

if x == null 
	then return 0;
if LEFT(x) == null and RIGHT(x) == null
	then return 1;
    else return getLeafCount(LEFT(x)) + getLeafCount(RIGHT(x));

 

✯ height 구하기

getHeight(x)

if x == null
	then return 0;
    else return 1 + max(getHeight(LEFT(x)), getHeight(RIGHT(x)));

 

 

 

 

 

 

※ 이진트리 구현

✯ BinaryNode.h

#include <iostream>

class BinaryNode{
protected:
    int data;
    BinaryNode* left;
    BinaryNode* right;
public:
    BinaryNode(int val = 0, BinaryNode *l = nullptr, BinaryNode *r = nullptr) : data(val), left(l), right(r) { }
    void setData(int val) { data = val; }
    void setLeft(BinaryNode* l) { left = l; }
    void setRight(BinaryNode* r) { right = r; }
    int getData() const { return data; }
    BinaryNode* getLeft() const { return left; }
    BinaryNode* getRight() const { return right; }
    bool isLeaf() const { return left == nullptr && right == nullptr; }
};

 

✯ BinaryTree.h

#include "BinaryNode.h"
using namespace std;

class BinaryTree {
private:
    BinaryNode *root;
public:
    BinaryTree() : root(nullptr) { };
    void setRoot(BinaryNode * node) { root = node; }
    BinaryNode * getRoot() { return root; }
    bool isEmpty() { return root == nullptr; }

    // 이진트리의 순회연산 (전위, 중위, 후위, 레벨 순회)
    void inorder() { cout << endl << "inorder: "; inorder(root); }
    void inorder(BinaryNode *node) {
        if (node != nullptr) {
            inorder(node->getLeft());
            cout << node->getData() << " ";
            inorder(node->getRight());
        }
    }
    void preorder() { cout << endl << "preorder: "; preorder(root); }
    void preorder(BinaryNode *node) {
        if (node!= nullptr) {
            cout << node->getData() << " ";
            preorder(node->getLeft());
            preorder(node->getRight());
        }
    }
    void postorder() { cout << endl << "postorder: "; postorder(root); }
    void postorder(BinaryNode *node) {
        if (node!= nullptr) {
            postorder(node->getLeft());
            postorder(node->getRight());
            cout << node->getData() << " ";
        }
    }

    // 이진트리의 추가 연산 (이진트리 노드의 개수 구하기 ,단말노드개수 구하기, 높이 구하기)
    int getCount() { return isEmpty() ? 0 : getCount(root); }
    int getCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        return 1 + getCount(node->getLeft()) + getCount(node->getRight());
    }
    int getLeafCount() { return isEmpty() ? 0 : getLeafCount(root); }
    int getLeafCount(BinaryNode *node) {
        if (node == nullptr) return 0;
        if (node->getLeft()) return 1;
        else
            return getLeafCount(node->getLeft()) + getLeafCount(node->getRight());
    }
    int getHeight() { return isEmpty() ? 0 : getHeight(root); }
    int getHeight(BinaryNode *node) {
        if (node == nullptr) return 0;
        int hLeft = getHeight(node->getLeft());
        int hRight = getHeight(node->getRight());
        return (hLeft > hRight) ? hLeft + 1 : hRight + 1;
    }
};

 

✯ main.cpp

#include "BinaryTree.h"
#include <iostream>
using namespace std;

int main() {
    BinaryTree tree;
    BinaryNode *d = new BinaryNode('D', nullptr, nullptr);
    BinaryNode *e = new BinaryNode('E', nullptr, nullptr);
    BinaryNode *b = new BinaryNode('B', d, e);
    BinaryNode *f = new BinaryNode('F', nullptr, nullptr);
    BinaryNode *c = new BinaryNode('C', f, nullptr);
    BinaryNode *a = new BinaryNode('A', b, c);
    tree.setRoot(a);

    tree.inorder();
    tree.preorder();
    tree.postorder();
    cout << endl;

    cout << "노드의 개수 = " << tree.getCount() << endl;
    cout << "leaf의 개수 = " << tree.getLeafCount() << endl;
    cout << "tree의 높이 =  " << tree.getHeight() << endl;


    /********************************************************************/
    //inorder: 68 66 69 65 70 67
    //preorder: 65 66 68 69 67 70
    //postorder: 68 69 66 70 67 65
    //노드의 개수 = 6
    //leaf의 개수 = 1
    //tree의 높이 =  3
}

 

 

※ 선형 자료구조

앞서 우리가 배운 것들은 모두 선형 자료구조에 속하는데 대표적으로 다음과 같다.

- stack, queue, deque, list . . . 

여기서, list와 관련해 알아보고자 한다.

 

cf-1). stack, queue, deque 자료구조와 list의 차이는 무엇일까?

=> 항목에 대한 접근방법이다.

- 스택,큐: 전단(front), 후단(rear)에 접근방식이 제한되어 새로운 객체 삽입 삭제 시 중간에 하는 것을 허용X

- 리스트: 위와 달리 임의의 위치에 있는 항목에 대한 연산이 되기에 선형 자료구조들 중 가장 활용이 자유롭다.

 

cf-2) 연결리스트와 리스트의 용어의 차이는 무엇일까?

- 리스트: 특정 자료구조를 말하는 것으로 대응되는 키워드는 스택, 큐 등이다

- 연결리스트: 그런 자료구조를 구현하는 프로그래밍 기법으로 대응되는 키워드는 배열과 같은 뉘앙스이다.

 

 

※ 선형 리스트 (Linear List)

리스트(list) 또는 선형 리스트(linear list)라 불리는 자료구조는 항목들이 순서나 위치를 갖는 것이다.

- array, stack, queue, deque, list . . . 

 

이때, 리스트와 집합이 다르다는 것을 알아야 한다.

- list(리스트): 각 항목간의 리스트에 들어있는 항목들 사이 순서가 존재한다.

- set(집합): 각 항목간의 순서의 개념이 없다.

 

또한 리스트는 자유로운 데이터 접근 방식으로 다양한 연산이 가능하다.

insert(pos, item)   // 리스트의 pos위치의 새 요소, item 삽입
delete(pos)         // 리스트의 pos위치의 요소 삭제
getEntry(pos)       // 리스트의 pos위치의 요소 반환
find(item)          // 리스트에 item이 있는지 검사
replace(pos, item)  // 리스트의 pos위치에 있는 요소를 item으로 바꿈

isEmpty()           // 리스트가 비어있는지 검사
isFull()            // 리스트가 가득 차있는지 검사
size()              // 리스트 안의 요소개수를 반환
display()           // 리스트 안의 모든 요소 출력

 

리스트를 구현하는 방식은 배열을 사용하는 방법과 연결리스트 방식으로 나눌 수 있는데 보통 연결리스트를 사용한다.

배열 구현의 장점: ADT를 가장 간단하게 구현할 수 있다.

배열 구현의 단점: 요소의 개수에 제한이 생기며 메모리 낭비 및 비효율성과 삽입 삭제시 많은 요소의 이동 등이 있다.

 

연결리스트의 장점: 필요 시에만 노드를 동적으로 생성해 연결하며 연속된 메모리 공간을 할당할 필요도 없다.

연결리스트의 단점: 배열에 비해 구현이 복잡하다.(= 프로그래밍이 어려워지고 오류발생가능성이 커짐)

                              또한 할당과 해제가 빈번하게 일어나 프로그램이 느려질 수 있다는 단점도 존재한다.

 

따라서 보통 포인터를 사용한 연결리스트(Linked List)로 구현하는데, 이 방법도 다음 3가지 방법으로 나눌 수 있다.

 단일 연결리스트  /  원형 연결리스트  /   이중 연결리스트

이때, NULL의 설정을 통해 마지막 노드를 표현한다.

 

 

 

 

 

※ 연결 리스트 구현

§ 단일 연결리스트 (Single Linked List)

- 삽입과정 (insertion) 

 

- 삭제과정 (deletion)

 

 

[구현] 

[Node class_header file]

#include <iostream>

class Node{
private:
    int data;   // node의 data 필드
    Node *link; // 다음 node를 가리키는 포인터변수
public:
    Node(int val) : data(val), link(nullptr) {}
    Node *getLink() { return link; }
    void setLink(Node *next) { link = next; }
    void display(){ printf(" <%2d>", data); }
    bool hasData(int val) { return data == val;}

    void insertNext(Node *node) {
        if (node != nullptr){
            node->link = link;  // 새로운 node의 link를 선행하는 node와 연결
            link = node;        // 후속 node의 link와 새 node를 연결
        }
    }
    Node *removeNext(){
        Node *removed = link;
        if (removed != nullptr) 
            link = removed->link;   // 현재 node의 link를 다음다음 node의 data와 연결
        return removed;             // 삭제된 node주소를 반환해야 함. 그렇지 않으면 삭제된 node메모리가 해제되지 않고 위치를 잃어버림
    }
};

[Linked List class_header file]

 

[main_cpp file]

#include "LinkedList.h"

int main() {
    LinkedList list;
    list.insert(0, new Node(10));   // 리스트 맨 앞 10 삽입
    list.insert(0, new Node(20));   // 리스트 맨 앞 20 삽입
    list.insert(1, new Node(30));   // 리스트 1위치에 30 삽입
    list.insert(list.size(), new Node(40)); // 리스트 마지막에 40 삽입
    list.insert(2, new Node(50));
    list.display();

    list.remove(2);     // 리스트 2위치 삭제
    list.remove(0);     // 리스트 맨 앞 삭제

    list.replace(1, new Node(365)); // 리스트 1위치 값 365로 변경
    list.display();

    list.clear();       // 리스트 모든 항목 삭제
    list.display();
}

 

 

 

§ 이중 연결리스트 (Doubly Linked List) 

이중 연결리스트에서 임의의 node를 가리키는 포인터를 p라하면 아래와 같은 관계가 항.상! 성립한다.

p == p->next->prev == p->prev->next

 

- 삽입과정 (insertion)

- 삭제과정 (deletion)

 

 

[구현]

[Node2 class_header file]

#include <iostream>

class Node2{
private:
    int data;       // node의 data 필드
    Node2 *prev;    // 선행 node를 가리키는 포인터
    Node2 *next;    // 후속 node를 가리키는 포인터
public:
    Node2(int val) : data(val), prev(nullptr), next(nullptr) {}
    Node2 *getPrev() { return prev; }
    Node2 *getNext() { return next; }

    void setPrev(Node2 *p) { prev = p; }
    void setNext(Node2 *n) { next = n; }
    void display() { printf(" <%2d>", data); }
    bool hasData(int val) { return data == val; }

    void insertNext (Node2 *node) {
        if (node != nullptr){
            // 삽입 node의 prev가 삽입node의 prev node의 data를 가리킴
            node->prev = this; 
            // 삽입 node의 next가 삽입node의 next node의 data를 가리킴
            node->next = next;
            
            // 삽입 node의 next node의 prev link가 삽입node의 data를 가리킴
            if (next != nullptr)
                next->prev = node;
            // 삽입 node의 prev node의 next link가 삽입node의 data를 가리킴
            next = node;
        }

    }

    Node2 *remove() {
        if (prev != nullptr)
            return prev->next = next;   // 현재 node기준 선행 node의 link가 후속 node의 data를 가리킴
        if (next != nullptr)
            return next->prev = prev;   // 현재 node기준 후속 node의 link가 선행 node의 data를 가리킴
    }

};

 

[DBLinkedList class_header file]

 

[main_cpp file]

#include "DBLinkedList.h"

int main() {
    DBLinkedList list;
    list.insert(0, new Node(10));   // 리스트 맨 앞 10 삽입
    list.insert(0, new Node(20));   // 리스트 맨 앞 20 삽입
    list.insert(1, new Node(30));   // 리스트 1위치에 30 삽입
    list.insert(list.size(), new Node(40)); // 리스트 마지막에 40 삽입
    list.insert(2, new Node(50));
    list.display();

    list.remove(2);     // 리스트 2위치 삭제
    list.remove(0);     // 리스트 맨 앞 삭제

    list.replace(1, new Node(365)); // 리스트 1위치 값 365로 변경
    list.display();

    list.clear();       // 리스트 모든 항목 삭제
    list.display();
}

 

 

 

※ List Container (STL) 

1. 관련 함수

§ iterator
begin(): beginning iterator 반환
end(): end iterator 반환

*iterator: iterator가 가리키는 원소에 접근
front(): 첫번째 원소 반환
back(): 마지막 원소 반환

§ 삽입 (insertion)
push_front(element): 리스트 맨 앞에 원소 추가
push_back(element): 리스트 맨 뒤에 원소 추가
insert(iterator, element): iterator가 가리키는 부분의 앞에 원소 추가

§ 삭제 (deletion)
pop_front(): 리스트 맨 앞의 원소 삭제
pop_back(): 리스트 맨 뒤의 원소 삭제
erase(iterator): iterator가 가리키는 부분의 원소 삭제

§ etc.
empty(): 리스트가 비어있는지 여부
size(): 리스트 사이즈 반환

 

2. 예시

#include <iostream>
#include <list>

using namespace std;

int main()
{
    // 리스트 생성 
    list<int> a;

    // 원소 추가 
    a.push_back(22);
    a.push_back(33);
    a.push_front(11);
    a.push_back(44);
    a.push_back(55);

    // 반복자 생성 
    list<int>::iterator iter = a.begin();

    // 리스트 출력 
    for(iter=a.begin(); iter!=a.end(); iter++)
    {
        cout << *iter << endl; // 원본 리스트: 11 22 33 44 55 
    }

    cout << "" << endl;
    cout << "" << endl;

    // 원소 삭제 
    a.pop_front();
    a.pop_back();

    for(iter=a.begin(); iter!=a.end(); iter++)
    {
        cout << *iter << endl; // 원소 삭제후 리스트: 22 33 44 
    }

    cout << "" << endl;

    // 리스트 사이즈 출력 
    cout << a.size() << endl; // 3 출력( 22, 33, 44 이므로) 

    // 리스트가 비어있는가 
    cout << a.empty() << endl; // 비어있지 않으므로 0 반환 

    // 리스트 첫번째 원소 출력 
    cout << a.front() << endl; // 22 

    // 리스트 마지막 원소 출력 
    cout << a.back() << endl; // 44 

    cout << "" << endl;

    // 반복자 2번째 위치로 이동 
    iter++; // 반복자가 두번째 원소(33)를 가리킴 
    iter++; // 반복자가 세번째 원소(44)를 가리킴 
    a.insert(iter, 55555);
    for(iter=a.begin(); iter!=a.end(); iter++)
    {
        cout << *iter << endl; //세번째 원소(44) 전에 추가하는 것(22,55555,33,44) 
    }
}

 

 

 

※ Recursion (순환 / 재귀)

흔히들 재귀라 부르는 recursion은 함수가 자기자신을 호출하여 문제를 해결하는 프로그래밍 기법이다.

보통 우리는 이렇게 반복을 요하는 문제에서는 반복문(for, while)을 사용하는 경우가 많다. 또한, 이런 반복문의 효율성은 무시하지 못할 것이다. 일반적으로 재귀보다는 반복문이 더욱 효율적이고 값 도출시간도 빠르다.

 

출처:&nbsp;https://www.scaler.com/topics/python/recursion-in-python/

 

 

§  최대공약수

auto gcd(int A, int B){
    if (B == 0)
        return A;
    else
        return gcd(B, A%B);
}

https://chan4im.tistory.com/58

 

[Baekjoon/백준] 13241번: [Recursion] (C/C++) ★★☆

※ 이번 문제에서 알게된 점 § 재귀함수로 구현하는 gcd, lcm 함수 // 최대공약수 auto gcd(long long A, long long B){ if (B == 0) return A; else return gcd(B, A%B); } // 최소공배수 auto lcm (long long A, long long B){ return A*B

chan4im.tistory.com

https://www.acmicpc.net/problem/13241

 

13241번: 최소공배수

정수 B에 0보다 큰 정수인 N을 곱해 정수 A를 만들 수 있다면, A는 B의 배수이다. 예: 10은 5의 배수이다 (5*2 = 10) 10은 10의 배수이다(10*1 = 10) 6은 1의 배수이다(1*6 = 6) 20은 1, 2, 4,5,10,20의 배수이다. 다

www.acmicpc.net

 

 

 

 

 

 

 

 

 

※ 재귀를 사용하는 이유

이렇게 반복문보다 효율이 떨어지는 재귀를 도대체 우리는 사용하는 이유와 배워야 하는 이유가 무엇일까?

바로 재귀만이 갖는 자기자신의 호출로 특정 문제나 알고리즘에서 매우 강력(powerful)한 경우가 있다.

 

재귀로 짜면 간단하게 2~3줄로 나올 코드가 반복문으로 몇십줄이 넘어갈 수 도 있으니 말이다.

 

대표적인 예를 들어보자면 다음과 같다.

- 거듭제곱, 팩토리얼의 계산

백준 10872번: https://www.acmicpc.net/problem/10872

 

10872번: 팩토리얼

0보다 크거나 같은 정수 N이 주어진다. 이때, N!을 출력하는 프로그램을 작성하시오.

www.acmicpc.net

- 피보나치 수열의 계산

백준 2747번: https://www.acmicpc.net/problem/2747

 

2747번: 피보나치 수

피보나치 수는 0과 1로 시작한다. 0번째 피보나치 수는 0이고, 1번째 피보나치 수는 1이다. 그 다음 2번째 부터는 바로 앞 두 피보나치 수의 합이 된다. 이를 식으로 써보면 Fn = Fn-1 + Fn-2 (n ≥ 2)가

www.acmicpc.net

- 하노이탑 문제

백준 1914번: https://www.acmicpc.net/problem/1914

 

1914번: 하노이 탑

세 개의 장대가 있고 첫 번째 장대에는 반경이 서로 다른 n개의 원판이 쌓여 있다. 각 원판은 반경이 큰 순서대로 쌓여있다. 이제 수도승들이 다음 규칙에 따라 첫 번째 장대에서 세 번째 장대로

www.acmicpc.net

 

 

#include <iostream>
using namespace std;

int fac(int num){
    if (num > 1)
        return num * fac(num-1);  // num: 해결된 부분  // fac(num-1): 남은부분
    else
        return 1;
}

int main() {
    int num;
    cin >> num;
    cout << fac(num);
}

이때, 재귀를 멈추는 부분 

if (num > 1)
    return num * fac(num-1);
else
    return 1;

위와 같은 재귀를 멈추는 부분이 없다면 시스템 stack을 모두 사용한 이후 Err가 발생하며 멈출 것이다. (stackoverflow)

 

 

이렇게 반복에 비해 알고리즘을 훨씬 명확하고 간결하게 나타낼 수 있다는 장점이 있으나 아래와 같이 수행속도면에서는 현저히 떨어지는 것을 볼 수 있다.

좌) for 반복문&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;우) 재귀함수

 

 

 

 

 

 

 

※ Recursion의 시간복잡도

재귀는 작은 동일한 문제들로 분해하여 해결하는 방법인 분할정복 알고리즘의 일종이다.

5! -> 4! -> 3! -> 2! -> 1! 으로 작은 문제 즉, 본래의 problem을 subproblem으로 나누어 최소 size부터 해결하는 방식이다.

최소 subproblem부터 다시 본래의 problem size로 돌아가면서 정복하는, 이런 방식을 분할정복 알고리즘이라 부른다.

 

그렇다면 이런 재귀함수의 시간복잡도 즉, 성능은 어떻게 될까?

이를 위한 예시는 피보나치 수열문제로 한다.

int fib(int num){
    if (num <= 1)
        return num; 
    else
        return fib(num-1) + fib(num-2);
}

위의 문제에서 피보나치식의 n번째 계산을 위한 연산횟수를 T(n)이라 하자.

T(0) = 1, T(1) = 1이므로 

T(n) = T(n-1) + T(n-2) + 1 > 2 x T(n-2) > 2x2 x T(n-4) > . . . 2 ^ (n/2) x T(0)이므로 

 

 

 

int fib(int num){
    int arr[100] = { 0, 1, };
    if (num < 2 || arr[num] != 0)
        return arr[num];
    else{
        arr[num] = fib(num-1) + fib(num-2);
        return arr[num];
    }
}

그렇다면 다음 예제는 어떨까?

이 기법은 이미 구한 값을 메모리에 저장해두고 다시 사용하는 메모이제이션 (memoization)을 채택한 방법이다.

위 방법의 시간복잡도를 구해보면

T(0) = 1, T(1) = 1이고

T(2) = T(1) + T(0) + 1 = 3

T(3) = T(2) + T(1) + 1 = 5

T(4) = T(3) + T(2) + 1에서 T(2)는 저장한 피보나치 수를 가져오기만 하면 되기 때문에 T(2) = 1이다.

T(3) + T(2) + 1 = 7이고, 같은 이유로 T(5) = T(4) + T(3) + 1에서 T(3) = 1이므로 T(5) = 9이다.

따라서 T(n) = 2n - 1이라고 할 수 이므로

 

 

 

※ 다중 순환 (multiple recursion)

재귀함수는 아래와 같이  3종류로 나눌 수 있다.

- 선형 순환 (linear recursion)

  ex) 팩토리얼계산, 거듭제곱 계산

- 이진 순환 (binary recursion)

  ex) 피보나치수열, 하노이탑 문제

- 다중 순환 (multiple recursion)

 

 

 

 

 

 

※ Queue

큐는 FIFO(First-In-First-Out) 입출력 형태, 즉 선입선출 방식을 가진 자료구조이다.

출처)https://velog.io/@awesomeo184/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%81%90Queue

 

enqueue와 dequeue의 시간복잡도는 둘 다 O(1)의 시간을 갖는다.

단, 탐색의 시간복잡도는 특정 data를 찾을 때까지 돌아야 하므로 O(n)의 시간을 갖는다.

 

 

 

※ queue Container (STL) ★

queue 컨테이너는 deque 클래스의 인터페이스를 제한해 전형적인 큐 메모리 구조의 인터페이스를 제공하는 형태이다.

출처)&nbsp;http://www.tcpschool.com/cpp/cpp_container_adapter

 

 

※ queue 선언방식

#include <queue>

queue<데이터타입> 이름;

 

 

 

[queue 관련 백준문제]

백준 10854번: https://www.acmicpc.net/problem/10854

 

10854번: Divisions

David is a young boy and he loves numbers. Recently he learned how to divide two numbers. David divides the whole day. He is happy if the result of the division is an integer, but he is not very amused if this is not the case. After quite a while he decide

www.acmicpc.net

백준 11866번: https://www.acmicpc.net/problem/11866

 

11866번: 요세푸스 문제 0

첫째 줄에 N과 K가 빈 칸을 사이에 두고 순서대로 주어진다. (1 ≤ K ≤ N ≤ 1,000)

www.acmicpc.net

백준 1158번: https://www.acmicpc.net/problem/1158

 

1158번: 요세푸스 문제

첫째 줄에 N과 K가 빈 칸을 사이에 두고 순서대로 주어진다. (1 ≤ K ≤ N ≤ 5,000)

www.acmicpc.net

백준 2164번: https://www.acmicpc.net/problem/2164

 

2164번: 카드2

N장의 카드가 있다. 각각의 카드는 차례로 1부터 N까지의 번호가 붙어 있으며, 1번 카드가 제일 위에, N번 카드가 제일 아래인 상태로 순서대로 카드가 놓여 있다. 이제 다음과 같은 동작을 카드가

www.acmicpc.net

 

백준 12789번(stack+queue): https://www.acmicpc.net/problem/12789

 

12789번: 도키도키 간식드리미

인하대학교 학생회에서는 중간, 기말고사 때마다 시험 공부에 지친 학우들을 위해 간식을 나눠주는 간식 드리미 행사를 실시한다. 승환이는 시험 기간이 될 때마다 간식을 받을 생각에 두근두

www.acmicpc.net

 

 

 

 

 

 

※ deque Container (STL) ★

deque 컨테이너는 double-ended queue를 의미하며 양쪽에 끝이 있는 큐이다.

이 컨테이너는 양 끝에서 빠르게 요소를 삽입, 삭제할 수 있다는 점에서 매우 powerful하다!

또한, 중간에서 원소를 추가, 삭제하는 경우(insert, erase), vector보다 효율이 좀 더 좋다.

(vector는 앞에서 원소를 추가하는 것이 불가능 하기 때문에 모든 원소를 뒤쪽으로 밀어야 하기 때문)

 

 

※ deque 선언방식

#include <deque>

deque<데이터타입> 이름;

 

[deque 관련 백준문제]

백준 10866번: https://www.acmicpc.net/problem/10866

 

10866번: 덱

첫째 줄에 주어지는 명령의 수 N (1 ≤ N ≤ 10,000)이 주어진다. 둘째 줄부터 N개의 줄에는 명령이 하나씩 주어진다. 주어지는 정수는 1보다 크거나 같고, 100,000보다 작거나 같다. 문제에 나와있지

www.acmicpc.net

백준 1021번: https://www.acmicpc.net/problem/1021

 

1021번: 회전하는 큐

첫째 줄에 큐의 크기 N과 뽑아내려고 하는 수의 개수 M이 주어진다. N은 50보다 작거나 같은 자연수이고, M은 N보다 작거나 같은 자연수이다. 둘째 줄에는 지민이가 뽑아내려고 하는 수의 위치가

www.acmicpc.net

백준 2161번:

https://www.acmicpc.net/problem/2161

 

2161번: 카드1

N장의 카드가 있다. 각각의 카드는 차례로 1부터 N까지의 번호가 붙어 있으며, 1번 카드가 제일 위에, N번 카드가 제일 아래인 상태로 순서대로 카드가 놓여 있다. 이제 다음과 같은 동작을 카드가

www.acmicpc.net

백준 2346번: https://www.acmicpc.net/problem/2346

 

2346번: 풍선 터뜨리기

1번부터 N번까지 N개의 풍선이 원형으로 놓여 있고. i번 풍선의 오른쪽에는 i+1번 풍선이 있고, 왼쪽에는 i-1번 풍선이 있다. 단, 1번 풍선의 왼쪽에 N번 풍선이 있고, N번 풍선의 오른쪽에 1번 풍선

www.acmicpc.net

백준 20301번: https://www.acmicpc.net/problem/20301

 

20301번: 반전 요세푸스

첫째 줄에 정수 $N$, $K$, $M$이 주어진다. ($1 \leq N \leq 5\ 000$, $1 \leq K, M \leq N$)

www.acmicpc.net

 

 

 

 

※ Queue 자료구조의 구현

위에서 STL 즉, 이미 구현되어 있는 자료구조를 사용한 것을 보았다. 우리는 그 자료구조가 어떻게 구현되어 있는지 뜯어볼 필요가 있다.

 

(pseudo code) 의사코드로 먼저 표현

 

 

 

※ Deque자료구조의 구현

위에서 STL 즉, 이미 구현되어 있는 자료구조를 사용한 것을 보았다. 우리는 그 자료구조가 어떻게 구현되어 있는지 뜯어볼 필요가 있다.

 

(pseudo code) 의사코드로 먼저 표현

 

 

 

 

 

§ 예제. 

 

 

 

 

 

 

 

 

※ Stack

스택은 LIFO(Last-In-First-Out) 입출력 형태, 즉 후입선출 방식을 가진 자료구조이다.

https://images.app.goo.gl/aei6cA:jvUhK2WN86

위의 그림에서 4, 5번째 그림을 보자.    Last In: 3  /  First Out: 3

즉, 가장 마지막에 들어온 data가 가장 먼저 나가는 것을 알 수 있다.

 

push와 pop의 시간복잡도는 둘 다 O(1)의 시간을 갖는다.

단, 탐색의 시간복잡도는 특정 data를 찾을 때까지 돌아야 하므로 O(n)의 시간을 갖는다.

 

그렇다면 Stack을 왜 사용하는 것일까?

 

※ Stack의 사용이유

undo 즉, Ctrl+z라는 키워드처럼 최근에 실행한 명령어를 취소해야 하기 때문이라는 예를 들 수 있다.

 

조금 더 Computer Science적으로 접근해보면 OS에서 process가 바라보는 memory 영역은 크게 4가지로 나뉜다.

- Code: process가 실행할 코드와 매크로 상수가 기계어로 저장된 공간

- Data: 전역변수, static변수 등의 변수들이 저장된 공간

- Heap: malloc, calloc, realloc, new 와 같은 동적할당을 위한 메모리 공간으로 사용한 후 반드시 해제해야한다!

- Stack: heap에 비해 할당할 수 있는 메모리공간이 적지만 포인터로 접근해야하는 heap에 비해 data처리속도가 빠르다.

 

여기서 Stack Segment의 data를 기록하고 종료하는 메커니즘이 후입선출방식(LIFO)으로 이루어진다.

 

 

 

 

 

 

※ Stack Container (STL) ★

stack 컨테이너는 vector 클래스의 interface를 제한해 전형적인 stack메모리 구조의 interface를 제공한다.

stack 컨테이너는 stack 헤더파일에 정의되어 있다. 

http://www.tcpschool.com/cpp/cpp_container_adapter

stack 컨테이너는 stack 메모리구조 표현을 위해 다음과 같은 멤버 함수를 제공한다.

http://www.tcpschool.com/cpp/cpp_container_adapter

 

 

※ Stack 선언방식

#include <stack>

stack<데이터타입> 이름;

#include <iostream>
#include <stack>
using namespace std;

int main() {
    stack<int> intStack1;
    stack<int> intStack2;

    intStack1.push(1);
    intStack1.push(2);
    intStack1.push(3);

    intStack2.push(10);
    intStack2.push(20);
    intStack2.push(30);

    swap(intStack1, intStack2);

    while (!intStack1.empty()) {
        cout << intStack1.top() << endl;
        intStack1.pop();
    }
}

/*
 * 출력결과
 * 30
 * 20
 * 10
 */

출처: https://life-with-coding.tistory.com/406 [코딩젤리:티스토리]

 

 

 

[Stack 관련 백준문제]

백준 10828번: https://www.acmicpc.net/problem/10828

 

10828번: 스택

첫째 줄에 주어지는 명령의 수 N (1 ≤ N ≤ 10,000)이 주어진다. 둘째 줄부터 N개의 줄에는 명령이 하나씩 주어진다. 주어지는 정수는 1보다 크거나 같고, 100,000보다 작거나 같다. 문제에 나와있지

www.acmicpc.net

백준 1874번: https://www.acmicpc.net/problem/1874

 

1874번: 스택 수열

1부터 n까지에 수에 대해 차례로 [push, push, push, push, pop, pop, push, push, pop, push, push, pop, pop, pop, pop, pop] 연산을 수행하면 수열 [4, 3, 6, 8, 7, 5, 2, 1]을 얻을 수 있다.

www.acmicpc.net

백준 3986번: https://www.acmicpc.net/problem/3986

 

3986번: 좋은 단어

이번 계절학기에 심리학 개론을 수강 중인 평석이는 오늘 자정까지 보고서를 제출해야 한다. 보고서 작성이 너무 지루했던 평석이는 노트북에 엎드려서 꾸벅꾸벅 졸다가 제출 마감 1시간 전에

www.acmicpc.net

백준 1725번: https://www.acmicpc.net/problem/1725

 

1725번: 히스토그램

첫 행에는 N (1 ≤ N ≤ 100,000) 이 주어진다. N은 히스토그램의 가로 칸의 수이다. 다음 N 행에 걸쳐 각 칸의 높이가 왼쪽에서부터 차례대로 주어진다. 각 칸의 높이는 1,000,000,000보다 작거나 같은

www.acmicpc.net

 

 

 

 

 

 

 

 

 

 

※ Stack 자료구조의 구현

위에서 STL 즉, 이미 구현되어 있는 자료구조를 사용한 것을 보았다. 우리는 그 자료구조가 어떻게 구현되어 있는지 뜯어볼 필요가 있다.

 

(pseudo code) 의사코드로 먼저 표현

 

 

§ 배열로 구현

#include <iostream>
using namespace std;

inline void Err (char* ErrMessage){
    printf("%s\n", ErrMessage);
    exit(1);  // 실패상태코드로 프로그램 강제 종료
}

const int Max_Stack_Size = 10;

class stack{
private:
    int top;
    int arr[Max_Stack_Size];
public:
    bool isEmpty();
    bool isFull();
    int push(int data);
    void pop();
    void show();
    void Quit();

    stack() { top = -1; }
    ~stack(){ }
};

bool stack::isEmpty() {
    return top == -1;
}
bool stack::isFull() {
    return top == Max_Stack_Size - 1;
}

int stack::push(int data) {
    if (isFull())
        Err("Stack is Full");
    arr[++top] = data;
}
void stack::pop(){
    if (isEmpty())
        Err("Stack is Empty");
    arr[--top];
}

void stack::show(){
    if (isEmpty())
        cout << "Empty Stack" << endl;
    if (isFull())
        cout << "Full Stack" << endl;
    for (int i = 0; i <= top; i++) {
        cout << arr[i] << ' ';
    }
    cout << endl;
}

void stack::Quit(){
    exit(0);
}

int main() {
    stack stack;
    int arr[Max_Stack_Size], i=0;
    int data;
    int num;  // 1. isEmpty  /  2. isFull / 3. push / 4. pop / 5. show / 6. Quit
    while (true){
        cout << "1. push / 2. pop / 3. isEmpty  / 4. isFull / 5. show / 6. Quit"<< endl;
        cin >> num;
        if (num == 1) {
            cout << "push할 숫자를 입력하세요" << endl;
            cin >> data;
            stack.push(data);
        }
        if (num == 2)
            stack.pop();
        if (num == 3)
            stack.isEmpty();
        if (num == 4)
            stack.isFull();
        if (num == 5)
            stack.show();
        if (num == 6)
            stack.Quit();
    }
}

 

 

 

feat. 백준 9012번: https://www.acmicpc.net/problem/9012

 

9012번: 괄호

괄호 문자열(Parenthesis String, PS)은 두 개의 괄호 기호인 ‘(’ 와 ‘)’ 만으로 구성되어 있는 문자열이다. 그 중에서 괄호의 모양이 바르게 구성된 문자열을 올바른 괄호 문자열(Valid PS, VPS)이라고

www.acmicpc.net

§ 예제. 괄호검사 알고리즘. 

조건 1): 왼쪽과 오른쪽 괄호 개수 동일

조건 2): 왼쪽이 먼저 나와야 함

조건 3): 서로 다른 타입의 왼쪽과 오른쪽 괄호쌍을 교차하지 말것.

 

Ex. 

C++로 쉽게 풀어쓴 자료구조 / 천인국.최영규 저

 

(pseudo code) 의사코드로 먼저 표현

 

※ STL을 포함한 stack 실전 구현

#include <iostream>
#include <stack>
#include <map>
using namespace std;

bool checkBracket(char* s){
    stack<char> stack;  // 괄호를 검사하기 위해 사용할 stack
    map<char, char> bracketMap = {{')', '('},
                                  {']', '['},
                                  {'}', '{'}};

    for(int i=0; s[i]!='\0'; i++){
        // 왼쪽 괄호를 만나는 경우 stack에 push
        if(s[i]=='(' || s[i]=='[' || s[i]=='{'){
            stack.push(s[i]);
        }
        // 오른쪽 괄호를 만나는 경우 stack의 top과 비교
        if(s[i]==')' || s[i]==']' || s[i]=='}'){
            if(stack.empty()){
                return false;   // stack이 비어있는 경우 false
            }else{
                if(bracketMap.find(s[i])->second != stack.top()){
                    return false;   // stack의 top과 괄호의 짝이 맞지 않는 경우 false
                }else{
                    stack.pop();    // stack의 top과 괄호의 짝이 맞는 경우 pop
                }
            }
        }
    }

    // 최종적으로 stack이 empty된 경우 true, 아닌경우 flase
    if(stack.empty()){
        return true;
    }else{
        return false;
    }
}

int main() {
    char* c1 = "1. {A[(i+1)]=0;}";
    char* c2 = "2. if((i==0)&&(j==0)";
    char* c3 = "3. A[(i+1])=0;";

    cout << c1 << " : " << (checkBracket(c1) ? "True" : "False") << endl;
    cout << c2 << " : " << (checkBracket(c2) ? "True" : "False") << endl;
    cout << c3 << " : " << (checkBracket(c3) ? "True" : "False") << endl;
    return 0;
}

 

※ Array (배열)

※ 1차원 배열

배열은 주로 여러 개의 동일한 자료형의 데이터를 한꺼번에 만들 때 사용된다.

예를 들어 a0, a1, a2, a3, a4, a5라는 6개의 정수형 변수를 A[6]으로 간단하게 선언할 수 있다.

 

배열의 가장 기본적인 특징은 <인덱스, 요소> 쌍의 집합으로 인덱스를 이용한 직접접근이 가능하다는 것이다.

이때, 배열과 대응되는 개념으로 연결리스트를 공부한다.

- 배열: index를 이용해서 원하는 요소에 직접접근이 가능

- 연결리스트: 맨 처음요소부터 하나씩 순차적으로 찾아가야 원하는 요소에 접근할 수 있다. (순차접근, sequential access)

 

배열의 특징은 index를 건너뛸 때 마다 배열의 자료형의 크기만큼 건너뛴다.

Ex) int arr[6]; 은 int형 배열인데, arr[0] => arr[1]로 건너뛴다면 sizeof(int) = 4byte 만큼 건너뛰게 되는 것이다.

 

단! 이때, 문자열에 대한 배열은 조심해야 한다.

문자열 선언방식은 char str[12]와 같이 하는데 이때, str은 문자열 배열의 변수이름이며

문자열의 마지막 index에는 '\0'이 들어간다! ('\0'은 ASCII값의 0을 뜻하며 즉, NULL을 의미한다.)

따라서 char str[12] = "game is over"는 ['g'] ['a'] ['m'] ['e'] [' '] ['i'] ['s'] [' '] ['o'] ['v'] ['\0'] 처럼

str[11]에 '\0' 값이 들어가 최종적으로 game is ov까지만 배열에 들어가게 된다.

 

 

 

※ 2차원 배열

1차원 배열이 여러개 모여서 이루어진 것이다.

int arr[3][4]의 경우를 해석하자면 [4]개의 1차원 배열이 [3]개 있다고 생각하면 된다.

[ ] [ ] [ ] [ ]     <=  4개짜리 배열

[ ] [ ] [ ] [ ] 

[ ] [ ] [ ] [ ]

 

 

 

 

 

※ 포인터와 참조자로 배열 나타내기

※ 1차원 배열

arr[4] = *(arr + 4)

int arr[4];       int (&ref)[4] = arr;

 

※ 2차원 배열

arr[3][4] = *(arr+3)[4]  =  *(arr[3] + 4)  =  *(*(arr+3)+4)

 

 

 

※ 매개변수로 배열의 길이 전달하기

int findMacVal(int arr[], int len){
    int max = arr[0];
    for (int i = 1; i < len; i++) {
        if (max < arr[i])
            max = arr[i];
    }
    return max;
}


int main() {
    int arr_len = 0;
    cin >> arr_len;
    int arr[arr_len];
    for (int i = 0; i < arr_len; i++) {
        cin >> arr[i];
    }

    int MaxVal = findMacVal(arr, arr_len);
    cout << MaxVal << endl;
}

- 배열의 이름이 포인터 역할을 하기에 함수 호출 시 배열의 이름을 매개변수로 전달하면 호출되는 함수에서는 이 주소를 이용해 배열의 모든 요소들을 접근할 수 있다.

- 이때, 배열의 길이도 매개변수로 함께 전달해야 한다.

 

이는 2차원 배열도 마찬가지로 2차원 배열은 가로크기가 5일 때만 적용가능한 함수가 되어버린다.

int findMacVal(int arr[][5], int h, int w){
    int max = 0;
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            if (max < arr[i][j])
                max = arr[i][j];
        }
    }
    return max;
}

 

Q. 만약 배열의 길이를 모르거나 매개변수로 전달하기 싫다면 어떻게 해야하나요?

 

A. Vector 클래스를 배열대신 사용한다면 가능하다.

 

 

 

※ array container (std::array) _ 고정크기배열

- memory를 자동으로 할당하고 해제하며 원소의 타입과 배열크기를 매개변수로 사용하는 템플릿클래스이며

다음과 같이 사용할 수 있다.

#include <iostream>
#include <array>
using namespace std;

int main(){
    array<int, 3> arr1;
    arr1[0] = 1;
    cout << arr1[0] << endl;

    array<int, 5> arr2;
    for(int i=0; i < arr2.size(); i++){
        cin >> arr2[i];
    }
    for (int i = 0; i < arr2.size(); i++) {
        cout << arr2[i] << ' ';
    }
}

 

위의 std::array에서는 배열원소에 접근할 때 [ ]를 사용했는데, arr.at() 함수를 사용해서 접근할 수도 있다.

출처) https://computer-science-student.tistory.com/80?category=1163589

arr.at() 함수를 이용한 접근은 아래와 같이 할 수 있다.

 

 

 

 

std::array를 매개변수로 하는 배열출력함수

[최대값 찾기]

#include <iostream>
#include<array>
using namespace std;

template<size_t N>

void MaxVal(array<int, N> arr){
    int max = 0;
    for (auto element : arr) {
        if (element > max)
            max = element;
    }
    cout << max;
}


int main() {
    array<int, 5> arr = { 1, 5, 3, 8, 2 };
    MaxVal(arr);
}

 

 

※ std::array 멤버함수 정리

https://computer-science-student.tistory.com/80?category=1163589

 

 

 

 

 

 

 

 

 

 

※ STL Container

STL에서 컨테이너(container)는 같은 타입의 여러 객체를 저장하는 일종의 집합이라 할 수 있습니다.

- 컨테이너 변수 선언 시, 컨테이너에 포함할 요소의 타입 명시가 가능.

- 컨테이너에는 복사 생성과 대입을 할 수 있는 타입의 객체만을 저장가능.

- 컨테이너는 요소의 추가 및 제거 등 다양한 작업을 도와주는 여러 멤버 함수를 포함한다.

 

먼저 STL에서 컨테이너는 자료를 저장하는 방식과 관리하는 방식에 따라  3가지 형태가 있다.

1. sequence container (시퀀스 컨테이너)

2. associative container (연관 컨테이너)

3. adapter container (컨테이너 어댑터)

출처) http://www.tcpschool.com/cpp/cpp_container_intro

이 중에서 우리는 시퀀스 컨테이너의 vetor에 대해서 알아볼 것이다.

 

 

 

 

 

 

 

 

 

 

※ vector container (std::vector) _가변배열

C++의 STL에 내장되어 있는 템플릿 클래스로 배열과 유사하며 Java의 ArrayList와 비슷한 기능을 한다.

배열에 여러가지 기능을 추가한 것으로 기존의 배열에서 불편했던 점을 개선하여 일반화 시킨 것이다.

 

- 벡터 컨테이너는 동적배열의 클래스 템플릿이다.

- 벡터 객체는 요소가 추가,삭제될 때 마다 자동으로 메모리를 재할당해 크기를 동적으로 변경한다.

- 임의의 위치에 있는 원소 접근과, 뒤에서 원소를 추가하는 연산은 O(1)  (분할상환분석)을 보장한다.

 

- vector<x> 타입 vec이 소요하는 메모리는 대략 sizeof(vector<X>) + vec.size()*sizeof(X)이다. (약 12byte)

 

 

 

 

 

※ vector 선언방법

#include <vector>
using namespace std;

vector<자료형> 변수명(숫자)    //숫자만큼 벡터 생성 후 0으로 초기화
vector<자료형> 변수명(숫자, 변수1);   //숫자만큼 벡터 생성 후 변수1으로 모든 원소 초기화
vector<자료형> 변수명{숫자, 변수1, 변수2, 변수3, ...};    // 벡터 생성 후 오른쪽 변수 값으로 초기화
vector<자료형> 변수명[]={ {변수1, 변수2}, {변수3, 변수4}, ...}   //2차원 벡터생성 (열은 고정, 행 가변)
vector<vector <자료형>> 변수명   //2차원 벡터생성 (열, 행 가변)

ex);
vector<int> vec1; // 크기가 0인 벡터 선언
vector<int> vec2(10); // 크기가 10인 벡터 선언
vector<int> vec3(10, 3); // 크기가 10이고 모든 원소가 3으로 초기화된 벡터
vector<int> vec4 = { 1,2,3,4,5 }; // 크기가 지정한 초기값으로 이루어진 벡터
vector<int> vec5[] = { {1,2},{3,4} }; // 벡터 배열 생성(행은 가변인지만, 열은 고정)
vector<vector<int>> vec6; // 2차원 벡터 생성(행과 열 모두 가변)

 

 

 

 

 

※ vector 요소 삽입, 제거, 변경

-  insert(), push_back() 같은 삽입연산자는 많은경우 list보다 vector에 좀 더 효율적이다.

#include <vector>
using namespace std;

int main() {
    vector<int> vec; // 크기가 0인 벡터 선언

    vec.push_back(10);
    vec.push_back(20); // vec = {10, 20}

    vec.insert(vec.begin() + 1, 100);  // vec = {10, 100, 20}

    vec.pop_back();       // vec = {10,100}

    vec.emplace_back(1);   // vec = {10, 100, 1}
    vec.emplace_back(2);   // vec = {10, 100, 1, 2}
    vec.emplace(vec.begin() + 2, -50); // vec = {10, 100, -50, 1, 2}

    vec.erase(vec.begin() + 1);    // vec = {1, -50, 1, 2}
    vec.resize(6); // vec = {1, -50, 1, 2, 0, 0}
    vec.clear();   // vec = empty()
}

 

※ vector의 복사생성자와 move

- Default: push_back()은 값을 넣을 때, 복사생성자를 호출한다. 이는 insert()도 새 메모리에 복사 후 넣는다.

- 이로인해 복사생성자로 인한 오버헤드가 커지고, 성능저하가능성이 있다.

=> 그래서 push_back() insert() 대신 emplace_back() emplac() 함수를 사용한다. (생성자만 호출)

 

 

 

 

 

 

 

※ vector의 iterators

vec.begin()  벡터 시작점의 주소값 반환
vec.end()    벡터 (끝부분+1) 주소값 반환
vec.rbegin() 벡터의 끝지점을 시작점으로 반환
vec.rend() 벡터의 (시작+1)을 끝부분으로 반환

 

 

※ vector의 index 접근

vec.at(i) 벡터의 i번째 요소 접근 (범위 검사O)
vec[i] (operator [])  벡터의 i번째 요소 접근 (범위 검사X)
vec.front 벡터의 첫 번째 요소 접근
vec.rend 벡터의 마지막 요소 접근

 

 

 

 

 

 

※ vec.at(i)와 vec.[i]의 차이점

- at은 범위를 검사하여 범위 밖의 요소에 접근 시 예외처리를 발생시킨다. (std::out_of_range)

- [ ]은 범위검사를 하지 않으며 예외처리를 발생시키지 않는다. (범위 밖 요소에 접근하면 디버깅 발생)

 

vector는 효율에 중점을 두기에 보통 [ ]를 권장한다.

 

 

 

 

 

 

※ vector의 크기 size, capacity

- size: vector의 크기, 즉 벡터에 실제로 저장된 원소의 개수를 뜻한다.

- capacity: vector의 용량, 즉 벡터의 최대 할당 크기를 뜻한다.

 

만약 벡터의 크기가 용량을 초과한다면 재할당이 발생한다. (기존 값 새 메모리에 복사 후 파괴)

if (size > capacity) {
    reallocate <- 모든 값 새 메모리에 복사 후 기존 벡터 파괴
}

 

Problem 1. 새 메모리의 복사과정에서 복사생성자가 발생해 성능이 저하될 수 있다.

Solution 1. reserve()라는 함수를 사용해서 벡터의 용량을 충분히 크게 생성할 수 있다.

 

Problem 2. reserve()를 너무 크게 잡으면 벡터가 불필요하게 늘어나 메모리를 많이 차지할 수 있다.

Solution 2. clear()로 벡터의 값을 지울 때, 벡터의 요소를 삭제할 수 있다.

 

Problem 3. clear()로 벡터의 요소는 없어지지만 capacity는 그대로 남아있다.

Solution 3. 이를 해결하기 위해 swap()을 사용하는데 이는 아래와 같다.

 

#include <vector>
using namespace std;

int main() {
    vector<int> vec = { 1,2,3,4,5 };
    vec.clear();
    cout << vec.capacity() << endl;  // 5 출력
    vector<int>().swap(vec);
    cout << vec.capacity() << endl;  // 0 출력
}

 

 

 

※ vector와 string  [  vector<char>    vs   string  ]

- vector<char>: char타입의 원소로 이루어진 시퀀스

- string: 문자시퀀스를 저장하는 것이 목적, 따라서 string은 문자열을 정렬하는 경우가 거의 없다. (string의 의미성이 소멸)

 

 

 

 

※ vector는 언제 사용하는게 좋을까?

- 크기 변경이 가능할 때

- 순차 접근이 가능할 때

- 랜덥 접근이 가능할 때

 

단점) 중간 삽입과 삭제가 용이하지 않기에 삽입과 삭제가 빈번할 경우, vector보다는 list나 deque를 사용하는 것이 낫다.

크기 변경 가능 O
중간 삽입, 삭제 X
순차 접근 가능 O
랜덤 접근 가능 O

 

 

 

 

 

 

※ Array vs Vector vs Linked List

 

 

 

 

 

출처 및 참고사이트) https://computer-science-student.tistory.com/83

+ Recent posts