算法入门篇(二)之算法介绍

目录

算法复杂性、贪心算法

算法复杂性

贪心算法

分治算法

合并排序

快速排序

STL应用

训练


算法复杂性、贪心算法

算法复杂性

定义:
算法复杂性是用来评估算法运行所需资源(如时间、空间)的多少。它分为时间复杂性和空间复杂性。
原理:
时间复杂性:衡量算法执行时间的长短。用大O记法(Big O notation)表示,如O(n)、O(n^2)、O(log n)等。其中,n代表输入数据的大小。时间复杂性越低,算法执行效率越高。
示例: 考虑一个求和算法,对于序列1到n的所有整数进行求和。
直接的for循环实现:需要循环n次,时间复杂性为O(n)。
使用高斯求和公式:直接计算 (n(n+1))/2,时间复杂性为O(1),即常数时间。
空间复杂性:衡量算法执行过程中所需存储空间的大小。也用大O记法表示。
示例: 考虑一个排序算法,如快速排序。在平均情况下,空间复杂性为O(log n),但由于递归调用栈的使用,最坏情况下空间复杂性可能达到O(n)。

贪心算法

1. 定义: 贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。
2. 原理:
贪心选择:在每一步选择中,根据某个标准选择当前最优的解。
无后效性:贪心选择后,不考虑之后的选择如何修改这个选择。
最优子结构:整体最优解可以由一系列局部最优解构成。
3. 示例:
1.找零问题:
给定一些面额不同的硬币,如1元、5元、10元,要找零n元,找零的硬币数量要尽可能少。
贪心策略:在每一步选择中,选择面额最大的硬币,直到找零的总金额达到n。
算法步骤: 初始化一个空列表,用于存储找零的硬币。
从面额最大的硬币开始,将尽可能多的这个硬币加入列表,直到总金额超过n。 如果总金额等于n,算法结束。
否则,将面额减小到次大的硬币,重复步骤2。
2.活动选择问题:
给定一系列活动,每个活动都有开始时间和结束时间,目标是选择尽可能多的互不相交的活动。
贪心策略:在每一步选择中,选择结束时间最早的活动,以便腾出更多时间给其他活动。
总结: 算法复杂性帮助我们评估算法的性能,从而选择合适的算法解决问题。 贪心算法则是一种基于贪心策略的优化算法,它在每一步选择中都采取当前状态下的最优决策,而不考虑未来的后果。虽然贪心算法可能无法得到全局最优解,但它通常能够得到一个近似最优解,并且计算效率高。

分治算法

合并排序

合并排序(Merge Sort)是一种经典的排序算法,其基本思想是将待排序的数组不断地分割成更小的子数组,直到每个子数组只有一个元素,然后再将这些子数组两两合并,直到最终得到完全有序的数组。以下是关于合并排序的详细解释:
基本步骤
分割:将待排序的数组从中间分割成两个子数组,直到每个子数组只包含一个元素。
递归排序:递归地对子数组进行排序,直到所有的子数组都是有序的。
合并:将两个有序的子数组合并成一个有序的数组,直到合并为1个完整的数组。
时间复杂度
合并步骤:合并两个有序数组的时间复杂度是线性的,即O(n),其中n表示待合并数组的长度。在合并过程中,每个元素只需要比较一次,因此合并步骤的时间复杂度是O(n)。
整体时间复杂度:合并排序的整体时间复杂度是O(nlogn),其中n是待排序数组的长度,logn表示分割数组的次数。每次分割的时间复杂度是O(1),而合并步骤的时间复杂度是O(n)。因此,合并排序的总体时间复杂度是O(nlogn)。
空间复杂度
合并排序的空间复杂度取决于实现方式。在递归实现中,由于需要递归调用栈来保存中间结果,空间复杂度为O(logn)。然而,如果使用迭代实现,空间复杂度可以降低到O(1)(除了用于合并的额外空间外,不占用额外空间)。但在实际应用中,由于合并操作需要额外的空间来存储合并后的结果,因此通常的空间复杂度为O(n)。
示例
假设有一个待排序的数组[38, 27, 43, 3, 9, 82, 10],合并排序的过程如下:
分割:将数组分割成更小的子数组,直到每个子数组只有一个元素:[38],[27],[43],[3],[9],[82],[10]。
递归排序:由于每个子数组只有一个元素,所以它们已经是有序的。
合并:
合并[38]和[27],得到[27, 38]
合并[43]和[3],得到[3, 43]
合并[9]和[82],得到[9, 82]
合并[27, 38]和[3, 43],得到[3, 27, 38, 43]
合并[9, 82]和[10],得到[9, 10, 82]
最后,合并[3, 27, 38, 43]和[9, 10, 82],得到[3, 9, 10, 27, 38, 43, 82]。
这样,就得到了一个完全有序的数组。

