CH10. 배열

이지용·2024년 4월 4일

배열이란?

  • 많은 값을 한꺼번에 저장할 수 있는 자료형
  • 동일한 타입의 데이터가 여러개 저장되어 있다.

배열을 사용하면 한꺼번에 여러개의 변수를 생성할 수 있다.
예를 들어 10개의 정수를 저장할 수 있는 배열은 다음과 같이 선언한다.
다음 문장에서 s는 배열의 이름이고 10은 배열의 크기이다. int는 자료형으로 정수 데이터를 저장할 수 있다는 뜻이다.

int s[10];

배열을 구성하는 각각의 항목을 배열요소(array element)또는 배열 원소라고 한다.
이 배열 원소에는 번호가 붙어 있는데 이것을 인덱스(index) 또는 첨자라고 부른다.
index=0 부터 시작하고, index=2인 세번째 배열은 다음과 같다.

s[2]

배열의 특징

  • 배열은 메모리의 연속적인 공간에 저장된다. 예시로 s[0]과 s[1]은 실제 메모리에서도 붙어있다.
  • 배열의 큰 장점은 서로 관련된 데이터를 차례로 접근해서 처리할 수 있다는 점이다.

배열의 선언

배열을 사용하려면 배열을 우선 선언해야 한다. 배열을 선언할 때는 자료형을 지정하고, 이름을 적고, 배열의 크기를 적으면 된다.

int scores[10];
//순서대로 자료형, 배열의 이름, 배열의 크기이다.

배열은 일반 변수들과 함께 선언되거나, 여러개의 배열을 하나의 라인에서 선언할 수 있다.

char src[10], dst[10]; 
//2개의 배열 동시 선언
int index, days[7]; 
//일반변수 index와 배열 동시 선언
  • 주의할점
    배열의 크기를 나타낼 때는 항상 상수를 사용하여야 한다. 배열의 크기를 변수, 음수, 실수로 하면 모두 컴파일 오류가 난다.
//오류예시
int score[];
int score[size];
int score[-2];
int score[6.7];
  • 보통 배열을 선언할 때는 배열의 크기를 #define 지시자로 만들어진 기호상수로 지정한다. 이러면 배열의 크기를 변경하기 쉬워진다.
#define SIZE 10
int scores[SIZE];
//배열의 크기를 바꾸고 싶으면 10만 바꾸면 됨

배열요소접근

배열요소들은 앞서 말했듯이 인덱스라는 번호를 통해 접근해야한다.
유효한 index의 범위는 0에서 (배열크기-1)까지다.
배열요소는 변수와 100% 동일하다. 값을 넣고 뺄 수 있다.

score[0]=80;
score[1]=score[0];

index는 정수 상수가 될 수 있고, 변수, 수식 등도 가능하다.

score[i]=10;
score[i+2]=20;
score[index[3]]=30;

배열 요소를 차례대로 처리할 때 for 반복문을 사용하면 쉽게 처리할 수 있다.

int score[5];
//score 배열에 값이 있을 때,
.
.
.
for(i=0;i<5,i++)
	printf("grade[%d]=%d\n",i,score[i]);
    //순서대로 배열의 값을 가져올 수 있다.

인덱스의 범위

인덱스가 배열의 크기를 벗어나게 되면 프로그램에 치명적인 오류를 발생시킨다. 컴파일러는 프로그래머가 유효 범위 안에 있는 인덱스를 사용하고 있는지를 확인하여 주지 않는다.
다음 예시를 보자

int score[5];

이런 배열이 있을 때

printf("%d",score[5]);//index 5는 불가하다

이렇게 범위를 넘어서는 인덱스를 사용하면 어떻게 될까?

위 문장은 컴파일 오류가 아니다. 따라서 컴파일도 되고 실행도 된다. 하지만 인덱스가 5이면 잘못된 값에 접근하고 있는 것이다. 따라서 score[5]에 접근하면 의미없는 쓰레기 값이 나오게 될 것이다. 하지만 컴퓨터 시스템이 정지하거나 프로그램이 중단되지는 않는다.
그렇지만 잘못된 인덱스에 접근하는 것이 아니라 아예 저장해버린다면 최악의 경우 시스템이 중단될 수 있다.

