대형 컴퓨터 시스템
💡 설계 및 구현 방법론
- 데이터 추상화
- 알고리즘 명세
- 성능 분석과 측정
대형 컴퓨터 프로그램
- 복잡하게 상호작용하는 부품들로 구성된 시스템 like 하드웨어
- 시스템 생명 주기가 있음 why? 유지보수를 위해
시스템 생명 주기 System Life Cycle
💡 요구사항 - 분석 - 설계 - 정제와 코딩 - 검증 (과정을 하나라도 생략하면 안 된다.)
- 요구사항 requirement
- 프로젝트들의 목적을 정의한 명세들의 집합
- 고객의 요구에 맞게 입력과 출력에 관한 정보를 기술
- 분석 analysis
- 문제들을 다룰 수 있는 작은 단위들로 나눔 = 분할 정복 Divide and Conquer
- 상향식 bottom-up VS 하향식 top-down
- 상향식은 C의 함수처럼 기능 위주로 나눈다. 버그 난 곳을 찾기 어렵다
- 하향식은 C++의 class처럼 객체 위주로 나눈다 → 객체지향
- 설계 design
- 추상 데이터 타입 Abstract data type 생성
- 메모리를 적게 차지하고 처리 시간을 빠르게 하는 게 목표 → 데이터 추상화 필요
- so 자료구조의 핵심은 데이터 추상화!
- 데이터 추상화는 이 데이터가 무엇이고 어떻게 구현할 것인가를 명확하게 구분하는 것
- 알고리즘 명세와 설계 기법 고려
- 추상 데이터 타입 Abstract data type 생성
- 정제와 코딩 refinement and coding
- 데이터 객체에 대한 표현 선택 → 알고리즘의 효율성 결정
- 수행되는 연산에 대한 알고리즘 작성
- 검증 verification
- 정확성 증명 correctness proof
- 수학적 기법들을 이용해서 증명
- 대형 시스템의 완벽한 증명이 어려움 → 정확성이 증명된 알고리즘 선택
- 테스트 testing
- 프로그램의 정확한 수행 검증
- 프로그램의 성능 검사
- 오류 제거 error removal
- 독립적 단위로 테스트 후 전체 시스템으로 통합
- 정확성 증명 correctness proof
객체 지향 설계
객체 지향적 분해
- 응용 분야의 개체를 모델링하는 객체의 집합
- 소프트웨어의 재사용성 조장
- 예) 학생 객체를 상담 프로그램에도, 수강신청에도 사용할 수 있다.
- 변화에 유연한 소프트웨어 시스템
- 직관적
- 직관적 → 프로그래머가 이해하기 쉬운 코드 → 유지보수가 쉽다.
기본 정의와 개념
- 객체 object
- 계산을 수행(함수)하고 상태(변수)를 갖는 개체
- 데이터 + 절차적 요소
- 모든 객체는 클래스에 속함.
- 객체 지향 프로그래밍 object-oriented programming
- 객체는 기본적인 구성단위 building block
- 각 객체는 어떤 타입(클래스)의 인스턴스 instance
- 클래스는 상속 inheritance 관계로 연관됨
- 상속은 코드 재사용성이 높음
C++
역사
- FORTRAN → Pascal, C → Modula → Smalltalk, Objective-C, C++
- C의 장점
- 효율성 - 하드웨어 직접 제어 가능
- 유연성 - 대부분의 응용 분야에 사용 가능
- 가용성 - 모든 컴퓨터에 C 컴파일러 존재
데이터 타입
- 데이터 타입 data type
- 객체들과 이 객체들에 대한 연산 operation의 집합
- C++ 데이터 타입
- 기본 데이터 타입
- char, int, float, double
-
- 수식어 short, long, signed, unsigned
- 파생 데이터 타입
- pointer, reference
- 데이터를 묶어주는 구조
- array, struct, class
- 추상 데이터 타입 abstract data type; ADT
- 객체와 연산에 대한 명세가 객체의 표현과 연산의 구현으로부터 분리된 방식으로 구성된 데이터 타입
- 기본 데이터 타입
기초
- C++ 프로그램의 구성
- 헤더 파일
- .h 접미사
- 선언 저장에 사용 - 추상화되어 있음
- 시스템/사용자 정의 파일
- 전처리기 명령 #include를 사용하여 파일에 포함시킴
- 소스 화일
- .cpp 접미사
- 소스 코드 저장에 사용
- 헤더 파일
- 프로그램 실행 과정
- 컴파일 → 링크 → 적재
- 포인터 선언
- 객체의 메모리 주소 저장
int i=25; int *np = &i;
- 참조 타입 선언
- 객체에 대한 또 다른 이름을 제공
int i=5; int& j=i; i=7; printf(“i=%d, j=%d”, i, j); // i=7, j=7
- 파일 입출력
- 헤더 화일 fstream 필요
#include <iostream> #include <fstream> main(){ ofstream outFile("my.out", ios::out); if (!outFile){ cerr << "cannot open my.out" << endl; //표준 오류 장치 return; } int n = 50; float f = 20.3; outFile << "n: " << n << endl; outFile << "f: " << f << endl; }
- 매개변수 전달
- 값에 의한 전달
- 전달된 객체는 그 함수의 지역 저장소에 복사
- 실인자에 영향을 주지 않음
- 참조에 의한 전달
- 인자 리스트의 타입 명세자에 & 첨가
- 전달된 객체의 주소만 함수의 지역 저장소에 복사
- 실인자에 직접 접근
- 전달된 객체가 큰 메모리를 요구하는 경우 더 빨리 수행
- 상수 참조
- 인자 변경 불가
- const T& a
- 값에 의한 전달
- 함수 다중화 function overloading
- 함수의 인자 리스트가 다르기만 하면 같은 이름을 가진 함수가 둘 이상 존재할 수 있음
- inline 함수
- 함수 정의에 키워드 inline을 첨가해서 선언
- 함수 호출과 인자 복사의 오버헤드를 줄임
- why? 함수를 부르는 게 아니라 코드에 직접 대입하기 때문
- new/delete 연산자
- 실행 시간에 heap를 할당받거나 반환
- new 다음에 반드시 delete를 해야 한다.
- 예외 처리
- try 블록에 발생할 수 있는 예외를 포함시켜서 처리
- catch 블록은 예외 타입을 나타내는 한 인자를 가짐
- union
- 제일 큰 멤버에 맞는 저장장소 할당
- 보다 효율적인 기억 장소 사용
- static(정적) 클래스 데이터 멤버
- 클래스에 대한 전역 변수 → 모든 클래스 객체가 하나의 사본을 공유
- 클래스 외부에서 정의
데이터 추상화와 캡슐화
- 데이터 캡슐화 data encapsulation
- 정보 은닉 information hiding like private, protected
- 외부로부터 데이터 객체의 자세한 구현을 은닉
- why? 굳이 보여줄 필요도 없고, 아무나 보고 수정하지 못하도록
- 데이터 추상화 data abstraction
- 객체의 명세 specification와 구현 implementation을 분리
- 무엇 what과 어떻게 how를 명확하게 구분
- 장점
- 소프트웨어 개발의 간소화
- 복잡한 작업을 부분 작업들로 분해
- 테스트와 디버깅
- 각 부분 작업을 독자적으로 테스팅, 디버깅
- 버그 검사할 부분이 줄어든다.
- 재사용성
- 자료 구조에 대한 코드와 연산을 추출해서 다른 소프트웨어 시스템에서도 사용
- 테이터 타입의 표현에 대한 수정
- 테이터 타입의 내부 구현에 직접 접근하는 연산들만 수정
- 소프트웨어 개발의 간소화
알고리즘
알고리즘 algorithm
- 특정 작업을 수행하는 명령어들의 유한 집합
- 요건
- 입력 : 외부에서 제공되는 데이터가 0개 이상
- 출력 : 적어도 한 개 이상의 결과 생성
- 명확성 : 각 명령은 명확하고 모호하지 않아야 함
- 유한성 : 알고리즘대로 수행하면 어떤 경우에도 반드시 종료
- 유효성 : 반드시 실행 가능해야 함
선택 정렬
- n ≥ 1 개의 서로 다른 정수의 집합을 정렬
- 정렬되지 않은 정수들 중에서 가장 작은 값을 찾아서 정렬된 리스트 다음 자리에 놓는다
- a[i]에서부터 a[n-1]까지의 정수 값을 검사한 결과 a[j]가 가장 작은 값 a[i]와 a[j]를 서로 교환한다.
void SelectionSort(int *a, const int n) {
// n개의 정수 a[0]부터 a[n-1]까지 비감소 순으로 정렬한다.
for (int i = 0; i < n; i++) {
int j = i;
// a[i]와 a[n-1] 사이에 가장 작은 정수를 찾는다.
for (int k = i + 1; k < n; k++)
if (a[k] < a[j]) j = k;
swap(a[i], a[j])
}
}
이원 탐색
- 이미 정렬된 배열 a[0]...a[n-1]에서 x = a[j]인 j를 반환
- a[middle]과 x를 비교해 범위 재설정
- x < a[middle] → right = middle-1
- x == a[middle] → middle 반환
- x > a[middle] → left = middle+1
int BinarySearch (int*a, const int x, const int n)
{// Search the sorted array a[0],…,a[n-1] for x.
int left=0, right =n-1;
while (left <= right) {// there are more elements
int middle = (left+right)/2;
if (x < a[middle]) right = middle-1;
else if (x > a[middle]) left = middle+1;
else return middle;
}
return -1; // not found
}
Recursive 알고리즘
- 수행이 완료되기 전에 자기 자신을 다시 호출
- 기술 방법
- 주어진 입력에 대해 함수가 마땅히 수행해야 할 작업에 대한 명령문 구성
- 자신에 대한 재귀적 호출이 제대로 이루어질 때 목적을 달성할 수 있는지 검증
- 함수를 유한한 횟수로 순환 호출하면 완료 조건을 만족시키는지 확인
- 완료 조건을 만족시켰을 때 함수가 올바른 계산을 하는지 확인
int BinarySearch(int *a, const int x, const int left, const int right)
{//정렬된 배열 a[left], ..., a[right]에서 x 탐색
if(left <= right){
int middle = (left + right) /2;
if(x < a[middle]) return BinarySearch(a, x, left, middle-1);
else if(x > a[middle]) return BinarySearch(a, x, middle+1, right);
return middle;
}
return -1; //발견되지 않음
}
순열 생성기
- 주어진 집합의 모든 가능한 순열 출력
void Permutations(char *a, const int k, const int m)
{// a[k], ..., a[m]에 대한 모든 순열 생성
if(k == m){ //순열을 출력
for (int i = 0 ; i <= m; i++)
cout << a[i] << " ";
cout << endl;
} else { //a[k:m]에는 하나 이상의 순열이 있다. 이 순열을 재귀적으로 생성
for(i=k;i<=m;i++) {
swap(a[k], a[i]);
Permutations(a, k+1, m);
swap(a[k], a[i]);
}
}
}
표준 템플릿 라이브러리 STL
accumulate STL 알고리즘
- accumulate(start, end, initialValue)
- 순차에 있는 원소들을 합산
- copy(start, end, to)
- start, start+1,..., end-1에서 to, to+1, ..., to+end-start로 복사
- next_permutation(start,end)
- [start,end) 범위에 있는 원소들의 다음으로 큰 사전식 순열 생성
- 그러한 다음 순열이 존재하면 true 반환
- sort(start, end)
- count(start, end, value)
- fill(start, end, value)
sort 실습
- 헤더 파일에서 사용한 함수는? sort()
- 디버그 하지 않고 시작 : Ctrl + F5
- 중단점 설정 및 해제 : F9
- 디버깅 시작 : F5
- 한 단계씩 코드 실행 : F11
- 프로시저(함수) 단위 실행 : F10
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
// 오름차순 정렬
bool myfunction(int i, int j) { return (i < j); }
// struct는 default가 public
struct myclass {
bool operator() (int i, int j) { return (i < j); }
} myobject;
int main() {
int myints[] = { 32,71,12,45,26,80,53,33 };
vector<int> myvector(myints, myints + 8); // 32 71 12 45 26 80 53 33
vector<int>::iterator it; // using default comparison (operator <):
// index 0부터 3까지 오름차순 정렬
sort(myvector.begin(), myvector.begin() + 4); //(12 32 45 71)26 80 53 33
// using function as comp - index 4부터 끝까지 오름차순 정렬
sort(myvector.begin() + 4, myvector.end(), myfunction); // 12 32 45 71(26 33 53 80)
// using object as comp
sort(myvector.begin(), myvector.end(), myobject); //(12 26 32 33 45 53 71 80)
// print out content:
cout << "myvector contains:";
for (it = myvector.begin(); it != myvector.end(); ++it)
cout << " " << *it; cout << endl;
return 0;
}
성능 평가 performance evaluation
공간 복잡도
- 프로그램을 실행시켜 완료하는 데 필요한 메모리 양
- 고정 부분
- 보통 명령어 공간, 단순 변수, 집합체, 상수를 위한 공간
- 가변 부분
- 변수, 참조된 변수가 필요로 하는 공간, 재귀 스택 공간
- 함수를 부르면 스택에 메모리를 할당한다.
- 변수, 참조된 변수가 필요로 하는 공간, 재귀 스택 공간
- 프로그램 P의 공간 요구 S(P) = c + Sp
- c : 상수
- Sp : 인스턴스 특성
- 문제에 따라 달라지는 값
- 인스턴스로 인해 필요로 하는 공간
float Abc(float a, float b, float c) {
return a+b+b*c+(a+b-c)/(a+b)+4.0;
} // a,b,c가 일정 -> Sp = 0
line float Sum(float *a, const int n) {
float s = 0;
for(int i = 0 ; i < n ; i++)
s+= a[i];
return s;
} // Sp(n) = 0
line float Rsum(float *a, const int n) {
if(n<=0) return 0;
else return(Rsum(a, n-1) + a[n-1]);
} // Rsum을 호출하면 n, a, 반환값, 반환 주소 등 적어도 4개의 워드가 필요하다
// 재귀 깊이가 n + 1이므로 재귀 스택 공간은 4(n + 1)이 필요
시간 복잡도
- 프로그램을 완전히 실행시키는데 필요한 시간
- T(P) = 컴파일 시간 + 실행시간 tp
- 컴파일 시간은 바꾸기 어렵고
- 실행 시간은 ADD, SUB, MUL, DIV 각 연산의 실행 횟수와 관련 있다.
프로그램 단계수
- 주석 : 0
- 선언문 0
- 함수문 : 0
- 비용이 이미 호출 문에 할당
- 산술식 및 지정문 : 1
- 반복문 : 제어 부분에 대해서만 단계수 고려
- 스위치 문 각 조건의 비용 = 자기의 비용 + 앞서 나온 모든 조건의 비용
- if-else 문
- , , <statement 2>에 따라 각각 단계수가 할당
- 함수 호출
- 인스턴스 특성 의존 값에 의한 전달 인자 포함하지 않을 경우: 1
- 인스턴스 특성 의존 값에 인한 전달 인자 포함할 경우 : 값 인자 크기의 합
- 재귀적인 경우 : 호출되는 함수의 인스턴스 특성 의존 지역 변수도 고려
- 메모리 관리 명령문 : 1
- new/delete
- 분기문 : 1
- 인스턴스 특성 관련 return 경우 : 단계 수
단계 수 테이블
- s/e
- step per execution
- 그 명령문의 실행 결과로 count가 변화하는 양
line float Sum(float * a, const int n) { // 0 1
float s = 0; // 1 1
for(int i = 0 ; i < n ; i++) // 1 n+1
s+= a[i]; // 1 n
return s; // 1 1
} // 0 1
// 총 단계 수 2n+3
line void Add(int **a, int **b, int**c, int m, int n)
{ // 0 0
for (int i = 0; i < m; i++) // 1 m+1
for (int j = 0; j <n; j++) // 1 m(n+1)
c[i][j] = a[i][j] + b[i][j]; // 1 mn
} // 0 1
// 총 단계수 2mn+2m+1
void Sum(float *a, contst int n)
{ // 0 1
for (int i = 0; i < n; i++) // 1 n+1
count += 2; // 1 n
count += 3; // 1 1
} // 0 1
// 총 단계 수 2n+3
line float Rsum(float * a, const int n) // n=0 n>0
{ // 0 1 0 1
if (n <= 0) return 0; // 1 1 1 0
else return (Rsum(a, n-1) + a[n-1]); // 1+t(n-1) 0 1+t(n-1) 1
} // 0 1 0 1
void Fibonacci(int n)
{//피보나치 수 Fn계산 // 0 1
if(n<=1) cout << n << endl; // 1 0
else { // 1 1
int fn; int fnm2 = 0; int fnm1 = 1; // 2 1
for(int i = 2 ; i <=n ;i++) // 1 n
{ // 1 0
fn = fnm1 + fnm2; // 1 n-1
fnm2 = fnm1; // 1 n-1
fnm1 = fn; // 1 n-1
} // 1 0
cout << fn << endl; // 1 1
} // 1 0
} // 1 0
// 4n-2
점근 표기법
빅오 big ‘oh’
- upperbound, worstcase를 생각해본다.
- 모든 n, n≥n0에 대해 f(n)≤cg(n)인 조건을 만족시키는 두 양의 상수 c와 n0가 존재하면 f(n)=O(g(n))
- O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!)
오메가 omega
- lowerbound 찾기
- 모든 n, n≥n0에 대해 f(n) ≥ cg(n)을 만족시키는 두 양의 상수 c와 n0가 존재하면 f(n) = Ω(g(n))
세타 theta
- 점근적 해석
- 모든 n, n≥n0에 대해 c1g(n)≤ f(n) ≤ c2g(n)을 만족시키는 세 양의 상수 c1, c2와 n0가 존재하면, f(n) = θ(g(n))
'[전공] > [자료구조]' 카테고리의 다른 글
[C++ 자료구조론] 3장 스택과 큐 (1) | 2022.10.30 |
---|---|
[C++ 자료구조론] 2장 배열 (1) | 2022.10.30 |