快速排序

快速排序(Quicksort)是一种高效的排序算法,其基本思想是分治策略。以下是关于快速排序的详细解释:
基本原理
快速排序采用的是分治(Divide and Conquer)的思想,具体步骤如下:
选择基准值:从待排序的数组中选择一个元素作为基准值(pivot)。
划分操作:通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行。
排序流程
设定分界值:首先设定一个分界值(通常选用数组的第一个数),通过该分界值将数组分成左右两部分。
数据分组:将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值。
递归处理:对左边和右边的数据独立进行快速排序,即再对左右两边的数据重复上述过程。
时间复杂度
最好情况:每次基准值都能将数组均匀地划分为两部分,此时时间复杂度为O(nlogn),其中n是数组的长度。
平均情况:快速排序的平均时间复杂度也是O(nlogn)。
最坏情况:如果待排序的数组已经有序或接近有序,那么每次划分只能使一边的元素减少一个,导致递归树极度不平衡,此时时间复杂度为O(n^2)。
空间复杂度
快速排序的空间复杂度主要取决于递归调用的深度。在最好的情况下,递归树的深度为logn,因此空间复杂度为O(logn)。然而,在最坏的情况下,递归树的深度为n,空间复杂度为O(n)。但是,由于快速排序是原地排序(in-place sort),所以只需要一个额外的空间来存储基准值,因此在实际应用中,空间复杂度通常可以视为O(logn)。
稳定性
快速排序是一种不稳定的排序算法,因为在划分过程中,相同的元素可能会因为基准值的选择和比较的顺序而改变它们之间的相对顺序。
总结
快速排序是一种非常高效的排序算法,它的平均时间复杂度为O(nlogn),并且在实际应用中表现优秀。然而,由于最坏情况下的时间复杂度为O(n^2),因此在某些特定情况下可能不是最优选择。此外,由于快速排序是不稳定的排序算法,因此在对稳定性有要求的场景中需要谨慎使用。

STL应用

1.容器
1.1 vector
底层数据结构:动态数组。内存分配位置:堆上。

特点:支持动态扩容,尾部插入和删除效率高,但中间插入和删除可能涉及元素移动。

方法:push_back(), pop_back(), at(), front(), back(), resize(), reserve(), erase(), insert()等。

1.2 list
底层数据结构:双向链表。

内存分配位置:堆上。

特点:支持快速增删,对于任何位置的元素插入或元素移除,时间复杂度都是常数。

方法:push_back(), pop_back(), push_front(), pop_front(), insert(), erase(), sort()等。

1.3 stack
介绍:容器适配器,专门用于后进先出(LIFO)操作。

默认底层容器:deque。

方法:push(), pop(), top(), empty(), size()等。

1.4 queue
介绍:容器适配器,提供先进先出(FIFO)操作。

默认底层容器:deque。

方法:push(), pop(), front(), back(), empty(), size()等。

1.5 deque
底层数据结构:双向开口的连续线性空间,可看作list和vector的结合品。

