- 문제의 입력를 두 개 이상의 작은 입력으로 나누고, 답을 얻기 위해 시도한다. - 답을 얻지 못했다면, 또 각각의 입력 사례에 대해 분할을 한 뒤, 다시 시도한다. - Divide and Conquer는 하향식(Top-Down) 방식의 접근법이다.
2.1 이분검색 (Binary Search)
- 이분 검색 알고리즘은 오름차순으로 정렬된 배열에서 실행가능하며, 수행 절차는 아래와 같다.
1. [Divide] 배열을 정 가운데 원소를 기준으로 반으로 분할한다. 찾고자 하는 수 x가 가운데 원소보다 작으면 왼쪽 배열을 선택한다. x가 가운데 원소보다 크면 오른쪽 배열을 선택한다. 2. [Conquer] 선택한 반쪽 배열을 정복한다. 즉, 선택한 반쪽 배열에 x가 있는지 재귀적으로 이분검색한다. 3. [Obtain] 선택한 반쪽 배열에서 얻은 답이 최종 답이다.
1. [Divide] 배열을 반으로 분할한다. (분할한 배열의 원소는 각각 n/2개 이다.) 2. [Conquer] 분할한 배열을 각각 따로 정렬한다. 3. [Obtain] 정렬한 두 배열을 합병하여 정렬한다.
void mergesort(int n, keytype S[])
{
if(n > 1) {
const int h = [n/2], m = n-h;
keytype U[1..h], V[1..m];
copy S[1] through S[h] to U[1] through U[h];
copy S[h+1] through S[n] to V[1] through V[m];
mergesort(h, U);
mergesort(m, V);
merge(h, m, U, V, S);
}
}
void merge(int h, int m, const keytype U[], const keytype V[], keytype S[])
{
index i, j, k;
i = 1; j = 1; k = 1;
while( i <= h && j <= m) {
if(U[i] < V[j]) {
S[k] = U[i];
i++;
}
else {
S[k] = V[j];
j++;
}
k++;
}
if(i > h)
copy V[j] through V[m] to S[k] through S[h+m];
else
copy U[i] through U[h] to S[k] through S[h+m];
}
The steps when searching with Merge Sort
- Merge의 Worst-case Time Complexity W(h,m)은 아래와 같다. (두 배열 U,V의 원소 개수는 h, m이다.)
$$ W\left(h,m\right)\ =\ h\ +\ m\ -\ 1 $$
- Mergesort의 Worst-case Time Complexity W(n)은 아래와 같다.
이므로, 실용적으로 사용하기 힘들다. 이에 Strassen은 곱셈을 기준으로 하든, 덧셈/뺄셈을 기준으로 하든지에 상관없이 시간복잡도가 3차보다 좋은 알고리즘을 발표하였다.
The partitioning into submatrics in Strassen's Algorithm
void strassen(int n, nxn_matrix A, nxn_matrix B, nxn_matrix& C)
{
if(n <= threshold)
compute C = A X B using the standard Algorithm.
else {
partition A into four matrices A11, A12, A21, A22;
partition B into four matrices B11, B12, B21, B22;
compute C = A X B using Strassen's method;
// example recursive call;
// strassen(n/2, A11 + A22, B11 + B22, M1);
}
}
- 교환 정렬(Exchange Sort) : 반복문을 통해 배열의 start index부터 end index까지 한번씩 검색하는 알고리즘.
void exchange(int n, keytype S[])
{
index i, j;
for (i = 1; i <= n-1; i++)
for(j = i+1; j <= n; j++)
if(S[j] < S[i])
exchange S[i] and S[j];
}
1.2 효율적인 알고리즘 개발의 중요성
* 순차검색 대 이분검색
- 순차 검색(Sequential Search)보다 이분 검색(Binary Search)를 통해 효율적으로 Search 할 수 있다.
- 이분 검색(Binary Search) : 배열이 정렬되어 있다는 가정이 필요. 1. 찾고 싶은 수를 x라고 할 때, 배열의 정중앙에 위치한 원소 y와 비교한다. 2. 만약 x를 찾았다면, 검색을 종료한다. 3. 만약 x를 찾지 못했을 때 x가 y보다 크면 배열의 앞부분을, x가 y보다 작다면 배열의 뒷부분을 이분 검색한다.
- 따라서, 재귀함수를 사용하여 피보나치 함수를 구현하면 구하는 항의 갯수 T(n)은 아래 관계를 만족한다.
$$ T(n) > 2^{\frac{2}{n}} $$
▶ 배열과 반복문을 사용하는 방법 (Dynamic Programming 기법)
- 이에 재귀함수를 사용하는 대신, 배열과 반복문을 사용하여 좀 더 효율적이게 피보나치 수를 구하는 알고리즘을 작성할 수 있다.
int fib2 (int n)
{
index i;
int f[0..n];
f[0] = 0;
if ( n > 0) {
f[1] = 1;
for ( i = 2; i <= n; i++)
f[i] = f[i-1] + f[i-2];
}
return f[n];
}
- 이 방법은 n번째 피보나치 항을 구하기 위해서 (n+1) 개의 항을 계산한다.
1.3 알고리즘의 분석 (Analysis of Algorithms)
* 시간복잡도 분석(Complexity Analysis)
- 일반적으로 알고리즘의 실행시간은 입력의 크기가 커지면 증가하고, 총 실행시간은단위 연산이 몇 번 수행되는가에 비례한다. - 알고리즘의 시간복잡도 분석(Time Complexity Analysis)는 입력크기를 기준으로 단위 연산을 몇번 수행하는지 구하는 것이다. - 교환정렬(Exchange Sort)의 시간복잡도는,
- 크기가 n이어도, 단위 연산을 수행하는 횟수가 달라질 수 있다. (ex) 순차검색 - 이에 시간복잡도를 측정할 때, 알고리즘이 실행할 단위연산의 최대 횟수를 사용하여 최악 시간복잡도(Worst-case Time Complexity)를 구하여 사용할 수 있다. - 순차 검색(Sequential Search)의 Worst-case time Complexity W(n) = n이다.
- 알고리즘이 평균적으로 얼마나 실행하는지를 알면, 더 효율적인 경우도 존재한다. - 이 경우에는, 평균 시간복잡도(Average-case Time Complexity)를 구하여 알고리즘을 분석하는 경우가 더 합리적일 수 있다. - 평균 시간복잡도를 구하기 위해서는 n개의 입력에 확률을 각각 부여해야 한다. - 순차검색(Sequential Search)에서 찾고자 하는 수 x가 배열 S에 있는 경우는, (x가 k번째 위치에 있을 확률 : 1/n)
- 알고리즘이 실행할 단위연산의 최소 횟수를 통해 최선 시간복잡도(Best case Time Complexity)를 구할 수 있다 - 순차 검색(Sequential Search)의 Best-Case Time Complexity B(n) = 1이다.
1.4 차수 (Order)
- 시간복잡도(Time Complexity)가 n, 100n과 같은 경우 linear-time Algorithm 이라고 한다. - 시간복잡도(Time Complexity)가 (n^2), (0.01n^2)과 같은 경우 quadratic-time Algorithm이라고 한다.
- 중간에 데이터 삽입 및 삭제가 자주 일어날 때 사용한다. - <list> header를 include 해준 뒤에, std::list를 사용할 수 있다. - 데이터에 Random Access 하는 경우가 적을 때, 사용하는 것이 좋다. (리스트는 순차적 접근만 가능하므로) - 저장할 데이터 개수가 많거나, 검색을 자주하는 경우에는 사용하지 않는 것이 좋다. - 가변적인 길이를 가질 수 있으며, 중간에 비어있는 데이터가 존재하지 않는다.
- 많은 자료들 사이에서 검색이 용이해야 할 때 사용한다. - <map> header를 include 해준 뒤에, std::map를 사용할 수 있다. - Key와 Value는 pair 형태이다. - C++의 std::map의 경우, key를 기준으로 오름차순 정렬을 자동으로 수행한다. - 많은 데이터를 저장해야 하고, 검색이 빨라야 할 때 사용하면 좋다. - 인덱스를 사용하지 않고, iterator를 사용한다. - 중복 Key value는 사용이 불가능하다. (= 각 Key value는 unique 해야 한다.)
- map 코드 예시
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main(){
// <key, value>의 pair 형태로 map을 선언함.
map<string, int> m;
// map에 데이터 삽입 (key는 중복불가, value는 중복가능)
m.insert({"ABC", 123});
m.insert({"BCD", 456});
m.insert({"CDE", 123});
// m.insert({"ABC", 789}); 는 key 중복으로 데이터 삽입이 되지 않음.
// 'map iterator'를 이용하여 map에 접근할 수 있음.
map<string, int>::iterator m_iter;
for(m_iter = m.begin(); m_iter != m.end(); m_iter++)
cout << "key: " << m_iter->first << ", value: " << m_iter->second << endl;
// map에서 데이터 검색 (iterator를 반환하고, 못찾을 시 m.end()를 반환)
map<string, int>::iterator find_iter;
find_iter = m.find("ABC");
if(find_iter != m.end())
cout << "found key: " << find_iter->first << ", found value: " << find_iter->second << endl;
else
cout << "can't find data in map!" << endl;
// map에서 데이터 변경
m["ABC"] = 789;
m["BCD"] = 101112;
return 0;
}
std::unordered_map (hash_map) (Key 중복 X, Value 중복 O)
- 많은 자료들 사이에서 검색이 용이해야 할 때 사용한다. - <unordered_map> header를 include 해준 뒤에, std::unordered_map를 사용할 수 있다. - Key와 Value는 pair 형태이다. - c++ std::map과 다르게, hash_map은 자동으로 정렬을 수행하지 않는다. (넣은 순서대로 데이터를 유지) - 많은 데이터를 저장해야 하고, 검색이 빨라야 할 때 사용하면 좋다. - 인덱스를 사용하지 않고, iterator를 사용한다. - 중복 Key value는 사용이 불가능하다. (= 각 Key value는 unique 해야 한다.)
- unordered_map (hash_map) 코드 예시
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main(){
// <key, value>의 pair 형태로 unordered_map을 선언함.
unordered_map<string, int> um;
// unordered_map에 데이터 삽입 (key는 중복불가, value는 중복가능)
um.insert({"ABC", 123});
um.insert({"BCD", 456});
um.insert({"CDE", 789});
// um.insert({"ABC", 789}); 는 key 중복으로 데이터 삽입이 되지 않음.
// 'unordered_map iterator'를 이용하여 unordered_map에 접근할 수 있음.
unordered_map<string, int>::iterator um_iter;
for (um_iter = um.begin(); um_iter != um.end(); um_iter++)
cout << "key: " << um_iter->first << ", value: " << um_iter->second << endl;
// unordered_map에서 데이터 검색 (iterator를 반환하고, 못찾을 시 m.end()를 반환)
unordered_map<string, int>::iterator find_iter;
find_iter = um.find("ABC");
if (find_iter != um.end())
cout << "found key: " << find_iter->first << ", found value: " << find_iter->second << endl;
else
cout << "can't find data in unordered_map!" << endl;
// unordered_map에서 데이터 삭제
um.erase(find_iter); // <"ABC", 123> pair 삭제
// unordered_map에서 데이터 변경
um["BCD"] = 888;
um["CDE"] = 999;
return 0;
}
std::set (데이터 중복 X, 순서보장 X)
- 많은 자료들 사이에서 검색이 용이해야 할 때 사용한다. - 인덱스를 사용하지 않고, iterator를 사용한다. - 중복 Key value는 사용이 불가능하다. (= 각 Key value는 unique 해야 한다.) - 이진 균형트리로 자료구조가 구성되어 있다.
- set 코드 예시
#include <iostream>
#include <string>
#include <set>
using namespace std;
int main(){
// int type의 데이터를 담는 set 선언
set<int> s;
// set에 데이터 삽입 (key는 중복불가, value는 중복가능)
s.insert(1); // s = {1}
s.insert(2); // s = {1, 2}
s.insert(3); // s = {1, 2, 3}
s.insert(4); // s = {1, 2, 3, 4}
//s.insert(1); 는 key 중복으로 데이터 삽입이 되지 않음.
// 'set iterator'를 이용하여 set에 접근할 수 있음.
set<int>::iterator s_iter;
for (s_iter = s.begin(); s_iter != s.end(); s_iter++)
cout << "value: " << *s_iter << endl;
// set에서 데이터 검색 (iterator를 반환하고, 못찾을 시 s.end()를 반환)
set<int>::iterator find_iter;
find_iter = s.find(1);
if (find_iter != s.end())
cout << "found value: " << *find_iter << endl;
else
cout << "can't find data in set!" << endl;
// set에서 데이터 삭제
s.erase(find_iter); // '1' value 삭제
return 0;
}
- c++에서는 문자열을 다루기 위해 std::string class를 사용할 수 있다. - <string> header를 include 해준 뒤에, string class를 사용할 수 있다. - 코딩테스트에서 문자열을 처리하는 문제가 빈번하게 나오니, 미리 사용법을 알아두면 좋을 것 같다.
std::string 사용법
- C++ code에서 string를 사용하기 위해서는 <string> header를 include해주어야 한다.
1. string에서의 위치 지칭 및 string의 크기 구하기 - N 크기의 길이를 가지는 string의 indexing은 0부터 N-1 까지 가능하다. - string class는 c언어의 char*(char배열)과는 달리 '\0'가 마지막에 삽입되지 않아도 된다. (예를 들어, 5크기의 길이를 가지는 string은 0~4까지의 index에 접근할 수 있다.)
2. string에서의 값 추가 및 삭제, 복사, 검색 - find()는 앞에서부터, rfind()는 뒤에서부터 검색 문자(열)을 찾아, 해당 index를 반환해준다. (못 찾았을 경우에, 'std::npos' 라는 쓰레기값을 반환해준다.)
- vector에서와 같이, string에서도 push_back()과 pop_back()이 가능하다. - string은 원하는 곳에 문자(열)을 삽입하거나 삭제할 수 있지만, 시간복잡도가 O(n)이 소요된다. - string에서 맨 뒤에 문자(열)을 삽입하는 것은 append()나 '+' operation을 사용하면 된다. - clear() 함수를 사용하면 해당 string 변수를 size가 0인 string으로 초기화 할 수 있다.
string s = "ABCDE";
int B_idx = s.find('B'); // B_index: 1
int D_idx = s.rfind('DE'); // D_index: 3
s.insert(0, "123"); // s: "123ABCDE"
s.append("456"); // s: "123ABCDE456"
s = s + "FGH"; // s: "123ABCDE456FGH"
s.erase(0, 3); // s: "ABCDE456FGH" (0~2[=3-1] index 값을 삭제)
s.erase(s.find('4')); // s: "ABCDE" (index 0부터 검색하여 최초의 '4' 위치 이후의 값을 다 삭제)
s.clear(); // Initialize string variable 's'
s.push_back('F'); // s: "F"
s.push_back('G'); // s: "FG"
s.pop_back(); // s: "F"
3. string class에서의 유용한 기능들 (비교, substring, char*로의 변환) - 문자열을 비교하는 compare()는 같을 때 0, 다를 경우 -1 혹은 1을 반환한다. ([숫자 < 영어대문자 < 영어소문자] 순으로 비교를 진행하여 자신이 더 크면 1, 작으면 -1을 반환한다.)
- 'substr()'의 경우, 한개의 parameter만 입력 시 문자열의 끝부분까지 잘라서 반환한다. (만약 2개의 parameter를 입력한다면, 그 길이만큼만 잘라서 반환한다.)
- 'c_str()'을 이용하여 c++ class인 string에서 c type의 char* 로의 변환이 가능하다.
4. string을 특정 delimiter을 통해 parsing(tokenizing)하기 (1) istringstream class (delimiter가 1개일 때) - string을 parsing(tokenizing)하는 방법 중 하나로 istringstream class을 사용할 수 있다. - stringstream class는 <sstream>을 include 한 뒤에 사용할 수 있다.
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
string line = "first second third fourth";
stringstream sstream(line);
string token;
while (getline(sstream, token, ' '))
cout << token << endl;
/* output:
first
second
third
fourth
*/
return 0;
}
(2) find와 substr을 이용하는 방법 (delimiter가 2개 이상일 때) - string을 parsing(tokenizing)하는 방법 중 하나로 istringstream class을 사용할 수 있다. - stringstream class는 <sstream>을 include 한 뒤에 사용할 수 있다.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main()
{
string s = "apple,pear,peach,melon";
vector<string> v;
int prev_idx = 0;
int cur_idx = s.find(",");
while (cur_idx != string::npos) {
string sub_s = s.substr(prev_idx, cur_idx - prev_idx);
v.push_back(sub_s);
prev_idx = cur_idx + 1;
cur_idx = s.find(',', prev_idx);
}
v.push_back(s.substr(prev_idx, cur_idx - prev_idx));
for (int i=0; i<v.size(); i++)
cout << v[i] << endl;
/* output:
apple
pear
peach
melon
*/
return 0;
}
- 코딩테스트를 준비하다보면, 필수적으로 정렬/순열 등을 사용해야 할 때가 있다. - 이 때, <algorithm> header에 있는 함수 한 두개를 사용함으로써 위 요소들을 구현할 수 있다. - C++ code에서 Algorithm header를 사용하기 위해서는 <algorithm> header를 include 해주면 된다. - 코딩테스트 내에서는, 주로 vector container와 함께 사용되는 경우가 많다.
- 기본(default)으로는 수들을 작은 것부터 큰 것으로 정렬하는 오름차순 정렬을 수행한다. - 내림차순 정렬을 수행하기 위해서는 <functional>의 greater<int>()를 사용해야 한다. - vector를 정렬하고 싶을 땐, sort 함수의 인자로 container의 시작과 끝 iterator 값을 넣어준다. - Return value는 none이다.
- bool binary_search(ForwardIterator first, ForwardIterator last, const T& val); - 위 함수들의 인자로 (1) container의 시작과 끝 iterator 값이나 (2) array의 주소값들을 넣어준다. - 컨테이너(배열에) 찾고자 하는 값 val이 있다면 true를, 없다면 false를 반환한다.
#include <stdio.h>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
vector<int> v = {10, 11, 12, 13, 14, 15, 16};
int arr[7] = {20, 21, 22, 23, 24, 25, 26};
int find_v = 14;
int find_a = 21;
if(binary_search(v.begin(), v.end(), find_v))
printf("the value %d is found in vector v!\n", find_v);
else
printf("the value %d is not found in vector v!\n", find_v);
if (binary_search(arr, arr+7, find_a))
printf("the value %d is found in array arr!\n", find_a);
else
printf("the value %d is not found in array arr!\n", find_a);
return 0;
}