트리(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

선택정렬

  가장 작은것을 선택해서 앞으로 보내는 방식으로 오름차순 형태로 정렬하는 기법이다.

  제자리 정렬(in-place Sorting) 알고리즘의 하나이다. 입력 배열(정렬되지 않은 값들)이외에 다른 추가 메모리를

  요구하지 않는 정렬 방법이다.

  

  가장 작은 것을 선택하는데에 N번, 앞으로 보내는데에 N번의 연산으로 O(N^2)의 시간 복잡도를 갖는다.

  예를 들어 2 5 1 4 3을 1 2 3 4 5 로 정렬하는데 작은수를 먼저 찾고 그 수를 앞으로

  보내는 방식을 말한다.

  

  해당 순서에 원소를 넣을 위치는 정해져 있고 어떤 원소를 넣을지 선택하는 알고리즘이다.

  첫번재 순서에는 첫번째 위치에 가장 최소값을 넣어준다.

  그리고 두번째 순서에는 두번째 위치에 남은 값들 중에서 최소값을 넣어준다.

  이런 방식으로 하나의 원소만 남을때까지 과정을 반복한다.

 

  순서는 다음과 같다.

  2 5 1 4 3 이라는 값을 입력해준다.

  최소값을 찾아 첫번째 인덱스에 있는 값과 바꿔준다.

  1 5 2 4 3 이렇게 변경해주는 방식으로 값의 개수만큼 반복해준다.

  1 2 5 4 3  ->   1 2 3 4 5

  마지막 값까지 다 확인해서 처리해준다.

 

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

int a[SIZE];

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

int main(void){
  int n, min, index;
  scanf("%d", &n);
  //입력할 값의 개수
  
  for(int i = 0; i < n; i++) scanf("%d", &a[i]);
  
  for(int i = 0; i < n; i++){
    min = INT_MAX;
    //작은 값을 찾아내기 위해 최대한 큰 값을 초기값으로 넣어준다.
    
    for(int j = i; j < n; j++){
      if(min > a[j]){
        min = a[j];
        index = j;
      }
    }
    swap(&a[i], &a[index]);
  }
  for(int i = 0; i < n; i++) printf("%d ", a[i]);
  system("pause");
  return 0;
}

 

  입력할 값의 개수를 먼저 입력하고 무작위의 값을 넣고자 하는 개수만큼 입력한다.

  첫 반복문에서는 입력값을 a배열에 넣어주게 되고 두번째 반복문에서는 가장 작은 값을 찾아내기 위해 가장 큰값을

  초기값으로 넣어준다.

  INT_MAX는 limit.h에 정의된 것으로 INT형에서의 최대값을 넘겨준다.

  세번째 반복문에서는 값이 하나 들어올때마다 뒤에 있는 값부터 확인해야 하므로 하나씩 증가하고 있는 i를 j로

  넣어주고 처리하도록 한다.

  그리고 min값이 a의 j번째 값보다 크다면 min값을 a의 j번째 값으로 바꿔준다. 그리고 index에 j를 넣어줌으로써

  인덱스 값 또한 받아준다.

  이 부분이 남은 값들 중에서 최소값을 찾는 부분이다.

  전체 값을 확인해서 최소값과 그 인덱스가 변수에 들어간 상태로 반복문을 빠져나오고 swap함수로 a[i]인 

  값을 넣어줘야 하는 위치, a[index]로 반복문에서 찾은 최소값의 위치를 넘겨준다.

  그럼 swap함수에서는 두 위치를 바꿔주는 작업을 처리한다.

  이 작업을 반복하며 값의 개수만큼 처리했다면 반복문을 종료하고 출력을 위한 반복문으로 출력 한다.

 

삽입정렬

  각 숫자를 적절한 위치에 삽입하는 정렬기법이다.

  배열의 모든 요소를 앞에서부터 차례대로 이미 정렬된 배열 부분과 비교하여, 자신의 위치를 찾아 삽입함으로써

  정렬을 완성하는 알고리즘이다.

  매 순서마다 해당 원소를 삽입할 수 있는 위치를 찾아 해당 위치에 넣는다.

  들어갈 위치를 선택하는데에 N번, 선택하는 횟수로 N번이므로 O(N^2)의 시간 복잡도를 갖는다.

 

  순서는 다음과 같다.

  값을 입력해준다. 2 5 1 4 3으로 입력한다고 했을때

  두번째 값과 첫번째 값을 비교한다. 5보다 2가 더 작기 때문에 바꾸지 않고 넘어간다.

  그 다음 세번째 값과 두번째 값을 비교한다. 1이 5보다 작기 때문에 2 1 5 4 3으로 위치를 바꿔준다.

  그리고 두번째 값과 첫번째 값을 비교한다. 1이 2보다 작기 때문에 1 2 5 4 3으로 위치를 바꿔준다.

  그 다음 네번째값, 다섯번째 값 역시 같은 방식으로 비교하여 1 2 3 4 5 로 정렬을 마친다.

 

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

int a[SIZE];

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

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

  입력할 값의 개수를 n에 입력해주고 그 개수만큼 값을 입력해서 a배열을 만들어준다.

  두번째 반복문에서는 n - 1만큼 반복한다고 했는데 예를들어 n이 5로 5개의 값을 입력한다고 했으면

  인덱스는 0~4까지만 있기 때문에 n - 1을 해줌으로써 i를 3까지만 가능하도록 해준다.

  3까지만 가능하도록 하는 이유는 j가 i의 값을 갖게 되고 swap에서 j번째와 j + 1번째를 바꾸도록 했기 때문이다.

  while문에서 보면 조건은 j가 0보다 크거나 같고 a의 j번째가 a의 j + 1번째 보다 커야한다.

  첫번째 반복이라고 가정했을 때 보면 j는 0이고 a[0]이 a[1]보다 크면 true가 된다.

  swap함수로 넘어가서 위치를 바꾸고 나면 j를 감소시키는데 2회차의 과정으로 확인해보면 i는 1이되고 j역시 1이된다.

  그럼 while문에서는 a[1]이 a[2]보다 크다면 교체해주고 j를 감소시킨다. 감소한 j는 0이기 때문에 while의 첫 조건에는

  true가 성립하고 a[0]과 a[1]을 비교하게 되는 것이다.

  인덱스를 하나씩 감소시키면서 비교해줘야 하기 때문에 j를 감소시키는 것이다.

  값의 개수만큼 반복한 후에는 반복문을 빠져나오고 출력하게 된다.

 

 

레퍼런스

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

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

계수정렬, 기수정렬  (0) 2021.01.09
퀵정렬  (0) 2021.01.07
큐(Queue)  (0) 2021.01.05
스택(Stack)  (0) 2021.01.02
연결리스트(Linked List)  (0) 2021.01.02

  큐(Queue)는 뒤로 들어가서 앞으로 나오는 자료구조로 선입선출(First In First Out, FIFO)형태이며

  이러한 특성때문에 스케쥴링, 탐색 알고리즘 등에서 다방면으로 활용된다.

  스택(Stack)에서와 마찬가지로 push는 데이터를 넣어주고 pop은 데이터를 뺀다.

  보통 큐를 사용할때는 enQueue, deQueue를 많이 사용한다고 한다.

  enQueue가 push와 같고 deQueue가 pop과 같다. 스택과 혼동할 수 있기 때문에 enQueue, deQueue로 사용한다.

 

 

  큐는 배열을 이용한 구현방법과 연결리스트를 이용한 구현방법으로 나누어지는데

  가장 기본적인 형태의 자료구조로 구현 난이도는 낮은편이다.

 

  배열을 이용한 구현방법

#include <stdio.h>
#define SIZE 10000
#define INF 99999999

int queue[SIZE];
int front = 0;
int rear = 0;

void enQueue(int data){
  if(rear >= SIZE){
    printf("큐 오버플로우 발생\n");
    return;
  }
  queue[rear++] = data;
}

int deQueue(){
  if(front == rear){
    printf("큐 언더플로우 발생.\n");
    return -INF;
  }
  return queue[front++];
}

void show(){
  for(int i = front; i < rear; i++){
    printf("%d\n", queue[i]);
  }
}

int main(void){
  enQueue(7);
  enQueue(5);
  enQueue(4);
  deQueue();
  enQueue(6);
  show();
  system("pause");
  return 0;
}

  결과값은 5 4 6이다.

  넘겨준 값이 queue의 rear번째에 들어가는데 rear의 초기값은 0이기 때문에 queue[0]에 첫 값이 들어가고

  rear는 후치증가로 값이 1 증가하게 된다.

  그래서 다음 값이 queue[1]에 들어가는 방식으로 처리된다.

  deQueue에서는 맨 앞에 있는 값이 삭제되는 방식이기에 따로 넘겨주는 값은 없고 front에 있는 값을 날려준다.

  출력에서 front가 1증가한 상태이기 때문에 i = 1이 되고 queue에서 0번인덱스부터가 아닌 1번 인덱스부터

  출력해줌으로써 5 4 6 만 출력이 되게 된다.

  

  연결리스트를 이용한 큐 구현

#include <stdio.h>
#include <stdlib.h>
#define INF 99999999

typedef struct{
  int data;
  struct Node *next;
} Node;

typedef struct{
  Node *front;
  Node *rear;
  int count;
} Queue;

void enQueue(Queue *queue, int data){
  Node *node = (Node*)malloc(sizeof(Node));
  node->data = data;
  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 data = node->data;
  queue->front = node->next;
  free(node);
  queue->count;
  return data;
}

void show(Queue *queue){
  Node *cur = queue->front;
  
  while(cur != NULL){
    printf("%d\n", cur->data);
    cur = cur->next;
  }
}

int main(void){
  Queue queue;
  queue.front = queue.rear = NULL;
  queue.count = 0;
  enQueue(&queue, 7);
  enQueue(&queue, 5);
  enQueue(&queue, 4);
  deQueue(&queue);
  enQueue(&queue, 6);
  show(&queue);
  system("pause");
  return 0;
}

 

  큐 삽입과정

  기본형태에서부터 front와 rear가 존재한다.

  그리고 enQueue를 하게 되면  rear뒤에 노드가 하나 들어오게 되는데 그럼 rear의 next가 새로운 노드를 가리키도록

  하고 새로운 노드를 rear로 만들어준다.

  rear의 next를 새로운 노드로 연결해주는 이유는 들어가있는 값이 있다면 그 마지막 노드인 rear의 next를 연결해줘야

  그 뒤로 새로운 노드를 연결하게 되는것이고 그 다음 rear를 새로 넣어준 노드로 다시 지정해주는 것이기 때문이다. 

  삭제의 경우는 앞에서부터 삭제하기 때문에 front를 두번째 노드로 옮기고 첫번째 노드를 메모리해제하면 된다.

 

 

레퍼런스

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

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

퀵정렬  (0) 2021.01.07
선택정렬, 삽입정렬  (0) 2021.01.06
스택(Stack)  (0) 2021.01.02
연결리스트(Linked List)  (0) 2021.01.02
자료구조  (0) 2020.12.30

스택이란 한쪽으로 들어가서 한쪽으로 나오는 구조이다.

후입선출(Last In First Out)으로 나중에 들어간 값이 먼저 빠져나온다.

이러한 특성대문에 수식계산등의 알고리즘에서 다방면으로 활용된다.

push 와 pop이 존재하며 push는 데이터 삽입, pop은 제거다.

스택은 배열을 이용한 구현방법과 연결 리스트를 이용한 구현방법으로 나누어진다.

가장 기본적인 형태의 자료구조로 구현난이도는 낮은편이다.

 

스택의 처리방식은 후입선출이라고 위에 말했듯이 들어간 값들 중 제일 마지막에 들어간 값부터

빠져나온다.

2 4 3 1 5 7 6 의 순서로 값을 넣어주었다고 했을 때 스택에서 값을 하나 없애라고 한다면

제일 마지막에 들어간 6이 제거된다.

 

배열을 이용한 구현

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define SIZE 10000
#define INF 99999999

int stack[SIZE];
int top = -1;

void push(int data){
  if(top == SIZE - 1){
    printf("스택 오버플로우 발생\n");
    return;
  }
  stack[++top] = data;
}

int pop(){
  if(top == -1){
    printf("스택 언더플로우 발생");
    return -INF;
  }
  return stack[top--];
}

void show(){
  for(int i = top; i >= 0; i--){
    printf("%d\n", stack[i]);
  }
}

int main(void){
  push(7);
  push(5);
  push(4);
  pop();
  push(6);
  show();
  system("pause");
  return 0;
}

  위 코드의 결과값은

  6

  5

  7 이다.

  여기서 top은 스택의 최상단을 의미한다. 최상단이란 스택의 입구이다.

  삽입은 기본적인 배열에서의 삽입과 마찬가지이다. top을 계속 증가시켜주면서 값을 넣어주기 때문에

  뒤 인덱스로 값이 들어간다.

  pop부분을 보면 top을 --로 후치감소 시키면서 stack을 리턴한다.

  

  main함수내에서 7 5 4 의 순서로 값을 넣어줬기 때문에 그 순서 그대로 stack에 들어가게 되고

  pop에서는 가장 뒤에있는 값을 지워주기 때문에 7 5만이 stack에 남고 6을 다시 넣어주었으니 6이 추가로 들어가게

  되면서 7 5 6 의 순서가 되는것이다.

  

  출력이 6 5 7 순서로 나오게 된 것은 스택의 입구가 맨 위라고 가정했을때의 출력이기 때문이다.

 

연결리스트를 이용한 스택

 

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

typedef struct{
  int data;
  struct Node *next;
} Node;

typedef struct{
  Node *top;
} Stack;

void push(Stack *stack, int data){
  Node *node = (Node*)malloc(sizeof(Node));
  node->data = data;
  node->next = stack->top;
  stack->top = node;
}

int pop(Stack *stack){
  if(stack->top == NULL){
    printf("스택 언더플로우 발생.\n");
    return -INF;
  }
  Node *node = stack->top;
  int data = node->data;
  stack->top = node->next;
  free(node);
  return data;
}

void show(Stack *stack){
  Node *cur = stack->top;
  
  while(cur != NULL){
    printf("%d\n", cur->data);
    cur = cur->next;
  }
}

int main(void){
  Stack stack;
  stack.top = NULL;
  push(&stack, 7);
  push(&stack, 5);
  push(&stack, 4);
  pop(&stack);
  push(&stack, 6);
  show(&stack);
  system("pause");
  return 0;
}

  이 코드역시 마찬가지로 결과값은

  6

  5 

  7 이다.

  처리방식은 배열과 동일하다. 첫 구조체인 Node에서는 연결리스트 형태이기 때문에 node를 정의하고

  Stack이라는 구조체를 따로 생성한 후에 top이라는 노드를 생성한다.

  

  push에서는 node를 새로 메모리할당 받고 그 node의 data에 원하는 값을 넣어준다.

  node의 next는 stack의 top에 연결되며 stack의 top은 node의 값을 갖게 된다.

  여기서 이해가 안되서 한참을 켜놓고 고민했는데

  top이라는 노드는 말그대로 최상단 노드를 의미하는것일 뿐이다.

  코드만 있는 그대로 보자면 node의 next가 top에 연결되는데 stack의 top은 node의 data가 된다?

  node7의 next는 top인데 top을 node7로 만들어준다는게 뭔소린가......... 그러고 계속 고민을 했다.

  top이라는 것은 stack구조체에서 만든 node이고 껍데기 정도라고 생각하면 될것 같다.

  그냥 타이틀정도? 니가 1등이야 이정도 생각하면 될것같다.

  1등은 node7이라는 것이고 node7은 다음 노드로 top을 갖고 있다는 것이다.

  다음 입력을 보면 5가 들어오는데 그럼 node5는 top을 next로 갖는것이지만 실질적으로 top은

  node7이기 때문에 node5는 next로 node7을 갖게 되고 top은 node5로 넘어가게 되는것이다.

  그렇다면 top인 node 5는 node7을 next로 갖게 되니까 5 7 형태로 연결되게 되는것이다.

  같은 형태로 4까지 들어가 4 5 7이 들어가고 다음 pop을 처리한다.

 

  pop에서는 top이 NULL이라면 스택에 아무런 노드가 존재하지 않는다는 것이 되므로 언더플로우가 발생했다고

  알린다.

  그게 아니라면 stack의 top을 의미하는 node를 정의하고 top의 data를 data라고 정의한다.

  여기서 stack의 top을 의미하는 node를 topNode라고 한다면 

  stack의 top은 topNode의 next가 되는 것인데 처리된 현황으로 본다면 4가 topNode이고 next는 5를 갖고 있으므로

  top은 node5가 되는 것이다.

  그런 다음 free()를 이용해 삭제하고자했던 노드의 메모리할당을 해제해준다.

  

  show의 경우는 아까와 같은 상단인 입구가 윗부분이라는 전제하에 출력하도록 한 부분이다.

 

 

레퍼런스

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

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

퀵정렬  (0) 2021.01.07
선택정렬, 삽입정렬  (0) 2021.01.06
큐(Queue)  (0) 2021.01.05
연결리스트(Linked List)  (0) 2021.01.02
자료구조  (0) 2020.12.30

연결리스트(Linked List)란?

  데이터를 순서대로 나열한 자료구조로 순서가 있는 데이터를 관리할 때 사용한다.

  배열처럼 메모리상에 연속적으로 위치하지 않으며 각 데이터는 포인터로 연결되어 있어 데이터끼리 물리적으로

  떨어져 있어도 된다.

  노드(node)와 링크(link)를 구조화시킨것인데 노드란 데이터를 담고 있는 그릇이며 링크는 순서유지를 위한

  연결고리이다.

  메모리동적할당을 기반으로 구현된 리스트이며 배열에 비해 추가 / 삭제가 용이한 점이 있지만 특정 데이터를

  찾기 위해서는 순차 탐색을 하므로 탐색속도가 느리며 추가적인 포인터 변수 사용으로 메모리 공간이 낭비된다는

  단점이 있지만 동적할당을 기반으로 한 리스트이기 때문에 크기를 미리 지정할 필요가 없다는 장점이 있다.

 

  연결리스트는 데이터를 선형적으로 저장하고 처리하는 한 방법이고 기존에 배열을 이용했을 때 보다 삽입과 삭제가

  많은 경우에서 효율적이다. 다만, 특정한 인덱스에 바로 참조해야 하는 처리과정이 많다면 배열을 이용하는것이

  효율적이다.

 

  일반적으로 연결리스트는 구조체와 포인터로 사용하게 되는데 리스트의 중간지점에 노드를 추가하거나 삭제할 수

  있어야 하고 필요할때마다 메모리공간을 할당받는다.

 

  단일 연결리스트는 포인터를 이용해 단방향으로 다음 노드를 가리킨다.

 

  data | next  ->  data | next  ->  data | next

  이런 형태인데 하나의 구조체안에 데이터를 담는 변수와 다음노드를 가리키는 포인터 변수인 next가 들어가게 된다.

  이때 일반적으로 연결리스트의 시작 노드를 헤드(Head)라고 하며 별도로 관리하며 다음노드가 없는 끝 노드의

  다음 위치값은 NULL을 넣어준다.

  next가 NULL이 된다면 그 리스트는 더이상 노드가 존재하지 않는 리스트의 마지막이라고 알려주는 것이다.

 

 

 

 

배열로 처리

 

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

int arr[INF];
int count = 0;

void addBack(int data){
  arr[count] = data;
  count++;
}

void addFirst(int data){
  for(int i = count; i >= 1; i--){
    arr[i] = arr[i - 1];
  }
  arr[0] = data;
  count++;
}

void show(){
  for(int i = 0; i < count; i++){
    printf("%d ", arr[i]);
  }
  printf("\n");
}

void removeAt(int index){
  for(int i = index; i < count - 1; i++){
    arr[i] = arr[i + 1];
  }
  count--;
}

int main(void){
  addFirst(4);
  addFirst(5);
  addFirst(1);
  addBack(7);
  addBack(6);
  addBack(8);
  removeAt(3);
  show();
  system("pause");
  return 0;
}

  위 예제는 배열형태로 처리하면서 제일 앞이나 제일 뒤에 값을 추가해주고 원하는 인덱스의 값을 삭제해주는

  예제이다.

  결과값은 1 5 4 6 8 이 출력되게 된다.

  addFirst의 경우는 처음 값이 들어갈때를 제외하고는 반복문을 실행하게 된다.

  첫 값인 4를 넣어주고 나면 count가 증가되기 때문에 다음 5를 넣어줄때는 반복문에 의해 arr[i] = arr[i - 1]이

  처리되는데 이때를 예로 들면 arr[1] = arr[0] 이 되는것이고 그럼 arr[1] = 4 라는 것이나 마찬가지다.

  0에 있던 첫번째 값인 4를 뒷칸으로 넘겨주고 나면 i는 다시 0이 되기 때문에 반복문을 빠져나오게 되는데

  arr[0] 과 arr[1]은 4의 값을 갖고있는 상태로 빠져나온다.

  그럼 첫번째 인덱스에 새로운 값을 넣어줘야 하기 때문에 arr[0] = data로 새로운 값인 5를 추가하게 되는것이다.

  그리고 마찬가지로 count값을 증가.

  count값을 계속 증가시키고 반복문에서 한 회차마다 i를 빼줘야 하는 이유는 앞 인덱스부터 뒤 인덱스로 밀어내는것이

  아닌 뒤 인덱스에서 앞 인덱스의 값을 끌어당겨줘야 제대로 처리가 되기 때문이다.

  앞 인덱스의 값을 먼저 뒤 인덱스로 넘겨버리면 그 뒤 인덱스의 값은 그대로 사라져버리기 때문이다.

 

  addBack의 경우는 제일 뒤에 추가해주는 것이기때문에 복잡할 것 없이 데이터만 추가해주게 된다.

 

  removeAt은 삭제하고자하는 인덱스번호를 받아서 처리하게 되는데 지우고자 하는 인덱스의 뒤에있는 값들을

  앞으로 당겨서 중간에 삭제한 인덱스가 출력되지 않도록 하기 위해서다.

 

  이 예제에서는 사실 삭제한다기보다는 덮어씌운다는 표현이 맞다.

  반복문을 살펴보면 3이라는 인덱스 번호를 받아오게되고 i = 3, count = 6 - 1이 된다.

  그럼 지우는 값인 3번인덱스인 arr[3]에 arr[4]값을 덮어 씌워야 하기 때문에 arr[i + 1]로 처리하게 되는것이고

  i를 증가시킴으로써 뒤에있는 인덱스들 역시 하나씩 앞으로 당겨주게 된다.

  

  그리고 count는 결국 현재 배열 인덱스의 마지막 번호인것이므로 반복문이 끝난 후에는 감소를 시켜주어야

  출력을 위한 함수인 show()에서 원하는 값만 출력이 가능하다.

 

  여기서 만약 show()에서 count 에 + 1을 해준다면 1 5 4 6 8 8이 출력된다.

  위에서 얘기했다시피 삭제한다기보다는 덮어씌운다는 의미가 이것이다. 그냥 값을 하나씩 앞에다가 복사하면서

  붙여준것 뿐 정말 삭제한것이 아니기 때문이다.

  

  여기서 좀 한참 고민했었는데 만약 그 뒤에도 값이 나오지 않게 하고 싶다면?

  그럼 removeAt에서 count - 1이 아닌 count로하면 1 5 4 6 8 0 이렇게 출력된다.

  기본적으로 모든 인덱스에 0이 들어가있는 상태이기 때문에 또 그 뒤에 있던 인덱스의 값을 가져왔기 때문에

  0이 출력되는 것이다.

  하지만 문제가 될 수 있는 부분은 지금은 배열 크기가 10000인데 7자리만 사용했으니까 가능한거고 만약

  6만큼의 크기의 배열에서 이렇게 작성하면 오류가 발생한다.

  크기가 6인 배열의 최대 인덱스는 5가 되어야 되는데 removeAt에서 arr[i + 1]을 하며 6번 인덱스를 찾으라고 하기

  때문에 오류가 발생하게 된다.

  자바로 코딩테스트 문제들 풀때 겪었던 오류부분인데 혹해서 작성하면 실수할 수 있는 부분이다.

 

  이렇게 배열기반 리스트는 배열로 만들었기 때문에 특정한 위치의 원소에 즉시 접근할 수 있다는 장점이 있지만

  데이터가 들어갈 공간을 미리 메모리에 할당해야 한다는 단점이 있으며 원하는 위치로의 삽입이나 삭제가 

  비효율적이다.

 

연결리스트

 

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

typedef struct{
  int data;
  struct Node *next;
} Node;

Node *head;

int main(void){
  head = (Node*)malloc(sizeof(Node));
  Node *node1 = (Node*)malloc(sizeof(Node));
  node1->data = 1;
  Node *node2 = (Node*)malloc(sizeof(Node));
  node2->data = 2;
  head->next = node1;
  node1->next = node2;
  node2->next = NULL;
  Node *cur = head->next;
  
  while(cur != NULL){
    printf("%d ", cur->data);
    cur = cur->next;
  }
  
  system("pause");
  return 0;
}

  위 코드는 전체적인 구조만을 파악하기 위한 코드이다.

  Node라는 구조체를 만들어주고 기본적으로 동적할당을 이용함으로써 필요한 메모리를 할당받도록 한다.

  head, node1, node2 라는 노드를 만들고 메모리 할당을 받은 후 node1에는 1, node2에는 2라는 값을 넣어준다.

  head->next = node1; 이라는 것은 head라는 노드의 next는 node1이 된다는 것이고

  next를 이용해 head의 다음 노드를 node1으로 연결해주는 것이다.

  node2는 마지막 노드이기 때문에 next로 NULL을 넣어주는 것이다.

  cur은 출력을 위해 만들어준 것이고 출력을 위해 head의 next 인 node1을 가리키도록 한다.

  cur->data로 출력하게 되면 결국 node1의 data를 출력하게 되고 cur->next로 다음 노드인 node2도 출력하게 된다.

 

  삽입, 삭제과정

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

typedef struct{
  int data;
  struct Node *next;
} Node;

Node *head;

void addFront(Node *root, int data){
  Node *node = (Node*)malloc(sizeof(Node));
  node->data = data;
  node->next = root->next;
  root->next = node;
}

void removeFront(Node *root){
  Node *front = root->next;
  root->next = front->next;
  free(front);
}

void freeAll(Node *root){
  Node *cur = head->next;
  
  while(cur != NULL){
    Node *next = cur->next;
    free(cur);
    cur = next;
  }
}

void showAll(Node *root){
  Node *cur = head->next;
  
  while(cur != NULL){
    printf("show : %d ", cur->data);
    cur = cur->next;
  }
}

int main(void){
  head = (Node*)malloc(sizeof(Node));
  head->next = NULL;
  addFront(head, 2);
  addFront(head, 2);
  addFront(head, 2);
  addFront(head, 2);
  addFront(head, 2);
  removeFront(head);
  showAll(head);
  freeAll(head);
  system("pause");
  return 0;
}

  제일 앞부분의 삽입하는 addFront와 제일 앞 노드를 삭제하는 removeFront, 전체 삭제하는 freeAll로 작성하였다.

 

  기본 삽입 처리는 head - node1 - node2 - NULL 이런 형태의 구조에서 node7을 맨 앞에 넣어주고 싶다면

  head의 next를 node7에 연결하고 node7의 next를 node1로 연결해주면 된다.

  addFront에서 node를 동적할당 받고 그 node의 data에는 함수로 넘겨준 data값을 넣어준다.

  그리고 그 node의 next는 root로 받은 head의 next에 연결하도록 하고

  root의 next는 해당 node로 연결하도록 하면 된다.

  위에서 예시로 써둔 node7이 삽입되는 과정으로 설명한것과 같은 형태이다.

 

  삭제과정은 삭제 노드의 이전노드가 삭제노드의 next를 가리키도록 하면 된다.

  위 코드에서는 제일 첫번째 노드를 삭제하도록 했다.

  그래서 root라는 head에 해당하는 노드를 받아 처리한다.

  front라는 노드를 새로 만들고 그 노드는 root의 next에 연결되어있는 노드로 정의한다.

  그럼 삭제하고자하는 head의 다음 노드가 front가 되고 root의 next를 front의 next로 연결해준다.

  그럼 head는 front의 다음 노드를 가리키게 되고 front와의 연결이 끊어진다.

  그 후 free()를 이용해 front의 메모리할당을 해제해준다.

 

  freeAll의 경우는 전체적으로 메모리할당을 해제해서 리스트 자체를 지운다면 이렇게 해야한다

  이런 느낌으로 강의에 넣으신것 같다.

  head의 next로 정의한 cur을 이용하며 마지막 값인 NULL을 만날때까지 반복하도록 반복문을 작성하고

  cur = cur->next로 다음 노드로 넘어가며 모든 노드를 메모리할당 해제하도록 한다.

  코드에서는 next라고 따로 만들어서 처리했지만 결국 같은 의미이다.

 

양방향 연결리스트

  양방향 연결리스트는 머리(head)와 꼬리(tail)을 모두 가진다는 특징이 있다.

  각 노드는 앞 노드와 뒷 노드의 정보를 모두 저장하고 있다.

 

  prev | head | next  <- ->  prev | node1 | next  <- ->  prev | node2 | next  <- ->  prev | tail | next

 

  이런 형태이다.

  양방향 연결리스트에서의 삽입은 삽입하고자하는 위치에 있는 노드의 next값이 삽입하는 노드의 prev를 향하도록

  하고 삽입하는 노드의 prev역시 해당 노드의 next를 향하도록 한다. 그 후 삽입하고자 하는 노드의 다음 노드의 

  prev가 삽입 노드의 next를 향하도록 하고 삽입노드 역시 다음노드의 prev를 향하도록 하면 된다.

  이 순서가 중요하다고 한다. 기본적으로 순서를 철저하게 잘 지키고 고려해야 안정적으로 끊김없이 리스트가

  이어질 수 있기 때문이다.

  head - node1 - node2 - tail 이 형태에서 node1과 node2 사이에 node7을 삽입한다면

 

  1. node1 | next -> prev | node7

  2. node1 | next <- -> prev | node7

  3. node7 | next <- prev | node2

  4. node7 | next <- -> prev | node2

  

  위의 순서대로 처리해야 한다.

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

typedef struct{
  int data;
  struct Node *prev;
  struct Node *next;
} Node;

Node *head, *tail;

void insert(int data){
  Node *node = (Node*)malloc(sizeof(Node));
  node->data = data;
  Node *cur;
  cur = head->next;
  
  while(cur->data < data && cur != tail){
    cur = cur->next;
  }
  
  Node *prev = cur->prev;
  prev->next = node;
  node->prev = prev;
  cur->prev = node;
  node->next = cur;
}

void removeFront(){
  Node *node = head->next;
  head->next = node->next;
  Node *next = node->next;
  next->prev = head;
  free(node);
}

void show(){
  Node *cur = head->next;
  while(cur != tail){
    printf("%d ", cur->data);
    cur = cur->next;
  }
}

int main(void){
  head = (Node*)malloc(sizeof(Node));
  tail = (Node*)malloc(sizeof(Node));
  head->next = tail;
  head->prev = head;
  tail->next = tail;
  tail->prev = head;
  insert(2);
  insert(1);
  insert(3);
  insert(9);
  insert(7);
  removeFront();
  show();
  system("pause");
  return 0;
}

  삽입과 삭제 그리고 출력하는 코드이다.

  삽입은 오름차순 순서로 들어가도록 했고 삭제는 맨 앞 노드를 삭제하도록 했다.

 

  삽입에서는 data만 받고 시작한다. node를 새로 메모리할당받아주고 node의 data에 받아온 data를 넣어준다.

  그리고 cur은 head의 next로 정의한다.

  그럼 첫 삽입부분에서는 cur은 tail이 된다.

  cur의 data가 받아온 data보다 작고 cur이 tail이 아니라면 반복문이 실행되기 때문에 첫 삽입에서는 동작하지 않는다.

  prev를 하나 또 만들어서 cur의 prev로 정의하는데 cur은 tail이고 prev는 head가 되기때문에 prev는 head가 된다.

  prev의 next를 node로 연결하면 2의 값을 가진 node를 head가 가리키게 된다.

  그뒤로 node가 head를 가리키고 tail이 node를 node의 next가 tail를 가리키도록 해준다.

 

  두번째 값을 넣어주는데 두번째 값은 1이다.

  그럼 처음 들어가있는 값인 2보다 작기때문에 2 앞에 들어와야 한다.

  처음과 동일하게 node로 할당받고 1이라는 값을 넣어준다.

  cur은 head의 next인데 그럼 2의 값을 갖고 있는 node가 된다.

  cur을 좀 더 보기편하게 node2라고 한다면 node2의 data인 2가 1의 값을 갖고 있는 data보다 작고 node2가 tail이

  아니라면 반복문을 실행한다.

  node2는 tail이 아니지만 갖고 있는 데이터인 2가 들어올 1보다 크기 때문에 false처리가 되어 반복문이 실행되지 않고

  prev는 node2의 prev인 head가 된다.

  head의 next를 node1로 연결하고 node1역시 head에 연결. node2를 node1에 연결 후 node1 역시 node2에 연결한다.

 

  다음으로는 3의 값을 넣어주는데 3은 2와 tail사이로 들어가야한다.

  node를 할당받고 3을 넣어준다.

  node3이라 칭하고 cur은 head의 next인 node1이 된다. 똑같이 cur을 node1이라 하며 설명.

  node1의 데이터인 1은 data인 3보다 작다. 그리고 node1은 tail이 아니기때문에 true가 되며 반복문이 동작한다.

  cur = cur->next; 가 처리되어 cur = node1->next가 되어 node2가 된다.

  하지만 node2의 데이터 역시 3보다 작고 tail이 아니기 때문에 반복문이 한번더 동작하여 cur = node2->next로

  tail이 된다. 여기서는 cur이 tail이 되었기 때문에 반복문이 종료된다.

  그럼 prev는 cur의 prev이기 때문에 tail의 prev인 node2가 prev가 된다.

  node2의 next를 node3로 그럼 node3의 prev를 node2로 연결.

  cur은 tail이기 때문에 tail의 prev를 node3에 연결 node3의 next를 tail로 연결한다.

  그럼 지금까지의 진행상황으로는 head - node1 - node2 - node3 - tail 형태로 오름차순으로 잘 들어가는것을

  확인할 수 있다.

  이와같은 형태로 나머지 두 값도 들어가게되어 1 2 3 7 9 형태로 완성된다.

 

  삭제과정이다.

 

  removeFront는 head의 바로 다음 노드를 삭제하는 과정으로 단방향연결리스트에서와 같다.

  root로 head를 받냐 안받냐의 차이인데 어차피 head를 이용해 삭제하는것이기 때문에 둘다 같은처리나 마찬가지다.

  node는 head의 next값인 node1로 정의한다.

  head의 next를 node1의 next인 node2에 연결한다. 그리고 next를 node의 next인 node2로 정의.

  next->prev 는 node2의 prev이므로 이것을 head에 연결한다.

  그럼 node1의 prev는 head에 next는 node2에 연결되어있지만 head의 next는 node2에 node2의 prev는 head에

  연결되어있기때문에 사실상 해제상태이다.

  그리고나서 free()를 이용한 메모리해제를 해준다.

 

 

  양방향이 작업이 많고 중간중간 헷갈리는 부분들이 있어서 구현시에 중간중간 체크가 좀 필요하며 너무 복잡하지 않고

  깔끔하게 작성하는것이 중요할것 같다.

 

 

레퍼런스

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

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

퀵정렬  (0) 2021.01.07
선택정렬, 삽입정렬  (0) 2021.01.06
큐(Queue)  (0) 2021.01.05
스택(Stack)  (0) 2021.01.02
자료구조  (0) 2020.12.30

자료구조란

  사전적 의미로는 자료(Data)의 집합을 의미하며, 각 원소들이 논리적으로 정의된 규칙에 의해 나열되며

  자료에 대한 처리를 효율적으로 수행할 수 있도록 자료를 구분하여 표현한 것이다.

  

  사용 목적으로는 자료를 더 효율적으로 저장하고, 관리하기 위해 사용하며 잘 선택된 자료구조는 실행시간을 단축시켜

  주거나 메모리 용량의 절약을 이끌어 낼 수 있다.

 

  자료의 처리를 보다 효율적으로 하기 위해서 처리시간, 자료의 크기, 활용빈도, 갱신정도, 프로그램의 용이성을

  고려하여 선택, 사용해야 한다.

 

특징

  1. 효율성

    자료구조의 목적은 효율적인 데이터의 관리 및 사용이다. 따라서 적절한 자료구조를 선택하여 사용한다면

    업무의 효율이 올라갈 것이다. 예를 들자면 데이터의 양이 많을 때 순차검색을 사용하는 것 보다 이분 검색을

    활용하는 것이 더 효율적이다. 이와같이 목적에 맞는 자료구조를 사용하는 것이 효율적이다.

 

  2. 추상화

    추상화란 복잡한 자료, 모듈, 시스템등으로부터 핵심적인 개념만 간추려 내는 것이다. 자료구조를 구현할 때 중요한

    것은 어느시점에 데이터를 삽입할 것이며, 어느 시점에 이러한 데이터를 어떻게 사용할 것인지에 대해서

    초점을 맞출수 있기 때문에 구현 외적인 부분에 더 시간을 쏟을 수 있다.  알고리즘 자체에는 중점을 두지 않는다.

    마찬가지로 자료구조 내부의 구현은 중요하지 않다. 어떻게 구현했는지보다 어떻게 사용해야 하는지를 알고 있어야

    한다. 예를 들어 어느 함수 내부 구현이 어떻게 되었는지는 중요하지 않다. 사람마다 다른 코드를 작성할 것이고,

    사용언어, 개발 툴등 환경적인 변수에 의해서 다른 코드가 나올것이기 때문에 추상적인 개념에 대해서만 이해하고

    있다면 사용할 수 있다.

 

  3. 재사용성

    자료구조를 설계할 때 특정 프로그램에서만 동작하게 설계하지는 않는다. 다양한 프로그램에서 동작할 수 있도록

    범용성 있게 설계하기 때문에 해당 프로젝트가 아닌 다른 프로젝트에서도 사용할 수 있다.

  

 

선형구조

  배열(Array), 연결리스트(Linked List), 스택(Stack), 큐(Queue)

 

비선형구조

  트리(Tree), 그래프(Graph)

 

프로그램의 성능 측정 방법론

  시간복잡도(Time Complexity)란 알고리즘에 사용되는 연산 횟수를 의미한다.

  공간복잡도(Space Complexity)란 알고리즘에 사용되는 메모리의 양을 의미한다.

  효율적인 알고리즘을 사용한다고 가정했을 때 일반적으로 시간과 공간은 반비례 관계이다.

  시간복잡도를 표현할때는 최악의 경우를 나타내는 Big-o표기법을 사용한다.

 

int main(void){
  int a, b;
  cin >> a >> b;
  int sum = 1;
  for(int i = 0; i < b; i++){
    sum += a;
  }
  cout << sum;
}

  위 알고리즘은 O(N)의 시간 복잡도를 가진다.

  i가 0부터 b까지 반복을 하는데 어떠한 변수만큼 반복한다고 해서 O(N)이라고 한다.

  

#include <iostream>

using namespace std;

int main(void){
  int n;
  cin >> n;
  for(int i = 0; i < n; i++){
    for(int j = 0; j <= i; j++){
      cout << "*";
    }
    cout << '\n';
  }
}

  이 알고리즘은 O(N^2)의 시간 복잡도를 가진다.

  이렇게 이중for문을 사용하는 경우는 n의 제곱만큼 반복이 되기 때문에 O(N^2)가 되는 것이다.

 

#include <iostream>

using namespace std;

int main(void){
  int a, b;
  cin >> a >> b;
  cout << a + b;
}

  이 알고리즘의 경우는 O(1)이다.

  그냥 a와 b를 더한 값을 바로 출력하기 때문에 1이다.

 

  N 이 1,000일 때

  N = 1,000번의 연산

  NlogN = 10,000번의 연산

  N^2 = 1,000,000번의 연산

  N^3 = 1,000,000,000번의 연산

 

  보통 연산횟수가 10억을 넘어가면 1초이상의 시간이 소요되기 때문에 10억을 넘기지 않도록 하는 것이 중요하다.

  사용자 입장에서는 1초가 굉장히 긴 시간처럼 느껴질 수 있기 때문이다.

  

  시간 복잡도를 표기할 때는 항상 큰 항과 계수만 표시한다.

  O(3N^2 + N) = O(N^2)

 

  공간복잡도는 일반적으로 MB단위로 표기한다.

  int a[1000] = 4KB

  int a[1000000] = 4MB

  int a[2000][2000] = 16MB

 

 

레퍼런스

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

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

퀵정렬  (0) 2021.01.07
선택정렬, 삽입정렬  (0) 2021.01.06
큐(Queue)  (0) 2021.01.05
스택(Stack)  (0) 2021.01.02
연결리스트(Linked List)  (0) 2021.01.02

+ Recent posts