内存分配位置:堆上。

特点:支持快速在两端插入和删除元素。

方法:push_front(), pop_front(), push_back(), pop_back(), at(), front(), back(), insert(), erase()等。

1.6 priority_queue
介绍:容器适配器,其中每个元素都有优先级。

默认底层容器:vector。

特点:元素以优先级顺序出队,而不是按插入顺序。

方法:push(), pop(), top(), empty(), size()等。

1.7 set/multiset
底层数据结构:红黑树/平衡二叉搜索树(RB-tree)。

内存分配位置:堆上。

特点:所有元素都会根据元素的键值自动被排序。set不允许重复元素,multiset允许。

方法:insert(), erase(), find(), count(), empty(), size()等。

1.8 map/multimap
底层数据结构:红黑树/平衡二叉搜索树(RB-tree)。

内存分配位置:堆上。

特点:所有元素都会根据元素的键值自动被排序。map的所有元素都是pair,map不允许两个元素拥有相同的键值,multimap允许。

方法:insert(), erase(), find(), count(), empty(), size()等。

1.9 bitset
介绍:用于处理固定大小的位序列。

特点:常用于位运算和状态压缩。

方法:位操作函数,如set(), reset(), flip(), test(), to_ulong(), to_string()等。

2.算法
2.1 lower_bound
功能:在有序序列中查找不小于(大于或等于)某值的第一个元素位置。

复杂度:对数时间(基于底层数据结构)。

2.2 upper_bound
功能:在有序序列中查找大于某值的第一个元素位置。

复杂度:对数时间(基于底层数据结构)。

2.3 unique
功能:删除序列中相邻的重复元素(假定序列已排序)。

复杂度:线性时间。

2.4 sort
功能:对序列进行排序。

复杂度:通常使用快速排序,平均时间复杂度为O(nlogn)。

2.5 nth_element
功能:对序列进行部分排序,使得第n个元素位于其最终排序位置,且所有小于它的元素都位于其左侧,所有大于它的元素都位于其右侧。

训练

  • HDU 3527

HDU 3527 是一个经典的算法题,通常涉及图论或动态规划。下面是对该问题的简要分析和解决方案。

问题描述
在这个问题中,给定一个图,要求找到从一个节点到另一个节点的最短路径,可能会有多个边和权重。通常需要考虑图的特性,比如是否有负权边,以及如何处理这些边。

解题思路
图的表示:

使用邻接表或邻接矩阵来存储图的信息。
算法选择:

如果没有负权边,可以使用 Dijkstra 算法。
如果存在负权边,则可以使用 Bellman-Ford 算法。
实现步骤:

读取输入数据,构建图。
根据图的特性选择合适的算法进行最短路径计算。
输出结果。
示例代码(Python)
以下是使用 Dijkstra 算法的示例代码:

import heapq

def dijkstra(graph, start):
    n = len(graph)
    dist = [float('inf')] * n
    dist[start] = 0
    priority_queue = [(0, start)]  # (distance, node)

    while priority_queue:
        current_dist, current_node = heapq.heappop(priority_queue)

        if current_dist > dist[current_node]:
            continue

        for neighbor, weight in graph[current_node]:
            distance = current_dist + weight
            if distance < dist[neighbor]:
                dist[neighbor] = distance
                heapq.heappush(priority_queue, (distance, neighbor))

    return dist

# 示例输入
n, m = map(int, input().split())  # n: 节点数, m: 边数
graph = [[] for _ in range(n)]

for _ in range(m):
    u, v, w = map(int, input().split())
    graph[u].append((v, w))
    graph[v].append((u, w))  # 如果是无向图

start_node = 0  # 起始节点
distances = dijkstra(graph, start_node)

print(distances)

注意事项
确保处理好输入输出格式。
对于不同的图类型(有向图、无向图),需要相应调整边的添加方式。
考虑边界情况,例如图中没有路径的情况。

  • POJ 1028

