공간 복잡도의 빅 오
//새로운 배열을 생성해 추가 메모리를 소모하는 공간복잡도 O(N)의 함수
function makeUppercase(array) {
let newArray = [];
for(let i = 0; i < array.length; i++) {
newArray[i] = array[i].toUpperCase();
}
return newArray;
}
//기존 배열을 사용하는 공간복잡도 O(1)의 함수
function makeUppercase(array) {
for(let i = 0; i < array.length; i++) {
array[i] = array[i].toUpperCase();
}
return array;
}
시간과 공간 트레이드오프
//버전 1
function hasDuplicateValue(array) {
for(let i = 0; i < array.length; i++) {
for(let j = 0; j < array.length; j++) {
if(i !== j && array[i] === array[j]) {
return true;
}
}
}
return false;
}
//버전 2
function hasDuplicateValue(array) {
let existingValues = {};
for(let i = 0; i < array.length; i++) {
if(!existingValues[array[i]]) {
existingValues[array[i]] = true;
} else {
return true;
}
}
return false;
}
버전 | 시간 복잡도 | 공간 복잡도 |
---|---|---|
버전1 | O(N2) | O(1) |
버전2 | O(N) | O(N) |
배열을 받아 중복 값이 있는지 확인해 반환하는 함수로 첫번째 버전은 중첩루프를 사용하여 시간복잡도가 O(N2)이지만 추가적인 메모리를 사용하지 않아 공간복잡도는 O(1)이다.
반대로 두번째 버전은 배열을 한번만 순회하여 객체를 통해 중복 여부를 검사하여 시간복잡도 O(N), 공간복잡도 O(N)을 가진다.
=> 어떤 알고리즘을 고를지는 상황에 따라서 애플리케이션의 속도가 중요한지, 혹은 메모리를 절약해서 써야하는 상황인지를 고려해서 선택해야 한다.
function hasDuplicateValue(array) {
array.sort((a, b) => (a < b) ? -1 : 1);
for(let i = 0; i < array.length - 1; i++) {
if (array[i] === array[i + 1]) {
return true;
}
}
return false;
}
세번째 버전은 정렬 이후에 중복 여부를 검사하는 함수로 가장 빠른 정렬 알고리즘 기준으로 시간복잡도는 O(NlogN)이고 퀵 정렬의 경우 공간복잡도가 O(logN)이다. 시간과 공간 모두의 효율성을 고려해야 할때 좋은 선택지가 될 수 있다.
재귀에 숨겨진 비용
//재귀를 사용하여 n부터 0까지 출력하는 함수
function resurse(n) {
if (n < 0) return;
console.log(n);
recurse(n - 1)
}
//루프를 사용하여 n부터 0까지 출력하는 함수
function loop(n) {
while (n >= 0) {
console.log(n);
n--
}
}
RangeError: Maximum call stack size exceeded
라는 메세지가 출력된다.시작점 : 상상할 수 있는 최상의 빅오
authors = [
{"author_id" => 1, "name" => "Virginia Woolf"},
{"author_id" => 2, "name" => "Leo Tolstoy"},
{"author_id" => 3, "name" => "Dr. Seuss"},
{"author_id" => 4, "name" => "J. K. Rowling"},
{"author_id" => 5, "name" => "Mark Twain"},
]
books = [
{"author_id" => 3, "title" => "Hop on Pop"},
{"author_id" => 1, "title" => "Mrs. Dalloway"},
{"author_id" => 4, "title" => "Harry Potter and the Sorcerer's Stone"},
{"author_id" => 2, "title" => "Anna Karenina"},
{"author_id" => 5, "title" => "The Advantures of Tom Sawyer"},
{"author_id" => 3, "title" => "The Cat in the Hat"},
{"author_id" => 2, "title" => "War And Peace"},
{"author_id" => 3, "title" => "Green Eggs and Ham"},
{"author_id" => 5, "title" => "The Adventures of Huckleberry Finn"},
]
def connect_books_with_authors(books, authors)
books_with_authors = []
books.each do |author|
authors.each do |author|
if book["author_id"] == author["author_id"]
books_with_authors <<
{title: book["title"]
author: author["name"]}
end
end
end
return books_with_authors
end
자료 구조 추가하기
author_hash_table =
{1 => "Virginia Woolf", 2 => "Leo Tolstoy", 3 => "Dr. Seuss",
4 => "J. K. Rowling" 5 => "Mark Twain"}
두 수의 합 문제
function twoSum(array) {
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < array.length; j++) {
if (i !== j && array[i] + array[j] === 10) {
return true;
}
}
}
return false;
}
[2, 0, 4, 1, 7, 9]
의 배열을 순회할때 첫번째 숫자인 2부터 루프가 시작되는데 이 배열 어딘가에 8이 있는지를 알면 O(1)만에 룩업할 수 있고 8을 2의 보수라고 부르기로 한다.{2: true, 0: true, 4: true, 1: true, 7: true, 9: true,}
배열을 해시 테이블로 바꾼다면 보수를 O(1)만에 찾을 수 있게 되고 시간복잡도가 O(N)으로 크게 개선된다.
패턴 인식
코드 최적화와 알고리즘 개발에 전반적으로 쓰이는 가장 유용한 전략 중 하나가 문제에서 패턴을 찾는 방법이다.
동전 게임
def game_winner(number_of_coins, current_player="you")
if number_of_coins <= 0
return current_player
end
if current_player == "you"
next_player = "them"
elsif current_player == "them"
next_player = "you"
end
if game_winner(number_of_coins - 1, next_player) == current_player ||
game_winner(number_of_coins - 2, next_player) == current_player
return current_player
else
return next_player
end
end
예제 생성
동전 개수 | 승자 |
---|---|
1 | 상대방 |
2 | 당신 |
3 | 당신 |
4 | 상대방 |
5 | 당신 |
6 | 당신 |
7 | 상대방 |
8 | 당신 |
9 | 당신 |
10 | 상대방 |
동전이 1개 ~ 10개일때의 승자를 나열했을 때 동전 1개에서 시작해 2번에 걸러 1번씩 상대방이 이기는 걸 알 수 있다.
def game_winner(number_of_coins)
if (number_of_coins - 1) % 3 == 0
return "them"
else
return "you"
end
end
합 교환(sum swap) 문제
array_1 = [5, 3, 2, 9, 1] # 합계: 20
array_2 = [1, 12, 5] # 합계: 18
# array_1의 인덱스 2와 array_2의 인덱스 0을 교환하면 합이 19로 같아진다.
두 숫자 배열의 각 합이 같아지도록 서로 교환할 수 있는 두 배열의 인덱스([2, 0]
)를 리턴하는 함수가 있다고 할때 중첩 루프를 통해 O(N × M)의 시간복잡도를 가진 알고리즘을 도출할 수 있다.
두 배열의 모든 숫자를 무조건 최소 한 번씩은 방문해야 하므로 최선의 빅오를 O(N + M)으로 정하고 목표로 삼자.
여러 예시를 살펴보면 아래와 같은 패턴이 도출된다.
합계가 각 18, 12인 배열이 있다면 교환시 각 배열의 합은 15가 되어야 하고 첫번째 배열은 3이 작아지고 두번째 배열은 3이 커져야 한다. 첫번째 배열이 [5, 3, 3, 7]
일때 첫번째 루프에서 5와 두번째 배열의 2를 교환해야 한다.
위 패턴대로 각 배열을 합을 통해 각 숫자의 보수를 바로 알 수 있다.
def sum_swap(array_1, array_2)
hash_table = {}
sum_1 = 0
sum_2 = 0
array_1.each_with_index do |num, index|
sum_1 += num
hash_table[num] = index
end
array_2.each do |num|
sum_2 += num
end
shift_amount = (sum_1 - sum_2) / 2
array_2.each_with_index do |num, index|
if hash_table[num + shift_amount]
return [hash_table[num + shift_amount], index]
end
end
return nill
end
array_1
의 숫자 N개를 전부 해시 테이블로 복사하므로O(N) 공간을 추가로 소모하지만 시간복잡도가 O(N + M)으로 개선되었다.(array_2를 두 번 순회하므로 2M이지만 상수는 무시한다.)탐욕 알고리즘
탐욕(greedy) 알고리즘은 매 단계마다 그 순간에 최선처럼 보이는 방법을 고르는 알고리즘을 뜻한다.
배열 최댓값
function max(array) {
let greatestNumberSoFar = array[0];
for(let i = 0; i < array.length; i++) {
if(array[i] > greatestNumberSoFar) {
greatestNumberSoFar = array[i];
}
}
return greatestNumberSoFar;
}
최대 부분 합
[3, -4, 4, -3, 5, -9]
//부트캠프에서 구현했던 탐욕 알고리즘을 적용한 최대 부분 합 문제
const LSCS = function (arr) {
let subArrSum = 0;
let max = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < arr.length; i++) {
subArrSum = subArrSum + arr[i];
if (subArrSum > max) max = subArrSum;
if (subArrSum < 0) {
subArrSum = 0;
}
}
return max;
};
탐욕스러운 주가 예측
[22, 25, 21, 18, 19.6, 17, 16, 20.5]
def increasing_triplet?(array)
lowest_price = array[0]
middle_price - Float::INFINITY
array.each do |price|
if price <= lowest_price
lowest_price = price
elsif price <= middle_price
middle_price = price
else
return true
end
end
자료 구조 변경
애너그램 검사기
두 문자열이 주어졌을 때 서로 애너그램인지 알아내는 함수에서 첫 번째 문자열의 모든 애너그램을 생성한 후 두 번째 문자열이 그중 하나와 같은지 확인할 경우 시간복잡도 O(N!)이 걸린다.
두 문자열의 각 문자를 최소 한번 씩은 방문해야 하므로 최상의 빅오를 O(N + M)으로 가정하고 효율적인 알고리즘을 찾아보자.
중첩루프를 사용하여 첫 번째 문자열을 순회하면서 두 번째 문자열에서 그 문자를 삭제하고 루프가 종료되었을때 두 번째 문자열의 문자가 모두 삭제되어 있는지 확인하는 알고리즘은 O(N × M)의 단계를 가지며, 배열을 순회하면서 그 배열의 항목을 삭제하는 오류가 발생할 수 있다.
두 문자열을 정렬하며 두 문자열이 완전히 같은지를 검사하는 알고리즘은 O(NlogN + MlogM)이다.
각 문자를 키로 가지고 개수를 값으로 가지는 해시 테이블을 생성하여 같은지 여부를 리턴하는 함수의 경우 목표로 삼았던 시간복잡도 O(N +M)을 가진다. (자바스크립트 같은 언어에서는 순회하면서 비교를 해야하므로 2(N +M)의 단계를 가지지만 상수를 제외하면 여전히 O(N +M)이다.)
def areAnagrams(firstString, secondSrting):
firstWordHashTable = {}
secondWordHashTable = {}
for char in firstString:
if firstWordHashTable.get(char):
firstWordHashTable[char] += 1
else:
firstWordHashTable[char] = 1
for char in secondSrting:
if firstWordHashTable.get(char):
secondWordHashTable[char] += 1
else:
secondWordHashTable[char] = 1
return firstWordHashTable = secondWordHashTable
그룹 정렬
["a", "c", "d", "b", "b", "c", "a", "d", "c", "b", "a", "d"]
값을 여러 개 포함하는 배열이 있을 때 같은 값끼리 묶어 아래 처럼 데이터를 다시 정렬한다고 하자. (단, 그룹의 순서는 중요하지 않다.)
["c", "c", "c", "a", "a", "a", "d", "d", "d", "b", "b", "b"]
def group_array(array)
hash_table = {}
new_array=[]
array.each do |value|
if hash_hash[value]
hash_table[value] += 1
else
hash_table[value] = 1
end
end
hash_table.each do |key, count|
count.times do
new_array << key
end
end
return new_array
end
이번 포스팅을 끝으로 이 책의 챕터를 모두 마치게 되었다. 책의 제목인 누구나 자료구조와 알고리즘
처럼 깊이 있는 내용이라기 보다는 어떤 자료구조와 알고리즘이 있는지 어떤 장점을 가지고 있고 효율성은 어떻게 분석하는지 등 기초적인 내용을 다루고 있어서 ruby, python 등 접하지 못한 언어로 작성된 코드들이 있음에도 어렵지 않게 이해할 수 있었다.
자료구조와 알고리즘에 대해 처음 접하는 사람들에게는 입문 도서로 좋다고 생각하고 나처럼 비전공 개발자이면서 부트캠프처럼 빠른 기간에 CS 지식을 습득한 사람이라면 복습과 기초를 다지기 위해서 읽어보는 것도 도움이 된다고 생각한다.
책에서 가장 도움이 되었던 부분은 알고리즘을 개선하기 위한 여러가지 예시들과 공간복잡도라는 새로운 개념들이 실무에 자료구조와 알고리즘을 어떻게 적용할 수 있는지 이해하는데 도움이 되었다.