너비우선탐색(Breadth First Search)

  너비우선탐색은 너비를 우선으로 하여 탐색을 수행하는 알고리즘이다.

  깊이우선탐색(DFS)와 마찬가지로 맹목적으로 전체노드를 탐색하고자 할 때 자주 사용되며

  큐(Queue)자료구조에 기초한다.

 

  너비우선 탐색은 고급 그래프 탐색 알고리즘에서 자주 활용된다.

  큐 자료구조에 기초한다는 점에서 구현이 간단하다.

  구현함에 있으서 큐STL을 사용하면 좋으며 탐색을 수행함에 있어서 O(N)의 시간이 소요된다.

  일반적인 경우 실제 수행시간은 DFS보다 좋은 편이다.

 

탐색과정

  탐색시작 노드를 큐에 삽입하고 방문처리한다.

  큐에서 노드를 꺼내 해당 노드의 인접 노드 중에서 방문하지 않은 노드들을 모두 큐에 삽입하고 방문처리한다.

  위 과정을 더이상 수행할 수 없을 때 까지 반복한다.

 

  이와같은 연결리스트가 있고 1부터 시작한다고 했을 때, 큐에 1이 먼저 들어가게 된다.

1        

  그리고 1과 인접한 모든 노드를 큐에 넣어주면서 1은 빼준다.

2 3 8    

  그 다음 2와 인접한 노드를 넣어주면서 2를 뺀다.

3 8 7    

  3을 빼면서 3과 인접한 4,5를 큐에 넣는다

8 7 4 5  

  8은 인접한 노드가 없으므로 큐에서 빼주기만한다.

7 4 5    

  7을 빼면서 인접노드 6을 큐에 넣는다.

4 5 6    

  4, 5, 6은 방문하지 않은 인접노드가 없으므로 큐에서 빼준다.

 

  그래서 방문순서 즉, 출력순서는

  1-2-3-8-7-4-5-6이 된다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define INF 99999999
#define MAX_SIZE 1001

typedef struct{
  int index;
  struct Node *next;
} Node;
//연결리스트 정의

typedef struct{
  Node *front;
  Node *rear;
  int count;
} Queue;
//큐 정의

Node **a; //인접한 정점의 정보를 담아야 하니 이차원 포인터를 사용
int n, m, c[MAX_SIZE];
//정점, 간선, 방문정보

void addFront(Node *root, int index){ //연결리스트 삽입함수
  Node *node = (Node*)malloc(sizeof(Node));
  node->index = index;
  node->next = root->next;
  root->next = node;
}

void enQueue(Queue *queue, int index){ //큐 삽입함수
  Node *node = (Node*)malloc(sizeof(Node));
  node->index = index;
  node->next = NULL;
  
  if(queue->count == 0){
    queue->front = node;
  }
  else{
    queue->rear->next = node;
  }
  queue->rear = node;
  queue->count++;
}

int deQueue(Queue *queue){//큐 추출함수
  if(queue->count == 0){
    printf("큐 언더플로우 발생.\n");
    return -INF;
  }
  
  Node *node = queue->front;
  int index = node->index;
  queue->front = node->next;
  free(node);
  queue->count--;
  return index;
}

void bfs(int start){//너비우선 탐색 함수
  Queue q;
  q.front = q.rear = NULL;
  q.count = 0;//큐 초기화
  enQueue(&q, start);
  c[start] = 1;
  //방문처리
  
  while(q.count != 0){//큐에 아무것도 없을때까지 반복
    int x = deQueue(&q);
    printf("%d ", x); //추출한 원소 출력
    Node *cur = a[x]->next; //인접한 노드
    
    while(cur != NULL){
      int next = cur->index;
      if(!c[next]){ //방문하지 않은 상태라면
        enQueue(&q, next); //큐에 삽입
        c[next] = 1; //방문처리
      }
      cur = cur->next; //반복을 위해 next
    }
  }
}

int main(void){
  scanf("%d %d", &n, &m);
  a = (Node**)malloc(sizeof(Node*) * (MAX_SIZE));
  
  for(int i = 1; i <= n; i++){ //1부터 n까지의 정점에 대해 연결리스트 초기화
    a[i] = (Node*)malloc(sizeof(Node));
    a[i]->next = NULL;
  }
  
  for(int i = 0; i < m; i++){ //간선의 개수만큼 입력
    int x, y;
    scanf("%d %d", &x, &y); //연결되어있는 간선을 다 입력
    addFront(a[x], y); //연결리스트에 추가
    addFront(a[y], x);
  }
  
  bfs(1); //노드 1을 기준으로 너비우선탐색 실행
  system("pause");
  return 0;
}

  5 4

  1 2

  1 3

  1 5

  2 5

  이렇게 입력하면

  1 5 3 2가 출력된다.

 

  5개의 정점과 4개의 간선이 있도록 하고

  각 노드를 만들며 next를 NULL로 초기화한다.

 

  연결리스트에 추가하는 부분을 보면 a[x], y와 a[y], x가 있는데 a[1], 2, a[2], 1 이 형태로 들어간다고 가정하고

  처리를 본다면

  node->index = 2

  node->next = root(a[1])->next(NULL) = NULL

  root(a[1])->next = node(index2)

 

  node->index = 1

  node->next = root(a[2])->next(NULL) = NULL

  root(a[2])->next = node(index1)

 

  이런 식으로 2와 1을 서로 인접한 노드로 만들어주는 것이다.

  그림으로 보면 다음과 같다.

  

  1부터 시작한다고 했으니 큐에 처음 1을 넣어준다음 방문처리를 한다.

  그리고 반복문으로 큐에 아무것도 남지 않을때까지 반복하도록 하고 큐에 담겨있는 원소를 추출한다.

  그럼 제일 먼저 시작 노드인 1이 빠져나오면서 출력되고 1의 인접노드를 cur에 담아 내부 반복문을

  수행한다. 내부반복문에서는 해당 노드의 모든 인접 노드를 방문하며 큐에 삽입해주고 방문처리를 한다.

  그럼 5, 3, 2순서로 큐에 들어가게 되고 다시 deQueue로 하나 추출한 다음 그 노드의 인접노드를 체크한다.

  여기서는 이미 1을 제외한 모든 노드인 5, 3, 2를 다 방문해서 큐에 넣어뒀기 때문에 체크만 하고

  하나씩 출력하게 된다.

  

  노드 1의 연결 순서가 5, 3, 2가 되는것은 연결리스트로 처음 들어간 2가 다음에 들어온 3의 next가 되고

  3은 그 뒤에 들어온 5의 next가 되기 때문이다.

 

  

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

'자료구조' 카테고리의 다른 글

AVL트리  (0) 2021.01.17
이진탐색트리(Binary Search Tree)  (0) 2021.01.16
깊이우선탐색(DFS)  (0) 2021.01.14
그래프(Graph)  (0) 2021.01.13
순차탐색, 이진탐색  (0) 2021.01.12

깊이우선탐색(Depth First Search)

  깊이우선탐색은 탐색을 함에 있어서 보다 깊은것을 우선적으로 하여 탐색하는 알고리즘이다.

  깊이우선탐색은 기본적으로 전체 노드를 맹목적으로 탐색하고자 할 때 사용하며

  스택(Stack) 자료구조에 기초한다.

 

  빠르게 모든 경우의 수를 탐색하고자 할 때 쉽게 사용할 수 있으며

  다음과 같은 알고리즘에 따라 구현할 수 있다.

 

  탐색 시작 노드를 스택에 삽입하고 방문처리를 한다.

  스택의 최상단 노드에게 방문하지 않은 인접노드가 하나라도 있으면 그 노드를 스택에 넣고 방문처리를 한다.

  방문하지 않은 인접노드가 없으면 스택에서 최상단 노드를 꺼낸다.

  위 과정을 더이상 수행할 수 없을 때 까지 반복한다.

 

깊이우선탐색 수행과정

 

  이러한 형태의 그래프를 탐색하며 1부터 출발한다고 가정한다.

  1부터 출발하면 1에서 연결된 노드 중 방문하지 않은 2를 방문하고 2에서도 방문하지 않은 7을 방문한다.

  7에서는 6을 방문하는데 6의 경우는 연결된 노드가 없기 때문에 스택에서 제거하고 7에 연결된 8을 방문한다.

  8은 1과 연결되어있으며 방문하지 않은 인접노드가 없기 때문에 8, 7, 2를 스택에서 제거한다.

  1에서 방문하지 않은 인접노드인 3을 방문하고 3은 4를 4는 5를 방문한 뒤 더이상 방문할 인접노드가 없기에

  스택을 비워준다.

 

  스택 형태로 보면 다음과 같다.

