본문 바로가기
[알고리즘 + 자료구조]

[알고리즘] Do it 자료구조와 알고리즘 6장 정리 1/2

by Hevton 2020. 11. 6.
반응형

연습문제_Q1 > 버블정렬의 각 패스에서 비교, 교환의 시작은 왼쪽 오른쪽 상관이 없다. 오름차순의 정렬일 경우에 왼쪽부터 시행하면 각 패스에서 가장 큰 값의 요소가 오른쪽 끝으로 옮겨지는 것이고, 오른쪽부터 시행하면 각 패스에서 가장 작은 값 요소가 왼쪽 끝으로 옮겨지는 것이다. 왼쪽부터 시행한 경우의 프로그램을 작성하세요

 #include <stdio.h>
 #include <stdlib.h>
 #define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

 // 뒤에서 앞으로 검사 & 오름차순. 기존
 void bubble(int a[], int n)
 {
     int i, j;
     
     for(i = 0; i < n - 1; i++) {
         for(j = n - 1; j > i; j--) {
             if(a[j - 1] > a[j])
                 swap(int, a[j - 1], a[j]);
         }
     }
 }

 //앞에서 뒤로 검사 & 오름차순. Q_1
 void bubble2(int a[], int n)
 {
     int i, j;
     
     for(i = 0; i < n - 1; i++) {
         for(j = 0; j < n - 1 - i; j++) {
             if(a[j] > a[j + 1])
                 swap(int, a[j], a[j + 1]);
         }
     }
 }


 int main() {
     int i, nx;
     int *x;
     
     puts("버블 정렬");
     printf("요소 개수 : ");
     scanf("%d", &nx);
     x = calloc(nx, sizeof(int));
     
     for(i = 0; i < nx; i++) {
         printf("x[%d] : ", i);
         scanf("%d", &x[i]);
     }
     
     bubble2(x, nx);
     
     puts("오름차순으로 정렬했습니다.");
     
     for(i = 0; i< nx; i++) {
         printf("x[%d] = %d\n", i, x[i]);
     }
     
     free(x);
     
     return 0;
 }
 
 /*
 아래는 제공된 정답. 내 답은 바깥포문은 이전과 그대로 일치시키고도 가능하다는 것을 보여준 것이고, 이 답은 바깥포문부터 변형시켜줌. 둘다상관없음.
 void bubble(int a[], int n)
 {
	int i, j;

	for (i = n - 1; i > 0; i--) {
		for (j = 0; j < i; j++)
			if (a[j] > a[j + 1])
				swap(int, a[j], a[j + 1]);
	}
}

 */

 

연습문제_Q2 > 비교, 교환 과정을 자세히 출력하면서 버블 정렬을 수행하는 프로그램을 작성하세요. 두 요소 사이에 교환을 수행하면 '+' 그렇지 않으면 '-'를 출력하고 정렬을 마치면 비교 횟수와 교환 횟수를 출력하세요.

#include <stdio.h>
#include <stdlib.h>
#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

// 뒤에서 앞으로 검사 & 오름차순 & 과정 출력
void bubble(int a[], int n)
{
    int i, j, z, y, co = 0,ch = 0;
    
    for(i = 0; i < n - 1; i++) {
        printf("패스 %d:\n", i+1);
        for(j = n - 1; j > i; j--) {
            
            for(int z = 0; z < j - 1; z++) {
                printf("%d   ", a[z]);
            }
            if(a[j - 1] > a[j]) {
                printf("%d + %d   ", a[j - 1], a[j]);
                swap(int, a[j - 1], a[j]);
                ch++;
            }
            else {
                printf("%d - %d   ", a[j - 1], a[j]);
            }
            co++;
            
            for(y = j + 1; y < n; y++) {
                printf("%d   ", a[y]);
            }
            
            putchar('\n');
        }
        // 각 패스가 끝난 뒤엔 결과를 한번 보여줌
        for(z = 0; z < n; z++) {
            printf("%d   ", a[z]);
        }
        putchar('\n');
    }
    
    printf("비교를 %d회 했습니다.\n교환을 %d회 했습니다.\n", co, ch);
}