POJ 1028 - 最小生成树
问题描述:
给定一个无向图,要求找到其最小生成树(MST),即连接所有顶点的边的权重之和最小的树。

解题思路
可以使用两种经典算法来解决这个问题:

Prim 算法:适合稠密图。
Kruskal 算法:适合稀疏图。
这里我们将使用 Prim 算法进行讲解。

Prim 算法步骤
初始化:

从任意一个节点开始,将其加入 MST。
使用优先队列(最小堆)来存储与当前 MST 相连的边,并按权重排序。
选择边:

每次从优先队列中取出权重最小的边,如果该边连接的节点不在 MST 中,则将其加入 MST,并将新节点的相连边加入优先队列。
重复:

重复上述步骤直到所有节点都被包含在 MST 中。
示例代码(Python)

import heapq

def prim(n, graph):
    mst_cost = 0
    visited = [False] * n
    min_heap = [(0, 0)]  # (cost, node)
    
    while min_heap:
        cost, u = heapq.heappop(min_heap)
        
        if visited[u]:
            continue
        
        visited[u] = True
        mst_cost += cost
        
        for v, weight in graph[u]:
            if not visited[v]:
                heapq.heappush(min_heap, (weight, v))
    
    return mst_cost

# 输入处理
n, m = map(int, input().split())  # n: 节点数, m: 边数
graph = [[] for _ in range(n)]

for _ in range(m):
    u, v, w = map(int, input().split())
    graph[u].append((v, w))
    graph[v].append((u, w))  # 无向图

# 计算最小生成树的总权重
result = prim(n, graph)
print(result)

注意事项
确保输入格式正确,特别是节点编号是否从 0 开始。
如果图是稀疏的,Kruskal 算法可能会更高效。
对于大规模图,可以考虑使用并查集优化 Kruskal 算法。

  • POJ 1915
    问题描述:动态规划,通常涉及背包问题或路径问题。

解法:构建 DP 数组,根据状态转移方程逐步填充。

  • HDU 1276
    问题描述:可能是与图相关的问题,如最短路径或流量问题。

解法:根据具体情况选择合适的算法(如 Dijkstra、Floyd-Warshall)。

  • HDU 6375
    问题描述:可能涉及复杂的数据结构或图形问题。

解法:分析题意,可能需要使用 DFS/BFS 或其他高级数据结构。

  • POJ 1442
    问题描述:通常是贪心算法问题。

解法:找出局部最优解,逐步构建全局最优解。

  • POJ 2443
    问题描述:图论或最优化问题。

解法:使用图的遍历方法,结合动态规划或贪心策略。

  • HDU 1412
    问题描述:动态规划或图论问题。

解法:根据题目要求构建 DP 状态,进行状态转移。

  • POJ 1281
    问题描述:常见的字符串处理或动态规划问题。

解法:使用 DP 数组处理子问题,逐步构建答案。

  • POJ 2418
    问题描述:通常是网络流或图论问题。

解法:使用 Edmonds-Karp 或 Dinic 算法求解最大流。

  • POJ 3481
    问题描述:图的最短路径或最小生成树问题。

解法:使用 Dijkstra 或 Kruskal 算法。

  • HDU 1263
    问题描述:可能涉及字符串匹配或动态规划。

解法:利用 KMP 或 DP 方法解决。

  • POJ 3579
    问题描述:动态规划或贪心问题。

解法:分析问题性质,构建相应的 DP 状态或贪心策略。

  • POJ 2388
    问题描述:图论或组合数学问题。

解法:利用 DFS/BFS 或组合数学公式求解。

  • POJ 1731
    问题描述:动态规划或最优路径问题。

解法:构建 DP 表,进行状态转移。

  • POJ 1256
    问题描述:字符串处理或动态规划问题。

解法:使用 DP 数组处理子问题,逐步构建答案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战族狼魂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值