1 2      
1 2 7    
1 2 7 6  
1 2 7 8  
1 3      
1 3 4    
1 3 4 5  
         

 

  방문순서를 정리해보면 1 2 7 6 8 3 4 5순서이다.

 

  스택자료구조에 기초한다는 점에서 구현이 간단하지만 실제로는 스택을 쓰지않아도 재귀함수를 이용하면 기본적으로

  스택과 비슷하게 동작한다는 점에서 재귀함수로 간단히 구현할 수 있으며 탐색을 수행함에 있어서 O(N)의 시간이

  소요되는 전수탐색 알고리즘이다.

 

import java.io.*;
import java.util.*;

public class Graph{
  private int V;//노드의 개수
  private LinkedList<Integer> adj[];//인접리스트
  
  Graph(int v){
    V = v;
    adj = new LinkedList[v];
    
    for(int i = 0; i < v; ++i){//인접리스트 초기화
      adj[i] = new LinkedList();
    }
  }
  
  void addEdge(int v, int w){
    adj[v].add(w);
    
    /*
      연결된 리스트 확인
      for(int i = 0; i < adj.length; i++){
        System.out.println("adj["+i+"] : "+adj[i]);
      }
    */
  }
  
  void DFSUtil(int v, boolean visited[]){
    //현재 노드를 방문한것으로 표시하고 값을 출력.
    visited[v] = true;
    System.out.print(v + " ");
    
    //방문한 노드와 인접한 모든 노드를 가져온다.
    Iterator<Integer> i = adj[v].listInteger();
    while(i.hasNext()){
      int n = i.next();
      /*
        다음값인 n의 값 확인
        System.out.println("i.next : "+n);
      */
      
      //방문하지 않은 노드면 해당 노드를 시작 노드로 다시 DFSUtil 호출
      if(!visited[n]){
        DFSUtil(n, visited);
      }
    }
  }
  
  void DFS(int v){
    //노드의 방문여부 판단 (초기값 : false)
    boolean visited[] = new boolean[V];
    
    //v를 시작노드로 DFSUtil 순환 호출
    DFSUtil(v, visited);
  }
  
  /*
  비 연결형 그래프일 경우
  void DFS(){
    //노드의 방문여부 판단
    boolean visited[] = new boolean[V];
    
    //모든 정점을 하나씩 방문
    for(int i = 0; i < V; ++i){
      if(visited[i] == false){
        DFSUtil(i, visited);
      }
    }
  }*/
  
  public static void main(String[] args){
    Graph g = new Graph(4);
    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(2, 0);
    g.addEdge(2, 3);
    g.addEdge(3, 3);
    
    g.DFS(2);
    //DFS(); 비연결형그래프
  }
}

  강의에서 본 C예제코드는 이해가 많이 떨어져서 java코드로 예제를 찾아 학습했다.

  위 코드의 결과값은 2 0 1 3이 출력된다.

  스택을 이용한 구현이 아닌 재귀함수를 이용한 구현이다.

 

  addEdge는 노드를 연결해주는 것인데 다 연결되고나면

  adj[0] = [1, 2]

  adj[1] = [2]

  adj[2] = [0, 3]

  adj[3] = [3]

  이런 형태가 된다.

 

  그림으로 본다면

 

  이렇게 연결된 그래프형태다.

 

  연결까지 다 처리되고 나면 DFS로 시작노드를 넘겨주면서 처리하게 되는데 visited[]는 boolean형태로 V만큼의 크기를

  설정함으로써 adj배열과 같은 크기로 설정해준다.

  이때 모든 기본값은 false가 된다.

 

  DFSUtil에서는 v와 visited를 넘겨 처리해준다.

  처음 넘겨주는 노드는 2로 임의로 정한건데 visited[2] = true를 해줌으로써 2번 노드는 방문했다는것을 확인할 수

  있도록 한다.

  방문한 노드는 우선적으로 출력을 해주고 Iterator로 방문한 노드와 인접한 모든 노드를 가져온다.

  이 경우 adj[2] = [0, 3]이기 때문에 0과 3을 가져온다.

  연결되어있는 다음노드가 존재한다면 반복문을 실행하고 n에 next값을 넣어준다.

 

  그럼 n은 0이 되고 visited[0]은 아직 방문하지 않은 false상태이기 때문에 DFSUtil함수를 호출하게 된다.

  이렇게 계속 재귀적으로 반복하며 모든 노드를 방문하도록 수행한다.

 

 

레퍼런스

● 패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html

'자료구조' 카테고리의 다른 글

이진탐색트리(Binary Search Tree)  (0) 2021.01.16
너비우선탐색(BFS)  (0) 2021.01.15
그래프(Graph)  (0) 2021.01.13
순차탐색, 이진탐색  (0) 2021.01.12
우선순위 큐  (0) 2021.01.11

그래프

  그래프란 사물을 정점(vertex)와 간선(Edge)으로 나타내기 위한 도구이다.

  그래프는 두가지 방식으로 구현할 수 있다.

  인접행렬(Adjacency Matrix) : 2차원 배열을 사용하는 방식

  인접리스트(Adjacency List) : 리스트를 사용하는 방식.

 

  노드를 정점 혹은 노드라고 부르고 그 노드들을 이어주는 선을 간선이라고 한다.

  위 그림에서 0, 1, 2는 노드 3, 7은 간선이다.

 

  인접행렬에서 그래프는 2차원 배열로 표현한다.

  표를 보면 0에서 0으로 이동하는것은 이동하지 않기 때문에 0인것이고

  0에서 1로 이동하는 것은 3, 2로 이동하는것은 7 이런 형태이다.

  즉, 이동하는 간선이 3, 7인 것이다.

  무한이라고 되어있는 경우는 간선이 없는 경우이다.

  1에서 2로 가는 경우나 2에서 1로 가는경우는 간선이 없기 때문에 무한이다.

  그래프는 이런 2차원 배열 형태로 만든다.

 

무방향 비가중치 그래프와 인접행렬

  모든 간선이 방향성을 가지지않는 그래프를 무방향 그래프라고 하고

  모든 간선에 가중치가 없는 그래프를 비가중치 그래프라고 한다.

  무방향 비가중치 그래프가 주어졌을 때 연결되어 있는 상황을 인접행렬도 출력할 수 있다.

  방향성을 가지지 않고 연결만 되어있는 그래프도 무방향 그래프라고 한다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int a[1001][1001];
int n, m;

int main(void){
  scanf("%d %d", &n, &m);
  
  for(int i = 0; i < m; i++){
    int x, y;
    scanf("%d %d", &x, &y);
    a[x][y] = 1;
    a[y][x] = 1;
  }
  
  for(int i = 1; i <= n; i++){
    for(int j = 1; j <= n; j++){
      printf("%d ", a[i][j]);
    }
    printf("\n");
  }
  system("pause");
}

  5 3

  1 2

  3 4

  4 5

  이렇게 입력하면

  0 1 0 0 0

  1 0 0 0 0

  0 0 0 1 0

  0 0 1 0 1

  0 0 0 1 0

  이렇게 출력된다.

 

  a[x][y] = 1

  a[y][x] = 1

  이 부분을 처리할 때 보면 x와 y는 입력값으로 처음에 1와 2이 들어간다.

  n은 정점, m은 간선으로 5와 3이 처음에 입력되고 x와 y는 그 다음 입력값부터 들어간다.

  a[1][2] = 1,  a[2][1] = 1 이 형태가 되는건데

  이차원 배열이므로 첫행 2번째 인덱스에 1, 두번째행 1번째 인덱스에 1이 들어간다.

  출력문에서 보면 a[1][2] = 1 이   0 1 0 0 0 으로 출력된것을 확인 할 수 있다.

  출력하는 반복문을 0부터가 아닌 1부터 시작했기 때문에 0번째 인덱스는 제외하고 1번 인덱스부터 출력이 되서

  보이는 숫자 그대로의 위치로 확인한다.

  4번행은 1이 두번들어가있는데 3 4로 한번 4 5로 한번 들어갔기 때문에 두개의 인덱스에 1이 들어간것이다.

 

방향 가중치 그래프와 인접리스트

  모든 간선이 방향을 갖는 그래프를 방향그래프라고 한다.

  모든 간선에 가중치가 있는 그래프를 가중치그래프라고한다.

  방향 가중치 그래프가 주어졌을 때 연결되어 있는 상황을 인접리스트로 출력할 수 있다.

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
//스택기반 연결리스트

