12、算法
1、深度优先搜索(Depth-First Search,简称 DFS)和广度优先搜索(Breadth-First Search,简称 BFS)是图论和树结构遍历中常用的两种算法策略,它们的主要区别如下:
搜索方式
深度优先搜索:沿着一条路径尽可能深地探索下去,直到达到叶子节点或者无法继续深入为止,然后回溯到上一个节点,继续探索其他分支。
广度优先搜索:从起始节点开始,首先访问它的所有邻居节点,然后按照这些邻居节点的顺序,依次访问它们的邻居节点,以此类推,一层一层地向外扩展。
使用的数据结构
深度优先搜索:通常使用递归或者栈来实现。递归实现时,系统栈会记录函数调用的顺序和状态;手动使用栈实现时,将节点压入栈中,然后从栈中弹出节点进行访问,并将其未访问的邻居节点压入栈中。
广度优先搜索:一般使用队列来实现。先将起始节点放入队列,然后从队列中取出节点进行访问,把该节点的未访问邻居节点依次加入队列,直到队列为空。
代码示例:
#include <iostream>
#include <vector>
// 定义图的邻接表表示
using Graph = std::vector<std::vector<int>>;
// 递归实现深度优先搜索
void dfs(const Graph& graph, int node, std::vector<bool>& visited) {
// 标记当前节点为已访问
visited[node] = true;
std::cout << node << " ";
// 遍历当前节点的所有邻居节点
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
// 递归访问未访问的邻居节点
dfs(graph, neighbor, visited);
}
}
}
int main() {
// 构建一个简单的图(邻接表表示)
Graph graph = {
{1, 2}, // 节点 0 的邻居节点为 1 和 2
{0, 3}, // 节点 1 的邻居节点为 0 和 3
{0, 3}, // 节点 2 的邻居节点为 0 和 3
{1, 2} // 节点 3 的邻居节点为 1 和 2
};
// 初始化访问标记数组
std::vector<bool> visited(graph.size(), false);
// 从节点 0 开始进行深度优先搜索
std::cout << "深度优先搜索结果: ";
dfs(graph, 0, visited);
std::cout << std::endl;
return 0;
}
2、冒泡排序、选择排序、插入排序、快速排序和归并排序是几种常见的排序算法,它们在原理、时间复杂度、空间复杂度、稳定性等方面存在明显区别,以下是详细对比:
1)原理
- 冒泡排序(Bubble Sort)
- 重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
- 这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端。
- 选择排序(Selection Sort)
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 以此类推,直到所有元素均排序完毕。
- 插入排序(Insertion Sort)
- 将未排序数据插入到已排序序列的合适位置。初始时,已排序序列仅包含第一个元素,然后依次将后续元素插入到已排序序列中,使得插入后的序列仍然有序。
- 快速排序(Quick Sort)
- 采用分治法的思想。选择一个基准值(pivot),将数组分为两部分,使得左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于等于基准值。
- 然后分别对左右两部分递归地进行快速排序,最终得到一个有序的数组。
- 归并排序(Merge Sort)
- 同样基于分治法。把一个数组分成两个子数组,分别对这两个子数组进行排序,然后将排好序的子数组合并成一个最终的有序数组。
- 合并过程是将两个已排序的子数组按顺序合并成一个新的有序数组。
2) 时间复杂度
- 冒泡排序:
- 最好情况:当数组已经有序时,只需进行一次遍历,时间复杂度为 O(n)。
- 最坏情况和平均情况:都需要进行多次比较和交换,时间复杂度为 O(n2)。
- 选择排序:无论数组初始状态如何,都需要遍历未排序部分来选择最小(大)元素,时间复杂度始终为 O(n2)。
- 插入排序:
- 最好情况:数组已经有序,每次插入操作只需比较一次,时间复杂度为 O(n)。
- 最坏情况和平均情况:时间复杂度为 O(n2)。
- 快速排序:
- 最好情况和平均情况:每次选择的基准值都能将数组均匀地分成两部分,时间复杂度为 O(nlogn)。
- 最坏情况:当数组已经有序或接近有序时,基准值的选择会导致划分极不均匀,时间复杂度退化为 O(n2)。
- 归并排序:无论数组初始状态如何,都将数组不断二分并合并,时间复杂度始终为 O(nlogn)。
3)空间复杂度
- 冒泡排序:只需要常数级的额外空间,用于交换元素,空间复杂度为 O(1)。
- 选择排序:同样只需要常数级的额外空间,空间复杂度为 O(1)。
- 插入排序:只需要常数级的额外空间,空间复杂度为 O(1)。
- 快速排序:
- 平均情况下,递归调用栈的深度为 O(logn),因此空间复杂度为 O(logn)。
- 最坏情况下,递归调用栈的深度为 O(n),空间复杂度为 O(n)。
- 归并排序:需要额外的与原数组大小相同的辅助空间来进行合并操作,空间复杂度为 O(n)。
4. 稳定性
- 稳定性定义:如果在排序过程中,相等元素的相对顺序保持不变,则称该排序算法是稳定的;否则是不稳定的。
- 冒泡排序:在比较和交换元素时,相等元素不会发生交换,因此是稳定的排序算法。
- 选择排序:在选择最小(大)元素并交换位置时,可能会改变相等元素的相对顺序,所以是不稳定的排序算法。
- 插入排序:在插入元素时,相等元素会插入到相等元素的后面,相对顺序不变,是稳定的排序算法。
- 快速排序:在划分过程中,相等元素可能会被交换到不同的部分,导致相对顺序改变,是不稳定的排序算法。
- 归并排序:在合并过程中,相等元素会按照原顺序放入新数组,是稳定的排序算法。
5. 适用场景
- 冒泡排序、选择排序和插入排序:由于它们的时间复杂度较高,通常适用于数据规模较小的情况。插入排序在数据基本有序时表现较好。
- 快速排序:适用于数据规模较大且分布随机的情况,是一种高效的排序算法,但在最坏情况下性能较差。
- 归并排序:适用于数据规模较大且对稳定性有要求的情况,虽然需要额外的空间,但能保证稳定的 O(nlogn) 时间复杂度。
总结
排序算法 | 原理 | 时间复杂度(最好 / 平均 / 最坏) | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|---|
冒泡排序 | 比较相邻元素并交换 | O(n) / O(n2) / O(n2) | O(1) | 稳定 | 数据规模小 |
选择排序 | 选择最小(大)元素放到合适位置 | O(n2) / O(n2) / O(n2) | O(1) | 不稳定 | 数据规模小 |
插入排序 | 将元素插入已排序序列 | O(n) / O(n2) / O(n2) | O(1) | 稳定 | 数据基本有序且规模小 |
快速排序 | 分治,选基准值划分 | O(nlogn) / O(nlogn) / O(n2) | O(logn) / O(n) | 不稳定 | 数据规模大且随机 |
归并排序 | 分治,拆分并合并 | O(nlogn) / O(nlogn) / O(nlogn) | O(n) |