1、冒泡排序
简单、两重for循环、两两比较交换位置,持续对数列的末尾进行,直到整个数列都排序完成。
不管序列是怎样,都是要比较n(n-1)/2 次的,最好、最坏、平均时间复杂度都为O(n²),需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j+1]和arr[j]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;}}}}
2、选择排序
简单、是冒泡排序的改进版,两重for循环,每次内部循环在未排序元素中选择最小(或最大)元素存放在序列的起始位置。
选择排序无论序列是怎样的都是要比较n(n-1)/2次的,最好、最坏、平均时间复杂度也都为O(n²),需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。
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;}}
// 交换arr[i]和arr[minIndex]
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;}}
3、插入排序
将一个记录插入到已经排好序的有序表中得到一个新的、记录数+1的有序表。
插入排序,如果序列是完全有序的,插入排序只要比较n次,无需移动,时间复杂度为O(n);如果序列是逆序的,插入排序要比较n²次和移动,所以时间复杂度为O(n²),最好为O(n),最坏为O(n²),平均时间复杂度为O(n²),排序过程中只要一个辅助空间,所以空间复杂度O(1)。
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;
// 将当前值key和前面的值进行比较,如果前面的值>key 则将值往后移1位
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;}
// 在不小当前值key的位置,插入当前值key
arr[j + 1] = key;}}
4、希尔排序
希尔排序是一种改进的插入排序算法,它的基本思想是将待排序的数组按照一定的间隔进行分组,对每组使用插入排序算法进行排序,然后缩小间隔,再对分组进行排序,直到间隔为1为止。逐渐减小间隔大小的方法有助于提高排序过程的效率,可以减少比较和交换的次数。这是希尔排序算法的一个关键特点
希尔排序的时间复杂度分析及其复杂,只需要记住结论就行,{1,2,4,8,…}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n²);{1,3,7,…,2k-1},这种序列的时间复杂度(最坏情形)为O(n1.5);Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3);其中最好的一个序列是{1,5,19,41,109,…},需要一个临时变量用来交换数组内数据位置,所以空间复杂度为O(1)。
public static void shellSort(int[] arr) {
int n = arr.length;
// 初始化间隔gap的值,它决定了每次迭代中子数组的大小
// 从数组长度的一半开始作为初始间隔值,gap就是分割的子数组数量
for (int gap = n / 2; gap > 0; gap /= 2) {
// 循环从间隔值开始,遍历数组直到数组的末尾;代表循环所有的子数组
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
// 将当前元素 arr[j] 的值替换为前一个元素 arr[j - gap] 的值。
// 通过这个操作,将较大的元素向后移动,为当前元素腾出位置
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;}
arr[j] = temp;}}}
5、快速排序
快速排序是一种分治思想的排序算法,它的基本思想是通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,然后再分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
快速排序最好情况下时间复杂度是O(nlogn),平均时间复杂度也是O(nlogn),最坏情况是序列本来就是有序的,此时时间复杂度为O(n²),快速排序的空间复杂度可以理解为递归的深度,而递归的实现依靠栈,平均需要递归logn次,所以平均空间复杂度为O(logn)。
// 接收一个数组 arr,一个低索引 low ,和一个高索引 high 作为参数
public static void quickSort(int[] arr, int low, int high) {
// 检查 low 是否小于 high。如果不是,则意味着数组只有一个元素或为空,因此不需要排序
if (low < high) {
int pivot = partition(arr, low, high);
quickSort(arr, low, pivot - 1);
quickSort(arr, pivot + 1, high);}}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high];// 将最后一个元素作为枢轴元素 arr[high]
int i = low - 1; // 将 i 初始化为 low - 1,用于跟踪较小元素的索引
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
// 如果当前元素 arr[j] 小于枢轴元素,则增加 i 并交换 arr[i] 和 arr[j]
i++; // 较小元素索引+1
// 将当前元素 arr[j] 放在较小元素索引位置
swap(arr, i, j);
}
// 其他情况,则较小元素索引没有增加,说明当前元素应该放在右边
}
// 将枢轴元素 arr[high] 与索引 i + 1 处的元素交换。
// 确保枢轴元素左边是较小元素,右边是较大元素
swap(arr, i + 1, high);
return i + 1;// 将 i + 1 作为枢轴索引返回
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
6、归并排序
归并排序是一种分治思想的排序算法,它的基本思想是将待排序的数组分成若干个子序列,每个子序列都是有序的,然后再将子序列合并成一个有序的数组。
归并排序需要一个临时temp[]来储存归并的结果,空间复杂度为O(n),时间复杂度为O(nlogn),可以将空间复杂度由 O(n) 降低至 O(1),然而相对的时间复杂度则由 O(nlogn) 升至 O(n²)。
7、堆排序
堆排序是一种树形选择排序算法,它的基本思想是将待排序的数组构建成一个大根堆(或小根堆),然后将堆顶元素与堆底元素交换位置,再将剩余元素重新构建成堆,重复执行交换和重构堆的操作,直到整个数组有序。
堆排序是一种基于堆数据结构的排序算法,堆排序的平均、最好、最坏时间复杂度都为O(nlogn)。堆排序是就地排序,空间复杂度为常数O(1)。
8、基数排序
基数排序是一种非比较排序算法,它的基本思想是将待排序的数组按照位数(个位、十位、百位)进行划分,然后依次对每个位上的数字进行排序,最终得到有序的数组。
基数排序对于 n 个记录,执行一次分配和收集的时间为O(n+r),如果关键字有 d 位,则要执行 d 遍,所以总的时间复杂度为O(d(n+r))。该算法的空间复杂度就是在分配元素时,使用的桶空间,空间复杂度为O(r+n)=O(n)。
9、桶排序
桶排序是一种非比较排序算法,它的基本思想是将待排序的数组分到有限数量的桶里,然后对每个桶进行排序,最后依次将所有桶中的元素取出来,组成有序的数组。
桶排序的时间复杂度为O(n),其中n为待排序元素的个数。
10、计数排序
计数排序是一种非比较排序算法,它的基本思想是统计数组中每个元素出现的次数,然后根据元素出现的次数依次将元素放入有序的数组中。
计数排序时间复杂度为O(n+k),其中k为待排序的元素的最大值。