typedef struct{
  int index;
  int distance;
  struct Node *next;
} Node;
//한개의 노드 정의
//어떤 인덱스로 어떠한 만큼의 가중치
//즉, 거리를 갖는지 정의할 수 있도록 한다.
//연결리스트니까 next로 다음 노드를 기록할 수 있도록 한다.

void addFront(Node *root, int index, int distance){
  Node *node = (Node*)malloc(sizeof(Node));
  node->index = index;
  node->distance = distance;
  node->next = root->next;
  root->next = node;
}
/*
  가장 앞단에 데이터를 추가한다.
  어떠한 노드에 어떠한 인덱스까지 가는 거리가 얼마인지를 넣어준다.
*/

void showAll(Node *root){
  Node *cur = root->next;
  
  while(cur != NULL){
    printf("%d(거리 : %d) ", cur->index, cur->distance);
    cur = cur->next;
  }
}

int main(void){
  int n, m;
  scanf("%d %d", &n, &m);
  Node **a = (Node**)malloc(sizeof(Node*) * (n + 1));
  
  for(int i = 1; i <= n; i++){
    a[i] = (Node*)malloc(sizeof(Node));
    a[i]->next = NULL;
  }
  
  for(int i = 0; i < m; i++){
    int x, y, distance;
    scanf("%d %d %d", &x, &y, &distance);
    addFront(a[x], y, distance);
  }
  
  for(int i = 1; i <= n; i++){
    printf("원소 [%d] : ", i);
    showAll(a[i]);
    printf("\n");
  }
  system("pause");
  return 0;
}
/*
  노드의 개수를 n, 간선의 개수를 m에 입력받는다.
  인접리스트로 구현할 때는 노드의 개수만큼 연결리스트가 필요하기 때문에
  노드의 개수에 +1을 더한만큼 동적할당을 해준다.
  여기서 출력문을 만들 때 0부터가 아닌 1부터 출력하도록 할것이기 때문이다.
  0부터 출력한다면 더하지 않아도 되는 부분이다.
  
  첫 반복문에서 각 노드를 동적할당으로 초기화
  next에는 NULL을 넣어서 root가 NULL값을 가리키도록 한다.
  
  두번째 반복문은 각 간선의 정보를 사용자로부터 입력받도록 한다.
  입력받은 값을 바로 리스트화 하기 위해 addFront로 보내 처리한다.
  
  마지막 반복문에서는 각 원소를 출력하고
  showAll에서 특정한 노드 정점에 연결되어있는 다른 정점을 출력하도록 한다.
*/

  5 4

  1 2 9

  1 3 8

  1 5 10

  3 4 8

  이렇게 입력해주면

 

  원소 [1] : 5(거리 : 10) 3(거리 : 8) 2(거리 : 9)

  원소 [2] :

  원소 [3] : 4(거리 : 8)

  원소 [4] :

  원소 [5] :

 

  이렇게 출력된다.

 

  5는 노드의 개수 4는 간선의 개수이다.

  1 2 9라는 것은 1이라는 노드와 2라는 노드의 거리가 9라는 것이다.

  1 2 9를 기준으로 보자면 입력받고나서 addFront(a[1], 2, 9) 형태로 넘어간다.

 

  그럼 addFront에서는 root에 a[1], index에 2, distance에 9가 들어가게되고

  새로 할당받은 node의 index는 2, distance는 9, next는 a[1]의 next니까 NULL root의 next는 node가 된다.

 

void addFront(Node *root, int index, int distance){//a[1], 2, 9
  Node *node = (Node*)malloc(sizeof(Node));
  node->index = index = 2;
  node->distance = distance = 9;
  node->next = root->next = NULL;
  root->next = node = index(2), distance(9);
}


void addFront(Node *root, int index, int distance){ //a[1], 3, 8
  Node *node = (Node*)malloc(sizeof(Node));
  node->index = index = 3;
  node->distance = distance = 8;
  node->next = root->next = node(index(2), distance(9));
  root->next = node = index(3), distance(8);
}

  그림으로 보면 다음과 같다.

 

 

  제일먼저 n의 크기를 갖도록 만들어주고

  addFront에서 node를 만들어 index과 distance를 정의한다.

  그리고 연결해주는 것이다.

 

 

  입력 순으로 이렇게 모두 연결된 뒤 출력하게 된다.

 

  인접행렬은 모든 정점들의 연결 여부를 저장하여 O(V^2)의 공간을 요구하므로 공간 효율성이 떨어지지만

  두 정점이 서로 연결되어 있는지 확인하기 위해 O(1)의 시간을 요구한다.

  인접리스트는 연결된 간선의 정보만을 저장하여 O(E)의 공간을 요구하므로 공간 효율성이 우수하지만

  두 정점이 서로 연결되어있는지 확인하기 위해 O(V)의 시간을 요구한다.

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

'자료구조' 카테고리의 다른 글

너비우선탐색(BFS)  (0) 2021.01.15
깊이우선탐색(DFS)  (0) 2021.01.14
순차탐색, 이진탐색  (0) 2021.01.12
우선순위 큐  (0) 2021.01.11
트리(Tree)  (0) 2021.01.10

순차탐색(Sequentail Search)

  순차탐색은 특정한 원소를 찾기 위해 원소를 순차적으로 하나씩 탐색하는 방법이다.

  아래의 배열에서 '배열'이라는 문자열을 찾는다고 하면

인덱스

0

1

2

3

4

원소

순차

탐색

정렬

배열

리스트

  앞에서부터 문자열을 순차적으로 탐색해서 찾은 문자열의 인덱스를 반환한다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LENGTH 100

char **array;
int founded;

int main(void){
  int n;
  char *word;
  word = malloc(sizeof(char) * LENGTH);
  scanf("%d %s", &n, word);
  array = (char**)malloc(sizeof(char*) * n);
  
  for(int i = 0; i < n; i++){
    array[i] = malloc(sizeof(char*) * n);
    scanf("%s", array[i]);
  }
  
  for(int i = 0; i < n; i++){
    if(!strcmp(array[i], word)){
      printf("%d번째 원소입니다.", i + 1);
      founded = 1;
    }
  }
  
  if(!founded) printf("원소를 찾을 수 없습니다.\n");
  system("pause");
  return 0;
}

  5 배열

  순차 탐색 정렬 배열 리스트

  이렇게 입력하면 '4번째 원소입니다' 라는 결과값이 출력된다.

 

  n은 배열의 크기, word는 문자열이다.

  첫번째 반복문으로 배열의 원소값을 입력하고 두번째 반복문에서 값을 찾는다.

  word를 기준으로 배열의 인덱스를 하나씩 증가시키며 찾아나가고 strcmp함수로 인덱스의 원소값, 그리고 찾고자하는

  문자열인 word를 비교해서 같다면 해당 인덱스값 + 1을 해서 출력을 해준다.

  +1을 한 값으로 출력하는것은 인덱스 번호는 0부터 시작하기 때문에 몇번째에 있는지 출력하기 위해 더했다.

 

  순차탐색은 데이터 정렬 유무에 상관없이 가장 앞에 있는 원소부터 하나씩 확인해야 한다는 점에서 O(N)의

  시간 복잡도를 갖는다.

 

이진탐색(Binary Search)

  이진탐색은 배열 내부 데이터가 이미 정렬되어 있는 상황에서만 사용가능한 알고리즘이다.

  탐색 범위를 절반씩 좁혀가며 데이터를 탐색하는 특징이 있다.

 

  이진 탐색의 탐색과정은 다음과 같다.

  찾을 원소값이 37이고

인덱스 0 1 2 3 4 5 6 7 8
원소 15 27 37 46 57 69 73 85 98

  탐색할 배열이 이렇게 정렬되어 있을 때 중앙의 인덱스 원소값을 기준으로 먼저 탐색한다.

  그럼 가운데 인덱스는 4고 원소값은 57인데 찾을 원소보다 크기 때문에 왼쪽으로 탐색을 하게 된다.

인덱스 0 1 2 3 4 5 6 7 8
원소 15 27 37 46 57 69 73 85 98

  왼쪽에 남은 인덱스인 0 1 2 3 중에서 중간값인 27을 기준으로 한번 더 체크하면 27이 더 작기 때문에

  오른쪽으로 탐색하고 바로 오른쪽 인덱스인 2번인덱스를 탐색하여 체크한다.

  

  이렇게 모든 인덱스를 탐색하지 않아도 되기 때문에 순차탐색보다 더 빠르게 찾을 수 있다.

  

  이진 탐색은 한번 확인할 때마다 봐야하는 원소의 개수가 절반씩 줄어든다는 점에서 탐색시간이 O(logN)의

  시간 복잡도를 갖는다.

 

  start, mid, end로 정의해서 mid위치에 있는 원소와 반복적으로 비교하게 된다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAX_SIZE 100000

