一、冒泡排序 冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
优化 :当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
空间复杂度:O(1)
时间复杂度:最好:O(n),最坏:O(n^2^),平均:O(n^2^)
稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static void bubbleSort (int [] arr, int n) { if (n <= 1 ) { return ; } for (int i = 0 ; i < n - 1 ; i++) { boolean flag = true ; for (int j = 0 ; j < n - i - 1 ; j++) { if (arr[j] > arr[j + 1 ]) { int temp = arr[j]; arr[j] = arr[j + 1 ]; arr[j + 1 ] = temp; flag = false ; } } if (flag) { break ; } } }
二、插入排序 将数组中的数据分为两个区间,已排序区间 和未排序区间 。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,排序结束。
空间复杂度:O(1)
时间复杂度:最好:O(n),最坏:O(n^2^),平均:O(n^2^)
稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void insertionSort (int [] arr, int n) { if (n <= 1 ) { return ; } for (int i = 1 ; i < n; i++) { int value = arr[i]; int j = i - 1 ; for (; j >= 0 ; j--) { if (value < arr[j]) { arr[j + 1 ] = arr[j]; } else { break ; } } arr[j + 1 ] = value; } }
三、选择排序 选择排序也分为已排序区间和未排序区间,每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
空间复杂度:O(1)
时间复杂度:最好:O(n^2^),最坏:O(n^2^),平均:O(n^2^)
不稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void selectSort (int [] arr, int n) { if (n <= 1 ) { return ; } for (int i = 0 ; i < n; i++) { int index = i; for (int j = i + 1 ; j < n; j++) { if (arr[index] > arr[j]) { index = j; } } int temp = arr[index]; arr[index] = arr[i]; arr[i] = temp; } }
小总结
以上这三种排序算法(时间复杂度都为 O(n^2^) ),实现代码都非常简单,对于小规模数据的排序,用起来非常高效。但是在大规模数据排序的时候,这个时间复杂度还是稍微有点高。
四、归并排序 归并排序的核心思想:如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
归并排序使用的就是分治思想(分治是一种解决问题的处理思想,递归是一种编程技巧)。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
空间复杂度:O(n)
时间复杂度:最好:O(nlogn),最坏:O(nlogn),平均:O(nlogn)
稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public void merge_sort (int [] arr, int n) { merge_sort_c(arr, 0 , n - 1 ); }public void merge_sort_c (int [] arr, int l, int r) { if (l >= r) { return ; } int mid = l + ((r - l) >> 1 ); merge_sort_c(arr, l, mid); merge_sort_c(arr, mid + 1 , r); merge(arr, l, mid, r); }public void merge (int [] arr, int l, int mid, int r) { int [] temp = new int [r - l + 1 ]; int i = l; int j = mid + 1 ; int k = 0 ; while (i <= mid && j <= r) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; } } while (i <= mid) { temp[k++] = arr[i++]; } while (j <= r) { temp[k++] = arr[j++]; } for (k = 0 ; k < temp.length; k++) { arr[l + k] = temp[k]; } }
五、快速排序 快排的思想:
如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
空间复杂度:O(1)
时间复杂度:最好:O(nlogn),最坏:O(n^2^),平均:O(nlogn)
不稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static void quickSort (int [] arr, int n) { quickSortInternally(arr, 0 , n - 1 ); }public static void quickSortInternally (int [] arr, int l, int r) { if (l >= r) { return ; } int i = l; int j = r; int temp = arr[i]; while (i < j) { while (j > i && arr[j] >= temp) { j--; } while (i < j && arr[i] <= temp) { i++; } if (i < j) { swap(arr, i, j); } } arr[l] = arr[i]; arr[i] = temp; quickSortInternally(arr, l, i - 1 ); quickSortInternally(arr, i + 1 , r); }public static void swap (int [] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
六、堆排 堆排序是一种原地的、时间复杂度为O(nlogn)的排序算法。
堆的要求:
大顶堆:每个节点的值都大于等于子树中每个节点值的堆
小顶堆:每个节点的值都小于等于子树中每个节点值的堆
堆中插入元素、删除元素的时间复杂度:O(logn)
堆排序的实现:
建堆(时间复杂度O(n))
排序
建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。
数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。
当堆顶元素移除之后,我们把原下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n-1 个元素重新构建成堆。
堆化完成之后,我们再取堆顶的元素,放到下标是 n-1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。(类似删除堆顶元素)
使用数组存储表示完全二叉树时,从数组下标为 1 开始存储数据,数组下标为 i 的节点,左子节点为 2i , 右子节点为 2i + 1 。 从数组下标为 0 开始存储数据,如果节点的下标是 i,那左子节点的下标就是 2 ∗ i + 1,右子节点的下标就是 2 ∗ i + 2,父节点的下标就是 (i-1)/2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public static void headSort (int [] arr, int n) { if (n < 1 ) { return ; } builderHead(arr); int length = arr.length - 1 ; while (length > 0 ) { swap(arr, 0 , length); heapify(arr, --length, 0 ); } }public static void builderHead (int [] arr) { for (int i = (arr.length - 1 ) / 2 ; i >= 0 ; i--) { heapify(arr, arr.length - 1 , i); } }public static void heapify (int [] arr, int n, int i) { while (true ) { int maxPos = i; int left = 2 * i + 1 ; int right = 2 * i + 2 ; if (left <= n && arr[maxPos] < arr[left]) { maxPos = left; } if (right <= n && arr[maxPos] < arr[right]) { maxPos = right; } if (maxPos == i) { break ; } swap(arr, maxPos, i); i = maxPos; } }public static void swap (int [] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
为什么快排比堆排序性能要好?
堆排序数据访问的方式没有快排友好:堆排序不是局部顺序访问,对CPU缓存不太友好
对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序
快排基于比较和交换,其交换次数不会多于逆序度
堆排开始是建堆,其会打乱数据的原有顺序,导致数据的有序度降低
七、桶排序 桶排序的核心思想:将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中
时间复杂度:O(n)
空间复杂度:O(n)
稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static void bucketSort (int [] arr, int bucketSize) { if (arr.length <= 1 ) { return ; } int max = arr[0 ]; int min = arr[0 ]; for (int i : arr) { max = Math.max(max, i); min = Math.min(min, i); } int bucketCount = (max - min) / bucketSize + 1 ; List<Integer>[] buckets = new List [bucketCount]; for (int i = 0 ; i < arr.length; i++) { int bucketIndex = (arr[i] - min) / bucketSize; if (buckets[bucketIndex] == null ) { buckets[bucketIndex] = new ArrayList <>(); } buckets[bucketIndex].add(arr[i]); } int k = 0 ; for (int i = 0 ; i < bucketCount; i++) { if (buckets[i] == null ) { continue ; } Collections.sort(buckets[i]); for (int j = 0 ; j < buckets[i].size(); j++) { arr[k++] = buckets[i].get(j); } } }
在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了
八、计数排序 计数排序其实是桶排序的一种特殊情况。当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
缺点 :
只能用在数据范围不大 的场景中,如果数据范围 k(最大值) 比要排序的数据 n 大很多,就不适合用计数排序了
只能给非负整数 排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数
时间复杂度:O(n)
稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public static void countingSort (int [] arr, int n) { if (n <= 1 ) { return ; } int max = arr[0 ]; for (int i = 0 ; i < n; i++) { max = Math.max(arr[i], max); } int [] c = new int [max + 1 ]; for (int i = 0 ; i < n; i++) { c[arr[i]]++; } for (int i = 1 ; i < c.length; i++) { c[i] += c[i - 1 ]; } int [] r = new int [n]; for (int i = n - 1 ; i >= 0 ; i--) { int index = c[arr[i]] - 1 ; r[index] = arr[i]; c[arr[i]]--; } for (int i = 0 ; i < n; i++) { arr[i] = r[i]; } }
为什么计数后还要开临时数组,为什么不直接通过计数直接赋值?
如果是单纯的数据排序可以,但是如果有对应的信息则不行,因为要清楚对应的分数所对应的信息。(如:成绩信息,小红 95,小李 95,那么第一个 95 对应的是那位同学需要通过后续的操作进行确定)
九、基数排序 基数排序对要排序的数据是有要求的,需要可以分割出独立的 “位” 来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。
时间复杂度:O(n)
稳定 的排序算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static void radixSort (int [] arr) { int max = arr[0 ]; for (int i = 0 ; i < arr.length; i++) { max = Math.max(arr[i], max); } for (int exp = 1 ; max / exp > 0 ; exp *= 10 ) { radixCountingSort(arr, exp); } }public static void radixCountingSort (int [] arr, int exp) { if (arr.length <= 1 ) { return ; } int [] c = new int [10 ]; for (int i = 0 ; i < arr.length; i++) { c[(arr[i] / exp) % 10 ]++; } for (int i = 1 ; i < arr.length; i++) { c[i] += c[i - 1 ]; } int [] r = new int [arr.length]; for (int i = arr.length - 1 ; i >= 0 ; i--) { int index = c[(arr[i] / exp) % 10 ] - 1 ; r[index] = arr[i]; c[(arr[i] / exp) % 10 ]--; } for (int i = 0 ; i < arr.length; i++) { arr[i] = r[i]; } }