n表示输入元素的数量,k表示元素的取值范围大小。
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
排序算法 | 时间复杂度-最好 | 时间复杂度-最差 | 时间复杂度-平均 | 空间复杂度 | 稳定性 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
快速排序 | O(n log n) | O(n^2) | O(n log n) | O(log n) | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
希尔排序 | O(n) | O(n^2) | O(n log n) | O(1) | 不稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
桶排序 | O(n) | O(n^2) | O(n^2) | O(n+k) | 稳定 |
基本思想:
通过构造一个有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
时间复杂度:
最好情况(输入数组已经是有序的):只需遍历一次数组即可完成排序,时间复杂度为 O(n)。
最坏情况(输入数组完全逆序):需要进行 n*(n-1)/2 次比较和最多同样次数的交换操作,时间复杂度为 O(n^2)。
平均情况:时间复杂度也为 O(n^2),因为平均情况下,插入排序需要进行接近于 n^2/2 的比较和交换操作。
空间复杂度:
插入排序是原地排序算法,不需要额外的空间存储数据,仅需要常数级别的临时空间存放中间变量,所以空间复杂度为 O(1)。
public class InsertionSort {
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
// 获取当前数组里面的值并赋值给 key
int key = arr[i];
int j = i - 1;
// 将大于key的元素向后移动一位
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
// 插入key到正确的位置
arr[j + 1] = key;
}
}
public static void main(String[] args) {
int[] array = {49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 1};
insertionSort(array);
System.out.println("排序后的数组:");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本思想:
通过相邻元素之间的比较和交换来逐渐将最大(或最小)的元素“浮”到数组的一端。
时间复杂度:
最好情况(已排序数组):冒泡排序只需遍历一次即可完成排序,因此最好情况下的时间复杂度为 O(n)。
最坏情况(逆序数组):需要进行 n*(n-1)/2 次比较和交换,时间复杂度为 O(n^2)。
平均情况:也是 O(n^2),因为在大多数情况下,需要进行接近于 n^2/2 的比较和交换操作。
空间复杂度:
冒泡排序是原地排序算法,它不需要额外的空间存储数据,只需要常数级别的临时空间存放中间变量,所以空间复杂度为 O(1)。
public class BubbleSort {
public static void bubbleSort(int[] arr) {
// 获取数组长度
int n = arr.length;
// 遍历所有数组元素
for (int i = 0; i < n - 1; i++) {
// 每轮遍历将最大的元素"冒泡"到末尾
for (int j = 0; j < n - 1 - i; j++) {
// 如果当前元素比下一个元素大,则交换它们的位置
if (arr[j] > arr[j + 1]) {
// 交换 arr[j] 和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
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 + " ");
}
}
}
基本思想:
每一次从未排序的部分中找到最小(或最大)的元素,并将其放到已排序部分的末尾。
时间复杂度:
最好情况(输入数组已经是有序的):即使数组已经有序,选择排序仍然需要进行 n-1 轮比较,每轮都需要遍历剩下的元素来确认最小值,所以时间复杂度仍为 O(n^2)。
最坏情况(输入数组完全逆序):同样需要进行 n*(n-1)/2 次比较和最多同样次数的交换操作,时间复杂度为 O(n^2)。
平均情况:由于无论初始顺序如何,选择排序都需要进行接近于 n^2/2 的比较,因此平均时间复杂度也是 O(n^2)。
空间复杂度:
选择排序是原地排序算法,不需要额外的空间存储数据,仅需要常数级别的临时空间存放中间变量,所以空间复杂度为 O(1)。
public class SelectionSort {
public static void selectionSort(int[] arr) {
// 获取数组长度
int n = arr.length;
// 遍历所有数组元素
for (int i = 0; i < n - 1; i++) {
// 找到[i, n)区间内的最小值的索引
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将找到的最小值与当前位置的元素交换
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
public static void main(String[] args) {
int[] array = {64, 34, 25, 12, 22, 11, 90};
selectionSort(array);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本思想:
快速排序是一种分治策略的排序算法,通过选取一个“基准”元素将数组划分为两个子序列,左边的元素小于基准,右边的元素大于基准。然后递归地对左右子序列进行同样的操作。
时间复杂度:
最好情况(每次划分都很均匀): O(n log n)
平均情况: O(n log n)
最坏情况(输入已经有序或逆序): O(n^2)
空间复杂度:
栈空间消耗: O(log n),递归栈深度在最坏情况下为O(n),但平均情况下是logn。
原地排序算法,不需要额外空间,辅助空间复杂度: O(1)
public class QuickSort {
// 快速排序方法,采用分治法策略
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找到分区点(pivot),将数组分为两部分:小于pivot的元素和大于pivot的元素
int pivotIndex = partition(arr, low, high);
// 对左右两边的子数组进行递归排序
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
// 分区操作,返回pivot的位置
private static int partition(int[] arr, int low, int high) {
// 选取最后一个元素作为基准值
int pivot = arr[high];
int i = (low - 1); // index of smaller element
for (int j = low; j <= high - 1; j++) {
// 如果当前元素小于或等于pivot,则交换位置并将较小元素索引后移
if (arr[j] <= pivot) {
i++;
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将基准值与i+1位置上的元素交换,保证基准值左边的都比它小,右边的都比它大
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
public static void main(String[] args) {
int[] array = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6};
quickSort(array, 0, array.length - 1);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本原理:
归并排序是一种稳定的排序算法,它将待排序的序列分成若干个子序列,分别进行排序,最后再将结果进行合并。其核心步骤是将两个已经排序好的子序列合并成一个新的、更大的已排序序列。时间复杂度:
归并排序无论在最好、最坏还是平均情况下,时间复杂度都是 O(n log n)。空间复杂度:
归并排序需要额外的空间来存储临时数组,所以空间复杂度为 O(n)。
public class MergeSort {
// 归并排序方法,采用分治法策略
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);
}
}
// 合并两个有序数组的方法
private static void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组用于存储左右两个有序数组
int[] L = new int[n1];
int[] R = new int[n2];
// 复制数据到临时数组
System.arraycopy(arr, left, L, 0, n1);
System.arraycopy(arr, mid + 1, R, 0, n2);
// 初始化指针
int i = 0, j = 0, k = left;
// 比较两个数组的元素,并按照升序合并到原数组中
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
arr[k++] = R[j++];
}
}
// 将剩余未合并的部分复制回原数组
while (i < n1) {
arr[k++] = L[i++];
}
while (j < n2) {
arr[k++] = R[j++];
}
}
public static void main(String[] args) {
int[] array = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6};
mergeSort(array, 0, array.length - 1);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本原理:
堆排序是一种基于比较的排序算法,通过构建一个“大顶堆”或“小顶堆”,使得父节点总是大于(或小于)其子节点。首先构建初始堆,然后把堆顶元素和最后一个元素交换位置,再重新调整堆,重复这个过程直到所有元素都有序。时间复杂度:
堆排序在最好、最坏和平均情况下时间复杂度都是 O(n log n),其中 n 是待排序数组的长度。空间复杂度:
堆排序是原地排序算法,只需要常数级别的额外空间用于临时存储交换的元素,所以空间复杂度为 O(1)。
public class HeapSort {
// 建立大顶堆
public 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) {
swap(arr, i, largest);
heapify(arr, n, largest);
}
}
// 交换数组中的两个元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 对整个数组进行堆排序
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--) {
swap(arr, 0, i);
heapify(arr, i, 0);
}
}
public static void main(String[] args) {
int[] array = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6};
heapSort(array);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本原理:
计数排序是一种非基于比较的排序算法,它适用于整数排序且数值范围不大的情况。通过对每一个输入元素x,确定出不大于x的元素个数,就可以直接得到x在输出序列中的位置。最后根据计数结果重建排序后的序列。
时间复杂度:
当输入数据范围不是特别大时,计数排序的时间复杂度为 O(n + k),n 是待排序数组的长度,k 是输入数据的最大值与最小值的差加一。
空间复杂度:
计数排序需要额外的空间来存储计数数组和输出数组,所以空间复杂度为 O(n + k)。但如果k远大于n,则该算法可能不适用。
public class CountingSort {
public static void countingSort(int[] arr) {
// 查找数组中的最大值
int maxVal = Arrays.stream(arr).max().getAsInt();
// 初始化计数数组,大小为最大值+1
int[] count = new int[maxVal + 1];
// 统计每个数出现的次数
for (int num : arr) {
count[num]++;
}
// 计算累计出现次数
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// 创建一个新的输出数组
int[] output = new int[arr.length];
// 按照顺序将元素放入输出数组
for (int i = arr.length - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
// 将排序好的元素复制回原数组
System.arraycopy(output, 0, arr, 0, arr.length);
}
public static void main(String[] args) {
int[] array = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6};
countingSort(array);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本原理:
希尔排序是插入排序的一种改进版本,通过定义一个间隔序列来分组元素,对每组使用直接插入排序。随着增量逐渐减少,元素变得越来越接近有序,最后当增量为1时,整个数组已经基本有序,此时执行一次插入排序就能得到完全有序的结果。
时间复杂度:
最好情况下的时间复杂度可以达到O(n log n),最坏情况下和平均情况下的时间复杂度取决于所使用的增量序列。经典的Hibbard增量序列下,希尔排序的时间复杂度为O(n^(3/2))。
空间复杂度:
希尔排序是原地排序算法,空间复杂度为O(1)。
public class ShellSort {
// 希尔排序实现
public static void shellSort(int[] arr) {
int len = arr.length;
// 增量序列,这里使用h(n) = 3^k - 1的方式,逐步减小增量
for (int gap = len / 2; gap > 0; gap /= 2) {
// 对每一个增量值,进行插入排序
for (int i = gap; i < len; i++) {
// 将arr[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;
}
}
}
public static void main(String[] args) {
int[] array = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6};
shellSort(array);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本原理:
桶排序是一种分布排序算法,它将要排序的数据分布到有限数量的桶里,对每个桶内的数据进行排序,然后依次合并所有桶中的数据即可完成排序。桶内排序可以采用任何适合的排序算法。
时间复杂度:
当输入数据均匀分布且桶的数量足够多时,桶排序的时间复杂度可以达到O(n + k),其中n是待排序元素个数,k是桶的数量。但如果元素分布不均匀或桶划分不合理,时间复杂度会变差。
空间复杂度:
空间复杂度主要取决于桶的数量,若假设每个桶大小固定,则空间复杂度为O(n+k),其中n是待排序元素个数,k是桶的数量。
public class BucketSort {
// 桶排序实现,假设输入数组为非负整数
public static void bucketSort(int[] arr, int maxVal) {
if (arr == null || arr.length == 0) {
return;
}
// 创建足够数量的空桶(这里以最大值 + 1来确定桶的数量)
List> buckets = new ArrayList<>();
for (int i = 0; i <= maxVal; i++) {
buckets.add(new ArrayList<>());
}
// 将元素分配到各个桶中
for (int num : arr) {
buckets.get(num).add(num);
}
// 对每个桶内的元素进行排序,可以使用插入排序等简单算法
for (List bucket : buckets) {
Collections.sort(bucket);
}
// 合并所有已排序的桶
int index = 0;
for (List bucket : buckets) {
for (Integer value : bucket) {
// 防止处理空桶
if (!bucket.isEmpty()) {
arr[index++] = value;
}
}
}
}
public static void main(String[] args) {
int[] array = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6};
int maxVal = Arrays.stream(array).max().getAsInt();
bucketSort(array, maxVal);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}
基本原理:
基数排序是一种非比较型整数排序算法,其基本思想是对要排序的数据按位数进行划分,从低位到高位逐次进行排序。它借助了稳定的计数排序,每次针对一位数字进行排序,直到所有位数都排序完成。时间复杂度:
在基数排序中,每一趟都是对不同位进行计数排序,所以每一轮的时间复杂度为O(n),由于一共需要log_radix(maxValue)轮,因此总的时间复杂度为O(k * n),其中k通常为radix(如10进制则k=10)。空间复杂度:
空间复杂度为O(n + k),其中n是待排序元素个数,k是基数(比如在十进制下k=10)。
public class RadixSort {
// 基数排序实现,假设输入数组为非负整数
public static void radixSort(int[] arr) {
// 获取数组中的最大数,用于确定需要遍历的位数
int maxNum = Arrays.stream(arr).max().getAsInt();
// 定义一个基数(例如十进制下基数为10)
final int RADIX = 10;
// 从最低有效位开始,对每一位进行排序
for (int exp = 1; maxNum / exp > 0; exp *= RADIX) {
// 使用计数排序对当前位进行排序
countingSortByDigit(arr, exp, RADIX);
}
}
// 计数排序对特定位数进行排序
private static void countingSortByDigit(int[] arr, int exp, int radix) {
int[] output = new int[arr.length];
int[] count = new int[radix];
// 初始化计数数组
Arrays.fill(count, 0);
// 统计每个桶中元素的数量
for (int i = 0; i < arr.length; i++) {
count[(arr[i] / exp) % radix]++;
}
// 计算每个桶的起始位置
for (int i = 1; i < radix; i++) {
count[i] += count[i - 1];
}
// 根据每个桶的位置逆序填充输出数组,并更新原数组
for (int i = arr.length - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % radix] - 1] = arr[i];
count[(arr[i] / exp) % radix]--;
arr[i] = output[i];
}
}
public static void main(String[] args) {
int[] array = {93, 72, 51, 112, 12, 234, 34, 10, 67};
radixSort(array);
System.out.println("Sorted array: ");
for (int num : array) {
System.out.print(num + " ");
}
}
}