int a[MAX_SIZE];
int founded = 0;

int search(int start, int end, int target){
  if(start > end) return -9999;
  
  int mid = (start + end) / 2;
  
  if(a[mid] == target) return mid;//mid값과 target값이 일치한다면 mid값을 반환
  else if(a[mid] > target) return search(start, mid - 1, target);//mid값이 target보다 크다면
  else return search(mid + 1, end, target);//mid값이 target보다 작다면
}

int main(void){
  int n, target;
  scanf("%d %d", &n, &target);
  
  for(int i = 0; i < n; i++) scanf("%d", &a[i]);
  
  int result = search(0, n - 1, target);
  
  if(result != -9999) printf("%d번째 원소입니다.\n", result + 1);
  else printf("원소를 찾을 수 없습니다.\n");
  
  system("pause");
  return 0;
}

  n은 배열의 크기 target은 찾고자하는 값이다.

  첫 반복문에서 배열의 원소값들을 입력해주는데 이때 원소값들은 정렬된 상태로 입력해줘야 한다.

  result는 search함수에서 처리하고 난 반환값이다.

  첫 시작은 0번인덱스가 되어야 하고 마지막 인덱스는 배열 크기 - 1이고 찾는 값을 입력한 값을 넘겨준다.

 

  search에서는 제일먼저 start와 end를 비교해서 start가 더 크다면 -9999를 반환하도록 한다.

  처음부터 이 조건문에 걸릴일은 없겠지만 처리과정을 반복하면서 찾고자하는 값이 배열에 없으면 더 진행하지않고

  반환하기 위해서 맨 위에 작성한다.

 

  mid는 가운데 인덱스 값을 찾기 위해 start와 end를 더한 값을 2로 나눠준다.

 

  a배열의 가운데 인덱스인 mid번째 인덱스의 원소값이 찾고자하는 값인 target과 같다면 더 처리할 필요가 없으므로

  인덱스 값인 mid를 반환한다.

 

  a[mid]가 target보다 크다면 왼쪽에서 탐색해야 하므로 search함수로 값을 다시 넘긴다.

  start는 그대로 0이어야 하니까 start로, end는 mid보다 하나 작은 인덱스까지만 탐색해야 하니까 mid - 1을 해준다.

 

  a[mid]가 target보다 작다면 오른쪽에서 탐색해야 하므로 start는 mid보다 하나 더 큰 값으로, end는 그대로 end를 

  넘겨 처리한다.

 

  출력은 result가 원소값이 없을때의 반환값인 -9999가 아니라면 몇번째 원소인지 출력해준다.

  이때 result + 1을 해준 이유는 배열은 0부터 시작하니까 1부터 시작하도록 만들어서 출력하기 위해서다.

  

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

'자료구조' 카테고리의 다른 글

깊이우선탐색(DFS)  (0) 2021.01.14
그래프(Graph)  (0) 2021.01.13
우선순위 큐  (0) 2021.01.11
트리(Tree)  (0) 2021.01.10
계수정렬, 기수정렬  (0) 2021.01.09

우선순위 큐

  우선순위 큐는 우선순위를 가진 데이터들을 저장하는 큐를 의미한다.

  데이터를 꺼낼 때 우선순위가 높은 데이터가 가장 먼저 나온다는 특징이 있어 많이 활용되고 있다.

 

  우선순위 큐는 운영체제의 작업스케쥴링, 정렬, 네트워크 관리 등의 다양한 기술에 적용되고 있다.

  

  일반적인 형태의 큐는 선형적인 형태를 갖고 있지만 우선순위 큐는 트리(Tree)구조로 보는 것이 합리적이다.

  일반적으로 우선순위 큐는 최대힙을 이용해 구현한다.

 

최대힙(Max Heap)

  최대힙은 부모노드가 자식노드보다 큰 완전 이진트리를 의미한다.

  완전 이진트리는 자식노드가 2개씩 있거나 아예 없어야 한다.

 

  위 두 트리는 완전이진트리가 아니다.

  왼쪽 트리의 경우는 위에서 얘기했듯이 7의 자식노드가 하나밖에 없기 때문에 완전이진트리가 성립하지않고

  오른쪽 트리의 경우는 자식노드가 부모노드보다 크기때문에 성립하지 않는다.

  이렇게 완전이진트리가 아니라면 최대힙에 해당하지 않는다.

 

  최대힙의 루트노드는 전체트리에서 가장 큰 값을 갖는다는 특징이 있다.

  항상 전체트리가 최대힙 구조를 유지하도록 자료구조를 만들 수 있다.

  큐를 기반으로 하기 때문에 enQueue, deQueue로 정의한다.

  enQueue는 우선순위 큐에 데이터를 삽입.

  deQueue는 우선순위 큐에서 데이터를 추출하는 것이다.

 

우선순위 큐의 삽입

  삽입할 원소는 완전 이진트리를 유지하는 형태로 순차적으로 삽입된다.

  삽입 이후에는 루트노드까지 거슬러 올라가면서 최대힙을 구성한다. 상향식이다.

 

  위와 같은 처리과정으로 9를 상위노드와 비교해서 올려줌으로써 최대힙을 유지하도록 한다.

 

우선순위 큐의 삭제

  삭제할때는 루트노드를 삭제하고 가장 마지막 원소를 루트노드의 위치로 옮겨준다.

  삭제이후에는 리프노드까지 내려가면서 최대힙을 구성한다. 하향식이다.

 

    루트노드를 삭제한 뒤 가장 하위 노드를 루트로 올려준다.

  루트노드의 하위노드중 더 큰값과 위치를 바꿔준다.

  바꿔준다음 또 그 하위 노드와 비교한다. 이 경우 1과 4를 하위노드로 갖기 때문에 더이상 내려가지 않는다.

 

예제코드

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAX_SIZE 10000

void swap(int *a, int *b){
  int temp = *a;
  *a = *b;
  *b = temp;
}

typedef struct{
  int heap[MAX_SIZE];
  int count;
} priorityQueue;

void enQueue(priorityQueue *pq, int data){
  if(pq->count >= MAX_SIZE) return;
  pq->heap[pq->count] = data;
  //count는 큐에 담겨있는 원소의 개수
  //제일 뒤로 데이터가 들어간다.
  
  int now = pq->count;
  //삽입된 데이터에 해당하는 인덱스를 now라고 정의.
  
  int parent = (pq->count - 1) / 2;
  //now에서 1을 빼고 2로 나누어진 값이 부모노드다.
  //3개층이 있는 완전 이진트리로 가정하면
  //오른쪽 최하단에 새로 들어오는 노드의 인덱스 6이다.
  //최하단 노드는 4개. 3 4 5 6 이기 때문에 
  //새로들어오는 노드의 부모노드 인덱스는 2가된다.
  //(6 - 1)/2 = 2 이 형태로 구하는 것.
  //상향식으로 힙을 구성하기 위해 부모노드 인덱스를 알아야 한다.
  
  while(now > 0 && pq->heap[now] > pq->heap[parent]){
    swap(&pq->heap[now], &pq->heap[parent]);
    now = parent;
    parent = (parent - 1) / 2;
  }
  //새로 들어온 값이 부모노드보다 크다면 바꿔주는 과정
  pq->count++;
  //값이 새로 들어왔으니 count를 ++해준다.
}