score[5]=60;

이렇게 되면 엉뚱한 변수의 값을 변경하게된다.

배열의 초기화

배열을 초기화하기 위해선 초기값들을 콤마로 분리하여 {}로 감싼뒤에, 배열을 선언할 때 대입해주면 된다.

int score[5]={10,20,30,40,50};

모든 배열 요소에 초기값을 부여하는 것이 원칙이다. 만약 초기값의 개수가 요소들의 개수보다 적으면 앞에 있는 요소들만 초기값으로 초기화되고, 나머지 요소는 0으로 초기화된다.

int score[5]={10,20,30}; //score 배열은 10, 20, 30, 0, 0 값이 들어있다.

이러한 성질을 이용하여 모두 0으로 초기화 하려면 이렇게 하면 된다.

int score[5]={0}; //첫번째 값 0으로, 나머지 값 안넣었으니 모두 0으로 초기화

초기화만 하고 배열의 크기를 비워 놓으면 자동으로 초기값들의 개수만큼의 배열 크기를 잡는다.

int score[]={10,20,30,40,50};//자동으로 배열의 크기가 5가 된다.

만약 초기값이 주어지지 않았고 지역 변수로 선언된 배열이라면, 일반적인 지역 변수와 마찬가지로 아무 의미없는 쓰레기 값이 들어가게 된다.

배열 요소의 개수를 계산하는 방법

배열 안에 들어있는 요소의 개수를 자동적으로 계산하기 위해선 sizeof 연산자를 사용하면 된다.

sizeof
sizeof 연산자는 자료형이나 변수의 크기를 바이트 단위로 계산하는 연산자이다.

sizeof 연산자를 이용하여 배열 전체의 크기를 구하고 이것을 배열 요소의 크기로 나누게 되면 배열 요소가 몇개 있는지 계산할 수 있다.

int size=sizeof(score)/sizeof(score[0]);

sizeof(score)은 배열의 크기이고, sizeof(score[0])는 배열 하나의 크기이다. 이를 통해 배열의 크기를 구할 수 있다.

배열의 복사

배열을 복사하기 위해선 단순히 b=a; 와 같은 문장으로 대입할 수는 없다. c언어에서 배열의 이름은 배열의 시작을 가리킨다.

//잘못된경우
int a[SIZE]={1,2,3,4,5};
int b[SIZE];

b=a;

//올바른 방법
int i;
int a[SIZE]={1,2,3,4,5};
int b[SIZE];

for (i=0;i<SIZE;i++)
	b[i]=a[i];
  • 배열을 복사하기 위해선 반복구조를 사용해서 하나씩 복사해주어야 한다.

배열의 비교

배열의 비교 역시 배열의 복사와 비슷하다.
배열의 비교를 할 때 (a와 b는 배열)
if (a==b)와 같은 식으로는 올바른 결과를 형성하지 않는다.
위의 예시처럼 반복문을 통해 하나의 요소마다 비교를 해주어야한다.

배열에서 최소값 혹은 최대값을 찾고싶다면 따로 max() 혹은 min()과 같은 함수는 없다.
따라서 최소값을 구하기 위해선 배열의 첫번째 값을 최소값으로 놓고, 
그 다음값들을 비교해가며 더 작은 값으로 최소값을 갱신해 나가면 된다. 최대값도 비슷하게 하면 된다.

배열과 함수

배열도 함수로 전달할 수 있다. 하지만 변수를 전달할 때와는 다르다.
변수를 함수에 전달할 때에는 인수의 값이 복사되어 매개변수에 전달된다. (값에 의한 호출)
배열의 경우는 값에 의한 호출이 아닌, 배열의 원본이 매개변수를 통해 전달된다.
이 개념은 포인터와 관련되어 있기 때문에 나중에 추가로 학습하면 더욱 이해가 쉬울 것이다.
지금으로는 그냥 배열의 경우엔 원본이 전달된다고만 알아두자.

간단한 예로, 학생들의 성적을 저장하고 있는 정수 배열을 만들고 평균을 계산하는 함수를 작성하여 호출해보자.

#include <stdio.h>
#define STUDENTS 5

int get_average(int scores[], int size);// 1번