int main() {
    int i, nx;
    int *x;
    
    puts("버블 정렬");
    printf("요소 개수 : ");
    scanf("%d", &nx);
    x = calloc(nx, sizeof(int));
    
    for(i = 0; i < nx; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    
    bubble(x, nx);
    
    puts("오름차순으로 정렬했습니다.");
    
    for(i = 0; i< nx; i++) {
        printf("x[%d] = %d\n", i, x[i]);
    }
    
    free(x);
    
    return 0;
}

 

연습문제_Q3 > 버블 정렬(버전2=패스에서 시도한 교환횟수가 0일 경우 정렬을 종료)의 아이디어는 배열이 정렬을 마쳤는지를 검사하는 데 응용할 수 있다. 전달받은 배열 a가 오름차순으로 정렬을 마쳤는지 검사하는 함수를 작성하세요. 오름차순으로 정렬을 마친 상태라면 1, 그렇지 않으면 0을 반환하도록 작성하세요.

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

// 배열이 정렬을 마쳤는지 확인하는 함수
int is_sorted(const int a[], int n) {
    int i;
    
    for(i = n - 1; i > 0; i--) {
        if(a[i - 1] > a[i]) {
            return 0; // 바로 0 리턴
        }
    }
    
    return 1; // 리턴되지 않았을 경우 리턴 1 (정렬되어있지않음)
}

int main() {
    int i, nx;
    int *x;
    
    puts("버블 정렬");
    printf("요소 개수 : ");
    scanf("%d", &nx);
    x = calloc(nx, sizeof(int));
    
    for(i = 0; i < nx; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    
    is_sorted(x, nx)?printf("정렬"):printf("비정렬");
        
    free(x);
    
    return 0;
}

 

연습문제_Q4 > 버블 정렬(버전2)을 Q2 처럼 비교, 교환 과정을 자세히 출력하는 프로그램으로 수정하세요.

#include <stdio.h>
#include <stdlib.h>
#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

// 뒤에서 앞으로 검사 & 오름차순 & 과정 출력 & 패스 단위 멈춤 도입
void bubble(int a[], int n)
{
    int i, j, z, y, co = 0,ch = 0;
    
    for(i = 0; i < n - 1; i++) {
        int exchg = 0;
        printf("패스 %d:\n", i+1);
        for(j = n - 1; j > i; j--) {
            
            for(int z = 0; z < j - 1; z++) {
                printf("%d   ", a[z]);
            }
            if(a[j - 1] > a[j]) {
                printf("%d + %d   ", a[j - 1], a[j]);
                swap(int, a[j - 1], a[j]);
                ch++;
                exchg++;
            }
            else {
                printf("%d - %d   ", a[j - 1], a[j]);
            }
            co++;
            
            for(y = j + 1; y < n; y++) {
                printf("%d   ", a[y]);
            }
            
            putchar('\n');
        }
        for(z = 0; z < n; z++) {
            printf("%d   ", a[z]);
        }
        putchar('\n');
        if(exchg == 0) {
            puts("정렬을 미리 마칩니다.");
            break;
        }
    }
    
    printf("비교를 %d회 했습니다.\n교환을 %d회 했습니다.\n", co, ch);
}


int main() {
    int i, nx;
    int *x;
    
    puts("버블 정렬");
    printf("요소 개수 : ");
    scanf("%d", &nx);
    x = calloc(nx, sizeof(int));
    
    for(i = 0; i < nx; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    
    bubble(x, nx);
    
    puts("오름차순으로 정렬했습니다.");
    
    for(i = 0; i< nx; i++) {
        printf("x[%d] = %d\n", i, x[i]);
    }
    
    free(x);
    
    return 0;
}

 

연습문제_Q5 > 버블정렬(버전3 = 마지막으로 교환을 수행한 위치를 저장하고 그 앞쪽은 정렬되어있으니 불필요하게 작업하지 않음)을 Q2와 마찬가지로 비교, 교환 과정을 자세히 출력하는 프로그램으로 수정하세요.

#include <stdio.h>
#include <stdlib.h>
#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

// 뒤에서 앞으로 검사 & 오름차순 & 과정 출력 & 패스 내부 멈춤 도입
void bubble(int a[], int n)
{
    int j, z, y, co = 0,ch = 0;
    
    int k = 0; // a[k] 보다 앞쪽의 요소는 정렬을 마친 상태입니다. ( 본래 버블정렬에서 바깥포문 변수가 그런 뜻을 내포함)
    int p = 0; // 패스 횟수
    
    while(k < n - 1) {
        printf("패스 %d:\n", ++p);
        int last = n - 1; //패스에서 교환을 하게 되면 값이 바뀌겠고, 교환을 하지 않게 되면 k = n-1 그대로가 되면서 정렬됨을 뜻하고 작업종료.
        for(j = n - 1; j > k; j--) {
            
            for(int z = 0; z < j - 1; z++) {
                printf("%d   ", a[z]);
            }
            if(a[j - 1] > a[j]) {
                printf("%d + %d   ", a[j - 1], a[j]);
                swap(int, a[j - 1], a[j]);
                last = j;
                ch++;
            }
            else {
                printf("%d - %d   ", a[j - 1], a[j]);
            }
            co++;
            
            for(y = j + 1; y < n; y++) {
                printf("%d   ", a[y]);
            }
            
            putchar('\n');
        }
        for(z = 0; z < n; z++) {
            printf("%d   ", a[z]);
        }
        putchar('\n');
        k = last;
    }
    
    printf("비교를 %d회 했습니다.\n교환을 %d회 했습니다.\n", co, ch);
}


int main() {
    int i, nx;
    int *x;
    
    puts("버블 정렬");
    printf("요소 개수 : ");
    scanf("%d", &nx);
    x = calloc(nx, sizeof(int));
    
    for(i = 0; i < nx; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    
    bubble(x, nx);
    
    puts("오름차순으로 정렬했습니다.");
    
    for(i = 0; i< nx; i++) {
        printf("x[%d] = %d\n", i, x[i]);
    }
    
    free(x);
    
    return 0;
}

 

연습문제_Q6 > 버블정렬(버전3)을 사용해도 효율의 효과가 없는 경우가 생길 수 있다. 이런 경우의 극복을 위해선 홀수 번째의 패스는 가장 작은 요소를 맨 앞으로 옮기고, 짝수 번째 패스는 가장 큰 요소를 뒤로 옮기는 방식(패스의 스캔 방향을 교대로 바꾸는 방식)을 사용하면 된다. 버전3의 프로그램을 개선하여 '양방향 버블 정렬'을 수행하는 프로그램을 작성하세요.

 

+ 추가로 개인적으로 과정출력도 더해줬음.

#include <stdio.h>
#include <stdlib.h>
#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

// 오름차순 & 과정 출력 & 패스 내부 멈춤 도입 & 양방향 버블 정렬(칵테일 정렬, 쉐이커 정렬)
void bubble(int a[], int n)
{
    int z, y, co = 0,ch = 0;
    
    int p = 0;
    
    int left = 0;
    int right = n - 1;
    int last = right; // 여기서도 마찬가지의 의미로 작동해줌.
    
    while(left < right) {
        int j;
        
        printf("패스 %d:\n", ++p);
        for (j = right; j > left; j--) { // 가장 작은 요소를 맨 앞으로
            
            for(int z = 0; z < j - 1; z++) {
                printf("%d   ", a[z]);
            }

            if(a[j - 1] > a[j]) {
                printf("%d + %d   ", a[j - 1], a[j]);
                swap(int, a[j - 1], a[j]);
                last = j;
                ch++;
            }
            else {
                printf("%d - %d   ", a[j - 1], a[j]);
            }
            co++;
            for(y = j + 1; y < n; y++) {
                printf("%d   ", a[y]);
            }
            
            putchar('\n');
        }
        // 패스 다끝나고 확인차
        for(z = 0; z < n; z++) {
            printf("%d   ", a[z]);
        }
        putchar('\n');

        left = last;
        
        if(left >= right) // 3번째에 끝났을 경우엔 아래 코드가 전부 작동하지 않도록 ( 과정 출력 도중에, 아래 포문이 작동하지 않더라도 패스 출력문이나 전체출력문이 출력되어서, 여기서도 while문의 탈출조건을 넣어줬음.
            break;
        
        printf("패스 %d:\n", ++p);
        for(j = left; j < right; j++) { // 가장 큰 요소를 맨 뒤로
            
            for(int z = 0; z < j; z++) {
                printf("%d   ", a[z]);
            }

            if(a[j] > a[j + 1]) {
                printf("%d + %d   ", a[j], a[j + 1]);
                swap(int, a[j], a[j + 1]);
                last = j;
                ch++;
            }
            else {
                printf("%d - %d   ", a[j], a[j + 1]);
            }
            co++;
            for(y = j + 2; y < n; y++) {
                printf("%d   ", a[y]);
            }
            putchar('\n');

        }
        // 패스 다끝나고 확인차
        for(z = 0; z < n; z++) {
            printf("%d   ", a[z]);
        }
        putchar('\n');

        right = last;
        
    }
    
    printf("비교를 %d회 했습니다.\n교환을 %d회 했습니다.\n", co, ch);
}


int main() {
    int i, nx;
    int *x;
    
    puts("양방향 버블 정렬");
    printf("요소 개수 : ");
    scanf("%d", &nx);
    x = calloc(nx, sizeof(int));
    
    for(i = 0; i < nx; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    
    bubble(x, nx);
    
    puts("오름차순으로 정렬했습니다.");
    
    for(i = 0; i< nx; i++) {
        printf("x[%d] = %d\n", i, x[i]);
    }
    
    free(x);
    
    return 0;
}

 

연습문제_Q7 > 요소의 교환 과정을 자세하게 출력할 수 있도록 단순 선택 정렬 프로그램을 수정하세요. 정렬하지 않은 부분의 첫 번째 요소 위에는 기호 *를, 정렬하지 않은 부분의 가장 작은 값의 요소 위에는 기호 +를 출력하세요.

#include <stdio.h>
#include <stdlib.h>
#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

// 단순 선택 정렬 & 과정 출력
void selection(int a[], int n) {
    int i, j, z, y;
    for(i = 0; i < n - 1; i++) {
        int min = i;
        for(z = 0; z < min; z++) {
            printf("  ");
        } printf(" *");

        for(j = i + 1; j < n; j++) {
            if(a[j] < a[min])
                min = j;
        }
        // z가 *의 위치로 되어있으니, +1로 그다음위치부터 현재 min보다 작을때까지 공백으로 채워줌.
        for(y = z + 1; y < min; y++) {
            printf("  ");
        } printf(" +\n");
        
        for(int x = 0; x < n; x++) {
            printf("%2d", a[x]);
        }
        putchar('\n');
        swap(int, a[i], a[min]); // 이렇게 하면 min == i 일 경우에도 실행은 되나 제자리 이동이므로 결과는 같음 (오히려 두 값이 다를 때에만 적용하게 비교하는 구문을 삽입하는게 전체 프로그램의 효율을 떨어뜨릴 수도 있음)
    }
}

int main() {
    int i, nx;
    int *x;
    
    puts("단순 선택 정렬");
    printf("요소 개수 : ");
    scanf("%d", &nx);
    x = calloc(nx, sizeof(int));
    
    for(i = 0; i < nx; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    
    selection(x, nx);
    
    puts("오름차순으로 정렬했습니다.");
    
    for(i = 0; i< nx; i++) {
        printf("x[%d] = %d\n", i, x[i]);
    }
    
    free(x);
    
    return 0;
}

/*
제공된 정답 : 조건식?A:B를 중첩으로 써서 훨씬 가독성 좋게 구현했음.
내 방법은 *이나 + 출력 전까지 공백을 채우고, *나 +를 따로 출력하는 과정을 일일이 가졌는데
정답의 방법은 이를 한번에 해결

//--- 단순 선택 정렬(정렬 과정 출력)---
//void selection(int a[], int n)
//{
//	int i, j, m;
//
//	for (i = 0; i < n - 1; i++) {
//		int min = i;
//		for (j = i + 1; j < n; j++)
//			if (a[j] < a[min])
//				min = j;
//		for (m = 0; m < n; m++)
//			printf((m == i) ? "  * " : (m == min) ? "  + " : "    ");
//		putchar('\n');
//
//		for (m = 0; m < n; m++)
//			printf("%3d ", a[m]);
//		putchar('\n');
//
//		swap(int, a[i], a[min]);
//	}
//	putchar('\n');
//	for (m = 0; m < n; m++)
//		printf("%3d ", a[m]);
//	putchar('\n');
//}

*/

 

연습문제_Q8 > 요소의 삽입 과정을 자세하게 출력할 수 있도록 단순 삽입 정렬 프로그램을 수정하세요. 현재 선택한 요소 아래에 기호 +, 삽입하는 위치의 요소 아래에 기호 ^, 그 사이에 기호 -를 출력하세요. 삽입하지 않는 경우에는 선택한 요소 아래에 +만 출력하면 됩니다.

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

 // 단순 삽입 정렬 함수 & 과정 출력
 void insertion(int a[], int n) {
     int i, j, z, y;
     for(i = 1; i < n; i++) {
         
         for(z = 0; z < n; z++) {
             printf("%2d ", a[z]);
         } putchar('\n');
         int tmp = a[i];
         for(j = i; j > 0 && a[j - 1] > tmp; j--) {
             a[j] = a[j - 1];
         }
         
         if(i!=j) { // 삽입 결과가 제자리가 아닌 경우
             for(z = 0; z < j; z++) {
                 printf("   ");
             }
             printf("^--");
             for(y = z+1; y < i; y++) {
                 printf("---");
             }
             printf("-+");
         } else { // 변화 없이 제자리인 경우
             for(z = 0; z < i; z++) {
                 printf("   ");
             }
             printf(" +");
         }
         putchar('\n');
         a[j] = tmp;
     }
     
     // 마지막으로 정리한거 출력
     for(int k = 0; k<n; k++) {
         printf("%2d ", a[k]);
     } putchar('\n');
 }

 int main() {
     int i, nx;
     int *x;
     
     puts("단순 삽입 정렬");
     
     printf("요소 개수 : ");
     scanf("%d", &nx);
     x = calloc(nx, sizeof(int));
     
     for(i = 0; i < nx; i++) {
         printf("x[%d] : ", i);
         scanf("%d", &x[i]);
     }
     
     insertion(x, nx);
     
     puts("오름차순으로 정렬했습니다.");
     
     for(i = 0; i< nx; i++) {
         printf("x[%d] = %d\n", i, x[i]);
     }
     
     free(x);
     
     return 0;
 }

//제공된 정답은 딱히.

 

연습문제_Q9 > 단순 삽입 정렬에서 배열의 첫 번째 요소(a[0])부터 데이터를 저장하지 않고 a[1] 부터 데이터를 저장하면 a[0]을 보초로 하여 삽입을 마치는 조건을 줄일 수 있다. 이를 기반으로 프로그램을 작성하세요.

 

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

// 단순 삽입 정렬 함수 & 보초법 적용(main함수 내용과 연관있음)
void insertion(int a[], int n) {
    int i, j;
    for(i = 2; i < n; i++) {
        int tmp = a[i];
        for(j = i; a[j - 1] > tmp; j--) {
            a[j] = a[j - 1];
        }
        a[j] = tmp;
    }
}

int main() {
    int i, nx;
    int *x;
    
    puts("단순 삽입 정렬");
    
    printf("요소 개수 : ");
    scanf("%d", &nx);
    nx++;
    x = calloc(nx, sizeof(int));
    
    x[0] = -1; // -1 값을 보초로 넣어놓음. 마이너스인 값을 넣어서 insertion함수에서 배열 범위를 벗어나는 체크를 할 필요가 없게 만들어줌.
    
    for(i = 1; i < nx; i++) {
        printf("x[%d] : ", i-1);
        scanf("%d", &x[i]);
    }
    
    insertion(x, nx);
    
    puts("오름차순으로 정렬했습니다.");
    
    for(i = 1; i< nx; i++) {
        printf("x[%d] = %d\n", i, x[i]);
    }
    
    free(x);
    
    return 0;
}

/*
제공된 정답 : main 함수 안에서 보초를 지정하지도 않아서 독립적이여서 좋고, 보초값으로 나 자신을 매번 선택함으로써 배열 안에 어떤 값들이 있던 신경쓰지않아도됌.
내 경우엔 가장작은 수를 -1로 임의지정하여 배열안의 수들은 그보다 크다는 전제의 코드.

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

void insertion(int a[], int n)
{
    int i, j;

    for (i = 1; i < n; i++) {
        int tmp = a[0] = a[i]; // 아예 현재 값을 보초값으로 넣어버림
        for (j = i; a[j - 1] > tmp; j--) {
            a[j] = a[j - 1];
        }
        a[j] = tmp; // if(j)로 감싸있었는데, 필요없는 코드인 것 같아 지워버림. if(j)가 false가 되려면 j가 0이되어야하고, 그뜻은 인덱스 0과 다른 인덱스간의 교환이 있다는건데 a[0] = a[i] 이므로 그런 경우는 절대 없지.
    }
}

int main(void)
{
    int i, nx;
    int *x;

    puts("단순 삽입 정렬");
    printf("요솟수 : ");
    scanf("%d", &nx);
    x = calloc(nx + 1, sizeof(int));    //a[0]를 보초로 사용하기 때문에 n + 1개의 배열을 생성

    for (i = 1; i < nx + 1; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }

    insertion(x, nx + 1);

    puts("오름차순으로 정렬했습니다.");
    for (i = 1; i < nx + 1; i++)
        printf("x[%d] = %d\n", i, x[i]);

    free(x);

    return 0;
}

*/

 

연습문제_Q10 > 단순 삽입 정렬은 배열의 요소 개수가 많아지면 많아질수록 요소 삽입에 필요한 비교, 대입 비용이 무시할 수 없을 정도로 커집니다. 이때 배열에서 이미 정렬된 부분은 이진 검색을 사용할 수 있기 때문에 삽입할 위치를 더 빨리 찾을 수 있습니다. 이진 검색을 사용하여 프로그램을 수정하세요. => 이진 삽입 정렬을 구현하세요.

 

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

// 이진 검색 + 단순 삽입 정렬 = 이진 삽입 정렬(binary insertion sort). 안정적이지 않은 알고리즘.
void insertion(int a[], int n) {
    int i;
    for(i = 1; i < n; i++) {
        int tmp = a[i];
        
        // 아래 if문을 만족하지 않을 경우엔 그냥 값을 그대로 냅둠. 책의 이전 예제들처럼 제자리 교환작업(i = j일 때, int tmp = a[i]; a[j] = tmp;)을 불필요하게 수행하지 않음.
        if(a[i - 1] > tmp){
            
            int pl = 0; // 검색 범위 맨 앞의 인덱스
            int pr = i - 1; // 검색 범위 맨 끝의 인덱스
            int pc; // 검색 범위 가운데의 인덱스

            do {
                pc = (pl + pr) / 2;
                if(a[pc] == tmp) { // 같은값이 있을 때
                    // 흐름을 들어가보니, pl <= pr 을 만족하지 못하고 while문이 종료될 때 pl 자리가 값이 들어갈 자리더라(pl or pr+1). 난 pl로 지정하기로 했으니 여기서도 pl에 넣어줌.
                    pl = pc; break;
                }
                else if(a[pc] < tmp)
                    pl = pc + 1;
                else
                    pr = pc - 1;
            } while(pl <= pr);
            
            //분석해보니 while문이 탈출될 때의 pl값이 tmp값이 들어갈 자리인 것을 알게됌. // pl or pr+1
            
            for(int z=i; z > pl; z--) {
                a[z] = a[z-1];
            }
            a[pl] = tmp; // 책의 이전 예제들과 달리 내 코드는 교환작업이 필요한 이 if문 안에서만 코드를 넣음으로써, a[i - 1] > tmp 가 아닐 경우엔 불필요한 제자리 체인지를 안해줌.
        }
    }
}

 int main() {
     int i, nx;
     int *x;
     
     puts("이진 삽입 정렬");
     
     printf("요소 개수 : ");
     scanf("%d", &nx);
     x = calloc(nx, sizeof(int));
     
     for(i = 0; i < nx; i++) {
         printf("x[%d] : ", i);
         scanf("%d", &x[i]);
     }
     
     insertion(x, nx);
     
     puts("오름차순으로 정렬했습니다.");
     
     for(i = 0; i< nx; i++) {
         printf("x[%d] = %d\n", i, x[i]);
     }
     
     free(x);
     
     return 0;
 }


//제공된 정답은 pc 자리가 아닌 pc+1자리에 넣는다는 것(동일한 값을 찾았을 때 앞에 넣는지 뒤에 넣는지 차이)과
//pl 대신 pr + 1을 사용한다는 것(pl<=pr로 인해 while문을 탈출하게 될 때 pl(=pr+1)자리가 들어갈 자리이다.)이 내 코드와 다를 뿐, 결과는 동일하다.

 

연습문제_Q11 > Q10알고리즘은 삽입할 위치의 검색은 빠르지만, 삽입을 위해 요소를 하나씩 뒤쪽으로 미는 작업 비용이 단순 삽입 정렬 알고리즘과 같다. 요소를 뒤쪽으로 미는 작업을 memmove함수를 사용하여 구현하면 비용을 줄일 수 있다. 이를 토대로 구현하세요.

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

// 이진 검색 + 단순 삽입 정렬 = 이진 삽입 정렬(binary insertion sort). 안정적이지 않은 알고리즘.
void insertion(int a[], int n) {
    int i;
    for(i = 1; i < n; i++) {
        int tmp = a[i];
        
        // 아래 if문을 만족하지 않을 경우엔 그냥 값을 그대로 냅둠. 책의 이전 예제들처럼 제자리 교환작업(i = j일 때, int tmp = a[i]; a[j] = tmp;)을 불필요하게 수행하지 않음.
        if(a[i - 1] > tmp){
            
            int pl = 0; // 검색 범위 맨 앞의 인덱스
            int pr = i - 1; // 검색 범위 맨 끝의 인덱스
            int pc; // 검색 범위 가운데의 인덱스

            do {
                pc = (pl + pr) / 2;
                if(a[pc] == tmp) { // 같은값이 있을 때
                    // 흐름을 들어가보니, pl <= pr 을 만족하지 못하고 while문이 종료될 때 pl 자리가 값이 들어갈 자리더라(pl or pr+1). 난 pl로 지정하기로 했으니 여기서도 pl에 넣어줌.
                    pl = pc; break;
                }
                else if(a[pc] < tmp)
                    pl = pc + 1;
                else
                    pr = pc - 1;
            } while(pl <= pr);
            
            //분석해보니 while문이 탈출될 때의 pl값이 tmp값이 들어갈 자리인 것을 알게됌. // pl or pr+1
            
            memmove(a+pl+1, a+pl, sizeof(int)*(i-pl)); // 메모리단위의 데이터 이동 함수, for문을 통해 요소를 뒤쪽으로 미는 작업보다 비용이 적지만 결과는 동일하여 좋음. memmove(dest, src, size) 형식을 갖는다.
            
            a[pl] = tmp; // 책의 이전 예제들과 달리 내 코드는 교환작업이 필요한 이 if문 안에서만 코드를 넣음으로써, a[i - 1] > tmp 가 아닐 경우엔 불필요한 제자리 체인지를 안해줌.
        }
    }
}

 int main() {
     int i, nx;
     int *x;
     
     puts("이진 삽입 정렬");
     
     printf("요소 개수 : ");
     scanf("%d", &nx);
     x = calloc(nx, sizeof(int));
     
     for(i = 0; i < nx; i++) {
         printf("x[%d] : ", i);
         scanf("%d", &x[i]);
     }
     
     insertion(x, nx);
     
     puts("오름차순으로 정렬했습니다.");
     
     for(i = 0; i< nx; i++) {
         printf("x[%d] = %d\n", i, x[i]);
     }
     
     free(x);
     
     return 0;
 }

 

공부 내용 정리

 

정렬 

이름, 키 등 핵심 항목(key)의 대소 관계에 따라 데이터 집합을 일정한 순서로 줄지어 늘어서도록 바꾸는 작업. 

 


∙ 오름차순 정렬 

키 값이 작은 데이터를 앞쪽에 놓아 정렬


∙ 내림차순 정렬 

키 값이 큰 데이터를 앞쪽에 놓아 정렬


정렬 알고리즘의 안정성


∙ 안정된 정렬 

같은 키 값인 요소의 순서가 정렬 전후에도 유지되는 정렬


내부 정렬과 외부 정렬


∙ 내부 정렬 

정렬할 모든 데이터를 하나의 배열에 저장할 수 있는 경우에 사용하는 알고리즘 

 


∙ 외부 정렬 

정렬할 데이터가 너무 많아서 하나의 배열에 저장할 수 없는 경우에 사용하는 알고리즘


정렬 알고리즘의 핵심 요소 3가지 

교환, 선택, 삽입 

 

 


버블 정렬 

이웃한 두 요소의 대소 관계를 비교하여 교환을 반복 

+ 앞에서 뒤로 / 뒤에서 앞으로 순서를 정할 수 있다. 

∙ 큰 틀 

for(i = 0; i < n - 1; i++) {
    /*
     이 포문 안에서 a[i], a[i+1], ... a[n-1]에 대해
     끝에서부터 앞쪽으로 스캔하면서 이웃하는
     두 요소를 비교하고 교환하는 작업을 구상
     */
}



∙ 과정 

6 4 3 7 1 9 8 배열을 오름차순으로 정렬할 예정이다. 뒤에서부터 작업한다. 

요소의 개수가 n인 배열에서 처음 n-1회 비교, 교환 작업을 거치고 나면 가장 작은 요소가 맨 처음으로 이동한다. 이런 일련의 과정을 패스라고 한다. 이후 맨 앞 요소를 고정시키고, 위 패스를 수행한다(n-2회. 패스를 수행할 때마다 정렬할 요소가 하나씩 줄어들기 때문). 패스를 k회 수행하면 앞쪽의 요소 k개가 정렬된다는 것을 알 수 있고, 따라서 모든 정렬이 끝나려면 n-1회의 패스가 수행되어야 한다. 그리고 각 k번째 패스에서는 n - k 번의 작업을 수행한다. 

+ 수행하는 패스의 횟수가 n회가 아니라 n-1회인 것은 n-1개 요소의 정렬이 끝나면 마지막 요소는 이미 끝에 놓이기 때문
+ 버블 정렬이라는 말은 액체 안의 공기방울이 위로 올라오는 모습에서 유래


∙ 버블 정렬 코드 

#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

void bubble(int a[], int n)
{
    int i, j;
    for(i = 0; i < n-1; i++) {
        //아래 포문이 패스
        for(j = n-1; j > i; j--) {
            if(a[j-1] > a[j])
                swap(int, a[j-1], a[j]); //교환작업
        }
    }
}



서로 이웃한 요소에 대해서만 교환하므로 이 정렬 알고리즘은 안정적이다. 

비교 횟수는 첫 번째 패스는 n-1회, 두 번째 패스는 n-2회... 1회 이므로 합계는 n(n-1)/2. 

실제 요소를 교환하는 횟수는 배열의 요솟값에 더 많이 영향을 받기 때문에(교환을 안할 수도 있음) 교환 횟수의 평균값은 비교 횟수의 절반인 n(n-1)/4.
또한 swap 함수 안에서 값의 이동이 3회 발생하므로 이동 횟수의 평균은 3n(n-1)/4.


버블 정렬 시간 복잡도 : O(n^2)


버블 정렬 알고리즘 개선 (1)


한 패스에서 요소의 교환 횟수가 0이면 더 이상 정렬할 요소가 없다는 뜻이기 때문에 정렬 작업을 멈추게 하여 이후의 불필요한 작업을 갖지 않는다. 

∙ 코드 

#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

void bubble(int a[], int n)
{
    int i, j;
    for(i = 0; i < n-1; i++) {
        //아래 포문이 패스
        int exchg=0; //패스에서 시도한 교환 횟수
        for(j = n-1; j > i; j--) {
            if(a[j-1] > a[j]) {
                swap(int, a[j-1], a[j]); //교환작업
                exchg++;
            }
        }
        if(exchg == 0) break; // 교환이 수행되지 않으면 정렬 멈춤.
    }
}



버블 정렬 알고리즘 개선 (2) 

패스에서 비교 , 교환을 하다가 어떤 시점 이후에 교환이 수행되지 않는다면 그보다 앞쪽의 요소는 이미 정렬을 마친 상태인 것. 다음 패스부터는 그 요소들을 제외한 요소들에서만 비교, 교환을 수행하도록 하면 됨. 

∙ 코드 

#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

void bubble(int a[], int n)
{
    int k = 0; // a[k]보다 앞쪽의 요소는 정렬을 마친 상태. 본래 버블정렬에서 바깥포문의 변수가 그런 뜻을 내포.
    while(k < n - 1) { //본래 버블정렬에서 바깥포문이 k는 0부터 n-2까지 바꾸면서 패스의 수행횟수 의미도 있겠지만, k가 정렬된 데이터의 개수 또는 어느인덱스까지 검사할지를 나타낼 수도 있음.
        int j;
        int last = n - 1; // 마지막으로 교환을 수행한 위치를 저장하는 변수.
        // 아래 포문이 패스
        for(j = n - 1; j > k; j--) {
            if(a[j - 1] > a[j]) {
                swap(int, a[j - 1], a[j]);
                last = j;
            }
        }
        k = last; // 마지막으로 교환을 수행한 위치까지 패스가 돌게끔 지정
    }
}




버블 정렬 알고리즘 개선(3) 

개선(2) + 홀수 번째 패스는 가장 작은 요소를 맨 앞으로, 짝수 번째 패스는 가장 큰 요소를 맨 뒤로 하여 패스의 스캔 방향을 교대로 바꿈 

= 양방향 버블 정렬 = 칵테일 정렬 = 쉐이커 정렬 


∙ 코드 

#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

/*--- 양방향 버블 정렬(셰이커 정렬) ---*/
void shaker(int a[], int n)
{
    int left = 0;
    int right = n - 1;
    int last = right;

    while (left < right) {
        int j;
        for (j = right; j > left; j--) {
            if (a[j - 1] > a[j]) {
                swap(int, a[j - 1], a[j]);
                last = j;
            }
        }
        left = last;

        for (j = left; j < right; j++) {
            if (a[j] > a[j + 1]) {
                swap(int, a[j], a[j + 1]);
                last = j;
            }
        }
        right = last; // 여기서 left==right 나오면 바로끝나지만, 첫번째 포문에서 나오면 불필요하게 이 문구를 한번 더 가짐(같은값끼리 교환)
    }
}



단순 선택 정렬(straight selection sort) 

가장 작은 요소부터 선택해 알맞은 위치로 옮겨서 순서대로 정렬하는 알고리즘. 

아직 정렬하지 않는 부분에서 값이 가장 작은 요소를 선택하고, 아직 정렬하지 않은 부분의 첫 번째 요소와 교환한다. 이 과정을 n - 1회 반복한다. 

∙ 큰 틀 

for(i = 0; i < n - 1; i++) {
    /*
     min = a[i], ..., a[n - 1]에서 가장 작은 값을 갖는 요소의 인덱스
     a[i]와 a[min]의 값을 교환
     */
}



∙ 코드 

#define swap(type, x, y) do { type t = x; x = y; y = t; } while(0)

// 단순 선택 정렬
void selection(int a[], int n)
{
    int i, j;
    for(i = 0; i < n - 1; i++) {
        int min = i; // 최솟값을 찾기 전에 기준값 역할 + 이 위치에 최솟값이 들어갈 자리(범위 내의 가장 앞자리)
        for(j = i+1; j < n; j++) {
            if(a[j] < a [min])
                min = j;
        }
        swap(int, a[i], a[min]); // i == min 일 경우 불필요하게 실행되긴 함.
    }
}


서로 떨어져 있는 요소를 교환하는 것이기 때문에 안정적이지 않다. 

요솟값을 비교하는 횟수는 n(n-1)/2회
( 안쪽 포문 동작을 보면 바깥쪽 포문에 대해 j에 따라 n-1, n-2, ....1회) 

단순 선택 정렬 시간 복잡도 : O(n^2)




단순 삽입 정렬(straight insertion sort) 

선택한 요소를 그보다 더 앞쪽의 알맞은 위치에 '삽입 하는' 작업을 반복하여 정렬하는 알고리즘. 단순 선택 정렬과 비슷하게 보일 수 있지만 단순 선택 정렬은 값이 가장 작은 요소를 선택에 알맞은 위치로 옮긴다는 점이 다르다. 

아직 정렬되지 않은 부분의 첫 번째 요소를 정렬된 부분의 알맞은 위치에 삽입한다. 이 작업을 n - 1회 반복하면 정렬을 마치게 된다. 

+ 단순 삽입 정렬은 2번째 요소부터 선택하여 진행한다. 

∙ 큰 틀

for(i = 1; i < n; i++) { //i = 1, 두 번째 요소를 기준으로 잡음
    /*
     tmp = a[i]
     a[0], ..., a[i - 1]의 알맞은 곳에 tmp를 삽입한다.
     */
}



∙ 코드

// 단순 삽입 정렬
void insertion(int a[], int n)
{
    int i, j;
    for(i = 1; i < n; i++) {
        int tmp = a[i];
        // 정렬된 열 안에서 요소가 tmp보다 큰 경우 밀어내기 반복
        for(j = i; j > 0 && a[j - 1] > tmp; j--) {
            a[j] = a[j - 1];
        }
        a[j] = tmp;
    }
}



단순 삽입 정렬 알고리즘은 떨어져 있는 요소들이 서로 뒤바뀌지 않아 안정적이다. 

요소의 비교 횟수와 교환 횟수는 n^2/2회. 

단순 삽입 정렬 시간 복잡도 : O(n^2) 

+ 버블 정렬 / 단순 선택 정렬 /  단순 삽입 정렬 세 알고리즘의 시간복잡도는 모두 O(n^2)이다. 

Q. 왜 단순 선택 정렬처럼 n(n-1)/2가 아닌진 모르겠음. 그리고 비교문이 두개라 교환횟수랑도 달라야 하지 않나.. 
-> 다른곳들 알아보니 단순 삽입 정렬의 비교, 교환 횟수도 n(n-1)/2라고 나와있음. 어쨌든 시간복잡도적인 면에선 동일한 값.

 

 

단순 삽입 정렬 알고리즘 개선 (1)

 

배열의 첫 번째 요소(a[0])부터 데이터를 저장하지 않고 a[1]부터 데이터를 저장하면 a[0]을 보초로 하여 삽입을 마치는 조건을 줄일 수 있다. - 보초법 도입

 

∙ 코드

 

// 단순 삽입 정렬 & 보초법
void insertion(int a[], int n)
{
    int i, j;

    for (i = 1; i < n; i++) {
        int tmp = a[0] = a[i];
        for (j = i; a[j - 1] > tmp; j--) {
            a[j] = a[j - 1];
        }
        a[j] = tmp; // 설명에 if(j)로 감싸져있었는데, j가 0인 경우는 실행되는 없으므(j=1일때 a[0] > tmp가 성립이 안되므로 무조건 탈출)로 쓸데없는 것 같아서 지움.
    }
}

 

단순 삽입 정렬 알고리즘 개선 (2)

 

단순 삽입 정렬은 배열의 요소 개수가 많아지면 많아질수록 요소 삽입에 필요한 비교, 대입 비용이 무시할 수 없을 정도로 커진다.

이때 배열에서 이미 정렬된 부분은 이진 검색을 사용할 수 있다. - 이진 삽입 정렬 (안정적이지 않은 알고리즘)

 

∙ 코드

 

// 단순 삽입 정렬 + 이진 검색 = 이진 삽입 정렬(binary insertion sort). 안정적이지 않은 알고리즘(이진검색이 들어가서).
void insertion(int a[], int n) {
    int i;
    for(i = 1; i < n; i++) {
        int tmp = a[i];
        
        // 아래 if문을 만족하지 않을 경우엔 그냥 값을 그대로 냅둠. 책의 이전 예제들처럼 제자리 교환작업(i = j일 때, int tmp = a[i]; a[j] = tmp;)을 불필요하게 수행하지 않음.
        if(a[i - 1] > tmp){
            
            int pl = 0; // 검색 범위 맨 앞의 인덱스
            int pr = i - 1; // 검색 범위 맨 끝의 인덱스
            int pc; // 검색 범위 가운데의 인덱스

            do {
                pc = (pl + pr) / 2;
                if(a[pc] == tmp) { // 같은값이 있을 때
                    // 흐름을 들어가보니, pl <= pr 을 만족하지 못하고 while문이 종료될 때 pl 자리가 값이 들어갈 자리더라(pl or pr+1). 난 pl로 지정하기로 했으니 여기서도 pl에 넣어줌.
                    pl = pc; break;
                }
                else if(a[pc] < tmp)
                    pl = pc + 1;
                else
                    pr = pc - 1;
            } while(pl <= pr);
            
            //분석해보니 while문이 탈출될 때의 pl값이 tmp값이 들어갈 자리인 것을 알게됌. // pl or pr+1
            
            //밀어내는 작업
            for(int z=i; z > pl; z--) {
                a[z] = a[z-1];
            }
            // 참고로, 밀어내는 작업을 memmove를 쓰면 더 비용이 절감됨
            // memmove(a+pl+1, a+pl, sizeof(int)*(i-pl)); 메모리단위의 데이터 이동 함수, for문을 통해 요소를 뒤쪽으로 미는 작업보다 비용이 적지만 결과는 동일하여 좋음. memmove(dest, src, size) 형식을 갖는다.

            a[pl] = tmp; // 책의 이전 예제들과 달리 내 코드는 교환작업이 필요한 이 if문 안에서만 코드를 넣음으로써, a[i - 1] > tmp 가 아닐 경우엔 불필요한 제자리 체인지를 안해줌.
        }
    }
}

 

 

 

출처 - 'Do it 자료구조와 함께 배우는 알고리즘 C언어편' 도서

반응형