int deQueue(priorityQueue *pq){
  //pq라는 매개변수로 큐를 받아서 처리
  
  if(pq->count <= 0) return -9999;
  
  int res = pq->heap[0];
  //루트노드를 담는다
  
  pq->count--;
  //삭제하는것이니 count를 1빼준다.
  
  pq->heap[0] = pq->heap[pq->count];
  //가장 마지막 노드를 루트노드에 넣어준다.
  
  int now = 0, leftChild = 1, rightChild = 2;
  int target = now;
  //target은 자기자신을 가리키도록 한다.
  //새 원소를 추출한 이후에 하향식으로 힙을 구성
  
  while(leftChild < pq->count){
    //leftChild가 카운트보다 작을때만 반복한다.
    //원소가 존재할때까지만 반복한다는 것이다.
    
    if(pq->heap[target] < pq->heap[leftChild]) target = leftChild;
    //target이 0이므로 root노드가 왼족 자식노드보다
    //작다면 둘을 바꿔주는 것이다.
    
    if(pq->heap[target] < pq->heap[rightChild] && rightChild < pq->count) target = rightChild;
    //루트노드가 오른쪽 자식노드보다 작고 오른쪽 자식노드가 카운트 보다 작다면
    //target을 rightChild로 바꾼다.
    //count보다 작다는 조건을 넣어놓는 이유는 인덱스를 벗어나는 경우를 방지하기 위해서다.
    //rightChild가 원소 개수보다 작을때에 한해서만 검사하도록 한다.
    
    if(target == now) break;
    //만약 두 자식노드보다 자기자신의 값이 더 크다면 이라는 조건이다.
    //target이 now가 된다는것은 위 조건문에서 false로 아무처리 되지 않고 그냥 내려왔기 때문에
    //가능한 것이므로 더이상 내려가지 않아도 된다는 것이다.
    //그럼 break로 빠져나가도록 한다.
    
    else{
      swap(&pq->heap[now], &pq->heap[target]);
      //자식노드가 더 크다면 변경해줘야 한다.
      //예를들어 leftChild가 더 큰 값이라 바꿔줘야 한다면
      //위 leftChild와 비교한 조건문에서 이미 target은 leftChild가 되었을 것이고
      //swap에서는 a와 b를 바꿔주므로 root인 now와 target인 leftChild의 순서가 바뀌게 된다.
      
      now = target;
      //아래로 더 체크해야 하기 때문에 now를 target으로 바꿔준다.
      
      leftChild = now * 2 + 1;
      rightChild = now * 2 + 2;
      //자리바꿈을 한 후에는 그 아래 노드들도 체크해야 하기 때문에 
      //now뿐만 아니라 두 자식노드의 값도 바꿔줘야 한다.
      //left로 내려왔다고 가정하고 보자면
      //now는 루트의 leftChild인 1이되고
      //leftChild는 1 * 2 + 1 = 3
      //rightChild는 1 * 2 + 2 = 4
      //이렇게 처리되어 1노드의 두 자식노드 인덱스가 된다.
    }
  }
  return res;
}

int main(void){
  int n, data;
  scanf("%d", &n);
  priorityQueue pq;
  pq.count = 0;
  //count를 0으로 초기화
  
  for(int i = 0; i < n; i++){
    scanf("%d", &data);
    enQueue(&pq, data);
  }
  
  for(int i = 0; i < n; i++){
    int data = deQueue(&pq);
    printf("%d ", data);
  }
  system("pause");
  return 0;
}

  위치를 잡고 설명하기 애매해서 대체로 주석처리했다.

  

  n은 데이터의 개수를 입력하는 것이고

  data에 값을 입력해서 넣어주면 큰수부터 작은수로 출력되는것을 확인할 수 있다.

  큰수부터 작은수로 출력되는것은 data = deQueue(&pq)인데 deQueue에서 반환되는 값은 res이고

  res는 heap[0]이기 때문에 가장 큰수부터 차례로 출력이 된다.

  가장 큰 수 부터 차례로 출력이 된다는 것은 제대로 처리가 되고 있다고 볼 수 있다.

 

  강의 볼 때는 몰랐었는데 정리하면서 출력이 이렇게 되는걸 늦게 알아서...

  한번만 추출하고 출력하거나 한번 반복할때마다 전체 출력하는 부분을 추가해봐야겠다.

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

'자료구조' 카테고리의 다른 글

그래프(Graph)  (0) 2021.01.13
순차탐색, 이진탐색  (0) 2021.01.12
트리(Tree)  (0) 2021.01.10
계수정렬, 기수정렬  (0) 2021.01.09
퀵정렬  (0) 2021.01.07

트리(Tree)

  나무의 형태를 뒤집은것과 같은 형태의 자료구조이다.

  일반적으로 트리의 최상단노드를 루트노드(Root node)라고 하고 가지로 인해 노드가 연결되는 것을 확인할 수 있다.

  트리에서 가장 마지막 끝에 해당하는 노드들을 리프노드(Leaf node)라고 한다.

  각 노드들은 부모자식관계를 갖게 된다.

  상위노드가 부모노드, 하위노드가 자식노드가 되며 자식노드가 여러개라면 형제노드라고 한다.

  

  트리에서 길이란 출발노드에서 목적지 노드까지 거쳐야 하는 가지수를 의미한다.

  예를 들어 루트노드에서 목적 노드까지 가는데 한개의 노드를 거쳐간다면 가지의 수가 2개이기 때문에 길이는 2가된다

 

  깊이란 루트노드에서 특정노드까지의 길이를 의미한다.

  루트노드의 바로 하위노드는 깊이가 1이고 그 아래 노드는 깊이가 2가 된다.

 

이진트리(Binary Tree)

  최대 2개의 자식을 가질 수 있는 트리이다.

  구현이 간단하다는 점에서 다방면으로 활용이 되는 자료구조중 하나이다.

 

  포화이진트리(Full Binary Tree)

    리프노드를 제외한 모든 노드가 두 자식을 갖고 있는 트리이다.

  

  완전이진트리(Complete Binary Tree)

    모든 노드들이 왼쪽 자식부터 차근차근 채워진 트리다. 왼쪽에서부터 채워져있는 트리를 의미한다.

 

  높이균형트리(Height Balanced Tree)

    왼쪽 자식 트리와 오른쪽 자식트리의 높이가 1이상 차이나지 않는 트리이다.

 

  이진 트리는 많은 양의 노드를 낮은 높이에서 관리할 수 있다는 점에서 데이터 활용의 효율성이 높아진다.

  데이터 저장, 탐색 등의 다양한 목적에서 사용할 수 있다.

 

구현 및 순회

  이진트리는 포인터를 이용하여 구현하면 효과적인 데이터 관리가 가능하다.

 

  이진트리의 순회

    이진트리에 담긴 데이터를 하나씩 방문하는 방법으로 대표적인 세가지 방법이 있다.

 

    전위순회(preorder)

      루트에서 출발했을 때 상위노드를 출력하고 왼쪽 하위노드를 출력한다음 오른쪽 하위노드를 출력한다.

      위 그림을 보면 root인 30을 먼저 출력한다. 순서가 상위 - 왼쪽  - 오른쪽이기 때문에 왼쪽 하위노드로 먼저 

      내려간다. 그래서 17을 출력해주고 17의 하위노드중 왼쪽에 있는 5을 먼저 출력 한 다음 23을 출력해준다.

      더 하위노드는 없기 때문에 23을 출력한 다음에는 루트의 오른쪽방향으로 내려가서 같은 형태로 48 - 37 - 

      50을 출력한다.

      만약 제일 왼쪽 리프노드인 5에 하위노드로 하나 더 있었다면 5와 23사이에 그 노드가 출력이 된다.

 

 

    중위순회(inorder)

      왼쪽 하위노드를 출력하고 상위노드를 출력 그다음 오른쪽 하위노드를 출력한다.

      왼쪽 하위노드를 먼저 출력하는 형태이기 때문에 5을 가장 먼저 출력하고 상위노드인 17을 출력.

      그 다음 오른쪽 하위노드인 23을 출력한다. 그럼 루트노드를 기준으로 왼쪽 노드들은 모두 출력했으니 루트노드를

      출력해주고 오른쪽 최하단 노드들 중에서 가장 왼쪽에 있는 37부터 시작하여 같은 형태로 출력해준다.

 

    후위순회(postorder)

      왼쪽 자식노드를 출력한다음 오른쪽 자식노드를 출력하고 부모노드를 출력한다.

      중위순회와 마찬가지로 왼쪽 최하위 노드부터 시작한다. 왼쪽 - 오른쪽 - 상위 순서이기 때문에 5를 출력한 다음

      23을 출력하게 되고 상위노드인 17을 출력한다. 그럼 루트노드 기준으로 왼쪽 하위노드들은 모두 출력이 끝났으니

      오른쪽 하위노드들을 출력해줘야 한다.

      오른쪽 하위노드들 중 왼쪽 최하단 노드인 37로 넘어가고 여기서부터 같은 형태로 출력하며 마지막에 루트노드를

      출력하며 마무리된다.

 

예제코드

 

#include <stdio.h>
#include <stdlib.h>

typedef struct{
  int data;
  struct Node* leftChild;
  struct Node* rightChild;
} Node;

