1 Sort
use case
void printArray(std::vector<int>& arr)
{
for (auto& item : arr) cout << item << " ";
cout << std::endl;
}
int main()int main()
{{
std::vector<int> arr = {4, 6, 2, 8, 3, 1, 3, 9, 8, 6, 3, 4}; std::vector<int> arr = {4, 6, 2, 8, 3, 1, 3, 9, 8, 6, 3, 4};
printArray(arr); printArray(arr);
quickSort(arr, 0, arr.size() - 1);quickSort(arr, 0, arr.size() - 1);
printArray(arr);
}
Merge sort
void merge(std::vector<int>& arr, int lo, int mid, int hi)
{
int num1 = mid - lo + 1;
int num2 = hi - mid;
std::vector<int> leftArr(arr.begin() + lo, arr.begin() + mid + 1);
std::vector<int> rightArr(arr.begin() + mid + 1, arr.begin() + hi + 1);
int i = 0;
int j = 0;
int k = lo;
while (i < num1 && j < num2)
{
if (leftArr[i] <= rightArr[j]) arr[k++] = leftArr[i++];
else arr[k++] = rightArr[j++];
}
while (i < num1)
{
arr[k++] = leftArr[i++];
}
while (i < num2)
{
arr[k++] = rightArr[j++];
}
}
void mergeSort(std::vector<int>& arr, int lo, int hi)void mergeSort(std::vector<int>& arr, int lo, int hi)
{{
if (lo < hi) { if (lo < hi) {
int mid = lo + (hi - lo) / 2; int mid = lo + (hi - lo) / 2;
mergeSort(arr, lo, mid);mergeSort(arr, lo, mid);
mergeSort(arr, mid + 1, hi);mid + 1, hi);
merge(arr, lo, mid, hi);(arr, lo, mid, hi);
}}
}
private static void merge(int[] arr, int lo, int mid, int hi) {
int[] help = new int[hi - lo + 1];
int i = 0;
int p1 = lo;
int p2 = mid + 1;
while (p1 <= mid && p2 <= hi) help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
while (p1 <= mid) help[i++] = arr[p1++];
while (p2 <= hi) help[i++] = arr[p2++];
for (i = 0; i < help.length; i++) arr[lo + i] = help[i];
}
private static void mergesort(int[] arr, int lo, int hi) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
mergesort(arr, lo, mid);
mergesort(arr, mid + 1, hi);
merge(arr, lo, mid, hi);
}
Quick sort
- 75. Sort Colors
int partition(std::vector<int>& arr, int lo, int hi)
{
int ridx = std::rand() % (hi - lo + 1) + lo;
int pivot = arr[ridx];
int i = lo - 1;
int j = hi + 1;
while (true) {
// Move the left index to the right at least once and while the element at the left index is less than the pivot
do {
i++;
} while (arr[i] < pivot);
// Move the right index to the left at least once and while the element at the right index is greater than the pivot
do {
j--;
} while (arr[j] > pivot);
// If the indices crossed, return
if (i >= j) return j;
// Swap the elements at the left and right indices
std::swap(arr[i], arr[j]);
}
}
void quickSort(std::vector<int>& arr, int lo, int hi)void quickSort(std::vector<int>& arr, int lo, int hi)
{{
if (lo < hi) { if (lo < hi) {
int p = partition(arr, lo, hi); int p = partition(arr, lo, hi);
quickSort(arr, lo, p);quickSort(arr, lo, p);
quickSort(arr, p + 1, hi);p + 1, hi);
}}
}
Heap sort
Heap structure
In many computer science applications, we only need to access the largest or smallest element in the dataset.
A priority queue is an abstract data type, while a Heap is a data structure. Therefore, a Heap is not a Priority Queue, but a way to implement a Priority Queue. There are multiple ways to implement a Priority Queue, such as array and linked list. However, operations except for insertion or deletion will have O(N).
A Heap is a special type of binary tree.
- Is a complete binary tree;
- The value of each node must be no greater than (or no less than) the value of its child nodes.
class MinHeap
{
public:
MinHeap(int heapSize)
: heapSize(heapSize)
{
data.resize(heapSize + 1);
data[0] = 0;
}
int peek() { return data[1]; }
int size() { return realSize; }
void add(int element) {
realSize++;
if (realSize > heapSize)
{
cout << "too many elements!" << endl;
realSize--;
return;
}
data[realSize] = element;
int curr = realSize;
// Note if we use an array to represent the complete binary tree
// and store the root node at index 1
// index of the parent node of any node is [index of the node / 2]
// index of the left child node is [index of the node * 2]
// index of the right child node is [index of the node * 2 + 1]
int parent = curr / 2;
// If the newly added element is smaller than its parent node,
// its value will be exchanged with that of the parent node
while (data[curr] < data[parent] && curr > 1)
{
std::swap(data[curr], data[parent]);
curr = parent;
parent = curr / 2;
}
}
int pop()
{
if (realSize < 1)
{
cout << "empty!" << endl;
return INT_MAX;
}
int removeElement = data[1];
// Put the last element in the Heap to the top of Heap
data[1] = data[realSize];
realSize--;
int curr = 1;
// When the element is not a leaf node
while (curr <= realSize / 2)
{
int left = curr * 2;
int right = curr * 2 + 1;
// If the element is larger than the left or right child
// its value needs to be exchanged with the smaller value
// of the left and right child
if (data[curr] <= data[left] && data[curr] <= data[right]) break;
int minChild = data[left] < data[right] ? left : right;
std::swap(data[curr], data[minChild]);
curr = minChild;
}
return removeElement;
}
void print()
{
if (realSize == 0) cout << "no element!" << endl;
std::string info = "[";
for (int i = 1; i < realSize; ++i)
{
info = info + std::to_string(data[i]) + ",";
}
info += std::to_string(data[realSize]) + "]";
cout << info << endl;
}
private:
std::vector<int> data;
int heapSize;
int realSize = 0;
};
int main()int main()
{{
// Test case // Test case
MinHeap minHeap(3);MinHeap minHeap(3);
minHeap.add(3);minHeap.add(3);
minHeap.add(1);1);
minHeap.add(2);2);
minHeap.print(); // [1,3,2]print(); // [1,3,2]
cout << minHeap.peek() << endl; // 1cout << minHeap.peek() << endl; // 1
cout << minHeap.pop() << endl; // 1op() << endl; // 1
minHeap.print(); // [2,3]minHeap.print(); // [2,3]
minHeap.add(4);add(4);
minHeap.add(5); // Add too many elements5); // Add too many elements
minHeap.print(); // [2,3,4]print(); // [2,3,4]
}
sort:
void heapSort(std::vector<int>& arr, int lo, int hi)
{
MinHeap minHeap(arr.size());
for (auto& item : arr) minHeap.add(item);
int curr = 0;
while (minHeap.size() != 0)
{
arr[curr++] = minHeap.pop();
}
}
2 Search
2.1 Binary search
3 Tree
Binary Tree
Traversal
- preorder
- use one stack(add root)
- algo
- do something
- push right
- push left
- LC144
- postorder
- two stacks(add root)
- algo
- push curr to stack2
- push left to stack1
- push right to stack2
- use stack2 to do something
- LC145
- inorder
- use one stack, don’t push root to the stack
- algo
- if stack is not empty or root is not empty, keep going
- if root/curr is not null, keep push to the stack, root updated to its left(idea: keep push left to the stack)
- if root/curr is null, then do other logic:
- take item from the stack
- do something
- root updated to the item’s right.
- LC94
- BFS
- use one queue(add root)
- algo
- pop, do something
- push left if not null
- push right if not null
- if you need level information, just create a hash map to record!
- LC102, LC104
recursive(there are 3 places to do something):
void f(Node* node)
{
// 1
if (node == nullptr) return;
// 1
f(node->left);
// 2
// 2
f(node->right);
// 3
// 3
}
preorder:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (nullptr == root) return res;
std::stack<TreeNode*> stack;
stack.push(root);
while (!stack.empty())
{
TreeNode* curr = stack.top();
stack.pop();
res.push_back(curr->val);
if (curr->right != nullptr) stack.push(curr->right); // be careful
if (curr->left != nullptr) stack.push(curr->left); if (curr->left != nullptr) stack.push(curr->left);
}}
return res; return res;
}
postorder:
vector<int> postorderTraversal(TreeNode* root) {
if (root == nullptr) return {};
vector<int> res;
std::stack<TreeNode*> stack1;
std::stack<TreeNode*> stack2;
stack1.push(root);
while (!stack1.empty())
{
TreeNode* curr = stack1.top();
stack1.pop();
stack2.push(curr);
if (curr->left != nullptr) stack1.push(curr->left);
if (curr->right != nullptr) stack1.push(curr->right);
}
while (!stack2.empty())
{
TreeNode* curr = stack2.top();
stack2.pop();
res.push_back(curr->val);
}
return res;
}
inorder:
vector<int> inorderTraversal(TreeNode* root) {
if (root == nullptr) return {};
std::vector<int> result;
std::stack<TreeNode*> stack;
while (!stack.empty() || root != nullptr)
{
if (root != nullptr)
{
stack.push(root);
root = root->left;
} else
{
TreeNode* curr = stack.top();
stack.pop();
result.push_back(curr->val);
root = curr->right;
}
}
return result;
}
BFS:
vector<vector<int>> levelOrder(TreeNode* root) {
if (root == nullptr) return {};
std::vector<std::vector<int>> res;
std::queue<TreeNode*> queue;
std::unordered_map<TreeNode*, int> levels;
queue.push(root);
levels.insert({root, 0});
while (!queue.empty())
{
TreeNode* curr = queue.front();
queue.pop();
int level = levels[curr];
if (res.size() < level + 1) res.resize(level + 1);
res[level].push_back(curr->val);
if (curr->left != nullptr) if (curr->left != nullptr)
{{
queue.push(curr->left); queue.push(curr->left);
levels[curr->left] = level + 1;levels[curr->left] = level + 1;
}}
if (curr->right != nullptr)if (curr->right != nullptr)
{ {
queue.push(curr->right); queue.push(curr->right);
levels[curr->right] = level + 1; levels[curr->right] = level + 1;
}}
}}
return res;return res;
}
Red-black tree
- It is a self-balanced BST. (Binary Search Tree)
- Every node has a color (black or red)
- Root is black
- When Parent’s color is red then the child MUST be black
- From the current node to the leaf, the “black nodes on each path” is the same
- On every leaf, there are 2 “None” nodes, color is black
What you need to take note of are Rules #4 and #5. They are the 2 important ones contributing to “self-balancing”.
4 Graph
General structure
struct Edge;
struct Node {
int id;
int in = 0;
int out = 0;
std::vector<Edge*> edges;
std::vector<Node*> nexts;
Node(int id) : id(id) {}
};
struct Edge {
int weight;
Node* from;
Node* to;
Edge(Node* from, Node* to) : from(from), to(to) {}
};
struct Graph
{
std::unordered_map<int, Node*> nodes;
std::unordered_set<Edge*> edges;
~Graph() {
for (auto edge : edges) delete edge;
for (auto node : nodes) delete node.second;
}
void Load(const vector<vector<int>>& flights)
{
for (auto& flight : flights)
{
int from = flight[0];
int to = flight[1];
int weight = flight[2];
// new nodes
if (nodes.find(from) == nodes.end())
{
nodes.insert({from, new Node(from) });
}
if (nodes.find(to) == nodes.end())
{
nodes.insert({ to, new Node(to) });
}
// new edges
Node* fromNode = nodes[from];
Node* toNode = nodes[to];
Edge* newEdge = new Edge(weight, fromNode, toNode);
fromNode->nexts.emplace_back(toNode);
fromNode->out++;
fromNode->edges.emplace_back(newEdge);
toNode->in++;
edges.insert(newEdge);
}
}
};
std::vector<std::vector<int>> adj_list(n);
for (auto& edge : edges)
{
adj_list[edge[0]].push_back(edge[1]);
adj_list[edge[1]].push_back(edge[0]);
}
Disjoint Set
Breadth-First Search(BFS)
- use queue and set
- 1971. Find if Path Exists in Graph
- 797. All Paths From Source to Target
void bfs(Node* node)
{
if (node == nullptr) return;
std::queue<Node*> queue;
std::unordered_set<Node*> set;
queue.push(node);
set.insert(node);
while (!queue.empty()) {
auto curr = queue.front();
queue.pop();
for (auto next : curr->nexts)
{
if (set.find(next) == set.end()) {
set.insert(node);
queue.push(node);
}
}
}
}
class Solution {
public:
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
vector<vector<int>> paths;
if (graph.size() == 0) {
return paths;
}
vector<int> path;
queue<vector<int>> q;
path.push_back(0);
q.push(path);
while (!q.empty()) {
vector<int> currentPath = q.front();
q.pop();
int node = currentPath.back();
for (int nextNode : graph[node]) {
vector<int> tmpPath(currentPath.begin(), currentPath.end());
tmpPath.push_back(nextNode);
if (nextNode == graph.size() - 1) {
paths.push_back(tmpPath);
} else {
q.push(tmpPath);
}
}
}
return paths;
}
};
or
class Solution {
public:
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
vector<vector<int>> res;
int n = graph.size();
queue<vector<int>> q;
vector<int> path;
path.push_back(0);
q.push(path);
while (!q.empty())
{
auto currentPath = q.front();
q.pop();
int curr = currentPath.back();
if (curr == n - 1) res.push_back(currentPath);
for (auto& adj : graph[curr])
{
vector<int> newPath = currentPath;
newPath.push_back(adj);
q.push(newPath);
}
}
return res;
}
};
Depth-First Search(DFS)
void dfs(Node* node)
{
if (node == nullptr) return;
std::stack<Node*> stack;
std::unordered_set<Node*> set;
stack.push(node);
set.insert(node);
// do something
while (!stack.empty())
{
auto curr = stack.top();
stack.pop();
for (auto next : curr->nexts)
{
if (set.find(node) == set.end())
{
// stack.push(curr)
stack.push(next);
set.insert(next);
// do something
//break;
}
}
}
}
Backtracking
- 797. All Paths From Source to Target
class Solution {
public:
int n;
std::vector<int> path;
void backtracking(std::vector<std::vector<int>>& paths, std::vector<std::vector<int>>& graph, int curr)
{
if (curr == n - 1)
{
paths.emplace_back(path);
return;
}
for (const auto& next : graph[curr])
{
path.emplace_back(next);
backtracking(paths, graph, next);
path.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
n = graph.size();
std::vector<std::vector<int>> paths;
path = { 0 };
backtracking(paths, graph, 0);
return paths;
}
};
Single Source Shortest Path Algorithm
- BFS can only solve the shortest path problem in unweighted graphs.
- edge relaxation operation(ie. updating the label) is a key element in solving the single source shortest path problem.
- Dijkstra’s algorithm can only be used to solve the “single source shortest path” problem in a graph with non-negative weights.
- greedy approach
- Each step selects the “minimum weight” from the currently reached vertices to find the “shortest path” to other vertices.
- Bellman-Ford algorithm, on the other hand, can solve the “single-source shortest path” in a weighted directed graph with any weights, including, of course, negative weights.
Dijkstra’s Algorithm
- LC743
- LC787
// Dijkstra's algorithm to find the shortest path from the source to all other nodes
void dijsktra(std::vector<std::pair<int, int>>* adj, std::vector<int>& tree, int source, int n) {
// Min-heap priority queue to get the node with the smallest distance/time
using WeightSumNodePair = std::pair<int, int>;
std::priority_queue<WeightSumNodePair, std::vector<WeightSumNodePair>, std::greater<WeightSumNodePair>> pq;
pq.push({ 0, source });
// Weight for the starting node is 0
tree[source] = 0;
while (!pq.empty())
{
// Get the current node and the weight
int currWeight = pq.top().first;
int currNode = pq.top().second;
pq.pop();
// In case other nodes updated the tree
if (currWeight > tree[currNode]) continue;
// Broadcast the signal to adjacent nodes
for (auto edge : adj[currNode])
{
int weightToAdd = edge.first;
int neighborNode = edge.second;
// if new weight less than the last weight, then update it
if (currWeight + weightToAdd < tree[neighborNode])
{
tree[neighborNode] = currWeight + weightToAdd; // update
// Push the neighbor node into the priority queue for further processing
pq.push({ tree[neighborNode], neighborNode });
}
}
}
}
using WeightDst = std::pair<int, int>;
int networkDelayTime(std::vector<std::vector<int>>& times, int n, int k) {
// Adjacency list, defined it as per the maximum number of nodes
// But can be defined with the input size as well
std::vector<WeightDst> adj[101];
// build the adj list
for (auto& time : times) {
int src = time[0];
int dst = time[1];
int weight = time[2];
adj[src].push_back({ weight, dst});
}
std::vector<int> tree(n + 1, INT_MAX);
dijsktra(adj, tree, k, n);
int answer = INT_MIN;
for (int i = 1; i <= n; ++i)
{
answer = std::max(answer, tree[i]);
}
return answer == INT_MAX ? -1 : answer;
}
int findCheapestPrice(int n, std::vector<std::vector<int>>& flights, int src, int dst, int k) {
using WeightDstPair = std::pair<int, int>;
using Next = std::vector<WeightDstPair>;
std::vector<Next> adj(n);
for (auto e : flights) {
adj[e[0]].push_back({ e[1], e[2] });
}
std::vector<int> stops(n, INT_MAX);
std::priority_queue<std::vector<int>, std::vector<std::vector<int>>, std::greater<std::vector<int>>> pq;
// {dist_from_src_node, node, number_of_stops_from_src_node}
pq.push({0, src, 0});
while (!pq.empty())
{
auto temp = pq.top();
pq.pop();
int dist = temp[0];
int node = temp[1];
int steps = temp[2];
// We have already encountered a path with a lower cost and fewer stops,
// or the number of stops exceeds the limit.
if (steps > stops[node] || steps > k + 1) continue;
stops[node] = steps;
if (node == dst) return dist;
for (auto& [neighbor, price] : adj[node]) {
pq.push({ dist + price, neighbor, steps + 1 });
}
}
return -1;
}