有一些算法已经被标准库实现了,但是由于这些算法太出名,很容易进行改编,因此需要掌握这些算法的具体实现

排序

归并排序

  • 二路合并
/* 合并左子数组和右子数组 */
void merge(vector<int> &nums, int left, int mid, int right) {
    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]
    // 创建一个临时数组 tmp ,用于存放合并后的结果
    vector<int> tmp(right - left + 1);
    // 初始化左子数组和右子数组的起始索引
    int i = left, j = mid + 1, k = 0; //k为临时数组索引
    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j]) //这里的<=保证了排序稳定性
            tmp[k++] = nums[i++];
        else
            tmp[k++] = nums[j++];
    }
    // 将左子数组和右子数组的剩余元素复制到临时数组中
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间
    for (k = 0; k < tmp.size(); k++) {
        nums[left + k] = tmp[k];
    }
}
  • 归并
/* 归并排序 */
void mergeSort(vector<int> &nums, int left, int right) {
    // 终止条件
    if (left >= right)
        return; // 当子数组长度为 1 时终止递归
    // 划分阶段
    int mid = left + (right - left) / 2;    // 计算中点
    mergeSort(nums, left, mid);      // 递归左子数组
    mergeSort(nums, mid + 1, right); // 递归右子数组
    // 合并阶段
    merge(nums, left, mid, right);
}

桶排序

适用范围较广,排序前首先需要除以序列中最大的绝对值,将整个序列变成 范围

/* 桶排序 */
void bucketSort(vector<float> &nums) {
    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
    int k = nums.size() / 2;
    vector<vector<float>> buckets(k);
    // 1. 将数组元素分配到各个桶中
    for (float num : nums) {
        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
        int i = num * k;
        // 将 num 添加进桶 bucket_idx
        buckets[i].push_back(num);
    }
    // 2. 对各个桶执行排序
    for (vector<float> &bucket : buckets) {
        // 使用内置排序函数,也可以替换成其他排序算法
        sort(bucket.begin(), bucket.end());
    }
    // 3. 遍历桶合并结果
    int i = 0;
    for (vector<float> &bucket : buckets) {
        for (float num : bucket) {
            nums[i++] = num;
        }
    }
}

计数排序

对于非负整数序列,并且序列最大值并不特别大的,可以使用计数排序;计数排序是桶排序的特殊情况;

/* 计数排序 */
// 简单实现,无法用于排序对象
void countingSortNaive(vector<int> &nums) {
    // 1. 统计数组最大元素 m
    int m = 0;
    for (int num : nums) {
        m = max(m, num);
    }
    // 2. 统计各数字的出现次数
    // counter[num] 代表 num 的出现次数
    vector<int> counter(m + 1, 0);
    for (int num : nums) {
        counter[num]++;
    }
    // 3. 遍历 counter ,将各元素填入原数组 nums
    int i = 0;
    for (int num = 0; num < m + 1; num++) {
        for (int j = 0; j < counter[num]; j++, i++) {
            nums[i] = num;
        }
    }
}

质数筛法

埃氏筛法

vector<int> prime;
vector<bool> isPrime(N, true);
 
void Era(int n){
	isPrime[0] = isPrime[1] = false;
	for(int i=2; i<=n; i++){
		if(isPrime[i]){
			prime.push_back(i);
			if((long long)i*i > n) continue;
			for(int j= i*i; j<=n; j += i){
				//因为从 2 到 i - 1 的倍数我们之前筛过了
				//这里直接从 i 的倍数开始筛选,提高筛选效率
				isPrime[j] = false;
			}
		}
	}
}

欧拉筛

vector<int> prime;
vector<bool> notPrime(N, true);
 
void Euler(int n){
	for(int i=2; i<=n; i++){
	if(!notPrime[i])}{
		prime.push_back(i);
	}
	for(int pri_j : prime){
		if(i*pri_j>n) break;
		notPrime[i*pri_j] = true;
		if(i%pri_j == 0){
			break;
		}
	}
}

对顶堆

一般用于频繁增删的数据中查找分位数,即排名第 k 的数; 核心:新来的数先入堆,再平衡堆的大小

//连续输入N个数,连续输出N次中位数
int N, mid;
priority_queue<int,vector<int>,greater<int>> minHeap;
priority_queue<int,vector<int>,less<int>> maxHeap;
 
for(int i=0; i<N; i++){
	int num=0;
	cin>>num;
	
	//先入堆
	if(minHeap.empty() || num>minHeap.top())
		minHeap.push(num);
	else
		maxHeap.push(num);
		
	//调整堆的大小,这里保持minHeap中元素更多
	if(minHeap.size()>maxHeap.size()+1){
		maxHeap.push(minHeap.top());
		minHeap.pop();
	}else if(minHeap.size()<maxHeap.size()){
		minHeap.push(maxHeap.top());
		maxHeap.pop();
	}
	
	if((i+1)%2==1){//有奇数个数
		mid=minHeap.top();
	}else{
		mid=(minHeap.top()+maxHeap.top())/2.0;
	}
	
	cout<<mid<< ((i==N-1)? "\n":" ");
}

图算法

DIJ 算法