Node* initNode(int data, Node* leftChild, Node* rightChild){
  Node* node = (Node*)malloc(sizeof(Node));
  node->data = data;
  node->leftChild = leftChild;
  node->rightChild = rightChild;
  return node;
}

void preorder(Node* root){
  if(root){
    printf("%d ", root->data);
    preorder(root->leftChild);
    preorder(root->rightChild);
  }
}

void inorder(Node* root){
  if(root){
    inorder(root->leftChild);
    printf("%d ", root->data);
    inorder(root->rightChild);
  }
}

void postorder(Node* root){
  if(root){
    postorder(root->leftChild);
    postorder(root->rightChild);
    printf("%d ", root->data);
  }
}

int main(void){
  Node* n7 = initNode(50, NULL, NULL);
  Node* n6 = initNode(37, NULL, NULL);
  Node* n5 = initNode(23, NULL, NULL);
  Node* n4 = initNode(5, NULL, NULL);
  Node* n3 = initNode(48, n6, n7);
  Node* n2 = initNode(17, n4, n5);
  Node* n1 = initNode(30, n2, n3);
  preorder(n1);
  printf("\n");
  inorder(n1);
  printf("\n");
  postorder(n1);
  system("pause");
  return 0;
}

  결과값

  30 17 5 23 48 37 50

  5 17 23 30 37 48 50

  5 23 17 37 50 48 30

 

  initNode로 데이터를 넘겨 트리구조를 먼저 만들어 준다.

 

  n1까지 모두 처리하면 위와 같은 형태의 트리구조가 생긴다.

  여기서 preorder, inorder, postorder 형태로 출력했을 때의 결과값이 출력이 된다.

  

  preorder에서 보면 root의 data를 제일 먼저 출력한다. 그럼 30이 출력이 되고 그 다음 preorder에 leftChild를

  넘겨준다. root에 leftChild가 넘어가니까 n1의 leftChild인 n2를 받아서 출력하고 17이 출력된다.

  그럼 또 leftChild가 존재하니까 넘겨주고 n4를 넘겨줘서 5를 출력. n4는 하위노드가 없기 때문에 다시 root가

  n2인 형태로 돌아온다. 그럼 rightChild를 넘겨주는 preorder를 처리해서 23을 출력하고 역시 하위노드가

  없으므로 n2로 돌아간다. 그럼 n2는 모든 코드를 처리했으므로 n1으로 돌아가고 n1에서는 rightChild를

  처리하도록 한다. 그리고 왼쪽 노드들을 출력한 방식과 동일하게 출력해 나가는 것이다.

 

  inorder는 왼쪽 최하단 노드먼저 출력하고 상위노드, 그다음 오른쪽 노드를 출력하므로 preorder와 형태는 같지만

  출력문이 leftChild와 rightChild를 넘겨주는 사이에서 처리된다.

  inorder로 leftChild를 넘겨주는 형태로 최하단까지 출력없이 계속 내려가기만 하고 최하단 노드에 도착했을 때부터

  출력한다.

  

  postorder 역시 왼쪽 최하단 노드가 시작점이기 때문에 inorder와 마찬가지로 leftChild를 넘겨 처리함으로써

  왼쪽 최하단 노드로 내려간다. 그리고 오른쪽 노드를 출력한다음 상위노드를 출력해야 하니 오른쪽 노드들을

  출력하도록 rightChild를 넘겨주고 제일 마지막에 상위노드를 출력하게된다.

 

 

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

'자료구조' 카테고리의 다른 글

순차탐색, 이진탐색  (0) 2021.01.12
우선순위 큐  (0) 2021.01.11
계수정렬, 기수정렬  (0) 2021.01.09
퀵정렬  (0) 2021.01.07
선택정렬, 삽입정렬  (0) 2021.01.06

계수정렬(Counting Sort)

  크기를 기준으로 데이터의 개수를 세는 정렬 알고리즘이다.

  각 데이터의 크기를 기준으로 분류하므로 O(N)의 시간 복잡도를 갖는다.

  데이터의 크기를 할당해준 뒤 처리해야 하기 때문에 데이터의 크기가 한정적일 때 사용할 수 있다.

 

  계수정렬의 처리방법은 배열의 인덱스를 기준으로 각 원소의 개수를 파악하고 그 개수만큼 출력하는 방식이다.

 

  계수정렬의 특징은 퀵정렬이나 다른 정렬 알고리즘에 비해서 보다 많은 메모리를 요구하는 대신에 빠른 처리가

  가능하다는 효율성을 제공한다는 것이다.

 

처리방법

  예를들어 2 1 0 2 2 1 3 1 0 3 이라는 데이터를 계수정렬로 처리한다고 할 때

  값과 동일한 인덱스에 개수를 원소값으로 넣어주는 것이다.

  정리하면 다음과 같다.

인덱스 0 1 2 3
원소 2 3 3 2

  데이터의 끝까지 다 확인해서 개수를 파악했다면 0번 인덱스부터 출력을 시작해서

  0 0 1 1 1 2 2 2 3 3 형태로 출력하게 된다.

 

예제코드

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAX_VALUE 10001

int n, m;
int a[MAX_VALUE];

int main(void){
  scanf("%d", &n);
  
  for(int i = 0; i < n; i++){
    scanf("%d", &m);
    a[m]++;
  }
  
  for(int i = 0; i < MAX_VALUE; i++){
    while(a[i] != 0){
      printf("%d ", i);
      a[i]--;
    }
  }
  system("pause");
}

  n에는 값의 개수를 넣어준다. 예제대로라면 10개의 값을 넣어주기 때문에 n은 10을 입력하고

  2 1 0 2 2 1 3 1 0 3을 입력해준다.

  값의 개수를 넣어주는 것은 입력값처리를 좀 더 편하게 하기 위한 것 뿐이다.

  값을 입력하는 반복문의 과정에서 값을 넣어준 뒤 a배열에서 값과 같은 인덱스에 원소값을 하나 증가시켜준다.

  첫 값인 2가 들어왔다면 m은 2가 되므로 a[2]의 값을 ++로 하나 증가시켜 주는것이다.

  그럼 a[2]는 1이 되고 처리과정을 반복하다가 네번째 값에서 2가 또 나오는데 이때 또 a[2]++을 해주게 된다면

  a[2]가 2가 되는 형태다.

  그렇게 반복문을 다 처리 해준다면 위 처리방법에서의 형태처럼 처리가 된다.

  즉, 첫 반복문에서 값의 입력과 정렬을 같이 처리하게되는 셈이다.

 

  출력문에서는 MAX_VALUE를 이용해서 배열의 전체를 훑고 원소값이 0이 아닌 배열을 출력하도록 한다.

  원소값이 0이 아닐 경우에는 한번 출력을 한 뒤 해당 인덱스의 값을 하나 감소시켜서 또 출력하는 형태로

  값이 0이 될때까지 반복해준다.

 

  MAX_VALUE의 값이 10001로 되어있고 a배열의 크기가 MAX_VALUE이므로 정렬하고자하는 최대값으로는 10000

  이상을 넣을 수 없다.

  만약 MAX_VALUE의 값을 101로 해두었다면 마찬가지로 정렬하고자하는 최대값은 100이상이 될 수 없다.

 

 

기수정렬(Radix Sort)

  자릿수를 기준으로 차례대로 데이터를 정렬하는 알고리즘이다.

  각 데이터를 자릿수 기준으로 분류하므로 가장 큰 자릿수를 D라고 했을 대 O(DN)의 시간 복잡도를 갖는다.

  일반적으로 자릿수가 10개를 넘어가면 억단위를 넘어가기 때문에 왠만해서는 D는 그렇게 큰 값이 아니도록 형성이

  된다.

  그래서 O(DN)은 N에 가까운 시간 복잡도를 갖기 때문에 계수정렬만큼 빠른 알고리즘이라고 할 수 있다.

  자리수의 개수만큼 N번 반복한다.

  일반적으로 100의 자리 1000의 자리 이런식으로 작은 자리수는 D가 3정도밖에 안되기 때문에 사실상 N이라고 볼 수

  있을 정도로 빠르다.

  계수정렬보다 약간 더 느리지만 숫자가 매우 큰 상황에서도 사용할 수 있다.

 

처리방법

  7 84 25 341 65 30 34 라는 값을 정렬하고자 한다면

  1의 자리수부터 시작하는데 계수정렬할때처럼 배열형태로 정렬해준다.

  위의 값들을 1의자리만 본다면

  7 4 5 1 5 0 4 가 되므로 계수정렬한것처럼 정리한다.

