前言
程序员常用的算法种类繁多,每种算法都有其特定的应用场景和优势。以下是一些常见的算法类型及其简要描述。
排序算法
排序算法是将一组数据元素按照某种顺序(如升序或降序)进行排列的过程。这些算法通常用于处理大量数据,以优化搜索、存储和计算效率。
冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
public class BubbleSortExample {
public static void main(String[] args) {
// 初始化一个未排序的整数数组
int[] array = {64, 34, 25, 12, 22, 11, 90};
// 调用冒泡排序方法
bubbleSort(array);
// 打印排序后的数组
System.out.println("Sorted array:");
for (int num : array) {
System.out.print(num + " ");
}
}
/**
* 冒泡排序方法
* @param array 要排序的数组
*/
public static void bubbleSort(int[] array) {
int n = array.length;
// 外层循环控制所有趟排序
for (int i = 0; i < n - 1; i++) {
// 内层循环负责一趟排序过程中的相邻元素两两比较
for (int j = 0; j < n - i - 1; j++) {
// 如果前一个元素大于后一个元素,则交换它们的位置
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
}
示例中:
2. 我们首先定义了一个未排序的整数数组 array
。
3. 然后我们调用 bubbleSort
方法来对数组进行排序。
4. bubbleSort
方法包含两个嵌套的循环。外层循环控制排序的趟数,内层循环负责每一趟排序过程中的相邻元素比较和可能的交换。
5. 在内层循环中,我们比较相邻的两个元素 array[j]
和 array[j + 1]
。如果前一个元素大于后一个元素(即顺序错误),我们就交换它们的位置。
6. 当内层循环结束后,当前最大的元素就像气泡一样“浮”到了它应该在的位置(数组的末尾)。
7. 外层循环继续执行下一趟排序,直到没有更多的元素需要交换,即数组已经排序完成。
8. 最后,我们打印出排序后的数组。
注:冒泡排序的效率并不是很高,对于大型数据集,其性能可能较差。它的时间复杂度是 O(n^2),其中 n 是数组的长度。因此,在实际应用中,通常会选择更高效的排序算法,如快速排序、归并排序等。不过,冒泡排序由于其简单性和易于理解
快速排序
快速排序(Quick Sort)是一种高效的排序算法,它使用了分治法的策略。通过选择一个基准元素(pivot),将数组分为两部分,一部分包含所有比基准元素小的元素,另一部分包含所有比基准元素大的元素,然后对这两部分递归地应用快速排序算法。
public class QuickSortExample {
public static void main(String[] args) {
// 初始化一个未排序的整数数组
int[] array = {64, 34, 25, 12, 22, 11, 90};
// 调用快速排序方法
quickSort(array, 0, array.length - 1);
// 打印排序后的数组
System.out.println("Sorted array:");
for (int num : array) {
System.out.print(num + " ");
}
}
/**
* 快速排序方法
* @param array 要排序的数组
* @param low 数组的起始索引
* @param high 数组的结束索引
*/
public static void quickSort(int[] array, int low, int high) {
if (low < high) {
// 划分操作,返回基准元素的索引
int pivotIndex = partition(array, low, high);
// 递归地对基准元素左边和右边的子数组进行排序
quickSort(array, low, pivotIndex - 1);
quickSort(array, pivotIndex + 1, high);
}
}
/**
* 划分操作
* @param array 要排序的数组
* @param low 数组的起始索引
* @param high 数组的结束索引
* @return 基准元素的索引
*/
public static int partition(int[] array, int low, int high) {
// 选择最右边的元素作为基准元素
int pivot = array[high];
int i = low - 1; // 小于基准元素的元素的索引
for (int j = low; j < high; j++) {
// 如果当前元素小于或等于基准元素
if (array[j] <= pivot) {
i++; // 增加小于基准元素的元素的索引
// 交换 array[i] 和 array[j]
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// 将基准元素放到正确的位置
int temp = array[i + 1];
array[i + 1] = array[high];
array[high] = temp;
// 返回基准元素的索引
return i + 1;
}
}
示例中:
quickSort
方法是快速排序的主要方法,它接受一个数组以及要排序部分的起始和结束索引。partition
方法执行划分操作,它选择一个基准元素(在这个示例中,我们选择最右边的元素),然后将数组中小于基准元素的元素移动到基准元素的左边,大于基准元素的元素移动到基准元素的右边。这个过程完成后,基准元素就处于其最终排序位置。- 在
partition
方法中,我们使用两个指针i
和j
。i
指向小于基准元素的元素的最后一个位置,j
则用于遍历数组中的元素。 - 当
array[j]
小于或等于基准元素时,我们增加i
的值,并交换array[i]
和array[j]
的位置。 - 在遍历完所有元素后,将基准元素放到正确的位置,即
array[i + 1]
。 quickSort
方法递归地对基准元素左边和右边的子数组进行排序,直到整个数组排序完成。- 最后,我们打印出排序后的数组。
快速排序是一种原地、不稳定的排序算法,其平均时间复杂度为 O(n log n),但在最坏情况下(例如输入数组已经排序或逆序)会退化到 O(n^2)。在实际应用中,通常会通过随机化选择基准元素或使用三数取中等策略来避免最坏情况的发生。
归并排序
归并排序(Merge Sort)是一种分治思想的排序算法,它将一个待排序的数组拆分成若干个子数组,每个子数组是一个有序的序列,然后再将有序子数组合并,得到完全有序的数组。
public class MergeSortExample {
public static void main(String[] args) {
// 初始化一个未排序的整数数组
int[] array = {64, 34, 25, 12, 22, 11, 90};
// 调用归并排序方法
mergeSort(array, 0, array.length - 1);
// 打印排序后的数组
System.out.println("Sorted array:");
for (int num : array) {
System.out.print(num + " ");
}
}
/**
* 归并排序方法
* @param array 要排序的数组
* @param left 数组的左边界
* @param right 数组的右边界
*/
public static void mergeSort(int[] array, int left, int right) {
if (left < right) {
// 找到中间位置
int mid = (left + right) / 2;
// 递归地对左半部分进行归并排序
mergeSort(array, left, mid);
// 递归地对右半部分进行归并排序
mergeSort(array, mid + 1, right);
// 合并两个有序数组
merge(array, left, mid, right);
}
}
/**
* 合并两个有序数组
* @param array 要合并的数组
* @param leftLeft 左半部分的左边界
* @param leftRight 左半部分的右边界
* @param rightRight 右半部分的右边界
*/
public static void merge(int[] array, int leftLeft, int leftRight, int rightRight) {
// 临时数组
int[] temp = new int[rightRight - leftLeft + 1];
int i = leftLeft; // 左半部分的指针
int j = leftRight + 1; // 右半部分的指针
int k = 0; // 临时数组的指针
// 将较小的元素复制到临时数组
while (i <= leftRight && j <= rightRight) {
if (array[i] <= array[j]) {
temp[k++] = array[i++];
} else {
temp[k++] = array[j++];
}
}
// 复制左半部分或右半部分剩余的元素到临时数组
while (i <= leftRight) {
temp[k++] = array[i++];
}
while (j <= rightRight) {
temp[k++] = array[j++];
}
// 将临时数组的元素复制回原数组
for (k = 0; k < temp.length; k++) {
array[leftLeft + k] = temp[k];
}
}
}
示例中:
mergeSort
方法是归并排序的递归入口点,它接收一个数组以及要排序部分的左边界和右边界。- 如果左边界小于右边界,说明还有元素需要排序,那么方法会找到中间位置,并递归地对左半部分和右半部分进行归并排序。
merge
方法用于合并两个已经排序好的子数组。它使用临时数组来存储合并后的结果,并通过比较左右两个子数组的元素来填充临时数组。- 最后,将临时数组中的元素复制回原数组,完成合并操作。
- 当所有的递归调用都返回后,整个数组就被排序好了。
- 我们打印出排序后的数组,以验证排序结果。
归并排序是一种稳定的排序算法,时间复杂度为 O(n log n),其中 n 是数组的长度。由于归并排序在合并过程中需要额外的空间,所以其空间复杂度也是 O(n)。尽管归并排序不是原地排序算法,但由于其良好的性能特点,它在实际应用中仍然非常有用,特别是在数据量较大且对稳定性有要求的情况下。
堆排序
堆排序(Heap Sort)是一种利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。堆排序可以分为两个主要步骤:建堆和堆调整排序。
public class HeapSortExample {
public static void main(String[] args) {
// 初始化一个未排序的整数数组
int[] array = {64, 34, 25, 12, 22, 11, 90};
// 调用堆排序方法
heapSort(array);
// 打印排序后的数组
System.out.println("Sorted array:");
for (int num : array) {
System.out.print(num + " ");
}
}
/**
* 堆排序方法
* @param array 要排序的数组
*/
public static void heapSort(int[] array) {
// 构建大顶堆
buildMaxHeap(array, array.length);
// 依次将堆顶元素与末尾元素交换,并调整堆结构
for (int i = array.length - 1; i > 0; i--) {
// 将堆顶元素与末尾元素交换
swap(array, 0, i);
// 重新调整堆结构
maxHeapify(array, 0, i);
}
}
/**
* 构建大顶堆
* @param array 要构建堆的数组
* @param length 数组的长度
*/
public static void buildMaxHeap(int[] array, int length) {
// 从最后一个非叶子节点开始向上构建堆
for (int i = length / 2 - 1; i >= 0; i--) {
maxHeapify(array, i, length);
}
}
/**
* 调整堆结构,使其满足大顶堆的性质
* @param array 数组
* @param root 根节点的索引
* @param heapSize 当前堆的大小
*/
public static void maxHeapify(int[] array, int root, int heapSize) {
int largest = root; // 初始化最大值为根节点
int left = 2 * root + 1; // 左子节点索引
int right = 2 * root + 2; // 右子节点索引
// 如果左子节点比根节点大,则更新最大值
if (left < heapSize && array[left] > array[largest]) {
largest = left;
}
// 如果右子节点比当前最大值大,则更新最大值
if (right < heapSize && array[right] > array[largest]) {
largest = right;
}
// 如果最大值不是根节点,则交换根节点和最大值,并继续调整
if (largest != root) {
swap(array, root, largest);
maxHeapify(array, largest, heapSize);
}
}
/**
* 交换数组中两个元素的位置
* @param array 数组
* @param i 第一个元素的索引
* @param j 第二个元素的索引
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
示例中:
heapSort
方法是堆排序的入口点,它首先调用buildMaxHeap
方法构建一个大顶堆,然后依次将堆顶元素(最大值)与数组末尾元素交换,并对剩余元素重新调整堆结构。buildMaxHeap
方法从最后一个非叶子节点开始,向上逐层调用maxHeapify
方法来构建大顶堆。maxHeapify
方法是调整堆结构的核心,它保证以指定节点为根的子树满足大顶堆的性质。方法通过比较当前节点的值与其左右子节点的值,找到最大值,如果最大值不是当前节点,则交换它们的位置,并递归地对受影响的子树进行同样的操作。swap
方法是一个简单的辅助方法,用于交换数组中两个元素的位置。- 当所有的堆调整操作完成后,数组就被排序好了
搜索算法
二分查找
二分查找(Binary Search)是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。
public class BinarySearchExample {
public static void main(String[] args) {
int[] array = {2, 3, 4, 10, 40}; // 必须是有序数组
int target = 10;
int index = binarySearch(array, target);
if (index != -1) {
System.out.println("元素 " + target + " 在数组中的索引为: " + index);
} else {
System.out.println("元素 " + target + " 不在数组中");
}
}
/**
* 二分查找方法
* @param array 有序数组
* @param target 要查找的目标元素
* @return 如果找到目标元素,则返回其在数组中的索引;否则返回-1
*/
public static int binarySearch(int[] array, int target) {
int left = 0; // 搜索区间的左边界
int right = array.length - 1; // 搜索区间的右边界
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (array[mid] == target) {
// 找到目标元素,返回其索引
return mid;
} else if (array[mid] < target) {
// 目标元素在右半部分
left = mid + 1;
} else {
// 目标元素在左半部分
right = mid - 1;
}
}
// 没有找到目标元素
return -1;
}
}
示例中:
main
方法初始化了一个有序数组array
和一个目标元素target
。binarySearch
方法是二分查找的核心实现。它使用left
和right
两个变量来维护当前的搜索区间。在每次循环中,它计算中间元素的索引mid
,并将array[mid]
与target
进行比较。- 如果
array[mid]
等于target
,则找到了目标元素,返回其索引。 - 如果
array[mid]
小于target
,则目标元素必定在右半部分,因此更新left
为mid + 1
。 - 如果
array[mid]
大于target
,则目标元素必定在左半部分,因此更新right
为mid - 1
。 - 如果循环结束时仍未找到目标元素,则返回
-1
。
注:二分查找的前提是数组必须是有序的。如果数组无序,则无法正确执行二分查找。
深度优先搜索
深度优先搜索(Depth-First Search, DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。当节点v的所在边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
import java.util.*;
public class DepthFirstSearchExample {
// 使用邻接列表表示图
private List<List<Integer>> adjList;
private boolean[] visited;
public DepthFirstSearchExample(int vertices) {
adjList = new ArrayList<>(vertices);
visited = new boolean[vertices];
// 初始化邻接列表
for (int i = 0; i < vertices; i++) {
adjList.add(new ArrayList<>());
}
}
// 添加边
public void addEdge(int src, int dest) {
adjList.get(src).add(dest);
}
// 深度优先搜索方法
public void dfs(int startVertex) {
visited[startVertex] = true;
System.out.print(startVertex + " ");
// 遍历当前顶点的所有相邻顶点
for (int neighbor : adjList.get(startVertex)) {
if (!visited[neighbor]) {
dfs(neighbor);
}
}
}
public static void main(String[] args) {
DepthFirstSearchExample graph = new DepthFirstSearchExample(5);
// 添加边
graph.addEdge(0, 1);
graph.addEdge(0, 2);
graph.addEdge(1, 2);
graph.addEdge(2, 0);
graph.addEdge(2, 3);
graph.addEdge(3, 3);
System.out.println("深度优先搜索(从顶点 2 开始):");
graph.dfs(2);
}
}
示例中:
DepthFirstSearchExample
类包含了一个邻接列表adjList
来表示图,和一个布尔数组visited
来跟踪哪些顶点已经被访问过。addEdge
方法用于向图中添加边。在这个例子中,图是无向的,所以添加边时需要在两个顶点的邻接列表中都添加对方。dfs
方法是深度优先搜索的核心。它首先标记当前顶点为已访问,并打印它。然后,它遍历当前顶点的所有未访问过的相邻顶点,并对每个相邻顶点递归调用dfs
方法。- 在
main
方法中,我们创建了一个DepthFirstSearchExample
对象,并添加了一些边来形成一个图。然后,我们从顶点 2 开始调用dfs
方法进行深度优先搜索,并打印出访问的顶点顺序。
注:深度优先搜索可能会因为图的结构和起始顶点的不同而得到不同的访问顺序。
图算法
图算法是计算机科学中用于处理图形结构数据的算法集合。图由顶点(或节点)和边组成,边连接顶点。图算法通常用于解决路径查找、网络流、最小生成树、最短路径等问题。
最短路径算法
最短路径算法是图论中用于解决从一个顶点到另一个顶点最短路径问题的算法。常见的最短路径算法有Dijkstra
算法和Floyd-Warshall
算法。
以下是Dijkstra
算法的Java实现示例:
import java.util.*;
public class DijkstraAlgorithmExample {
// 图的顶点类
static class Vertex implements Comparable<Vertex> {
int id;
double distance;
public Vertex(int id, double distance) {
this.id = id;
this.distance = distance;
}
@Override
public int compareTo(Vertex other) {
return Double.compare(this.distance, other.distance);
}
}
// 图的数据结构
private List<List<Vertex>> graph;
private double[] distances;
private boolean[] visited;
public DijkstraAlgorithmExample(int numVertices) {
graph = new ArrayList<>(numVertices);
for (int i = 0; i < numVertices; i++) {
graph.add(new ArrayList<>());
}
distances = new double[numVertices];
visited = new boolean[numVertices];
}
// 添加带权重的边
public void addEdge(int src, int dest, double weight) {
graph.get(src).add(new Vertex(dest, weight));
}
// Dijkstra算法实现
public void dijkstra(int startVertex) {
// 初始化距离数组
Arrays.fill(distances, Double.POSITIVE_INFINITY);
distances[startVertex] = 0;
// 使用优先队列存储待访问的顶点,并按距离排序
PriorityQueue<Vertex> queue = new PriorityQueue<>();
queue.add(new Vertex(startVertex, 0));
while (!queue.isEmpty()) {
// 取出当前距离最短的顶点
Vertex currentVertex = queue.poll();
int currentId = currentVertex.id;
// 如果该顶点已经被访问过,则跳过
if (visited[currentId]) {
continue;
}
visited[currentId] = true;
// 更新相邻顶点的距离
for (Vertex neighbor : graph.get(currentId)) {
int neighborId = neighbor.id;
double newDistance = distances[currentId] + neighbor.distance;
if (newDistance < distances[neighborId]) {
distances[neighborId] = newDistance;
queue.add(new Vertex(neighborId, newDistance));
}
}
}
}
// 打印最短路径结果
public void printDistances(int startVertex) {
System.out.println("从顶点 " + startVertex + " 到其他顶点的最短距离:");
for (int i = 0; i < distances.length; i++) {
System.out.println("到顶点 " + i + " 的距离: " + distances[i]);
}
}
public static void main(String[] args) {
DijkstraAlgorithmExample graph = new DijkstraAlgorithmExample(5);
// 添加边及其权重
graph.addEdge(0, 1, 1);
graph.addEdge(0, 3, 6);
graph.addEdge(1, 2, 5);
graph.addEdge(1, 3, 2);
graph.addEdge(1, 4, 1);
graph.addEdge(2, 3, 4);
graph.addEdge(3, 4, 3);
// 从顶点0开始计算最短路径
graph.dijkstra(0);
// 打印结果
graph.printDistances(0);
}
}
示例中:
DijkstraAlgorithmExample
类包含了一个图的数据结构,用列表的列表表示。graph
中的每个内部列表包含了指向当前顶点的所有边的信息,包括目标顶点和权重。addEdge
方法用于向图中添加带权重的边。dijkstra
方法是Dijkstra算法的实现。它使用优先队列(PriorityQueue
)来维护待访问的顶点,并根据它们的当前最短距离进行排序。算法迭代地选择当前距离最短的顶点
最小生成树算法
最小生成树(Minimum Spanning Tree, MST)是图论中的一个经典问题,通常用于在给定的加权无向图中找到一棵边权值和最小的生成树。常见的求解最小生成树的算法有两种:Prim
算法和Kruskal
算法。
Prim算法
Prim算法从任意一个顶点开始,每次选择与当前生成树距离最短的顶点,并将其与生成树连接起来。重复这个过程,直到所有顶点都加入到生成树中。
import java.util.*;
class Graph {
int V; // 顶点数量
List<List<Edge>> adj; // 邻接表表示图
class Edge implements Comparable<Edge> {
int src, dest, weight;
Edge(int src, int dest, int weight) {
this.src = src;
this.dest = dest;
this.weight = weight;
}
@Override
public int compareTo(Edge compareEdge) {
return this.weight - compareEdge.weight;
}
}
Graph(int v) {
V = v;
adj = new ArrayList<>(v);
for (int i = 0; i < v; ++i)
adj.add(new ArrayList<>());
}
void addEdge(int v, int w, int e) {
adj.get(v).add(new Edge(v, w, e));
}
void primMST() {
PriorityQueue<Edge> pq = new PriorityQueue<>();
boolean[] inMST = new boolean[V];
int[] parent = new int[V];
int[] key = new int[V];
Arrays.fill(key, Integer.MAX_VALUE);
key[0] = 0; // 第一个顶点作为起始点
pq.add(new Edge(0, 0, 0)); // 初始边,权重为0
while (!pq.isEmpty()) {
Edge u = pq.poll();
int src = u.src;
if (inMST[src])
continue;
inMST[src] = true;
Iterator<Edge> i = adj.get(src).iterator();
while (i.hasNext()) {
Edge e = i.next();
int dest = e.dest;
if (!inMST[dest] && e.weight < key[dest]) {
parent[dest] = src;
key[dest] = e.weight;
pq.add(e);
}
}
}
printMST(parent, key);
}
void printMST(int[] parent, int[] key) {
System.out.println("Edge \tWeight");
for (int i = 1; i < V; ++i)
System.out.println(parent[i] + " - " + i + "\t" + key[i]);
}
public static void main(String args[]) {
Graph g = new Graph(5);
g.addEdge(0, 1, 2);
g.addEdge(0, 3, 6);
g.addEdge(1, 2, 3);
g.addEdge(1, 3, 8);
g.addEdge(1, 4, 5);
g.addEdge(2, 4, 7);
g.addEdge(3, 4, 9);
g.primMST();
}
}
Kruskal算法
Kruskal算法的基本思想是从小到大选择边,每次选择的边连接的两个顶点必须不在同一个连通分量中,这样可以保证最终得到的是一棵树。为了实现这个算法,我们需要一个数据结构来高效地检查两个顶点是否在同一连通分量中,通常使用并查集(Union-Find)数据结构。
import java.util.*;
class Graph {
int V; // 顶点数量
List<Edge> edges; // 存储所有的边
class Edge implements Comparable<Edge> {
int src, dest, weight;
Edge(int src, int dest, int weight) {
this.src = src;
this.dest = dest;
this.weight = weight;
}
@Override
public int compareTo(Edge compareEdge) {
return this.weight - compareEdge.weight;
}
}
// 并查集类
class DisjointSet {
int[] parent;
int[] rank;
DisjointSet(int v) {
parent = new int[v];
rank = new int[v];
for (int i = 0; i < v; i++) {
parent[i] = i;
rank[i] = 0;
}
}
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]);
return parent[x];
}
void union(int x, int y) {
int xroot = find(x);
int yroot = find(y);
if (xroot == yroot) return;
if (rank[xroot] < rank[yroot])
parent[xroot] = yroot;
else if (rank[xroot] > rank[yroot])
parent[yroot] = xroot;
else {
parent[yroot] = xroot;
rank[xroot]++;
}
}
}
Graph(int v) {
V = v;
edges = new ArrayList<>();
}
void addEdge(int src, int dest, int weight) {
edges.add(new Edge(src, dest, weight));
}
void kruskalMST() {
Collections.sort(edges); // 对边按照权重排序
DisjointSet ds = new DisjointSet(V);
List<Edge> mst = new ArrayList<>(); // 存储最小生成树的边
for (Edge e : edges) {
int x = ds.find(e.src);
int y = ds.find(e.dest);
// 如果两个顶点不在同一个集合(即它们不连通),则添加这条边到最小生成树中
if (x != y) {
mst.add(e);
ds.union(x, y); // 合并两个集合
}
}
// 打印最小生成树的边
System.out.println("Edges in the constructed MST");
for (Edge e : mst)
System.out.println(e.src + " -- " + e.dest + " == " + e.weight);
}
public static void main(String args[]) {
Graph g = new Graph(4);
g.addEdge(0, 1, 10);
g.addEdge(0, 2, 6);
g.addEdge(0, 3, 5);
g.addEdge(1, 3, 15);
g.addEdge(2, 3, 4);
g.kruskalMST();
}
}
示例中:
- 首先定义了一个
Graph
类,该类包含一个边的列表edges
以及一个内部类Edge
来表示图的边 - 我们还定义了一个
DisjointSet
类来实现并查集的功能。 kruskalMST
方法则实现了Kruskal算法:先对边进行排序,然后遍历排序后的边列表,对于每条边,检查它的两个端点是否属于同一个集合。如果不属于同一个集合,则将它们添加到最小生成树中,并将两个集合合并。
动态规划算法
动态规划(Dynamic Programming,简称 DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
/**
** 假设我们有一个背包,其最大承重为 W 公斤。我们有一组物品,每个物品都有自己的重量和价值。
** 问题是要找出在不超过背包最大承重的前提下,如何选取物品,使得背包中物品的总价值最大。
**/
public class KnapsackProblem {
// 物品的重量数组
private int[] weights;
// 物品的价值数组
private int[] values;
// 背包的最大承重
private int W;
// dp[i][j] 表示前 i 个物品,在背包承重不超过 j 的情况下的最大价值
private int[][] dp;
public KnapsackProblem(int[] weights, int[] values, int W) {
this.weights = weights;
this.values = values;
this.W = W;
this.dp = new int[weights.length + 1][W + 1];
}
public int solve() {
// 初始化 dp 数组
for (int i = 0; i <= weights.length; i++) {
for (int j = 0; j <= W; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 0;
}
}
}
// 动态规划填表
for (int i = 1; i <= weights.length; i++) {
for (int j = 1; j <= W; j++) {
if (weights[i - 1] <= j) {
// 当前物品可以放入背包时,比较放入和不放入两种情况下的最大价值
dp[i][j] = Math.max(values[i - 1] + dp[i - 1][j - weights[i - 1]], dp[i - 1][j]);
} else {
// 当前物品不能放入背包时,只能使用前 i-1 个物品
dp[i][j] = dp[i - 1][j];
}
}
}
// 返回最终结果
return dp[weights.length][W];
}
public static void main(String[] args) {
int[] weights = {2, 3, 4, 5}; // 物品的重量
int[] values = {3, 4, 5, 6}; // 物品的价值
int W = 5; // 背包的最大承重
KnapsackProblem kp = new KnapsackProblem(weights, values, W);
int maxValue = kp.solve();
System.out.println("背包中物品的最大价值为: " + maxValue);
}
}
示例中
- 定义了一个
KnapsackProblem
类,它包含一个solve
方法用于求解背包问题。我们创建了一个二维数组dp
来存储子问题的解,其中dp[i][j]
表示前i
个物品在背包承重不超过j
的情况下的最大价值。 - 通过遍历物品和背包的承重来填充
dp
数组。对于每个物品,我们检查它是否可以放入当前承重的背包中。如果可以,我们比较放入和不放入两种情况下的最大价值;如果不可以,我们只能使用前i-1
个物品。 solve
方法返回dp[weights.length][W]
,即所有物品都考虑在内,且背包承重为W
时的最大价值。
注:在实际应用中,我们可能需要根据具体问题调整状态的定义、状态转移方程和边界条件。
贪心算法
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法并不总是能得到最优解,但是对于很多问题它能产生很好的近似最优解。
下面是一个使用贪心算法解决找零问题的 Java 示例:
/**
** 假设我们有面值为 1, 5, 10, 25 的硬币,我们需要找出最少硬币数量来凑齐一定金额的钱。
**/
import java.util.Arrays;
public class GreedyChange {
// 硬币面值数组,按照面值从大到小排序
private static final int[] COIN_VALUES = {25, 10, 5, 1};
public static int minCoins(int amount) {
if (amount < 0) {
return -1; // 如果金额为负,返回错误值
}
int[] dp = new int[amount + 1]; // 初始化dp数组,dp[i]表示凑齐金额i所需的最少硬币数
Arrays.fill(dp, amount + 1); // 初始化为一个比目标金额大的数
dp[0] = 0; // 凑齐金额为0所需的最少硬币数为0
for (int i = 1; i <= amount; i++) {
for (int coin : COIN_VALUES) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount]; // 如果无法凑齐金额,返回错误值
}
public static void main(String[] args) {
int amount = 63; // 假设需要凑齐63元
int minCoinsNeeded = minCoins(amount);
if (minCoinsNeeded != -1) {
System.out.println("最少需要硬币数量:" + minCoinsNeeded);
} else {
System.out.println("无法凑齐金额。");
}
}
}
示例中,这里使用动态规划的思想,但是采用了贪心算法的策略。
- 初始化一个
dp
数组来存储凑齐每个金额所需的最少硬币数,然后从金额为 1 开始,逐步计算到目标金额amount
。 - 对于每个金额,我们尝试使用每一种硬币,并更新
dp
数组。我们选择硬币时,总是优先使用面值最大的硬币,这是贪心策略的关键。
注:贪心算法并不是在所有情况下都能得到最优解,但在这个找零问题中,由于硬币面值的设定,贪心算法能够得到最优解。在其他问题中,可能需要进一步分析才能确定贪心算法是否适用。
分治算法
分治算法(Divide and Conquer)是一种将大问题分解为几个小问题,分别解决小问题,然后将小问题的解合并起来,从而得到大问题的解的算法。这种算法在许多场景中都非常有用,特别是在处理大规模数据或复杂问题时。
下面是一个使用分治算法解决归并排序问题的 Java 示例:
public class MergeSort {
public static void main(String[] args) {
int[] arr = {7, 3, 20, 4, 1, 15, 6, 25, 8, 10};
System.out.println("原始数组:");
printArray(arr);
mergeSort(arr, 0, arr.length - 1);
System.out.println("\n排序后的数组:");
printArray(arr);
}
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
// 找到中间位置
int mid = (left + right) / 2;
// 对左半部分进行排序
mergeSort(arr, left, mid);
// 对右半部分进行排序
mergeSort(arr, mid + 1, right);
// 合并左右两部分
merge(arr, left, mid, right);
}
}
public static void merge(int[] arr, int left, int mid, int right) {
// 创建一个临时数组,用于存储合并后的结果
int[] temp = new int[right - left + 1];
// 初始化两个指针,分别指向左半部分和右半部分的起始位置
int i = left, j = mid + 1, k = 0;
// 合并两个有序数组到临时数组中
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 复制左半部分剩余的元素到临时数组中
while (i <= mid) {
temp[k++] = arr[i++];
}
// 复制右半部分剩余的元素到临时数组中
while (j <= right) {
temp[k++] = arr[j++];
}
// 将临时数组中的元素复制回原数组
for (i = 0; i < temp.length; i++) {
arr[left + i] = temp[i];
}
}
public static void printArray(int[] arr) {
for (int num : arr) {
System.out.print(num + " ");
}
System.out.println();
}
}
示例中:
mergeSort
方法是分治算法的核心。它将数组分为左右两部分,并递归地对这两部分进行排序。当递归到最底层时,每个子数组只包含一个元素,因此它们已经是有序的。- 通过
merge
方法将有序的子数组合并成一个大的有序数组。这个过程一直递归到原始数组被完全排序。
注:分治算法的优点在于它将大问题分解为小问题,从而降低了问题的复杂度。在归并排序中,通过将数组不断地二分,每次处理的子数组规模都在减小,这使得算法的时间复杂度达到了 O(n log n),其中 n 是数组的长度。这使得归并排序在处理大规模数据时非常高效。
后语
这只是程序员常用算法的一部分,实际上还有很多其他类型的算法,如哈希算法、字符串匹配算法等,每种算法都有其特定的用途和优势。选择哪种算法取决于具体的问题和需求。