연결 리스트
class Node
attr_accessor :data, :next_node
def initialize(data)
@data = data
end
end
class LinkedList
attr_accessor :first_node
def initialize(first_node)
@first_node = first_node
end
end
list = LinkedList.new(node_1)
읽기
def read(index)
current_node = first_node
current_index = 0
while current_index < index do
current_node = current_node.next_node
current_index += 1
return nil unless current_node
end
return current_node.data
end
검색
def index_of(value)
current_node = first_node
current_index = 0
begin
if current_node.data == value
return current_index
end
current_node = current_node.next_node
current_index += 1
end while current_node
return nil
end
삽입
def insert_at_index(index, value)
new_node = Node.new(value)
if index == 0
new_node.next_node = first_node
self.first_node = new_node
return
end
current_node = first_node
current_index = 0
while current_index < (index - 1) do
current_node = current_node.next_node
current_index += 1
end
new_node.next_node = current_node.next_node
current_node.next_node = new_node
end
삭제
def delete_at_index(index)
if index == 0
self.first_node = first_node.next_node
return
end
current_node = first_node
current_index = 0
while current_index < (index - 1) do
current_node = current_node.next_node
current_index += 1
end
node_after_deleted_node = current_node.next_node.next_node
current_node.next_node = node_after_deleted_node
end
삽입과 마찬가지고 맨 앞의 노드를 삭제할 경우에 O(1)이 걸리지만 맨 마지막 인덱스의 노드를 삭제할 경우 N + 1 => O(N)의 단계가 걸린다.
연산 | 배열 | 연결 리스트 |
---|---|---|
읽기 | O(1) | O(N) |
검색 | O(N) | O(N) |
삽입 | O(N)(끝에서 하면 O(1)) | O(N)(앞에서 하면 O(1)) |
삭제 | O(N)(끝에서 하면 O(1)) | O(N)(앞에서 하면 O(1)) |
전체적으로 보면 시간 복잡도 면에서 연결 리스트는 그다지 매력적이지 않지만 조회를 제외한 실제 삽입, 삭제 단계는 O(1)이기 때문에 이미 올바른 노드에 접근해 있는 시나리오에서 효과적으로 사용할 수 있다.
1,000개의 이메일 리스트를 검토해서 유효하지 않은 형식의 이메일을 삭제한다고 할때 배열의 경우 리스트를 조회하는데 1,000단계, 만약 유효하지 않은 주소가 100개일때 삭제할 때 천개를 시프트해야 할 수 있으니 삭제에 추가로 약 100,000단계가 걸린다.
연결 리스트는 1,000단계의 읽기 단계와 100개의 삭제 단계 딱 1,100단계만 걸린다.
이중 연결 리스트
class Node
attr_accessor :data, :next_node, :previous_node
def initialize(data)
@data = data
end
end
class DoublyLinkedList
attr_accessor :first_node, :last_node
def initialize(first_node=nil, last_node=nil)
@first_node = first_node
@last_node = last_node
end
end
이중 연결 리스트(doubly linked list)는 노드 하나에 앞 노드의 링크와 다음 노드의 링크를 가지고 있는 연결 리스트로 첫 번째 노드외에 마지막 노드도 항상 기록되는 자료구조이다.
삽입
//이중 연결 리스트의 끝에 노드를 삽입하는 합수
def insert_at_end(value)
new_node = Node.new(value)
if !first_node
@first_node = new_node
@last_node = new_node
else
new_node.previous_node = @last_node
@last_node.next_node = new_node
@last_node = new_node
end
end
연결 리스트는 앞으로만 이동할 수 있지만 이중 연결 리스트는 노드에 이전 노드와 다음 노드의 링크가 저장되어 있기 때문에 앞과 뒤 모두 이동이 가능하다.
이중 연결 리스트 기반 큐
class Queue
attr_accessor :queue
def initialize
@data = DoublyLinkedList.new
end
def enqueue(element)
@data.insert_at_end(element)
end
def dequeue
removed_node = @data.remove_from_front
return removed_node.data
end
def read
return nil unless @data.first_node
return @data.first_node.data
end
end
트리
이진 탐색 트리(binary search tree)
검색
삽입
삽입 순서
//부트캠프에서 구현한 이진 탐색 트리 구현 class
class BinarySearchTree {
//BST의 constructor를 구현합니다.
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
// tree에 value를 추가합니다.
insert(value) {
// 인자의 value가 this.value보다 작을 경우, 왼쪽 노드에서 진행합니다.
if (value < this.value) {
// this.left에 아무것도 없을 경우, 새로운 자식 노드를 추가합니다.
if (this.left === null) {
this.left = new BinarySearchTree(value);
}
// this.left의 자식 노드가 있을 경우, 자식 노드에서 insert 재귀를 사용합니다.
else {
this.left.insert(value);
}
}
// 인자의 value가 this.value보다 클 경우, 오른쪽 노드에서 진행합니다.
else if (value > this.value) {
// this.right에 아무것도 없을 경우, 새로운 자식 노드를 추가합니다.
if (this.right === null) {
this.right = new BinarySearchTree(value);
}
// this.left의 자식 노드가 있을 경우, 자식 노드에서 insert 재귀를 사용합니다.
else {
this.right.insert(value);
}
} else {
// 이미 value값을 포함하고 있습니다.
}
}
// tree의 value값을 탐색합니다.
contains(value) {
// 찾는 value값이 노드의 value와 일치한다면, true를 리턴합니다.
if (value === this.value) {
return true;
}
// 찾는 value값이 노드의 value 보다 작다면, 왼쪽에서 contains의 재귀를 진행합니다.
if (value < this.value) {
if (this.left !== null && this.left.contains(value)) return true;
else return
}
// 찾는 value값이 노드의 value 보다 크다면, 오른쪽에서 contains의 재귀를 진행합니다.
if (value > this.value) {
if (this.right !== null && this.right.contains(value)) return true;
else return false
}
}
//tree를 전위 순회 합니다.
preorder(callback) {
callback(this.value);
if (this.left !== null) {
this.left.preorder(callback);
}
if (this.right !== null) {
this.right.preorder(callback);
}
}
// tree를 중위 순회 합니다
inorder(callback) {
if (this.left !== null) {
this.left.inorder(callback);
}
callback(this.value);
if (this.right !== null) {
this.right.inorder(callback);
}
}
//tree를 후위 순회 합니다
postorder(callback) {
if (this.left !== null) {
this.left.postorder(callback);
}
if (this.right !== null) {
this.right.postorder(callback);
}
callback(this.value);
}
}
삭제
def delete(valueToDelete, node):
if node is None:
return None
elif valueToDelete < node.value:
node.leftChild = delete(valueToDelete, node.leftChild)
return node
elif valueToDelete > node.value:
node.rightChild = delete(valueToDelete, node.rightChild)
return node
elif valueToDelete == node.value:
if node.leftChild is None:
return node.rightChild
elif node.rightChild is None:
return node.leftChild
else:
node.rightChild = lift(node.rightChild, node)
return node
def lift(node, nodeToDelete):
if node.leftChild:
node.leftChild = lift(node.leftChild, nodeToDelete)
return node
else:
nodeToDelete.value = node.value
return node.rightChild
이진 탐색 트리 삭제의 효율성
이진 탐색 트리 순회
우선순위 큐
힙
힙 속성
힙 삽입
힙에 새 값을 삽입하려면 다음 알고리즘을 수행한다.
//1차원 배열로 구현한 최대 힙 구현 함수
//루트 노드가 0번째 인덱스 값, 다음 자식 노드가 1,2, 그다음 자식이 3,4,5,6...
function swap(idx1, idx2, arr) {
[arr[idx1], arr[idx2]] = [arr[idx2], arr[idx1]];
}
function getParentIdx(idx) {
return Math.floor((idx - 1) / 2);
}
function insert(heap, item) {
heap.push(item);
let childId = heap.length - 1
let parentId = getParentIdx(childId)
while(parentId >= 0 && heap[childId] > heap[parentId]) {
swap(childId, parentId, heap);
childId = parentId;
parentId = getParentIdx(childId)
}
return heap
}
const binaryHeap = function (arr) {
return arr.reduce((heap, item) => {
return insert(heap, item);
}, []);
};
새 노드를 위로 올리는 과정을 트리클링(trickling)이라고 표현하며 약 log(N)개의 줄을 갖는 트리 꼭대기까지 트리클링해야 하므로 O(logN)의 시간 복잡도를 가진다.
힙 삭제
힙 대 정렬된 배열
정렬된 배열 | 힙 | |
---|---|---|
삽입 | O(N) | O(logN) |
삭제 | O(1) | O(logN) |
정렬된 배열은 삽입에 있어 힙보다 느리지만 삭제는 힙보다 빠르다.
정렬된 배열 | 힙 | |
---|---|---|
삽입 | 느림 | 매우 빠름 |
삭제 | 엄청나게 빠름 | 매우 빠름 |
그러나 O(N)의 시간복잡도가 상대적으로 느리기 때문에 삽입, 삭제가 모두 빠른 힙을 사용하는 것이 효율적이다.
마지막 노드 문제
이번 파트를 통해 연결 리스트와 이진 탐색트리, 힙의 구조와 연산의 효율성에 대해서 알아보고 어떤 장단점을 가지고 있는지를 알 수 있었다. 연결 리스트는 처음 배우는 자료구조였고 다른 자료 구조는 함수로 구현한 적이 있지만 알고리즘의 효율성에 대해서는 책을 통해서 처음 알게 되었다. 실제 현업에서 활용할때 삽입이나 삭제 중 어떤 연산이 자주 일어나는지 또는 정렬이 중요한 기능인지 등을 생각해서 적절한 자료구조를 선택해야 한다고 느꼈다.