0 1 2 3 4 5 6 7 8 9
1 1 0 0 2 2 0 1 0 0

  여기서 각 원소값을 누적형태로 바꿔준다.

0 1 2 3 4 5 6 7 8 9
1 2 0 0 4 6 0 7 0 0

  이렇게 누적형태로 정리하고 난 뒤에는 값을 정렬해주게 되는데 정렬하는 순서는 뒤에서 앞으로 처리한다.

  7 84 25 341 65 30 34 의 값을 정렬하기 시작하면 제일 뒤에 있는 34부터 시작하게 된다.

  34는 1의 자리수가 4이고 4번 인덱스의 값은 4이다.

  그럼 34의 위치는 4가 되는데 이때 4의 위치는 0부터 시작하는 배열상 4의 위치가 아닌 1부터 시작한 위치에서 4이다.

  이렇게 값을 위치해주면서 자리수 배열 내에서는 4번 인덱스의 값을 하나 감소시켜준다.

      34      
0 1 2 3 4 5 6 7 8 9
1 2 0 0 3 6 0 7 0 0

  이런 형태가 된다. 다음 값인 30을 정렬할때는 1의자리가 0이었기 때문에 0인덱스의 값인 1번 위치에 넣어주고

  마찬가지로 0번인덱스의 원소값을 하나 감소시킨다.

30     34      
0 1 2 3 4 5 6 7 8 9
0 2 0 0 3 6 0 7 0 0

  전부 정렬하면 아래와 같은 형태가 된다.

30 341 84 34 25 65 7
0 1 2 3 4 5 6 7 8 9
0 1 0 0 2 4 0 6 0 0

 

  여기서 인덱스의 원소값을 보면 0이 아닌 값들이 있지만 신경써야하는 부분이 아니다.

  여기까지가 1의자리수를 이용한 정렬이다.

 

  여기서 최대값인 341은 100의자리까지 있기 때문에 10의자리, 100의자리를 이용한 정렬을 해줘야 한다.

 

  다음으로는 10의자리를 이용한 정렬이다.

  1의자리때와 마찬가지로 자릿수 값에 대한 누적값을 구해줘야 한다.

 

30 341 84 34 25 65 7
0 1 2 3 4 5 6 7 8 9
1 0 1 2 1 0 1 0 1 0

 

0 1 2 3 4 5 6 7 8 9
1 0 2 4 5 0 6 0 7 0

 

  이렇게 누적값까지 구하고 나면 1의자릿수에서 정렬한것과 같은 방법으로 정렬한다.

  똑같이 맨 뒤의 값 부터 정렬을 시작하게 되고 7은 10의자리수가 0이기 때문에 0인덱스의 원소값처럼 1번째에

  위치하고 마찬가지로 누적값에서 해당 인덱스의 값을 하나 감소시킨다.

7            
0 1 2 3 4 5 6 7 8 9
0 0 2 4 5 0 6 0 7 0

 

  위와 같은 방법으로 전부 정렬하면 다음과 같다.

 

7 25 30 34 341 65 84
0 1 2 3 4 5 6 7 8 9
0 0 1 2 4 0 5 0 6 0

 

  마지막으로 100의자리를 정렬한다.

  지금까지와 마찬가지로 10의자리에서 정렬한 값을 가져와서 사용한다.

 

7 25 30 34 341 65 84
0 1 2 3 4 5 6 7 8 9
6 0 0 1 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9
6 0 0 7 0 0 0 0 0 0

 

  누적값을 구했으니 같은 방법으로 뒤에서부터 정렬한다.

  그럼 84는 6번째, 65는 5번째, 341은 7번째, 34는 4번째, 30은 3번째 이렇게 정렬해준다.

7 25 30 34 65 84 341

  그럼 이렇게 정렬되게 되고 1000의 자리는 없기 때문에 정렬이 끝나게 된다.

 

 

예제코드

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAX 10000

void radixSort(int *a, int n){
  int res[MAX], maxValue = 0;
  int exp = 1;
  
  for(int i = 0; i < n; i++){
    if(a[i] > maxValue) maxValue = a[i];
  }
  
  while(maxValue / exp > 0){
    int bucket[10] = { 0 };
    
    for(int i = 0; i < n; i++) bucket[a[i] / exp % 10]++; //자릿수 배열처리
    
    for(int i = 0; i < 10; i++) bucket[i] += bucket[i - 1]; //누적 처리
    
    for(int i = n - 1; i >= 0; i--){
      res[--bucket[a[i] / exp % 10]] = a[i];
    }
    
    for(int i = 0; i < n; i++) a[i] = res[i]; //기본 배열 갱신
    
    exp *= 10;
  }
}

int main(void){
  int a[MAX];
  int i, n;
  scanf("%d", &n);
  
  for(i = 0; i < n; i++){
    scanf("%d", &a[i]);
  }
  
  radixSort(a, n);
  
  for(i = 0; i < n; i++){
    printf("%d ", a[i]);
  }
  system("pause");
}

  n에 입력할 값의 개수를 입력하고 그만큼 반복문을 돌려 리스트에 값을 넣어준다.

 

  radixSort에 리스트와 값의 개수인 n을 넘겨준다.

  

  radixSort함수의 첫 반복문에서는 리스트를 전체 다 훑으며 가장 큰 값을 찾아 maxValue에 넣어준다.

 

  while문에서 maxValue / exp를 하는데 자릿수를 확인하는 것이다.

  처음 체크한다면 1의자리는 당연히 존재하니까 0보다 크겠지만 위 예제처럼 100의자리까지만 있는 값이라면

  자릿수 정렬 후 exp가 1000이 되었을 때 값이 0보다 작아지게 되고 그럼 1000의자리수는 존재하지 않는다는 의미가

  된다.

 

  while안의 첫 for문에서는 자릿수의 배열처리를 한다. exp 뒤에 % 10으로 나머지를 왜 구하는지 헷갈렸었는데

  꼭 구해야한다.

  예를들어 a[i]가 84라고 하고 exp가 아직 1이라고 했을 때 84/1 = 84이다. 하지만 exp가 1이라는 것은

  1의 자리수를 이용한 정렬을 하고 있는 것이기 때문에 4만 가져와야 한다.

  그래서 % 10으로 나머지값을 구해서 원하는 자리수만 가져오기 위함이다.

  그리고 뒤에 있는 ++로 해당 인덱스의 원소값을 하나 올려주는 것이고 값의 개수만큼 반복한다.

 

  두번째 for문은 누적값을 계산한다. 처리방법에서 표에 작성했듯이 누적값이 필요한데 그 부분을 처리해주는

  반복문이다.

  i번 인덱스에 i - 1번 인덱스의 값을 더해라 라는 처리이므로 앞 인덱스의 값을 현재 인덱스에 더하라는 것이다.

 

  세번째 반복문에서 res에 값을 넣어주며 정렬을 하게 되는 것이다.

  리스트의 제일 뒤에서부터 시작해서 자리를 찾아가는 과정이 이 부분이다.

 

  마지막 for문은 a배열을 res에 정렬해놓은 배열로 갱신하는 것이다.

 

  그리고 마지막에 exp에 10을 곱해주는데 그래야 10의자리, 100의자리, 1000의 자리를 체크하고 정렬 할 수 있다.

 

  인강에서는 코드에 대한 별 설명이 없이 그냥 슥 넘어가서 이해 안되는 부분들이 있었다.

  그래서 출력문을 중간중간 적어주면서 이해했다 ㅠㅠ

  

  아래코드가 출력문 다 적어둔 코드...

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAX 10000

void radixSort(int *a, int n) {
  int res[MAX], maxValue = 0;
  int exp = 1;
	
  for (int i = 0; i < n; i++) {
    if (a[i] > maxValue) maxValue = a[i];
  }

  while (maxValue / exp > 0) { //자릿수 체크
    int bucket[10] = { 0 };
		
    for (int i = 0; i < n; i++) { //자릿수 배열처리
      bucket[a[i] / exp % 10]++;
      
      printf("bucket first : "); //자릿수 배열처리 과정 출력
      for (int j = 0; j < 10; j++) {
        printf("%d ", bucket[j]);
      }
      printf("\n");
    }
		
    for (int i = 1; i < 10; i++) { //누적계산
      bucket[i] += bucket[i - 1];

      printf("bucket accu : "); //누적계산 과정 출력
      for (int j = 0; j < 10; j++) {
        printf("%d ", bucket[j]);
      }
      printf("\n");

      }

    for (int i = n - 1; i >= 0; i--) { //기본 배열 갱신
      res[--bucket[a[i] / exp % 10]] = a[i];
      
      printf("res : "); //배열 갱신과정 출력
      for (int j = 0; j < 7; j++) {
        printf("%d ", res[j]);
      }
      printf("\n");
    }

    printf("a prev : "); //정렬 전 배열
    for (int j = 0; j < n; j++) {
      printf("%d ", a[j]);
    }
    printf("\n");

    for (int i = 0; i < n; i++) {
      a[i] = res[i];
    }

    printf("check : "); //갱신된 배열
    for (int i = 0; i < n; i++) {
      printf("%d ", a[i]);
    }
    printf("\n");
    
    exp *= 10;
  }
}

