1. 简单排序算法
冒泡排序(Bubble Sort)
- 原理:冒泡排序是一种非常直观的排序方法,它通过重复遍历数组,比较相邻元素的大小。如果发现前一个元素比后一个元素大(对于升序排序),就交换它们的位置。这个过程会一直重复,直到整个数组完全有序。由于每次遍历都会将最大的元素“冒泡”到数组的末尾,因此得名“冒泡排序”。
- 时间复杂度:在平均和最坏情况下,时间复杂度为 (O(n^2)),因为需要进行多次嵌套循环的比较和交换操作。不过,在最好情况下,即数组已经完全有序时,时间复杂度可以达到 (O(n)),因为只需要进行一次遍历就可以发现数组已经有序,无需进一步交换。
- 特点:冒泡排序的优点是实现简单,代码量少,容易理解。它是一种稳定的排序算法,即相等元素的相对顺序不会因排序而改变。然而,由于其效率较低,通常不适用于大规模数据的排序。它更适合用于教学目的或对小规模数据进行排序,例如在一些简单的数据处理任务中,或者当数据已经接近有序时,冒泡排序可以较快地完成排序操作。
Java 实现:
public static void bubbleSort(int[] arr) {int n = arr.length;boolean swapped;for (int i = 0; i < n - 1; i++) {swapped = false;for (int j = 0; j < n - 1 - i; j++) {if (arr[j] > arr[j + 1]) {// 交换元素int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;swapped = true;}}// 如果没有发生交换,说明数组已经有序if (!swapped) break;}
}
示例:假设有一个数组 [5, 3, 8, 4, 2]
,进行冒泡排序的过程如下:
- 第一次遍历:
- 比较 5 和 3,交换位置 →
[3, 5, 8, 4, 2]
- 比较 5 和 8,不交换 →
[3, 5, 8, 4, 2]
- 比较 8 和 4,交换位置 →
[3, 5, 4, 8, 2]
- 比较 8 和 2,交换位置 →
[3, 5, 4, 2, 8]
- 第二次遍历:
- 比较 3 和 5,不交换 →
[3, 5, 4, 2, 8]
- 比较 5 和 4,交换位置 →
[3, 4, 5, 2, 8]
- 比较 5 和 2,交换位置 →
[3, 4, 2, 5, 8]
- 第三次遍历:
- 比较 3 和 4,不交换 →
[3, 4, 2, 5, 8]
- 比较 4 和 2,交换位置 →
[3, 2, 4, 5, 8]
- 第四次遍历:
- 比较 3 和 2,交换位置 →
[2, 3, 4, 5, 8]
最终,数组变为有序:[2, 3, 4, 5, 8]
。
选择排序(Selection Sort)
- 原理:选择排序的核心思想是将数组分为已排序部分和未排序部分。在每次迭代中,算法从未排序部分中找到最小(或最大)的元素,并将其与未排序部分的第一个元素交换位置,从而将该元素移动到已排序部分的末尾。通过不断重复这个过程,直到整个数组都被排序完成。
- 时间复杂度:无论数据的初始状态如何,选择排序的时间复杂度始终为 (O(n^2))。这是因为每次迭代都需要遍历未排序部分的元素来找到最小(或最大)值,而这个过程需要进行 (n) 次,每次遍历的长度逐渐减少,但总体复杂度仍然是 (O(n^2))。
- 特点:选择排序的优点是实现简单,且不需要额外的存储空间。它是一种稳定的排序算法。不过,由于其时间复杂度较高,对于大规模数据的排序效率较低。选择排序在某些特定场景下可能比较有用,例如当数据存储在只允许顺序访问的存储设备(如磁带)上时,选择排序的特性可以减少数据的移动次数。
Java 实现:
public static void selectionSort(int[] arr) {int n = arr.length;for (int i = 0; i < n - 1; i++) {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;}
}
示例:假设有一个数组 [5, 3, 8, 4, 2]
,进行选择排序的过程如下:
- 第一次选择:
- 找到最小值 2,将其与第一个元素 5 交换 →
[2, 3, 8, 4, 5]
- 第二次选择:
- 在剩余部分
[3, 8, 4, 5]
中找到最小值 3,它已经在正确位置,不交换 →[2, 3, 8, 4, 5]
- 第三次选择:
- 在剩余部分
[8, 4, 5]
中找到最小值 4,将其与第三个元素 8 交换 →[2, 3, 4, 8, 5]
- 第四次选择:
- 在剩余部分
[8, 5]
中找到最小值 5,将其与第四个元素 8 交换 →[2, 3, 4, 5, 8]
最终,数组变为有序:[2, 3, 4, 5, 8]
。
插入排序(Insertion Sort)
- 原理:插入排序可以类比为整理扑克牌的过程。它将数组分为已排序部分和未排序部分。从未排序部分取出一个元素,然后将其插入到已排序部分的合适位置,使得已排序部分始终保持有序。这个过程会一直重复,直到未排序部分为空,整个数组就变得有序了。
- 时间复杂度:在平均和最坏情况下,插入排序的时间复杂度为 (O(n^2)),因为需要对每个元素进行多次比较和移动操作。然而,在最好情况下,即数组已经完全有序时,时间复杂度为 (O(n)),因为每个元素只需要进行一次比较就可以确定其位置,无需移动。
- 特点:插入排序的优点是实现简单,且对于部分有序的数据效率较高。它是一种稳定的排序算法,能够保持相等元素的相对顺序。此外,插入排序在实际应用中具有一定的适应性。例如,当处理的数据量较小时,插入排序的简单性和较低的常数因子使其成为一个不错的选择。另外,对于动态数据,即在排序过程中可能会有新元素插入的情况,插入排序也可以很好地适应,因为它可以方便地将新元素插入到已排序部分的合适位置。
Java 实现:
public static void insertionSort(int[] arr) {int n = arr.length;for (int i = 1; i < n; i++) {int key = arr[i];int j = i - 1;// 将当前元素插入到已排序部分的合适位置while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j--;}arr[j + 1] = key;}
}
示例:假设有一个数组 [5, 3, 8, 4, 2]
,进行插入排序的过程如下:
- 取出 3,插入到已排序部分
[5]
的前面 →[3, 5, 8, 4, 2]
- 取出 8,插入到已排序部分
[3, 5]
的后面 →[3, 5, 8, 4, 2]
- 取出 4,插入到已排序部分
[3, 5, 8]
的中间 →[3, 4, 5, 8, 2]
- 取出 2,插入到已排序部分
[3, 4, 5, 8]
的前面 →[2, 3, 4, 5, 8]
最终,数组变为有序:[2, 3, 4, 5, 8]
。
2. 高级排序算法
希尔排序(Shell Sort)
- 原理:希尔排序是插入排序的一种改进版本。它通过引入“增量”序列,将数组分成若干个子序列,每个子序列的元素间隔为某个增量值。然后对每个子序列分别进行插入排序。随着排序过程的进行,增量值逐渐减小,直到最后减小为1,此时整个数组成为一个大序列,再进行一次插入排序,完成最终的排序。通过这种方式,希尔排序能够在早期快速地将元素移动到接近其最终位置的地方,从而减少后续排序的比较和移动次数。
- 时间复杂度:希尔排序的时间复杂度取决于所选择的增量序列。不同的增量序列会导致不同的性能表现。一般来说,其时间复杂度介于 (O(n \log n)) 和 (O(n^2)) 之间。例如,使用希尔增量序列(1, 4, 13, 40, ...)时,时间复杂度接近 (O(n^{1.3})),而使用其他更优的增量序列,如Hibbard增量序列(1, 3, 7, 15, ...),时间复杂度可以接近 (O(n \log n))。
- 特点:希尔排序的优点是突破了简单插入排序的 (O(n^2)) 时间复杂度限制,能够更高效地处理大规模数据。它是一种不稳定的排序算法,但在实际应用中,由于其相对较高的效率和较低的实现复杂度,仍然是一个常用的排序方法。希尔排序特别适合用于对中等规模的数据进行排序,或者作为其他排序算法的预处理步骤,以提高整体排序效率。
Java 实现:
public static void shellSort(int[] arr) {int n = arr.length;// 使用希尔增量序列for (int gap = n / 2; gap > 0; gap /= 2) {for (int i = gap; i < n; i++) {int temp = arr[i];int j = i;// 对每个子序列进行插入排序while (j >= gap && arr[j - gap] > temp) {arr[j] = arr[j - gap];j -= gap;}arr[j] = temp;}}
}
示例:假设有一个数组 [5, 3, 8, 4, 2, 7, 1, 6]
,使用增量序列 [4, 1]
进行希尔排序的过程如下:
- 增量为 4:
- 子序列1:
[5, 4, 7, 6]
→ 排序后[4, 5, 6, 7]
- 子序列2:
[3, 2, 1, 8]
→ 排序后[1, 2, 3, 8]
- 合并后数组:
[4, 1, 6, 2, 7, 3, 5, 8]
- 增量为 1(普通插入排序):
- 最终排序结果:
[1, 2, 3, 4, 5, 6, 7, 8]
归并排序(Merge Sort)
- 原理:归并排序是一种典型的分治算法。它将数组分成两部分,分别对这两部分递归地进行排序,然后将排序好的两部分合并成一个有序数组。这个合并过程是归并排序的关键步骤,它通过比较两个子数组的元素大小,依次将较小的元素放入最终的有序数组中,直到所有元素都被合并完毕。通过递归地应用这种分治策略,整个数组最终被排序完成。
- 时间复杂度:归并排序的时间复杂度为 (O(n \log n))。这是因为每次递归都将数组分成两部分,总共需要进行 (O(\log n)) 层递归,而在每一层递归中,合并操作的时间复杂度为 (O(n)),因此总的时间复杂度为 (O(n \log n))。
- 特点:归并排序是一种稳定的排序算法,能够保持相等元素的相对顺序。它适用于处理大规模数据,因为其时间复杂度较低,且对数据的初始状态不敏感。然而,归并排序的一个缺点是需要额外的存储空间来存放合并后的数组,这在某些内存受限的场景下可能会成为一个问题。归并排序在实际应用中非常广泛,例如在外部排序(处理无法全部加载到内存的大规模数据)中,归并排序常被用于合并已经排序好的数据块;在多线程或分布式计算环境中,归并排序的分治特性也使其能够很好地利用并行计算资源。
Java 实现:
public static void mergeSort(int[] arr) {mergeSortHelper(arr, 0, arr.length - 1);
}private static void mergeSortHelper(int[] arr, int left, int right) {if (left < right) {int mid = left + (right - left) / 2;mergeSortHelper(arr, left, mid);mergeSortHelper(arr, mid + 1, right);merge(arr, left, mid, right);}
}private 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) {temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];}while (i <= mid) {temp[k++] = arr[i++];}while (j <= right) {temp[k++] = arr[j++];}System.arraycopy(temp, 0, arr, left, temp.length);
}
示例:假设有一个数组 [5, 3, 8, 4, 2, 7, 1, 6]
,进行归并排序的过程如下:
- 分解:
[5, 3, 8, 4]
和[2, 7, 1, 6]
- 进一步分解为
[5, 3]
、[8, 4]
、[2, 7]
、[1, 6]
- 最终分解为单个元素:
[5]
、[3]
、[8]
、[4]
、[2]
、[7]
、[1]
、[6]
- 合并:
[5]
和[3]
合并为[3, 5]
[8]
和[4]
合并为[4, 8]
[2]
和[7]
合并为[2, 7]
[1]
和[6]
合并为[1, 6]
[3, 5]
和[4, 8]
合并为[3, 4, 5, 8]
[2, 7]
和[1, 6]
合并为[1, 2, 6, 7]
[3, 4, 5, 8]
和[1, 2, 6, 7]
合并为[1, 2, 3, 4, 5, 6, 7, 8]
最终,数组变为有序:[1, 2, 3, 4, 5, 6, 7, 8]
。
快速排序(Quick Sort)
- 原理:快速排序同样是基于分治思想的排序算法。它通过选择一个“基准”元素,将数组分为两部分:一部分包含小于基准的元素,另一部分包含大于基准的元素。然后对这两部分分别递归地进行快速排序。这个过程一直重复,直到数组被分解为只有一个或零个元素的子数组,此时整个数组就变得有序了。快速排序的关键在于如何选择基准元素以及如何高效地划分数组。
- 时间复杂度:在平均情况下,快速排序的时间复杂度为 (O(n \log n)),这是因为每次划分都能将数组大致分成两个相等的部分,从而形成一个平衡的递归树。然而,在最坏情况下,快速排序的时间复杂度为 (O(n^2)),这通常发生在数组已经完全有序或逆序,且基准元素选择不当(如总是选择第一个或最后一个元素作为基准)时。为了避免这种情况,可以采用一些优化策略,如随机选择基准元素、三数取中法等。
- 特点:快速排序是一种非常高效的排序算法,其平均时间复杂度较低,且常数因子较小,因此在实际应用中速度很快。它是不稳定的排序算法,但在大多数情况下,这种不稳定性对排序结果的影响不大。快速排序的优点是不需要额外的存储空间,除了递归调用所需的栈空间外,不需要额外的内存分配。它适用于处理大规模数据,是许多编程语言标准库中排序函数的默认实现算法。例如,在对一个包含大量用户数据的数据库进行排序时,快速排序能够快速地将用户按照某种属性(如年龄、姓名等)进行排序,从而提高数据处理效率。
Java 实现:
public static void quickSort(int[] arr) {quickSortHelper(arr, 0, arr.length - 1);
}private static void quickSortHelper(int[] arr, int low, int high) {if (low < high) {int pivotIndex = partition(arr, low, high);quickSortHelper(arr, low, pivotIndex - 1);quickSortHelper(arr, pivotIndex + 1, high);}
}private static int partition(int[] arr, int low, int high) {int pivot = arr[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;
}
示例:假设有一个数组 [5, 3, 8, 4, 2, 7, 1, 6]
,选择第一个元素 5 作为基准进行快速排序的过程如下:
- 分区:
- 小于 5 的部分:
[3, 4, 2, 1]
- 等于 5 的部分:
[5]
- 大于 5 的部分:
[8, 7, 6]
- 对
[3, 4, 2, 1]
和[8, 7, 6]
分别递归排序:
[3, 4, 2, 1]
排序后为[1, 2, 3, 4]
[8, 7, 6]
排序后为[6, 7, 8]
- 合并结果:
- 最终排序结果:
[1, 2, 3, 4, 5, 6, 7, 8]
3. 基于堆的排序算法
堆排序(Heap Sort)
- 原理:堆排序的核心是利用堆的性质。首先,将数组构建成一个最大堆(对于升序排序)或最小堆(对于降序排序)。最大堆的性质是每个父节点的值都大于或等于其子节点的值。然后,将堆顶元素(最大值)与数组的最后一个元素交换,这样最大值就到了数组的末尾。接着,对剩下的元素重新调整堆,使其再次满足堆的性质。重复这个过程,直到整个数组都被排序完成。
- 时间复杂度:堆排序的时间复杂度为 (O(n \log n))。这是因为构建初始堆的时间复杂度为 (O(n)),而每次调整堆的操作时间复杂度为 (O(\log n)),总共需要进行 (n) 次调整操作,因此总的时间复杂度为 (O(n \log n))。
- 特点:堆排序是一种不稳定的排序算法,因为它可能会改变相等元素的相对顺序。它的优点是不需要额外的存储空间,除了用于堆调整的少量变量外,不需要额外的内存分配。堆排序适用于处理大规模数据,且对数据的初始状态不敏感。在实际应用中,堆排序常被用于实现优先队列,例如在任务调度系统中,根据任务的优先级对任务进行排序,以便按照优先级顺序执行任务。
Java 实现:
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 temp = arr[i];arr[i] = arr[largest];arr[largest] = temp;heapify(arr, n, largest);}
}
示例:假设有一个数组 [5, 3, 8, 4, 2, 7, 1, 6]
,进行堆排序的过程如下:
- 构建最大堆:
- 初始数组:
[5, 3, 8, 4, 2, 7, 1, 6]
- 调整后最大堆:
[8, 6, 7, 4, 2, 3, 1, 5]
- 排序:
- 将堆顶 8 与最后一个元素 5 交换 →
[5, 6, 7, 4, 2, 3, 1, 8]
- 调整堆:
[7, 6, 5, 4, 2, 3, 1]
- 将堆顶 7 与最后一个元素 1 交换 →
[1, 6, 5, 4, 2, 3, 7, 8]
- 调整堆:
[6, 4, 5, 1, 2, 3]
- 将堆顶 6 与最后一个元素 3 交换 →
[3, 4, 5, 1, 2, 6, 7, 8]
- 调整堆:
[5, 4, 3, 1, 2]
- 将堆顶 5 与最后一个元素 2 交换 →
[2, 4, 3, 1, 5, 6, 7, 8]
- 调整堆:
[4, 1, 3, 2]
- 将堆顶 4 与最后一个元素 2 交换 →
[2, 1, 3, 4, 5, 6, 7, 8]
- 调整堆:
[3, 1, 2]
- 将堆顶 3 与最后一个元素 2 交换 →
[2, 1, 3, 4, 5, 6, 7, 8]
- 调整堆:
[2, 1]
- 将堆顶 2 与最后一个元素 1 交换 →
[1, 2, 3, 4, 5, 6, 7, 8]
最终,数组变为有序:[1, 2, 3, 4, 5, 6, 7, 8]
。
4. 基于计数的排序算法
计数排序(Counting Sort)
- 原理:计数排序是一种非比较排序算法,它适用于整数数据。其核心思想是统计每个整数在数组中出现的次数,然后根据统计结果直接确定每个元素的最终位置。具体来说,首先创建一个大小为 (k) 的计数数组(假设数组中元素的范围为 (0) 到 (k-1)),然后遍历原始数组,将每个元素的计数值加1。接下来,根据计数数组中的值,依次将元素放入最终的有序数组中。
- 时间复杂度:计数排序的时间复杂度为 (O(n + k)),其中 (n) 是数组的长度,(k) 是元素的范围。这是因为需要遍历原始数组来统计计数,以及根据计数数组来确定元素的最终位置。
- 特点:计数排序是一种稳定的排序算法,能够保持相等元素的相对顺序。它的优点是时间复杂度较低,对于整数范围较小的情况非常高效。然而,计数排序的缺点是需要额外的存储空间来存放计数数组,且当元素范围 (k) 很大时,空间复杂度会显著增加。计数排序适用于处理整数数据,例如在对一组学生成绩进行排序时,如果成绩范围是固定的(如0到100分),计数排序可以快速地完成排序任务。
Java 实现:
public static void countingSort(int[] arr) {int max = Integer.MIN_VALUE;int min = Integer.MAX_VALUE;for (int num : arr) {max = Math.max(max, num);min = Math.min(min, num);}int range = max - min + 1;int[] count = new int[range];for (int num : arr) {count[num - min]++;}for (int i = 1; i < range; 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] - min] - 1] = arr[i];count[arr[i] - min]--;}System.arraycopy(output, 0, arr, 0, arr.length);
}
示例:假设有一个数组 [4, 2, 2, 8, 3, 3, 1]
,元素范围为 1 到 8,进行计数排序的过程如下:
- 创建计数数组,大小为 9(范围从 0 到 8):
- 初始计数数组:
[0, 0, 0, 0, 0, 0, 0, 0, 0]
- 遍历原始数组,统计每个元素的出现次数:
- 计数数组:
[0, 1, 2, 2, 1, 0, 0, 0, 1]
- 根据计数数组,将元素放入最终的有序数组:
- 1 出现 1 次 →
[1]
- 2 出现 2 次 →
[1, 2, 2]
- 3 出现 2 次 →
[1, 2, 2, 3, 3]
- 4 出现 1 次 →
[1, 2, 2, 3, 3, 4]
- 8 出现 1 次 →
[1, 2, 2, 3, 3, 4, 8]
最终,数组变为有序:[1, 2, 2, 3, 3, 4, 8]
。
基数排序(Radix Sort)
- 原理:基数排序是一种基于数字位数的排序算法,主要用于整数排序。它通过逐位比较数字的每一位来实现排序。具体来说,从最低位(个位)开始,对所有数字的该位进行排序,然后依次对更高位进行排序,直到最高位排序完成。在每一轮排序中,可以使用计数排序或其他稳定的排序算法来对当前位进行排序。通过这种方式,基数排序能够将数字按照每一位的大小顺序排列,最终得到整个数字的有序序列。
- 时间复杂度:基数排序的时间复杂度为 (O(nk)),其中 (n) 是数字的个数,(k) 是数字的位数。这是因为需要对每一位进行排序,而每一轮排序的时间复杂度为 (O(n))。
- 特点:基数排序是一种稳定的排序算法,能够保持相等元素的相对顺序。它适用于处理大规模的整数数据,尤其是当数字的位数相对较少时,排序效率较高。例如,在对一组电话号码进行排序时,由于电话号码的位数是固定的,基数排序可以快速地完成排序任务。然而,基数排序的缺点是需要额外的存储空间来存放中间排序结果,且对于非整数数据(如浮点数)需要进行特殊处理。
Java 实现:
public static void radixSort(int[] arr) {int max = Arrays.stream(arr).max().orElse(Integer.MIN_VALUE);for (int exp = 1; max / exp > 0; exp *= 10) {countingSortByDigit(arr, exp);}
}private static void countingSortByDigit(int[] arr, int exp) {int[] output = new int[arr.length];int[] count = new int[10];Arrays.fill(count, 0);for (int i = 0; i < arr.length; i++) {count[(arr[i] / exp) % 10]++;}for (int i = 1; i < 10; i++) {count[i] += count[i - 1];}for (int i = arr.length - 1; i >= 0; i--) {output[count[(arr[i] / exp) % 10] - 1] = arr[i];count[(arr[i] / exp) % 10]--;}System.arraycopy(output, 0, arr, 0, arr.length);
}
示例:假设有一组数字 [170, 45, 75, 90, 802, 24, 2, 66]
,进行基数排序的过程如下:
- 个位排序:
- 分组:
[170, 90, 802, 2]
(0)、[24, 45]
(4)、[75]
(5)、[66]
(6) - 排序后:
[170, 90, 802, 2, 24, 45, 75, 66]
- 十位排序:
- 分组:
[802, 2]
(0)、[24]
(2)、[45]
(4)、[66]
(6)、[170, 75]
(7)、[90]
(9) - 排序后:
[802, 2, 24, 45, 66, 170, 75, 90]
- 百位排序:
- 分组:
[2, 24, 45, 66, 75, 90]
(0)、[170]
(1)、[802]
(8) - 排序后:
[2, 24, 45, 66, 75, 90, 170, 802]
最终,数组变为有序:[2, 24, 45, 66, 75, 90, 170, 802]
。
桶排序(Bucket Sort)
- 原理:桶排序是一种基于分桶思想的排序算法。它将数据分到有限数量的桶中,每个桶再单独进行排序。具体来说,首先根据数据的范围和分布情况,将数据划分到若干个桶中。然后,对每个桶内的数据分别进行排序,可以使用任何排序算法(如插入排序、快速排序等)。最后,将所有桶中的数据依次取出,合并成最终的有序数组。
- 时间复杂度:桶排序的时间复杂度为 (O(n + k)),其中 (n) 是数据的个数,(k) 是桶的数量。这是因为需要将数据分配到桶中,以及从桶中取出数据,这两个过程的时间复杂度均为 (O(n))。而每个桶内的排序时间复杂度取决于桶内数据的数量和所使用的排序算法。
- 特点:桶排序是一种稳定的排序算法,能够保持相等元素的相对顺序。它适用于数据分布较为均匀的情况,因为如果数据分布不均匀,可能会导致某些桶中的数据过多,从而影响排序效率。桶排序的优点是时间复杂度较低,且可以通过合理选择桶的数量和排序算法来优化性能。例如,在对一组学生的身高数据进行排序时,如果身高分布较为均匀,桶排序可以快速地完成排序任务。
Java 实现:
public static void bucketSort(float[] arr) {int n = arr.length;if (n <= 0) return;float max = Arrays.stream(arr).max().orElse(Float.MIN_VALUE);float min = Arrays.stream(arr).min().orElse(Float.MAX_VALUE);float range = max - min;List<List<Float>> buckets = new ArrayList<>();for (int i = 0; i < n; i++) {buckets.add(new ArrayList<>());}for (float num : arr) {int index = (int) Math.floor((num - min) / range * (n - 1));buckets.get(index).add(num);}int index = 0;for (List<Float> bucket : buckets) {Collections.sort(bucket);for (float num : bucket) {arr[index++] = num;}}
}
示例:假设有一组数据 [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
,数据范围为 [0, 1)
,使用 10 个桶进行桶排序的过程如下:
- 分配到桶:
- 桶 0:
[0.12]
- 桶 1:
[0.17]
- 桶 2:
[0.21, 0.23, 0.26]
- 桶 3:
[0.39]
- 桶 6:
[0.68]
- 桶 7:
[0.72, 0.78]
- 桶 9:
[0.94]
- 其余桶为空。
- 对每个桶排序(使用插入排序):
- 桶 2:
[0.21, 0.23, 0.26]
- 其余桶无需排序。
- 合并结果:
- 最终排序结果:
[0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94]
5. 其他排序算法
二叉树排序(Binary Tree Sort)
- 原理:二叉树排序是基于二叉搜索树(BST)的排序算法。它首先将数组中的元素依次插入到一棵二叉搜索树中。在二叉搜索树中,左子树的所有节点值都小于根节点值,右子树的所有节点值都大于根节点值。然后,通过中序遍历二叉搜索树,可以得到一个有序的序列。中序遍历的顺序是先遍历左子树,然后访问根节点,最后遍历右子树。
- 时间复杂度:在平均情况下,二叉树排序的时间复杂度为 (O(n \log n)),因为插入操作的时间复杂度为 (O(\log n)),总共需要进行 (n) 次插入操作。然而,在最坏情况下,当数据已经完全有序或逆序时,二叉搜索树会退化成一个链表,此时插入操作的时间复杂度为 (O(n)),总的时间复杂度为 (O(n^2))。
- 特点:二叉树排序的优点是能够动态地处理数据,即在排序过程中可以方便地插入或删除元素。它适用于处理动态数据集合,例如在实时数据处理系统中,需要不断地对数据进行排序和更新。然而,二叉树排序的缺点是需要额外的存储空间来存放二叉搜索树的节点,且在最坏情况下性能较差。为了克服这个问题,可以使用平衡二叉树(如AVL树、红黑树)来实现排序,从而保证时间复杂度始终为 (O(n \log n))。
Java 实现:
class TreeNode {int val;TreeNode left, right;TreeNode(int val) {this.val = val;}
}public static void binaryTreeSort(int[] arr) {TreeNode root = null;for (int num : arr) {root = insertIntoBST(root, num);}int[] sorted = new int[arr.length];inOrderTraversal(root, sorted, new int[]{0});System.arraycopy(sorted, 0, arr, 0, arr.length);
}private static TreeNode insertIntoBST(TreeNode root, int val) {if (root == null) return new TreeNode(val);if (val < root.val) root.left = insertIntoBST(root.left, val);else root.right = insertIntoBST(root.right, val);return root;
}private static void inOrderTraversal(TreeNode root, int[] sorted, int[] index) {if (root == null) return;inOrderTraversal(root.left, sorted, index);sorted[index[0]++] = root.val;inOrderTraversal(root.right, sorted, index);
}
示例:假设有一个数组 [5, 3, 8, 4, 2]
,进行二叉树排序的过程如下:
- 构建二叉搜索树:
5/ \3 8/ /
2 4
- 中序遍历二叉搜索树:
- 遍历结果:
[2, 3, 4, 5, 8]
最终,数组变为有序:[2, 3, 4, 5, 8]
。
外部排序(External Sort)
- 原理:外部排序是一种用于处理无法全部加载到内存的大规模数据的排序算法。它通常分为两个阶段:首先是分块排序阶段,将大规模数据分成若干个小块,每个小块可以被加载到内存中进行排序;然后是合并阶段,将排序好的小块数据按照一定的顺序合并成一个最终的有序序列。在合并阶段,可以使用多路归并排序等方法来高效地合并数据。
- 时间复杂度:外部排序的时间复杂度取决于具体实现,但总体上为 (O(n \log n))。这是因为分块排序阶段的时间复杂度为 (O(n \log m)),其中 (m) 是每个小块的大小;合并阶段的时间复杂度为 (O(n \log k)),其中 (k) 是小块的数量。在实际应用中,通过合理选择小块的大小和合并策略,可以优化外部排序的性能。
- 特点:外部排序适用于处理大规模数据,例如在数据库管理系统中,需要对存储在磁盘上的大量数据进行排序。由于数据无法全部加载到内存,外部排序通过分块处理和磁盘操作,能够有效地完成排序任务。然而,外部排序的缺点是需要进行大量的磁盘读写操作,这会显著影响排序速度。因此,在设计外部排序算法时,需要尽量减少磁盘I/O操作,以提高排序效率。
示例:假设有一组大规模数据 [4, 2, 7, 1, 5, 3, 8, 6, 9, 0]
,无法全部加载到内存,使用外部排序的过程如下:
- 分块排序:
- 将数据分成若干小块,每块大小为 3:
- 块 1:
[4, 2, 7]
→ 排序后[2, 4, 7]
- 块 2:
[1, 5, 3]
→ 排序后[1, 3, 5]
- 块 3:
[8, 6, 9]
→ 排序后[6, 8, 9]
- 块 4:
[0]
→ 排序后[0]
- 块 1:
- 合并阶段:
- 使用多路归并排序合并这些小块:
- 合并
[2, 4, 7]
和[1, 3, 5]
→[1, 2, 3, 4, 5, 7]
- 合并
[6, 8, 9]
和[0]
→[0, 6, 8, 9]
- 最终合并
[1, 2, 3, 4, 5, 7]
和[0, 6, 8, 9]
→[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
最终,数据变为有序:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
。
- 合并