int main(void)
{
	int scores[STUDENTS]={1,2,3,4,5}; //초기화
    int avg;
    
    avg= get_average(scores,STUDENTS);
    printf("평균은 %d입니다.\n", avg);
    return 0;
}
//배열에 들어있는 값들의 평균을 계산하는 함수
int get_average(int scores[],int size)
{	
	int i;
    int sum=0;
    
    for (i=0;i<size;i++)
    	sum+=scores[i];
        
    return sum/size;
}

함수가 배열을 매개변수로 받기 위해서는 위의 1번 문장과 같이 원형선언과 함께 함수 정의를 해야한다.
1번 문장의 첫번째 매개변수 int scores[]가 배열을 나타낸다. 이때는 배열의 크기를 지정하지 않아도 된다.
새롭게 배열을 만드는 것이 아니기 때문이다.
우리는 매개변수 scores를 통해 원본 배열 scores를 참조한다.
두번째 매개변수 int size는 배열의 크기를 받는 매개변수이다.
호출된 함수에서는 scores 가 배열이라는 것만 알 수 있고 배열의 크기는 알 수 없기 때문에 배열의 크기도 매개변수로 전달하는 것이다.

원본 배열의 변경

배열은 매개변수를 통하여 원본을 참조하는 것이기 때문에 항상 조심해야한다.
만약 함수 안에서 매개변수를 통해 배열요소를 변경한다면 이것은 원본 배열을 변경시키는 결과를 가져온다.
다음 함수를 봐보자.

void modify_array(int a[], int size)
{
	int i;
    
    for (i=0; i<size; i++)
    	++a[i];
}

이 함수는 a라는 배열을 매개변수로 받고, a 배열의 각 요소를 1 씩 증가시킨다.
배열은 원본이 전달되므로 호출된 함수가 배열의 요소를 수정하면 함수 내에서만 값이 바뀌는 것이 아니라 원본 배열의 내용도 동시에 수정된다.

오류주의!
배열 요소를 인수로 하여서 함수를 호출하면 복사본이 전달된다.
배열은 원본이 전달되지만 배열 요소는 복사본이 전달되므로 착각하면 안된다.

정렬

정렬은 오름차순이나 내림차순으로 나열하는 것을 말한다.

선택정렬

선택정렬은 가장 이해하기 쉬운 정렬 방법이다.
먼저 왼쪽 배열, 오른쪽 배열, 이렇게 두 개의 배열이 있다고 가정하자.
왼쪽 배열에는 정렬이 완료된 숫자들이 들어가게 되며 오른쪽 배열에는 정렬되지 않은 숫자들이 들어있다.
초기 상태에는 왼쪽 배열은 비어있고, 정렬되어야 할 숫자들은 모두 오른쪽배열에 있다.
선택정렬은 오른쪽 배열에서 가장 작은 숫자를 선택하여 왼쪽 배열로 이동하는 작업을 되풀이한다.
이는 오른쪽 배열이 공백상태가 될 때 까지 이 과정을 되풀이한다.

이 방법은 입력배열과는 별도로 똑같은 크기의 배열이 하나 더 필요하다.
메모리를 절약하기 위해 하나의 배열로 처리하는 방법도 존재한다.

배열에서 최소값을 찾은 후, 이 최소값을 첫번째 원소와 교환한다.
그 후 첫번째 원소를 제외한 나머지 원소들 중에서 가장 작은 값을 선택하고 이를 두번째 원소와 교환한다.
이를 (배열의 크기-1)만큼 되풀이한다. 아래 그림을 참고하자.

이를 코드로 구현하면 다음과 같다.

#include <stdio.h>
#define SIZE 10

int main(void)
{
	int list[SIZE]={3,2,9,7,1,4,8,0,6,5};
    
    int i, j, temp, least;
    
    for(i=0;i<SIZE-1;i++)
    {
    //최소값 찾는 과정. i가 하나 증가할 때마다 배열 요소가 교환됨.
    //least는 가장 작은 배열요소의 index
    	least=i;        
        for(j=i+1; j<SIZE; j++)
        	if(list[j]<list[least])
            	least=j;
               
        //temp를 사용하여 값을 교환
        temp=list[i];
        list[i]=list[least];
        list[least]=temp;
    }
    for(i=0; i<SIZE; i++)
    	printf("%d",list[i]);
    printf("\n")
    return 0;
}