int main(void) {
  int a[MAX];
  int i, n;
  scanf("%d", &n);

  for (i = 0; i < n; i++) {
    scanf("%d", &a[i]);
  }

  radixSort(a, n);

  printf("final : ");
  for (i = 0; i < n; i++) {
    printf("%d ", a[i]);
  }
	
  system("pause");
}

 

 

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

'자료구조' 카테고리의 다른 글

우선순위 큐  (0) 2021.01.11
트리(Tree)  (0) 2021.01.10
퀵정렬  (0) 2021.01.07
선택정렬, 삽입정렬  (0) 2021.01.06
큐(Queue)  (0) 2021.01.05

퀵정렬

  빠른정렬 알고리즘이며 피벗을 기준으로 큰 값과 작은 값을 서로 교체하는 정렬기법이다.

  값을 서로 교체하는데에 N번, 엇갈린 경우 교체 이후에 원소가 반으로 나누어지므로 전체원소를 나누는데에

  평균적으로 logN번이 소요되므로 평균적으로 θ(NlogN)의 시간복잡도를 갖는다.

  불안정 정렬에 속하며, 다른 원소와의 비교만으로 정렬을 수행하는 비교정렬에 속한다.

  분할 정복 알고리즘의 하나로, 평균적으로 매우 빠른 수행속도를 자랑하는 정렬방법이다.

 

처리과정

  리스트 안에 있는 한 요소를 선택하는데 이렇게 고른 원소를 피벗이라고 한다.

  피벗을 기준으로 피벗보다 작은 요소들은 모두 피벗의 왼쪽으로 옮겨지고 피벗보다 큰 요소들은 모두 피벗의

  오른쪽으로 옮겨 비균등한 크기로 분할한다.

  그리고 그렇게 분할된 부분리스트를 정렬한 다음 합하여 전체가 정렬된 리스트로 만들어주는 방법이다.

 

  5 4 3 2 9 6 7 1 10 8 의 순서로 값을 입력하고 퀵정렬로 처리한다고 했을 때

  제일 앞에 있는 5가 피벗이 된다. 피벗을 기준으로 다음 인덱스 방향으로 피벗보다 큰 값을 찾고

  제일 뒤에서부터 앞으로 오며 피벗보다 작은 값을 찾는다.

  그럼 큰 값으로는 9 작은값으로는 1이 선택되고 두 값의 위치를 바꿔준다.

 

  5 4 3 2 1 6 7 9 10 8

 

  이 과정을 반복해서 처리하는데 다음 큰 값과 작은 값은 1과 6이 된다.

  하지만 순서로 봤을 때 1과 6은 서로 교차하게 된다.

  뒤에서 앞으로 오며 값을 찾았을 때 1을 갖게 되고 앞에서 뒤로 가면서 6을 큰값으로 갖게 되기 때문이다.

  이렇게 교차되는 상황이 되었다면 작은 값을 피벗과 위치를 변경해준다.

 

  1 4 3 2 5 6 7 9 10 8

 

  이렇게 위치를 바꿔주면 피벗인 5를 기준으로 왼쪽은 작은값, 오른쪽은 큰 값으로 나눠지고 분할하게 되는것이다.

  

  1 4 3 2    5    6 7 9 10 8 이렇게 분할 된 리스트를 앞쪽리스트 따로 뒤쪽리스트 따로 퀵정렬을 수행하게 되고

  1 2 3 4    5    6 7 8 9 10 이렇게 완료 된다면 이대로 출력을 시켜주는 것이다.

 

코드 예제

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define SIZE 1000

int a[SIZE];

int swap(int *a, int *b){
  int temp = *a;
  *a = *b;
  *b = temp;
}

void quickSort(int start, int end){
  if(start >= end) return;
  
  int key = start, i = start + 1, j = end, temp;
  
  while(i <= j){
    while(i <= end && a[i] <= a[key]) i++;
    //큰 값을 찾는다
    while(j > start && a[j] >= a[key]) j--;
    //작은값을 찾는다
    if(i > j) swap(&a[key], &a[j]);
    //작은값과 큰값이 엇갈린 상태라면
    else swap(&a[i], &a[j]);
    //작은값과 큰값이 엇갈리지 않은 상태라면
  }
  quickSort(start, j - 1);
  quickSort(j + 1, end);
}

int main(void){
  int n;
  scanf("%d", n);
  for(int i = 0; i < n; i++) scanf("%d", &a[i]);
  quickSort(0, n - 1);
  
  for(int i = 0; i < n; i++) printf("%d ", a[i]);
  system("pause");
  return 0;
}

  값의 개수를 n에 입력하고 n개의 값을 a배열에 입력한다.

  입력이 끝난 후 quickSort의 start에는 0을 end에는 n - 1을 넣어준다.

  end가 n - 1이어야 하는 이유는 n은 값의 총 개수 즉. 배열의 크기인건데 end는 인덱스로서 사용해야 하고

  인덱스는 1이 아닌 0부터 시작하기 때문에 1을 빼서 넘겨줘야 한다.

  처리과정의 예시대로 예를 들자면

  end에는 9가 들어가게 될것이고 key = 0, i = 1, j = 9가 된다.

 

  i는 j보다 작기 때문에 반복문을 실행하고

  첫 반복문에서는 i(1) <= end(9) && a[i](4) <= a[key](5) 이런 상태이고 조건이 만족하기 때문에

  i을 증가시켜준다. 주석처리한것을 보면 큰 값을 찾는 부분이기 때문에 i번째의 값이 피벗인 key번째의 값보다 작다면

  i를 증가시켜서 다음 값을 확인함으로써 큰값을 찾아 나간다.

 

  아래 반복문 역시 j(9) > start(0) && a[j](8) >= a[key](5) 가 성립하므로 j를 감소시키는데

  작은 값을 찾아야 하므로 j번째 값이 key번째 값보다 크다면 j를 감소시켜 배열의 뒤에서 앞으로 가며 찾는것이다.

 

  조건문의 경우 i가 j보다 크다는 것은 작은값과 큰 값이 서로 엇갈렸다는 건데

  5 4 3 2 1 6 7 9 10 8  엇갈린 이 예시에서 보자면 i는 큰값이기 때문에 5가 되고 j는 작은값이기 때문에 4가 된다.

  즉, i가 더 크다는 것은 값이 엇갈린 상태라는 것이고 그래서 작은값과 피벗값의 위치를 바꾸기 위해

  a[key]와 a[j]를 넘겨서 위치를 바꿔준다.

 

  else의 경우 엇갈리지 않았다는 것이므로 두 값의 위치만 바꿔주면 되기 때문에 a[i] 와 a[j]만 넘겨서 위치를 

  변경해준다.

 

  제일 외부에 있는 반복문이 끝나고 나면 피벗을 기준으로 작은값 큰값이 나누어졌다는 의미가 된다.

  즉, if문에서 else로 가지않고 조건이 만족해서 swap함수를 처리했다면 외부 반복문도 false가 되므로

  재귀적으로 처리하여 피벗 기준 양쪽 리스트를 다시 재정렬 해주게 된다.

 

 

  퀵 정렬은 편향된 분할이 발생할 때 연산의 양이 O(N^2)다. 실제로 정렬을 함에 있어서는 퀵 정렬을 직접 구현하지

  않고 C++의 algorithm라이브러리를 사용한다고 한다. Algorithm 라이브러리의 sort()함수는 퀵 정렬을 기반으로 하되

  O(NlogN)을 보장한다.

 

 

레퍼런스

패스트캠퍼스 올인원 패키지 - 소프트웨어 베이직

'자료구조' 카테고리의 다른 글

트리(Tree)  (0) 2021.01.10
계수정렬, 기수정렬  (0) 2021.01.09
선택정렬, 삽입정렬  (0) 2021.01.06
큐(Queue)  (0) 2021.01.05
스택(Stack)  (0) 2021.01.02

+ Recent posts