Explore the CS Algorithm and Machine Learning Algorithm sections, where I showcase my algorithm implementations in computer science and machine learning. The LeetCode and BOJ sections highlight my preparation for competitive programming, including the ICPC. Here, I've organized my problem-solving journey by difficulty and programming languages, reflecting my commitment to tackling complex challenges.
C++
void init(Stack *s) { s->top = -1;}
bool empty(Stack *s) {
if (s->top == -1) return true;
return false;
}
bool full(Stack *s) {
if (s->top == MAX_SIZE -1) return true;
return false;
}
void push(Stack *s, element item) {
if (full(s)) {
cout << "Stack is saturated";
return;
}
s->top += 1;
s->array[s->top] = item;
}
void pop(Stack *s) {
if (empty(s)) {
cout << "Stack is empty";
return;
}
s->top -= 1;
}
A stack is a data structure that allows adding and removing elements only from one end, known as the top of the stack. The last element added is the first one to be removed, following the Last In, First Out (LIFO) principle. The time complexity for both push and pop operations is O(1).
C++
typedef int element;
typedef struct {
int front;
int rear;
element array[MAX_SIZE];
} Queue;
void init(Queue *q) {
q->front = -1;
q->rear = -1;
}
bool empty(Queue *q) {
if (q->front == q->rear) return true;
return false;
}
bool full(Queue *q) {
if (q->rear == MAX_SIZE - 1) return true;
return false;
}
void enqueue(Queue *q, element item) {
if (full(q)) {
cout << "Queue is saturated\n";
return;
}
q->array[++q->rear] = item;
}
void dequeue(Queue *q) {
if (empty(q)) {
cout << "Queue is empty";
return;
}
int result = q->array[++q->front];
cout << result << " was removed\n";
}
A queue is a data structure that allows elements to be added at the rear and removed from the front. It follows the First In, First Out (FIFO) principle, ensuring the order of elements as they were inserted. The time complexity for both enqueue and dequeue operations is O(1).
C++
void init(Deque *dq) { dq->front = dq->rear = 0; }
bool empty(Deque *dq) {
if (dq->front == dq->rear) return true;
return false;
}
bool full(Deque *dq) {
if( (dq->rear + 1) % MAX_SIZE == dq->front) return true;
return false;
}
// append-rear
void append(Deque *dq, element item) {
if(full(dq)) return;
dq->rear = (dq->rear + 1) % MAX_SIZE;
dq->array[dq->rear] = item;
}
// append-front
void appendleft(Deque *dq, element item) {
if(full(dq)) return ;
dq->array[dq->front] = item;
dq->front = ((dq->front - 1)+ MAX_SIZE) % MAX_SIZE;
}
// pop-rear
void pop(Deque *dq) {
if(empty(dq)) return;
cout << dq->array[dq->rear]<<" was removed\n";
dq->rear = (dq->rear - 1 + MAX_SIZE) % MAX_SIZE;
}
// pop-front
void popleft(Deque *dq) {
if(empty(dq)) return;
dq->front = (dq->front + 1) % MAX_SIZE;
int result = dq->array[dq->front];
cout << result << " was removed\n";
}
A deque (double-ended queue) allows insertion and removal of elements from both the front and rear ends,
providing a flexible data structure.
The time complexity for all operations (append, append left, pop, and pop left) is O(1).
C++
template
class priority_queue {
private:
int cnt; // number of elements in priority_queue
T heap[MAX_N];
public:
int size() { return cnt; }
priority_queue() { cnt = 0; } // init
void swap(int aIdx, int bIdx) {
int temp = heap[aIdx];
heap[aIdx] = heap[bIdx];
heap[bIdx] = temp;
}
void push(T item) {
heap[++cnt] = item;
int cur = cnt;
while (cur > 1) {
int par = cur >> 1;
if (heap[cur] < heap[par]) break;
swap(cur, par);
cur = par;
}
}
void pop() {
heap[1] = heap[cnt--];
int cur = 1;
while (cur <= cnt) {
int left = (cur << 1) <= cnt ?
(cur << 1) : -1;
int right = (cur << 1 | 1) <= cnt ?
(cur << 1 | 1) : -1;
int child = cur;
if (left == -1) break;
if (heap[cur] < heap[left]) child = left;
if (right != -1 && heap[child] < heap[right])
child = right;
if (child == left) {
swap(cur, left);
cur = left;
}
else if (child == right) {
swap(cur, right);
cur = right;
} else break;
}
}
T top() { return heap[1]; }
bool empty() { return cnt == 0; }
};
This implementation of a priority queue uses a binary heap to efficiently manage elements.
Time Complexity:
- Insertion (push
): O(log n)
- Removal (pop
): O(log n)
- Access (top
): O(1)
This Priority Queue implementation is based on a Binary Heap. A heap is a complete binary tree structure where each parent node is always greater than its child nodes (in the case of a max-heap).
When a new element is inserted, it is added to the last position of the heap. Then, it is compared with its parent node, and the upheap process is performed to maintain the heap property.
The top element of the heap is removed, and the last element is moved to the top. Then, it is compared with its child nodes, and the downheap process is performed to maintain the heap property.
C++
void init(Queue *q) {
q->front = q->rear = 0;
}
bool empty(Queue *q) {
if(q->front == q->rear) return true;
return false;
}
bool full(Queue *q) {
if ((q->rear + 1) % MAX_SIZE == q->front) return true;
return false;
}
void enqueue(Queue *q, element item) {
if (full(q)) {
cout <<"Queue is saturated \n";
return;
}
// maintaining the shape of a circular queue
q->rear = (q->rear + 1) % MAX_SIZE;
q->array[q->rear] = item;
}
void dequeue(Queue *q) {
if(empty(q)) {
cout << "Queue is empty";
return;
}
q->front = (q->front + 1) % MAX_SIZE;
int result = q->array[q->front];
cout<< result<< " was removed\n";
}
A circular queue is an advanced version of a regular queue where the end of the queue wraps around to the beginning, forming a circle. This C++ implementation ensures efficient use of space, allowing the queue to be reused without shifting elements. The time complexity for both enqueue and dequeue operations is O(1).
C++
typedef int element;
typedef struct Node {
element data;
struct Node *next, *prev;
} Node;
Node* makeNode(element item) {
Node* newNode = (Node *)malloc(sizeof(Node));
newNode->data = item;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
void insert_left(Node* head, element item) {
Node* p = makeNode(item);
p->next = head->next;
head->next = p;
p->prev = head;
}
void insert_right(Node* head, element item) {
Node *newNode = makeNode(item);
Node *cur = head;
while (cur->next) cur = cur->next;
cur->next = newNode;
newNode->prev = cur;
}
void pop_left(Node* cur) {
Node* temp = cur;
cur->prev->next = cur->next;
if (cur->next) cur->next->prev = cur->prev;
free(temp);
}
void pop_right(Node *head) {
Node *cur = head, *temp;
while (cur->next) cur = cur->next;
temp = cur;
if (cur->prev) cur->prev->next = NULL;
free(temp);
}
A doubly linked list is a linear data structure where each element, or node, points to both
its previous and next node, allowing bidirectional traversal.
Time Complexity:
- Insertion at the beginning (insert_left): O(1)
- Insertion at the end (insert_right): O(n)
- Removal from the beginning (pop_left): O(1)
- Removal from the end (pop_right): O(n)
C++
size_t djb2(const char* str) {
// str = "blue"
size_t hash = 5381;
for(; *str; ++str) {
hash = ((hash << 5) + hash) + *str;
}
return hash;
}
const int MAX_N = 1e4 + 4;
const int MAX_LEN = 5e2 + 5;
typedef struct Node {
char str[MAX_LEN]; // key
Node* next;
} Node;
// Memory Pool Technique
int node_count;
Node nodes[MAX_N];
Node* new_node(const char str[MAX_LEN]) {
strcpy(nodes[node_count].str, str);
nodes[node_count].next = NULL;
return &nodes[node_count++];
}
class HashMap {
static constexpr size_t TABLE_SIZE = 1 << 12;
static constexpr size_t DIV = TABLE_SIZE - 1;
Node hash_table[TABLE_SIZE];
public:
HashMap() {}
void init() {
memset(hash_table, 0, sizeof(hash_table));
node_count = 0;
}
void insert(const char str[MAX_LEN]) {
Node* prev_node = get_prev_node(str);
if(prev_node->next == NULL) {
prev_node->next = new_node(str);
}
}
void remove(const char str[MAX_LEN]) {
Node* prev_node = get_prev_node(str);
if(prev_node->next != NULL) {
prev_node->next = prev_node->next->next;
}
}
Node* get(const char str[MAX_LEN]) {
return get_prev_node(str)->next;
}
private:
Node* get_prev_node(const char str[MAX_LEN]) {
Node* prev_ptr = &hash_table[djb2(str) & DIV];
while(prev_ptr->next != NULL &&
strcmp(prev_ptr->next->str, str) != 0) {
prev_ptr = prev_ptr->next;
}
return prev_ptr;
}
};
This HashMap uses the djb2 hash function to hash strings to integer values,
which are then used to store and retrieve values from a hash table.
djb2 Hash Function
The djb2 hash function calculates the hash value by iterating over each
character of the string and performing the calculation:
hash = hash * 33 + c;
Where hash * 33
is computed as (hash << 5) + hash
.
This allows the hash value to be updated efficiently by using bit-shifting operations.
This method is efficient and provides a good distribution of hash values for strings.
Memory Pool Technique
To optimize memory allocation, we use a memory pool technique. Instead of dynamically
allocating memory for each node, a fixed array of nodes is allocated at the start. This
allows us to quickly allocate new nodes from this pre-allocated pool, reducing the overhead
of frequent memory allocations.
C++
typedef int element;
typedef struct Node {
element data;
struct Node *left, *right;
} Node;
Node* makeNode(element item) {
Node* newNode = (Node *)malloc(sizeof(Node));
newNode->data = item;
return newNode;
}
void fillTree(Node* root, Node* l, Node* r) {
root->left = l;
root->right = r;
}
void preorder(Node *root) {
if(root != NULL) {
printf("%d ", root->data);
preorder(root->left);
preorder(root->right);
}
}
void inorder(Node *root) {
if(root != NULL) {
inorder(root->left);
printf("%d ", root->data);
inorder(root->right);
}
}
void postorder(Node *root) {
if(root != NULL) {
postorder(root->left);
postorder(root->right);
printf("%d ", root->data);
}
}
A binary tree is a tree data structure where each node has at most two children,
referred to as the left and right child. In this implementation, we define a binary tree
node and three types of tree traversal methods:
Preorder Traversal: Visit the root node first, then traverse the left
subtree, followed by the right subtree.
Inorder Traversal: Traverse the left subtree first, visit the root node,
and then traverse the right subtree.
Postorder Traversal: Traverse the left subtree, then the right subtree,
and visit the root node last.
C++
vector adj[MAX_V];
int prim(vector& selected) {
selected.clear();
vector added(V, false);
vector minWeight(V, INF), parent(V, -1);
int ret = 0; // sum of weight
minWeight[0] = 0; // Starting Node
parent[0] = 0;
for (int iter = 0; iter < V; ++iter) {
int u = -1;
for (int v = 0; v < V; ++v) {
if ((!added[v]) &&
(u == -1 || minWeight[v] < minWeight[u]))
u = v;
}
printf("iter: %d | u: %d\n", iter, u);
if (u == -1) break;
if (parent[u] != u)
selected.push_back({parent[u], u});
ret += minWeight[u];
added[u] = true;
// check all adjacent edges
for (int i = 0; i < adj[u].size(); ++i) {
int v = adj[u][i].first;
int weight = adj[u][i].second;
if (!added[v] && minWeight[v] > weight) {
parent[v] = u;
minWeight[v] = weight;
}
}
}
return ret;
}
This algorithm starts from the starting vertex and expands the spanning tree set step by step.
The frame is expanded by selecting the vertex connected to the lowest edge among adjacent vertices.
Kruskal's algorithm is an edge-based algorithm, while Prim's algorithm is a vertex-based algorithm.
- O(n²) with an array
- O(m log n) with a binary heap
C++
typedef pair<int, int> pii;
typedef pair<int, pii> ippi;
// Find function for union-find
int find(int x) {
if (par[x] < 0) return x;
return par[x] = find(par[x]);
}
// Union function for union-find
void merge(int a, int b) {
a = find(a);
b = find(b);
if (a == b) return;
if (arr[a] < arr[b]) par[b] = a;
else par[a] = b;
}
// Kruskal's algorithm to find MST
int kruskal(vector &edges, int n) {
// Sort edges by weight
sort(edges.begin(), edges.end());
memset(par, -1, sizeof(par));
int mst_cost = 0;
for (auto &edge : edges) {
int weight = edge.first;
int u = edge.second.first;
int v = edge.second.second;
// If u and v are not connected
if (find(u) != find(v)) {
merge(u, v); // Merge u and v
// Add edge weight to MST cost
mst_cost += weight;
}
}
return mst_cost;
}
- Always need to verify that it provides the optimal answer.
Space: |E|
Time: |E|log(|E|)
C++
const int MAX_N = 5e4 + 4;
// 2^x + 2^(x-1) + 2^(x-2) >= 50000
const int MAX_B = 16;
vector adj[MAX_N];
int n, m, ST[MAX_N][MAX_B], dep[MAX_N];
void makeTree(int cur, int par) {
for(int child : adj[cur]) {
// Don't search node that
// is adjacent but parent-related
if(child == par) continue;
ST[child][0] = cur;
dep[child] = dep[cur] + 1;
// Configure trees with adjacent nodes
makeTree(child, cur);
}
}
int lca(int u, int v) {
// Desired situation: dep[u] > dep[v]
// Control of situation as desired
if(dep[u] < dep[v]) return lca(v, u);
int diff = dep[u] - dep[v];
// Move node as high as possible
for(int i=MAX_B-1; diff && i>=0; --i) {
if((1 << i) <= diff) {
u = ST[u][i];
diff -= (1 << i);
}
}
if(u == v) return u;
// The height of vertex u and
//that of vertex v are the same
for(int i = MAX_B - 1; i >= 0; --i) {
// Two nodes belong to different component
if(ST[u][i] != ST[v][i]) {
u = ST[u][i];
v = ST[v][i];
}
}
return ST[u][0];
}
The LCA algorithm finds the lowest common ancestor of two nodes in a tree.
This implementation uses the binary lifting method to preprocess the tree, allowing
for efficient LCA queries in O(log n) time.
- makeTree: Constructs the tree and initializes the sparse table for binary lifting.
- lca: Finds the LCA of two nodes u and v by moving them up to the same level and then
to their common ancestor.
Python
class Trie:
def __init__(self):
# Key: number, Value: Trie
self.child = {}
self.finished = False
def insert(self, phone):
node = self
for digit in phone:
# Operate when there's no targeted number
if digit not in node.child:
node.child[digit] = Trie()
# Move to next dictionary (Trie)
node = node.child[digit]
# Inform the end of trie
node.finished = True
def search(self):
rst = True
for cur_node in self.child.values():
if cur_node.finished:
if len(cur_node.child):
return False
else:
return True
# If anyone is false, the result is false
temp = cur_node.search()
if temp is False:
rst = False
# Check for all cases
# Prevent one trie from being a subset of another trie
# Trie 1) 1-2-3-4: 4(end)
# Trie 2) 1-2-3-4-5-6-7: 7(end)
# Trie 2 passes Trie 1's end
return rst
A Trie is a tree-like data structure used to efficiently store and search a dynamic set of strings.
In this implementation, we use a Trie to store phone numbers and ensure that no number is a prefix of
another.
- insert: Inserts a phone number into the Trie.
- search: Checks if any phone number is a prefix of another number, ensuring no
subset relationship exists.