탐색

배열에서 특정한 값을 찾는 것을 말한다.
단순히 정수들이 배열에 저장되어 있고 여기서 특정한 정수를 찾는다고 가정하자.
이 특정한 정수를 탐색키라고 한다.
두가지 탐색 방법, 즉 순차 탐색과 이진 탐색을 살펴본다.

순차 탐색

순차 탐색은 탐색 방법 중 가장 간단하고 직접적인 탐색 방법이다.
순차 탐색은 배열의 원소를 순서대로 하나씩 꺼내서 탐색키와 비교하여 원하는 값을 찾아가는 방법
이다. 순차 탐색은 일치하는 항목을 찾을 때까지 비교를 계속한다.
이는 첫번째 원소에서 성공할 수도 있고 마지막 원소까지 가야할 수도 있다.
하지만 선형탐색은 배열이 크면 클수록 시간이 오래걸린다.

//예시
for(i=0;i<SIZE;i++)
	if(list[i]==key)
    	find_index=i;

이진 탐색

이진 탐색은 아주 속도가 빠르지만 탐색하려는 배열이 정렬되어 있어야 가능하다.
이진 탐색은 배열의 중앙에 있는 값을 탐색키와 비교한다. 탐색키와 같으면 성공이다.
탐색키가 중앙원소의 값보다 작으면 원하는 값은 배열의 전반부에 있을 것이다.
만약 탐색키가 중앙 원소의 값보다 크면 배열의 후반부에 있을 것이다.
이진 탐색에서는 한번 비교할 때마다 탐색의 범위가 절반으로 줄어든다.

//예시
int low=0;
int high=n-1 //n은 배열의 크기
int middle;

while(low<=high)
{
	middle=(low+high)/2; //중간위치 계산
    if(key==list[middle]) 
    	return middle;
    else if(key>list[middle]) //key 값이 더 크면
    	low=middle+1;		  //middle 보다 큰 구간 탐색하기 위해 low 값 재설정
    else
    	high=middle-1;
}

2차원 배열

2차원 배열에서는 요소들이 2차원적으로 배열되어 있다.

2개의 대괄호를 이용해서 2차원 배열을 선언한다.
첫번째 대괄호 안에는 행의 개수, 두번째 대괄호 안에는 열의 개수를 적는다.
이 배열은 3개의 행으로 이루어져 있고, 각 행에는 5개의 요소가 있다고 할 수 있다.

2차원 배열에서 요소 참조

2차원 배열에서 하나의 요소를 참조하려면 2개의 인덱스가 필요하다.
s[i][j]는 배열 s의 i번째 행과 j번째 열의 요소이다.
2차원 배열의 요소에 값을 저장하려면 중첩 for 문을 통해 값을 입력할 수 있다.

2차원 배열의 초기화

2차원 배열도 선언과 동시에 초기화 할 수있다.
2차원 배열에선 같은 행에 속하는 초기값들은 중괄호{}로 따로 묶어주어야 한다.

int s[3][5] =
{
	{0,1,2,3,4}
    {5,6,7,8,9}
    {10,11,12,13,14}
}

2차원 배열을 초기화할 때 안쪽의 중괄호는 생략될 수도 있다.

int s[3][5]=
{
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
};

1차원 배열과 마찬가지로 초기값이 부족하면 나머지 요소는 모두 0이 된다.
초기값이 지정되지 않으면 배열이 전역변수일 경우엔 0, 지역변수일 경우엔 쓰레기값으로 초기화된다.
선언과 동시에 초기화할 경우 행의 개수는 지정하지 않아도 된다. 하지만 열의 개수를 무조건 지정해야한다.

int s[][5]=
{
	{0,1,2,3,4}
    {5,6,7,8,9}
    {10,11,12,13,14}
}

보통 2차원 배열에서 행과 요소들의 index로는 r(row)과 c(column)를 많이 사용한다.

2차원 배열도 1차원 배열과 마찬가지로 함수에 인수로 전달할 수 있다.
이 때도 원본이 전달된다.

0개의 댓글