diff --git a/README.md b/README.md index 90729af..9642935 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,84 @@ ## 미션 목표 -- 자바스크립트로 정렬 알고리즘 구현하기 +- 자바스크립트로 자료 구조 구현하기 +- 자바스크립트로 힙 정렬 구현하기 ## 요구사항 -다음 정렬 알고리즘을 각각 JavaScript 함수로 구현해 주세요. +다음 자료 구조를 구현해 `algorithm` 폴더에 저장해 주세요. -### 선택 정렬 (Selection sort) -- [x] 숫자형 배열을 파라미터로 받고, 해당 배열을 수정하도록 구현합니다. +### 링크드 리스트 (Linked List) +- 파일 이름: `LinkedList.js` +- 클래스 이름: `LinkedList` +- 메서드: + - [x] `addNode(value)`: 리스트의 끝에 새 노드를 추가 + - [x] `findNode(value)`: 주어진 값을 가지는 노드를 찾아 리턴 + - [x] `insertAfter(targetValue, newValue)`: 특정 값을 가진 노드 뒤에 새 노드 추가 + - [x] `removeAfter(targetValue)`: 특정 값을 가진 노드 뒤의 노드를 삭제 -### 삽입 정렬 (Insertion sort) -- [x] 숫자형 배열을 파라미터로 받고, 해당 배열을 수정하도록 구현합니다. +### 이중 링크드 리스트 (Doubly Linked List) +- 파일 이름: `DoublyLinkedList.js` +- 클래스 이름: `DoublyLinkedList` +- 메서드: + - [x] `addToHead(value)`: 리스트의 앞쪽에 노드 추가 + - [x] `addToTail(value)`: 리스트의 뒤쪽에 노드 추가 + - [x] `insertAfter(targetValue, newValue)`: 특정 값을 가진 노드 뒤에 새 노드 추가 + - [x] `findNode(value)`: 값을 가진 노드를 찾아 반환합니다. + - [x] `removeNode(value)`: 특정 값을 가진 노드 삭제 -### 병합 정렬 (Merge sort) -- [x] 숫자형 배열을 파라미터로 받고, 정렬된 새로운 배열을 리턴하도록 구현합니다. +### 큐 (Queue) +- 파일 이름: `Queue.js` +- 클래스 이름: `Queue` +- 메서드: + - [x] `enqueue(value)`: 큐의 맨 뒤에 값을 추가 + - [x] `dequeue()`: 큐의 앞에서 값을 제거하고 그 값을 리턴 + - [x] `peek()`: 큐의 앞에 있는 값을 제거하지 않고 리턴 + - [x] `isEmpty()`: 큐가 비어 있는지 불린형으로 리턴 -### 퀵 정렬 (Quick sort) -- [x] 숫자형 배열을 파라미터로 받고, 해당 배열을 수정하도록 구현합니다. +### 스택 (Stack) +- 파일 이름: `Stack.js` +- 클래스 이름: `Stack` +- 메서드: + - [x] `push(value)`: 스택의 맨 위에 값을 추가 + - [x] `pop()`: 스택의 맨 위 값을 제거하고 그 값을 리턴 + - [x] `peek()`: 스택의 맨 위 값을 제거하지 않고 그 값을 리턴 + - [x] `isEmpty()`: 스택이 비어 있는지 불린형으로 리턴 -### 함수 예시: -해당 배열을 직접 수정하는 예시 -```js -const nums = [3, 1, 2]; -console.log(nums); // [3, 1, 2]; -selectionSort(nums); -console.log(nums); // [1, 2, 3] -``` +### 이진 탐색 트리 (Binary Search Tree) +- 파일 이름: `BinarySearchTree.js` +- 클래스 이름: `BinarySearchTree` +- 메서드: + - [x] `insert(value)`: 트리에 값 추가 + - [x] `find(value)`: 주어진 값을 찾고 해당 노드를 리턴 + - [x] `remove(value)`: 트리에서 해당 값을 삭제 + +다음 알고리즘을 JavaScript로 구현해 `algorithm/sorts.js` 파일에 추가로 작성해 주세요. + +### 힙 정렬 +- [x] 함수 이름: `heapsort()` +- 숫자형 배열을 받아서 받은 배열을 정렬된 상태로 수정 ### 제출 안내 -`algorithm` 폴더를 만들고 `sorts.js` 파일에 구현한 함수들을 작성해 주세요. +`algorithm` 폴더를 만들고 그 아래에 만든 파일들을 저장해 주세요. --- ## 멘토에게 - 기본 요구사항은 모두 충족하였으며, 추가 구현한 내용을 아래에 기재합니다. -### Tree Sort -- Binary Search Tree 기반의 정렬 알고리즘을 구현했습니다. -- 새로운 배열을 반환하며, 중위 순회를 이용해 오름차순 결과를 얻습니다 +### Hash Table +- 문자열 해시 함수 +- Chaining 방식 충돌 처리 +- `put / get / remove / has` 포함 + +### Graph +- 인접 리스트 기반 +- 단방향 / 양방향 간선 추가, 정점 조회 기능 포함 + +### BFS / DFS +- 인접 리스트를 대상으로 하는 기본 탐색 알고리즘 ### CI -- `vitest` 기반 테스트 & `GitHub Actions CI`를 추가했습니다. +- `Vitest` 테스트 코드를 작성했습니다. - 테스트 케이스는 아래와 같습니다. - - 기본 정렬 테스트 - - 큰 배열(200개) 정렬 테스트 - - 중복 요소 포함된 배열 테스트 + - `sort.test.ts`(3): HeapSort 추가 + - `datastructures.test`(12): LinkedList, DoublyLinkedList, Queue, Stack, BinarySearchTree + - `extra-datastructures.test.js`(10): HashTable, Graph, BFS, DFS diff --git a/algorithm/BinarySearchTree.js b/algorithm/BinarySearchTree.js new file mode 100644 index 0000000..0587396 --- /dev/null +++ b/algorithm/BinarySearchTree.js @@ -0,0 +1,152 @@ +/** + * @file BinarySearchTree.js + * @description BST(이진 탐색 트리) 구현 클래스 + * + * @details + * - 왼쪽 서브트리는 항상 작은 값 + * - 오른쪽 서브트리는 항상 큰 값 + * - 탐색, 삽입, 삭제가 평균 O(log n) 시간에 수행됨 + */ + +/** + * BST 노드 구조 + */ +class TreeNode { + /** + * @param {number} value - 노드에 저장할 값 + */ + constructor(value) { + this.value = value; + this.left = null; + this.right = null; + } +} + +/** + * BST 트리 + */ +export class BinarySearchTree { + constructor() { + /** + * @type {TreeNode | null} + */ + this.root = null; + } + /** + * 값을 트리에 삽입합니다. + * + * @param {number} value - 삽입할 값 + * @returns {void} + */ + insert(value) { + this.root = this.#insertNode(this.root, value); + } + + /** + * 내부 재귀용 삽입 함수 + * + * @private + * @param {TreeNode | null} node + * @param {number} value + * @returns {TreeNode} + */ + #insertNode(node, value) { + if (node === null) return new TreeNode(value); + + if (value < node.value) { + node.left = this.#insertNode(node.left, value); + } else { + node.right = this.#insertNode(node.right, value); + } + + return node; + } + + /** + * 값을 가진 노드를 탐색해 반환합니다. + * + * @param {number} value - 찾을 값 + * @returns {TreeNode | null} 찾은 노드, 없으면 null + */ + find(value) { + return this.#findNode(this.root, value); + } + + /** + * 내부 재귀용 탐색 함수 + * + * @private + * @param {TreeNode | null} node + * @param {number} value + * @returns {TreeNode | null} + */ + #findNode(node, value) { + if (node === null) return null; + + if (value === node.value) return node; + if (value < node.value) return this.#findNode(node.left, value); + + return this.#findNode(node.right, value); + } + + /** + * 값을 가진 노드를 트리에서 삭제합니다. + * + * @param {number} value - 삭제할 값 + * @returns {void} + */ + remove(value) { + this.root = this.#removeNode(this.root, value); + } + + /** + * 내부 재귀용 삭제 함수 + * + * @private + * @param {TreeNode | null} node + * @param {number} value + * @returns {TreeNode | null} + */ + #removeNode(node, value) { + if (node === null) return null; + + // 삭제 대상 값 탐색 + if (value < node.value) { + node.left = this.#removeNode(node.left, value); + return node; + } else if (value > node.value) { + node.right = this.#removeNode(node.right, value); + return node; + } + + // 일치 노드 발견 → 삭제 처리 시작 + // 1. 자식 없음 (리프 노드) + if (!node.left && !node.right) return null; + + // 2. 자식이 하나 (좌 or 우) + if (!node.left) return node.right; + if (!node.right) return node.left; + + // 3. 자식이 둘 다 있음 + // - 오른쪽 서브트리 min 값을 찾아 교체 + const successor = this.#findMin(node.right); + node.value = successor.value; + node.right = this.#removeNode(node.right, successor.value); + + return node; + } + + /** + * 오른쪽 서브트리에서 min 노드룰 찾는 함수 + * + * @private + * @param {TreeNode} node + * @returns {TreeNode} + */ + #findMin(node) { + while (node.left) { + node = node.left; + } + return node; + } +} diff --git a/algorithm/DoublyLinkedList.js b/algorithm/DoublyLinkedList.js new file mode 100644 index 0000000..6859b51 --- /dev/null +++ b/algorithm/DoublyLinkedList.js @@ -0,0 +1,145 @@ +/** + * @file DoublyLinkedList.js + * @description 이중 연결 리스트(Doubly Linked List) 구현 클래스 + */ + +/** + * 이중 연결 리스트 노드 구조 + */ +class Node { + constructor(value) { + this.value = value; + this.prev = null; + this.next = null; + } +} + +/** + * Doubly Linked List Class + * @description head ↔ tail 양방향으로 이동 가능한 연결 리스트 + */ +export class DoublyLinkedList { + constructor() { + /** + * @type { Node | null} + */ + this.head = null; + + /** + * @type { Node | null} + */ + this.tail = null; + } + + /** + * head에 새 노드를 추가합니다. + * + * @param {any} value - 추가할 값 + * @returns {void} + */ + addToHead(value) { + const newNode = new Node(value); + + if (!this.head) { + this.head = this.tail = newNode; + return; + } + + newNode.next = this.head; + this.head.prev = newNode; + this.head = newNode; + } + + /** + * tail에 새 노드를 추가합니다. + * + * @param {any} value - 추가할 값 + * @returns {void} + */ + addToTail(value) { + const newNode = new Node(value); + + if (!this.tail) { + this.head = this.tail = newNode; + return; + } + + newNode.prev = this.tail; + this.tail.next = newNode; + this.tail = newNode; + } + + /** + * 특정 값을 가진 노드 뒤에 새 노드를 삽입합니다. + * + * @param {any} targetValue - 삽입 기준 값 + * @param {any} newValue - 새로 삽입할 값 + * @returns {boolean} 성공 시 true, 실패 시 false + */ + insertAfter(targetValue, newValue) { + let current = this.head; + + while (current) { + if (current.value === targetValue) break; + current = current.next; + } + if (!current) return false; + + const newNode = new Node(newValue); + newNode.prev = current; + newNode.next = current.next; + + if (current.next) { + current.next.prev = newNode; + } else { + this.tail = newNode; + } + + current.next = newNode; + return true; + } + + /** + * 값을 가진 노드를 찾아 반환합니다. + * + * @param {any} value - 찾을 값 + * @returns {Node | null} 찾은 노드, 없으면 null + */ + findNode(value) { + let current = this.head; + while (current) { + if (current.value === value) return current; + current = current.next; + } + return null; + } + + /** + * 특정 값을 가진 노드를 삭제합니다. + * + * @param {any} value - 삭제할 값 + * @returns {boolean} 성공 시 true, 실패 시 false + */ + removeNode(value) { + let current = this.head; + + while (current && current.value !== value) current = current.next; + if (!current) return false; + + // head 제거 + if (current.prev) { + current.prev.next = current.next; + } else { + this.head = current.next; + } + + // tail 제거 + if (current.next) { + current.next.prev = current.prev; + } else { + this.tail = current.prev; + } + + return true; + } +} diff --git a/algorithm/HashTable.js b/algorithm/HashTable.js new file mode 100644 index 0000000..0cbebef --- /dev/null +++ b/algorithm/HashTable.js @@ -0,0 +1,115 @@ +/** + * 해시 테이블 (Hash Table) + * @description 해시 함수 + Chaining(연결 리스트) 기반 충돌 처리 방식 + */ + +import { LinkedList } from "./LinkedList"; + +/** + * 기본 해시 함수 + * 문자열 key를 받아 해시 인덱스를 반환 + * + * @param {string} key + * @param {number} size - 배열 크기 + * @returns {number} 해시 인덱스 + */ +const hashFunc = (key, size) => { + let hash = 0; + for (let i = 0; i < key.length; i++) { + hash = (hash + key.charCodeAt(i) * 31) % size; + } + return hash; +}; + +export class HashTable { + /** + * @param {Number} size - 버킷 크기 + */ + constructor(size = 32) { + this.size = size; + this.buckets = Array.from({ length: size }, () => new LinkedList()); + } + + /** + * 내부 버킷에서 key에 해당하는 노드를 찾는 함수 + * + * @param {LinkedList} bucket + * @param {string} key + */ + _findNode(bucket, key) { + let current = bucket.head; + while (current) { + if (current.value.key === key) return current; + current = current.next; + } + return null; + } + + /** + * 값 삽입 / 갱신 + * + * @param {string} key + * @param {*} value + */ + put(key, value) { + const index = hashFunc(key, this.size); + const bucket = this.buckets[index]; + + const existingNode = this._findNode(bucket, key); + + if (existingNode) existingNode.value.value = value; // 갱신 + else bucket.append({ key, value }); // 삽입 + } + + /** + * 값 조회 + * + * @param {string} key + * @returns {*} value | undefined + */ + get(key) { + const index = hashFunc(key, this.size); + const bucket = this.buckets[index]; + + const node = this._findNode(bucket, key); + return node ? node.value.value : undefined; + } + + /** + * key 존재 여부 확인 + * + * @param {string} key + * @returns {boolean} + */ + has(key) { + return this.get(key) !== undefined; + } + + /** + * 값 삭제 + * + * @param {string} key + * @returns {boolean} 삭제 성공 여부 + */ + remove(key) { + const index = hashFunc(key, this.size); + const bucket = this.buckets[index]; + + let current = bucket.head; + let prev = null; + + while (current) { + if (current.value.key === key) { + // 삭제 처리 + if (prev) prev.next = current.next; + else bucket.head = current.next; + + return true; + } + prev = current; + current = current.next; + } + + return false; + } +} diff --git a/algorithm/LinkedList.js b/algorithm/LinkedList.js new file mode 100644 index 0000000..89b4eba --- /dev/null +++ b/algorithm/LinkedList.js @@ -0,0 +1,105 @@ +/** + * @file LinkedList.js + * @description 단일 연결 리스트(Linked List) 구현 클래스 + */ + +/** + * 단일 연결 리스트 노드 구조 + */ +class Node { + /** + * @param {any} value - 노드에 저장할 값 + */ + constructor(value) { + this.value = value; + this.next = null; + } +} + +/** + * Linked List Class + * @description head부터 시작하는 단일 연결 리스트를 제공합니다. + */ +export class LinkedList { + /** + * @type { Node | null} + */ + constructor() { + this.head = null; + } + + /** + * 리스트 끝에 새 노드를 추가합니다. + * @param {any} value - 추가할 값 + * @returns {void} + */ + addNode(value) { + const newNode = new Node(value); + + if (!this.head) { + this.head = newNode; + return; + } + + let current = this.head; + while (current.next) current = current.next; + + current.next = newNode; + } + + /** + * HashTable용 append 메서드 추가 + */ + append(value) { + this.addNode(value); + } + + /** + * 주어진 값을 가진 노드를 찾아 반환합니다. + * + * @param {any} value - 찾을 값 + * @returns {Node | null} 값을 찾을 노드 | 없으면 null + */ + findNode(value) { + let current = this.head; + + while (current) { + if (current.value === value) return current; + current = current.next; + } + + return null; + } + + /** + * 특정 값을 가진 노드 뒤에 새 노드를 삽입합니다. + * + * @param {any} targetValue - 삽입 기준 값 + * @param {any} newValue - 새로 삽입할 값 + * @returns {boolean} 성공 시 true | 실패(타깃 없음) 시 false + */ + insertAfter(targetValue, newValue) { + const target = this.findNode(targetValue); + if (!target) return false; + + const newNode = new Node(newValue); + newNode.next = target.next; + target.next = newNode; + + return true; + } + + /** + * 특정 값을 가진 노드 뒤의 노드를 삭제합니다. + * + * @param {any} targetValue - 삭제 기준 값 + * @returns {boolean} 성공 시 true | 실패(타깃 없음) 시 false + */ + removeAfter(targetValue) { + const target = this.findNode(targetValue); + if (!target || !target.next) return false; // !target?.next로 처리 가능하나 코드 구현 의도 표현을 위해 or 처리함. + + target.next = target.next.next; + return true; + } +} diff --git a/algorithm/Queue.js b/algorithm/Queue.js new file mode 100644 index 0000000..578451e --- /dev/null +++ b/algorithm/Queue.js @@ -0,0 +1,50 @@ +/** + * @file Queue.js + * @description Queue 자료구조 구현 클래스(FIFO 방식) + */ + +export class Queue { + constructor() { + /** + * @type {any[]} + */ + this.items = []; + } + + /** + * 값을 큐의 맨 뒤에 추가합니다. + * + * @param {any} value - 추가할 값 + * @returns {void} + */ + enqueue(value) { + this.items.push(value); + } + + /** + * 큐의 맨 앞 값을 제거하고 반환합니다. + * + * @returns {any | null} 제거된 값, 큐가 비어 있으면 null + */ + dequeue() { + return this.items.length ? this.items.shift() : null; + } + + /** + * 큐의 맨 앞 값을 제거하지 않고 반환합니다. + * + * @returns + */ + peek() { + return this.items.length ? this.items[0] : null; + } + + /** + * 큐가 비어있는지 여부를 반환합니다. + * + * @returns {boolean} 비어 있으면 true, 아니면 false + */ + isEmpty() { + return this.items.length === 0; + } +} diff --git a/algorithm/Stack.js b/algorithm/Stack.js new file mode 100644 index 0000000..9b48af0 --- /dev/null +++ b/algorithm/Stack.js @@ -0,0 +1,54 @@ +/** + * @file Stack.js + * @description Stack 자료구조 구현 클래스 (LIFO 방식) + */ + +/** + * Stack 클래스 + * @description 배열 기반의 LIFO 스택 구조 + */ +export class Stack { + constructor() { + /** + * @type {any[]} + */ + this.items = []; + } + + /** + * 값을 스택의 맨 위에 추가합니다. + * + * @param {any} value - 추가할 값 + * @returns {void} + */ + push(value) { + this.items.push(value) + } + + /** + * 스택의 맨 위 값을 제거하고 반환합니다. + * + * @returns {any | null} 제거된 값, 스택이 비어 있으면 null + */ + pop() { + return this.items.length ? this.items.pop() : null; + } + + /** + * 스택의 맨 위 값을 제거하지 않고 반환합니다 + * + * @returns {any | null} 맨 위에 있는 값, 없으면 null + */ + peek() { + return this.items.length ? this.items[this.items.length - 1] : null; + } + + /** + * 스택이 비어있는지 여부를 반환합니다. + * + * @returns {boolean} 비어 있으면 true, 아니면 null + */ + isEmpty() { + return this.items.length === 0; + } +} diff --git a/algorithm/graph/BFS.js b/algorithm/graph/BFS.js new file mode 100644 index 0000000..e18121e --- /dev/null +++ b/algorithm/graph/BFS.js @@ -0,0 +1,30 @@ +/** + * BFS (너비 우선 탐색) + * + * @param {Record} graph - 인접 리스트 기반 그래프 + * @param {string} start - 시작 노드 + * @returns {string} BFS 탐색 순서 + */ +export const bfs = (graph, start) => { + const visited = new Set(); + const queue = []; + const order = []; + + queue.push(start); + visited.add(start); + + while (queue.length > 0) { + const node = queue.shift(); + order.push(node); + + const neighbors = graph[node] || []; + for (const next of neighbors) { + if (!visited.has(next)) { + visited.add(next); + queue.push(next); + } + } + } + + return order; +}; diff --git a/algorithm/graph/DFS.js b/algorithm/graph/DFS.js new file mode 100644 index 0000000..8e4f6d2 --- /dev/null +++ b/algorithm/graph/DFS.js @@ -0,0 +1,27 @@ +/** + * DFS (깊이 우선 탐색) + * @description 재귀 호출로 한 경로를 끝까지 찾는 탐색 + * + * @param {Record} graph - 인접 리스트 기반 그래프 + * @param {string} start - 시작 노드 + * @returns {string[]} DFS 탐색 순서 + */ +export const dfs = (graph, start) => { + const visited = new Set(); + const order = []; + + const traverse = (node) => { + if (visited.has(node)) return; + + visited.add(node); + order.push(node); + + const neighbors = graph[node] || []; + for (const next of neighbors) { + traverse(next); + } + }; + + traverse(start); + return order; +}; diff --git a/algorithm/graph/Graph.js b/algorithm/graph/Graph.js new file mode 100644 index 0000000..34aced5 --- /dev/null +++ b/algorithm/graph/Graph.js @@ -0,0 +1,62 @@ +/** + * 그래프 (Graph) + * @description 인접 리스트 기반 그래프 구현 + * + * @function + * - addVertex(v): 정점 추가 + * - addEdge(v1, v2): 간선 추가 (단방향) + * - addUndirected(v1, v2): 간선 추가 (양방향) + * - getNeighbors(v): 인접 정점 리스트 반환 + * - vertices: 모든 정점 목록 반환 + */ +export class Graph { + constructor() { + this.adjList = {}; + } + + /** + * 정점 추가 + * @param {string} v + */ + addVertex(v) { + if (!this.adjList[v]) this.adjList[v] = []; + } + + /** + * 단방향 간선 추가 (v1 → v2) + * @param {string} v1 + * @param {string} v2 + */ + addEdge(v1, v2) { + this.addVertex(v1); + this.addVertex(v2); + this.adjList[v1].push(v2); + } + + /** + * 양방향 간선 추가 (v1 ↔ v2) + * @param {string} v1 + * @param {string} v2 + */ + addUndirected(v1, v2) { + this.addEdge(v1, v2); + this.addEdge(v2, v1); + } + + /** + * 인접 정점 목록 반환 + * @param {string} v + * @returns {string[]} + */ + getNeighbors(v) { + return this.adjList[v] || []; + } + + /** + * 그래프 내 정점 목록 반환 + * @returns {string[]} + */ + vertices() { + return Object.keys(this.adjList); + } +} diff --git a/algorithm/sorts.js b/algorithm/sorts.js index c3f6a65..935ea9f 100644 --- a/algorithm/sorts.js +++ b/algorithm/sorts.js @@ -195,3 +195,55 @@ export const treeSort = (arr) => { inorderTraversal(root, res); return res; }; + +/** + * 힙 부모-자식 관계를 유지하는 헬퍼 함수 + * + * @param {number[]} arr - 힙 구성 배열 + * @param {number} length - 힙의 현재 유효 길이 + * @param {number} i - 부모 노드 인덱스 + */ +const heapify = (arr, length, i) => { + let largest = i; + const left = i * 2 + 1; + const right = i * 2 + 2;; + + // 왼쪽 자식이 부모보다 큰 경우 + if (left < length && arr[left] > arr[largest]) largest = left; + + //오른쪽 자식이 최대값보다 큰 경우 + if (right < length && arr[right] > arr[largest]) largest = right; + + // 부모가 최대값이 아니면 교환 → 재귀 호출 + if (largest !== i) { + [arr[i], arr[largest]] = [arr[largest], arr[i]]; + heapify(arr, length, largest); + } +} + +/** + * 힙 정렬 (Heap Sort) + * 배열을 최대 힙으로 구성한 뒤 하나씩 꺼내며 정렬합니다. + * 원본 배열을 수정합니다. + * + * @param {number[]} arr - 정렬할 배열 + * @returns {number[]} 정렬된 배열(원본 수정) + */ +export const heapSort = (arr) => { + const length = arr.length; + + // 최대 힙 구성 + for (let i = Math.floor(length / 2) - 1; i >= 0; i--) { + heapify(arr, length, i); + } + + // 루트를 끝으로 보내고 → 힙 크기 줄여가며 재정렬 + for (let i = length - 1; i > 0; i--) { + [arr[0], arr[i]] = [arr[i], arr[0]]; + + // 줄어든 것만 heapify + heapify(arr, i, 0); + } + + return arr; +} \ No newline at end of file diff --git a/tests/datastructures.test.js b/tests/datastructures.test.js new file mode 100644 index 0000000..feaa7b4 --- /dev/null +++ b/tests/datastructures.test.js @@ -0,0 +1,173 @@ +import { describe, it, expect } from "vitest"; + +import { LinkedList } from "../algorithm/LinkedList"; +import { DoublyLinkedList } from "../algorithm/DoublyLinkedList"; +import { Queue } from "../algorithm/Queue"; +import { Stack } from "../algorithm/Stack"; +import { BinarySearchTree } from "../algorithm/BinarySearchTree"; + + +/** + * LinkedList + */ +describe("LinkedList 테스트", () => { + it("노드 추가 및 탐색", () => { + const list = new LinkedList(); + list.addNode(1); + list.addNode(2); + list.addNode(3); + + expect(list.findNode(2).value).toBe(2); + expect(list.findNode(4)).toBe(null); + }); + + it("노드 삽입 (insertAfter)", () => { + const list = new LinkedList(); + list.addNode(1); + list.addNode(2); + + list.insertAfter(1, 10); + + expect(list.findNode(10).value).toBe(10); + expect(list.findNode(2).value).toBe(2); + }); + + it("노드 삭제 (removeAfter)", () => { + const list = new LinkedList(); + list.addNode(1); + list.addNode(2); + list.addNode(3); + + list.removeAfter(1); + + expect(list.findNode(2)).toBe(null); + expect(list.findNode(3).value).toBe(3); + }); +}); + +/** + * DoublyLinkedList + */ +describe('DoublyLinkedList 테스트', () => { + it('head/tail 추가', () => { + const list = new DoublyLinkedList(); + list.addToHead(2); + list.addToHead(3); + list.addToHead(1); + + expect(list.head.value).toBe(1); + expect(list.tail.value).toBe(2); + }); + + it('노드 삽입 (insertAfter)', () => { + const list = new DoublyLinkedList(); + list.addToHead(1); + list.addToHead(2); + + list.insertAfter(1, 10); + expect(list.findNode(10).value).toBe(10); + }) + + it('노드 삭제 (removeNode)', () => { + const list = new DoublyLinkedList(); + list.addToHead(1); + list.addToHead(2); + list.addToHead(3); + + list.removeNode(2); + + expect(list.findNode(2)).toBe(null); + expect(list.findNode(3).value).toBe(3); + }); +}); + +/** + * Queue + */ +describe('Queue 테스트', () => { + it('enqueue/dequeue 동작 확인', () => { + const q = new Queue(); + + q.enqueue(1); + q.enqueue(2); + + expect(q.dequeue()).toBe(1); + expect(q.dequeue()).toBe(2); + expect(q.dequeue()).toBe(null); + }); + + it('peek/isEmpty 동작 확인', () => { + const q = new Queue(); + + q.enqueue(10); + expect(q.peek()).toBe(10); + expect(q.isEmpty()).toBe(false); + + q.dequeue(); + expect(q.isEmpty()).toBe(true); + }); +}) + +/** + * Stack + */ +describe('Stack 테스트', () => { + it('push/pop 동작 확인', () => { + const s = new Stack(); + + s.push(1); + s.push(2); + + expect(s.pop()).toBe(2); + expect(s.pop()).toBe(1); + expect(s.pop()).toBe(null); + }) + + it('peek/isEmpty 동작 확인', () => { + const s = new Stack(); + s.push(5) + + expect(s.peek()).toBe(5); + expect(s.isEmpty()).toBe(false); + + s.pop(); + expect(s.isEmpty()).toBe(true); + }) +}) + +/** + * BST + */ +describe('BinarySearchTree 테스트', () => { + it('삽입 및 탐색', () => { + const bst = new BinarySearchTree(); + + bst.insert(5); + bst.insert(3); + bst.insert(7); + + expect(bst.find(3)?.value).toBe(3); + expect(bst.find(999)).toBe(null); + }); + + it('노드 삭제 (leaf/단일 자식/두 자식)', () => { + const bst = new BinarySearchTree(); + + bst.insert(5); + bst.insert(3); + bst.insert(7); + bst.insert(6); + bst.insert(8); + + bst.remove(8); + expect(bst.find(8)).toBe(null); + + bst.remove(7); + expect(bst.find(7)).toBe(null); + + bst.insert(4); + bst.remove(3); // 3은 4를 child로 가짐 + expect(bst.find(3)).toBe(null); + expect(bst.find(4)?.value).toBe(4); + }) +}) diff --git a/tests/extra-datastructures.test.js b/tests/extra-datastructures.test.js new file mode 100644 index 0000000..7214265 --- /dev/null +++ b/tests/extra-datastructures.test.js @@ -0,0 +1,128 @@ +import { describe, it, expect } from "vitest"; + +import { HashTable } from "../algorithm/HashTable"; +import { Graph } from "../algorithm/graph/Graph"; +import { bfs } from "../algorithm/graph/BFS"; +import { dfs } from "../algorithm/graph/DFS"; + +/** + * HashTable 테스트 + */ +describe("HashTable 테스트", () => { + it("값 추가 및 조회", () => { + const table = new HashTable(); + + table.put("name", "김치볶음밥"); + table.put("price", 12000); + + expect(table.get("name")).toBe("김치볶음밥"); + expect(table.get("price")).toBe(12000); + }); + + it("값 갱신", () => { + const table = new HashTable(); + + table.put("lang", "JS"); + table.put("lang", "TS"); + + expect(table.get("lang")).toBe("TS"); + }); + + it("키 존재 여부 확인", () => { + const table = new HashTable(); + table.put("카푸치노", "카페라떼"); + + expect(table.has("카푸치노")).toBe(true); + expect(table.has("아메리카노")).toBe(false); + }); + + it("값 삭제", () => { + const table = new HashTable(); + + table.put("key1", "value1"); + expect(table.remove("key1")).toBe(true); + expect(table.get("key1")).toBe(undefined); + expect(table.remove("nothing")).toBe(false); + }); +}); + +/** + * Graph 테스트 + */ +describe("Graph 테스트", () => { + it("정점 및 간선 추가", () => { + const g = new Graph(); + + g.addVertex("A"); + g.addVertex("B"); + g.addEdge("A", "B"); + + expect(g.getNeighbors("A")).toEqual(["B"]); + expect(g.vertices()).toContain("A"); + expect(g.vertices()).toContain("B"); + }); + + it("양방향 간선 추가", () => { + const g = new Graph(); + g.addUndirected("X", "Y"); + + expect(g.getNeighbors("X")).toEqual(["Y"]); + expect(g.getNeighbors("Y")).toEqual(["X"]); + }); +}); + +/** + * BFS 테스트 + */ +describe("BFS 테스트", () => { + it("너비 우선 탐색 순서 확인", () => { + const graph = { + A: ["B", "C"], + B: ["D", "E"], + C: ["F"], + D: [], + E: ["F"], + F: [], + }; + + const result = bfs(graph, "A"); + expect(result).toEqual(["A", "B", "C", "D", "E", "F"]); + }); + + it("고립된 정점에서 시작", () => { + const graph = { + X: [], + Y: ["X"], + }; + + expect(bfs(graph, "X")).toEqual(["X"]); + }); +}); + +/** + * DFS 테스트 + */ +describe("DFS 테스트", () => { + it("깊이 우선 탐색 순서 확인", () => { + const graph = { + A: ["B", "C"], + B: ["D", "E"], + C: ["F"], + D: [], + E: ["F"], + F: [], + }; + + const result = dfs(graph, "A"); + expect(result).toEqual(["A", "B", "D", "E", "F", "C"]); + }); + + it("고립된 정점에서 시작", () => { + const graph = { + X: [], + Y: ["X"], + }; + + expect(dfs(graph, "X")).toEqual(["X"]); + }); +}); diff --git a/tests/sort.test.ts b/tests/sort.test.ts index 8710229..922015b 100644 --- a/tests/sort.test.ts +++ b/tests/sort.test.ts @@ -5,6 +5,7 @@ import { mergeSort, quickSort, treeSort, + heapSort, } from "../algorithm/sorts"; // 기본 @@ -77,10 +78,33 @@ describe("정렬 알고리즘 테스트", () => { // 스트레스 테스트 (편향/정렬) // --------------------- it("퀵 정렬 (이미 정렬된 배열)", () => { - expect(quickSort([...sorted])).toEqual(sorted); + expect(quickSort([...sorted])).toEqual(sorted); }); it("트리 정렬 (편향 케이스)", () => { - expect(treeSort(sorted)).toEqual(sorted); + expect(treeSort(sorted)).toEqual(sorted); + }); +}); + +/** + * HeapSort + */ +describe("HeapSort 테스트", () => { + it("기본 정렬", () => { + const arr = [...unsorted]; + heapSort(arr); + expect(arr).toEqual(sorted); + }); + + it("중복 포함 정렬", () => { + const arr = [...duplicates]; + heapSort(arr); + expect(arr).toEqual(duplicatesSorted); + }); + + it("큰 배열 정렬", () => { + const arr = [...bigArray]; + heapSort(arr); + expect(arr).toEqual(bigSorted); }); });