Java 常见算法
一、目录
- 排序算法
- 冒泡排序
- 快速排序
- 希尔排序
- 堆排序
- 计数排序
- 基数排序
- 查找算法
- 顺序查找
- 二分查找
- 贪心算法
- 动态规划算法
- 回溯算法
- 分治算法
- 图算法
- Prim 算法
- Kruskal 算法
- Dijkstra 算法
- 其他算法
- 斐波那契堆
二、排序算法
冒泡排序
原理:重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
boolean swapped;
do {
swapped = false;
for (int i = 0; i < n - 1; i++) {
if (arr[i] > arr[i + 1]) {
// 交换 arr[i+1] 和 arr[i]
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
swapped = true;
}
}
n--;
} while (swapped);
}
}
快速排序
原理:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
if (high - low + 1 < 10) {
insertionSort(arr, low, high);
} else {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = medianOfThree(arr, low, high);
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
private static int medianOfThree(int[] arr, int low, int high) {
int mid = low + (high - low) / 2;
if (arr[low] > arr[mid]) {
swap(arr, low, mid);
}
if (arr[low] > arr[high]) {
swap(arr, low, high);
}
if (arr[mid] > arr[high]) {
swap(arr, mid, high);
}
swap(arr, mid, high - 1);
return arr[high - 1];
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
private static void insertionSort(int[] arr, int low, int high) {
for (int i = low + 1; i <= high; i++) {
int key = arr[i];
int j = i - 1;
while (j >= low && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
}
希尔排序
原理:希尔排序是插入排序的一种改进版本。它先将待排序的数组分割成若干个子序列,分别对这些子序列进行插入排序,然后逐步减少子序列的间隔,重复进行插入排序,直到间隔为 1 时,就相当于对整个数组进行了一次插入排序。
public class ShellSort {
public static void shellSort(int[] arr) {
int n = arr.length;
int gap = n / 2;
while (gap > 0) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
gap /= 2;
}
}
}
堆排序
原理:堆排序利用了二叉堆这种数据结构。首先将待排序的数组构建成一个最大堆(或最小堆),然后将堆顶元素(最大或最小元素)与数组的最后一个元素交换,再对剩余的元素进行堆调整,重复这个过程,直到整个数组有序。
public class HeapSort {
public static void heapSort(int[] arr) {
int n = arr.length;
// 自下而上构建最大堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 逐个取出堆顶元素并调整堆
for (int i = n - 1; i > 0; i--) {
// 交换堆顶和当前最后一个元素
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对新的堆进行调整
heapify(arr, i, 0);
}
}
private static void heapify(int[] arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest!= i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// 递归调整子树
heapify(arr, n, largest);
}
}
}
计数排序
原理:计数排序假设输入的数据都在一个确定的范围内,通过统计每个元素在输入数组中出现的次数,然后根据统计结果将每个元素放到正确的位置上。
public class CountingSort {
public static void countingSort(int[] arr) {
if (arr.length == 0) {
return;
}
int minVal = findMin(arr);
int maxVal = findMax(arr);
int range = maxVal - minVal + 1;
int[] count = new int[range];
int[] output = new int[arr.length];
// 统计每个元素出现的次数
for (int i = 0; i < arr.length; i++) {
count[arr[i] - minVal]++;
}
// 计算累计次数,确定每个元素在输出数组中的位置
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// 将元素放入输出数组
for (int i = arr.length - 1; i >= 0; i--) {
output[count[arr[i] - minVal] - 1] = arr[i];
count[arr[i] - minVal]--;
}
// 将排序后的结果复制回原数组
System.arraycopy(output, 0, arr, 0, arr.length);
}
private static int findMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
private static int findMin(int[] arr) {
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < min) {
min = arr[i];
}
}
return min;
}
}
基数排序
原理:基数排序是一种非比较型整数排序算法。它将整数按位数切割成不同的数字,然后按每个位数分别进行比较和排序。先从最低有效位开始进行排序,然后逐步向高位进行,直到对最高有效位排序完成。
public class RadixSort {
public static void radixSort(int[] arr) {
int max = findMax(arr);
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortForRadix(arr, exp);
}
}
private static void countingSortForRadix(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10];
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
System.arraycopy(output, 0, arr, 0, n);
}
private static int findMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
}
三、查找算法
顺序查找
原理:从数组的第一个元素开始,逐个与要查找的关键字进行比较,直到找到为止。
public class SequentialSearch {
public static int sequentialSearch(int[] arr, int key) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == key) {
return i;
}
}
return -1;
}
}
二分查找
原理:也称为折半查找,要求待查找的数组是已排序的。每次取中间位置的元素与要查找的关键字进行比较,如果相等则返回中间位置;如果关键字小于中间元素,则在左半部分继续查找;如果关键字大于中间元素,则在右半部分继续查找。
public class BinarySearch {
public static int binarySearch(int[] arr, int key) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] == key) {
return mid;
} else if (arr[mid] < key) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
}
四、贪心算法
贪心算法是一种在每一步选择中都采取当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。
例如,活动选择问题:有一组活动,每个活动有开始时间和结束时间,要求选择出尽可能多的活动,使得这些活动之间互不冲突。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class ActivitySelection {
public static List<Integer> selectActivities(int[] startTimes, int[] endTimes) {
int n = startTimes.length;
int[][] activities = new int[n][2];
for (int i = 0; i < n; i++) {
activities[i][0] = startTimes[i];
activities[i][1] = endTimes[i];
}
Arrays.sort(activities, Comparator.comparingInt(a -> a[1]));
List<Integer> selectedActivities = new ArrayList<>();
int lastEndTime = 0;
for (int i = 0; i < n; i++) {
if (activities[i][0] >= lastEndTime) {
selectedActivities.add(i);
lastEndTime = activities[i][1];
}
}
return selectedActivities;
}
}
五、动态规划算法
动态规划算法通常用于解决具有重叠子问题和最优子结构性质的问题。
例如,求解斐波那契数列问题,使用动态规划可以避免重复计算。
public class Fibonacci {
public static int fibonacci(int n) {
if (n <= 1) {
return n;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
六、回溯算法
回溯算法是一种通过穷举所有可能情况来找到所有解或最优解的算法。它通常采用深度优先搜索策略,在搜索过程中,当发现当前路径不可能得到解时,就回溯到上一步,尝试其他可能的路径。
例如,八皇后问题:在 8×8 的国际象棋棋盘上放置八个皇后,使得任意两个皇后都不能互相攻击。
public class EightQueens {
private static final int N = 8;
public static void solveEightQueens() {
int[] board = new int[N];
placeQueens(board, 0);
}
private static void placeQueens(int[] board, int row) {
if (row == N) {
printBoard(board);
return;
}
for (int col = 0; col < N; col++) {
if (isSafe(board, row, col)) {
board[row] = col;
placeQueens(board, row + 1);
}
}
}
private static boolean isSafe(int[] board, int row, int col) {
for (int i = 0; i < row; i++) {
int diff = Math.abs(board[i] - col);
七、分治算法
分治算法的基本思想是将一个大问题分解为若干个规模较小、相互独立且与原问题形式相同的子问题,分别求解这些子问题,然后再将子问题的解合并起来,得到原问题的解。
例如,归并排序:
原理:将待排序的数组不断地分割成两个子数组,直到子数组中只有一个元素为止。然后再将这些子数组合并起来,在合并的过程中进行排序。
public class MergeSort {
public static void mergeSort(int[] arr) {
if (arr.length <= 1) {
return;
}
int mid = arr.length / 2;
int[] left = new int[mid];
int[] right = new int[arr.length - mid];
System.arraycopy(arr, 0, left, 0, mid);
System.arraycopy(arr, mid, right, 0, arr.length - mid);
mergeSort(left);
mergeSort(right);
merge(arr, left, right);
}
private static void merge(int[] arr, int[] left, int[] right) {
int i = 0, j = 0, k = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
}
while (i < left.length) {
arr[k++] = left[i++];
}
while (j < right.length) {
arr[k++] = right[j++];
}
}
}
八、图算法
Prim 算法
原理:用于在连通加权无向图中找到一棵包含所有顶点且边权总和最小的生成树。从一个任意的顶点开始,每次选择一条连接已加入树的顶点和未加入树的顶点的最小权边,将新顶点加入树中,直到所有顶点都在树中。
import java.util.ArrayList;
import java.util.List;
class GraphPrim {
private int V;
private List<Edge>[] adj;
GraphPrim(int v) {
V = v;
adj = new ArrayList[v];
for (int i = 0; i < v; ++i) {
adj[i] = new ArrayList<>();
}
}
void addEdge(int u, int v, int weight) {
adj[u].add(new Edge(u, v, weight));
adj[v].add(new Edge(v, u, weight));
}
class Edge {
int source;
int destination;
int weight;
Edge(int source, int destination, int weight) {
this.source = source;
this.destination = destination;
this.weight = weight;
}
}
void primMST() {
boolean[] inMST = new boolean[V];
int[] key = new int[V];
int[] parent = new int[V];
for (int i = 0; i < V; i++) {
key[i] = Integer.MAX_VALUE;
}
key[0] = 0;
parent[0] = -1;
for (int count = 0; count < V - 1; count++) {
int u = minKey(key, inMST);
inMST[u] = true;
for (Edge edge : adj[u]) {
int v = edge.destination;
int weight = edge.weight;
if (!inMST[v] && weight < key[v]) {
parent[v] = u;
key[v] = weight;
}
}
}
printMST(parent);
}
private int minKey(int[] key, boolean[] inMST) {
int min = Integer.MAX_VALUE, minIndex = -1;
for (int v = 0; v < V; v++) {
if (!inMST[v] && key[v] < min) {
min = key[v];
minIndex = v;
}
}
return minIndex;
}
private void printMST(int[] parent) {
System.out.println("Edge Weight");
for (int i = 1; i < V; i++) {
System.out.println(parent[i] + " - " + i + " " + key[i]);
}
}
}
Kruskal 算法
原理:也是用于在连通加权无向图中找到最小生成树。将所有边按照权值从小到大排序,然后从权值最小的边开始,依次添加边到生成树中,如果添加某条边会形成环,则不添加该边,直到生成树包含所有顶点。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class GraphKruskal {
private int V;
private List<Edge> edges;
GraphKruskal(int v) {
V = v;
edges = new ArrayList<>();
}
void addEdge(int u, int v, int weight) {
edges.add(new Edge(u, v, weight));
}
class Edge implements Comparable<Edge> {
int source;
int destination;
int weight;
Edge(int source, int destination, int weight) {
this.source = source;
this.destination = destination;
this.weight = weight;
}
@Override
public int compareTo(Edge other) {
return this.weight - other.weight;
}
}
int find(int[] parent, int i) {
if (parent[i] == -1) {
return i;
}
return find(parent, parent[i]);
}
void union(int[] parent, int x, int y) {
int xset = find(parent, x);
int yset = find(parent, y);
parent[xset] = yset;
}
void kruskalMST() {
Edge[] result = new Edge[V];
int[] parent = new int[V];
Arrays.fill(parent, -1);
edges.sort(null);
int e = 0;
int i = 0;
while (e < V - 1 && i < edges.size()) {
Edge nextEdge = edges.get(i++);
int x = find(parent, nextEdge.source);
int y = find(parent, nextEdge.destination);
if (x!= y) {
result[e++] = nextEdge;
union(parent, x, y);
}
}
printMST(result);
}
private void printMST(Edge[] result) {
System.out.println("Edge Weight");
for (int i = 0; i < result.length; i++) {
System.out.println(result[i].source + " - " + result[i].destination + " " + result[i].weight);
}
}
}
Dijkstra 算法
原理:用于在带权有向图中找到一个源点到其他所有顶点的最短路径。从源点开始,每次选择一个距离源点最近且未确定最短路径的顶点,然后更新该顶点的邻接顶点到源点的距离,直到所有顶点的最短路径都确定。
import java.util.ArrayList;
import java.util.List;
class GraphDijkstra {
private int V;
private List<Edge>[] adj;
GraphDijkstra(int v) {
V = v;
adj = new ArrayList[v];
for (int i = 0; i < v; ++i) {
adj[i] = new ArrayList<>();
}
}
void addEdge(int u, int v, int weight) {
adj[u].add(new Edge(u, v, weight));
}
class Edge {
int source;
int destination;
int weight;
Edge(int source, int destination, int weight) {
this.source = source;
this.destination = destination;
this.weight = weight;
}
}
void dijkstra(int src) {
int[] dist = new int[V];
boolean[] sptSet = new boolean[V];
for (int i = 0; i < V; i++) {
dist[i] = Integer.MAX_VALUE;
}
dist[src] = 0;
for (int count = 0; count < V - 1; count++) {
int u = minDistance(dist, sptSet);
sptSet[u] = true;
for (Edge edge : adj[u]) {
int v = edge.destination;
int weight = edge.weight;
if (!sptSet[v] && dist[u]!= Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
printSolution(dist);
}
private int minDistance(int[] dist, boolean[] sptSet) {
int min = Integer.MAX_VALUE, minIndex = -1;
for (int v = 0; v < V; v++) {
if (!sptSet[v] && dist[v] <= min) {
min = dist[v];
minIndex = v;
}
}
return minIndex;
}
private void printSolution(int[] dist) {
System.out.println("Vertex Distance from Source");
for (int i = 0; i < V; i++) {
System.out.println(i + " " + dist[i]);
}
}
}
九、其他算法
斐波那契堆
斐波那契堆是一种可合并堆的数据结构,它在某些情况下可以比二叉堆更快地执行某些操作。
主要特点:
- 支持快速的插入和减小关键字操作,时间复杂度为 O (1)(amortized)。
- 删除最小元素和合并操作的时间复杂度为 O (log n)(amortized)。
class FibonacciHeapNode {
int key;
boolean marked;
int degree;
FibonacciHeapNode parent;
FibonacciHeapNode child;
FibonacciHeapNode left;
FibonacciHeapNode right;
public FibonacciHeapNode(int key) {
this.key = key;
marked = false;
degree = 0;
parent = null;
child = null;
left = this;
right = this;
}
}
class FibonacciHeap {
private FibonacciHeapNode minNode;
private int size;
public FibonacciHeap() {
minNode = null;
size = 0;
}
public void insert(int key) {
FibonacciHeapNode node = new FibonacciHeapNode(key);
if (minNode == null) {
minNode = node;
} else {
node.right = minNode;
node.left = minNode.left;
minNode.left.right = node;
minNode.left = node;
if (node.key < minNode.key) {
minNode = node;
}
}
size++;
}
public int extractMin() {
if (minNode == null) {
return -1;
}
int minKey = minNode.key;
FibonacciHeapNode childList = minNode.child;
if (childList!= null) {
FibonacciHeapNode temp = childList.right;
while (temp!= childList) {
temp.parent = null;
temp = temp.right;
}
childList.parent = null;
// 将孩子节点合并到根链表中
childList.right = minNode.right;
childList.left = minNode.left;
minNode.left.right = childList;
minNode.right.left = childList;
}
// 删除最小节点
minNode.left.right = minNode.right;
minNode.right.left = minNode.left;
if (minNode == minNode.right) {
minNode = null;
} else {
minNode = minNode.right;
consolidate();
}
size--;
return minKey;
}
private void consolidate() {
int maxDegree = (int) (Math.log(size) / Math.log(2));
FibonacciHeapNode[] degreeTable = new FibonacciHeapNode[maxDegree + 1];
FibonacciHeapNode current = minNode;
do {
FibonacciHeapNode next = current.right;
int degree = current.degree;
while (degreeTable[degree]!= null) {
FibonacciHeapNode other = degreeTable[degree];
if (current.key > other.key) {
FibonacciHeapNode temp = current;
current = other;
other = temp;
}
if (other == minNode) {
minNode = current;
}
link(other, current);
degreeTable[degree] = null;
degree++;
}
degreeTable[degree] = current;
current = next;
} while (current!= minNode);
minNode = null;
for (int i = 0; i < degreeTable.length; i++) {
if (degreeTable[i]!= null) {
if (minNode == null) {
minNode = degreeTable[i];
} else {
degreeTable[i].right = minNode;
degreeTable[i].left = minNode.left;
minNode.left.right = degreeTable[i];
minNode.left = degreeTable[i];
if (degreeTable[i].key < minNode.key) {
minNode = degreeTable[i];
}
}
}
}
}
private void link(FibonacciHeapNode y, FibonacciHeapNode x) {
y.left.right = y.right;
y.right.left = y.left;
if (x.child == null) {
x.child = y;
y.right = y;
y.left = y;
} else {
y.right = x.child;
y.left = x.child.left;
x.child.left.right = y;
x.child.left = y;
}
y.parent = x;
x.degree++;
y.marked = false;
}
}