DIJ 算法是在一个图中寻找从源点到其他所有点的最短路径的算法,最终结果可以用一个 vector<int> dist 来表示;

//使用邻接矩阵来存储图,矩阵中的数值表示路径权重
vector<vector<int>> graph;
 
//方便起见我们用pair类将距离和索引打包
//第一个值为距离,第二个值为索引
//定义优先队列的比较类
class cmp{
	public:
	bool operator()(pair<int,int> &a, pair<int,int> &b){
		return a.first > b.first; //基于第一个值(距离)进行排序
	}
};
 
void dij(vector<vector<int>> &graph, int src, vector<int> &dist){
	priority_queue<pair<int,int>, vector<pair<int,int>>, cmp> pq;
	dist[src]=0;
	pq.push(make_pair(0, src));
	
	while(!pq.empty()){
		int min_index = pq.top().second;
		int min_dis = pq.top().first;
		pq.pop();
		
		if(min_dis>dist[min_index])
			continue;
		
		for(int v=0;v<V;v++){ //V是一个全局变量,表示节点个数
			//这里默认graph中-1表示没有边
			if(graph[min_index][v]!=-1 && dist[min_index]!=INT_MAX && graph[min_index][v]+dist[min_index]<dist[v]){
				//符合条件,更新参数
				dist[v]=graph[min_index][v]+dist[min_index];
				pq.push(make_pair(dist[v], v));
			}
		}
	}
}

最小生成树算法

最小生成树

Kruskal 算法

核心:就是从小到大依次选边;但是有些边不能选,因为选了就会产生回路,可以使用并查集来判断是否会产生回路

int v_num, e_num;
 
// 初始化并查集
vector<int> DJSet(v_num + 1);
void initSet(){
    for (int i = 1; i <= v_num; i++){
        DJSet[i] = i;
    }
}
 
//定义查询
int findRoot(int x){
    return DJSet[x] == x ? x : DJSet[x] = findRoot(DJSet[x]);
}
 
//定义合并
void Merge(int x, int y){
    int x_root = findRoot(x);
    int y_root = findRoot(y);
    //根结点相互连接
    if(x_root<y_root){
        DJSet[y_root] = x_root;
    }else{
        DJSet[x_root] = y_root;
    }
}
 
bool cmp(array<int, 3> a, array<int, 3> b){
    return a[0] < b[0];
}
 
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
 
    cin >> v_num >> e_num;
 
    //初始化并查集
    initSet();
 
    vector<array<int, 3>> edges;
    edges.reserve(e_num); //分配内存
    for (int i = 0; i < e_num; i++){
        int u, v, w;
        cin >> u >> v >> w;
        edges.push_back({w, u, v}); //内存分配后可以这样操作
    }
 
    sort(edges.begin(), edges.end(), cmp);
 
    for (auto &[w, u, v] : edges){
    	int root_u = findRoot(u);
        int root_v = findRoot(v);
        if (root_u != root_v){
            Merge(u, v);
            cout << min(u, v) << "," << max(u, v) << "," << w << endl;
        }
    }
}

Prim 算法

每次加入距离最小的点,用堆来维护

//某个结点的邻接结点结构体
struct adjNode{
    int index;    // 连接的顶点
    int cost; // 边的权值
};
 
int v_num, e_num, src;//全局变量
 
void Prim(vector<vector<adjNode>> adj){
	//初始化关键数据结构
	vector<bool> visited(v_num);
	//将将结点和边一起存储,顺序为cost,u,v
	priority_queue<array<int, 3>, vector<array<int, 3>>, greater<>> pq;
	
	//初始化源点
	visited[src]=true;
	for(auto &adj_node:adj[src]){
		pq.push({adj_node.cost, src, adj_node.index});
	}
	
	int edgeChosen=0; //已经选中的边,最多v_num-1条
	while(edgeChose<v_num-1 && !pq.empty()){
		auto [cost, u, v]=pq.top();
		pq.pop();
		if(visited[v])
			continue;
		
		//选中这条边
		visited[v]=true;
		edgeChosen++;
		
		//输出结果
		cout<<u<<" "<<v<<" "<<cost<<endl;
		
		//刷新邻接结点
		for(auto &adj_node:adj[v]){
			if(!visited[adj_node.index]){
				pq.push({adj_node.cost, v, adj_node.index});
			}
		}
	}
}
 
int main(){
    cin >> v_num >> e_num >> src;
 
    vector<vector<adjNode>> adj(v_num + 1); //邻接结点矩阵
    for (int i = 0; i < e_num;i++){
        int u, v, w;
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }
 
    Prim(adj);
}
指向原始笔记的链接

并查集

可以直接使用数组 vector 来实现,最重要的功能就是查询合并

int v_num; //结点个数
vector<int> set(v_num);
 
// 初始化
for(int i=0; i<v_num; i++){
	set[i]=i;
}
 
//查找
int findRoot(int x)
{ return set[x]==x ? x : set[x]=findRoot(set[x]); }
 
//合并
void Merge(int x, int y){
	int x_root = findRoot(x);
	int y_root = findRoot(y);
	//这里将编号较小的根作为新的根
	if(x_root<y_root){
		set[y_root]=x_root;
	}else{
		set[x_root]=y_root;
	}
}