参考菜鸟教程:https://www.runoob.com/w3cnote/ten-sorting-algorithm.html
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用以下两张图概括:
名词解释:
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。其基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n) | O( n) | O(n2) | O(1) | 稳定 |
LC 75. 颜色分类
LC 283. 移动零
class BubbleSort{
public static int[] bubbleSort(int[] nums){
int temp=0; // 用于交换临时存储值
for(int i=0;i<nums.length-1;i++){
int swap = 0; //设置为发生交换标志,进行剪枝
for(int j=0;j<nums.length-1-i;j++){
if(nums[j]>nums[j+1]){
temp=nums[j];
nums[j]=nums[j+1];
nums[j+1]=temp;
swap=1; //有交换发生
}
}
if(swap==0) break; //本趟比较中未出现交换则结束排序
}
return nums;
}
}
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。它最大的特点就是交换移动数据的次数相当少。最好的情况下,移动0次;最差的情况下,交换次数也仅为n-1次。尽管与冒泡排序时间复杂度同为O(n2),但是性能还是略优于冒泡排序。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n) | O(n2) | O(n2) | O(1) | 不稳定 |
无论最好最差的情况,选择排序的比较次数都是一样多的,需要比较的次数为: ∑ i = 2 n ( i − 1 ) = 1 + 2 + 3 + ⋅ ⋅ ⋅ + ( n − 1 ) = n ( n − 1 ) 2 \sum_{i=2}^n(i-1) = 1+2+3+···+(n -1)=\frac{n(n-1)}{2} i=2∑n(i−1)=1+2+3+⋅⋅⋅+(n−1)=2n(n−1)
LC 215. 数组中的第K个最大元素
LC 912. 排序数组
class SelectionSort{
public static int[] SelectionSort(int[] nums){
if (nums.length == 0) return nums;
for(int i = 0; i < nums.length - 1; i++){ // 总共要经过 N-1 轮比较
int minIndex = i;
for(int j = i + 1; j < nums.length; j++){ // 每轮需要比较的次数 N-i
if(nums[j] < nums[minIndex]){
minIndex = j; // 记录目前能找到的最小值元素的下标
}
}
int temp = nums[minIndex];
nums[minIndex] = nums[i];
nums[i] = temp;
}
return nums;
}
}
插入排序是一种最简单直观的排序算法,它的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
插入排序适合于小序列的排序,当序列长度小于等于20或20左右时,使用插入排序效率更高。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n2) | O(n) | O(n2) | O(1) | 稳定 |
插入排序的性能比冒泡和选择排序要高一些
LC 912. 排序数组
LC 147. 对链表进行插入排序
class Insertsort{
public static int[] Insertsort(int[] arr){
// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < arr.length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
arr[j] = tmp;
}
}
return arr;
}
}
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序的算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log2 n) | O(n log2 n) | O(1) | 不稳定 |
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。需要注意的是,增量序列的最后一个增量必须等于1。
LC 912. 排序数组
LC 506. 相对名次
class SortArray{
public static int[] sortArray(int[] nums) {
int length = nums.length;
int temp;
for(int step = length / 2; step >= 1; step /= 2){
for(int i = step; i < length; i++){
temp = nums[i];
int j = i;
while(j >= step && nums[j - step] > temp){
nums[j] = nums[j - step];
j -= step;
}
nums[j] = temp;
}
}
return nums;
}
}
}
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
它的原理是:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]([x]表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,···,重复到有一个长度为n的有序子序列为止。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
LC 剑指Offer 51. 数组中的逆序对
LC 面试题10.01. 合并排序的数组
class MergeSort{
public static int[] mergeSortMain(int[] arr) {
// 空数组 或 只有一个元素的数组,则什么都不做。
if (arr == null || arr.length <= 1) return arr;
mergeSort(arr, 0, arr.length - 1);
return arr;
}
private static void mergeSort(int[] arr, int left, int right){
if (left>= right) return;
// 计算出中间值,这种算法保证不会溢出。
int mid = left+ ((right - left) >> 1);
// 先对左边排序
mergeSort(arr, left, mid);
// 先对右边排序
mergeSort(arr, mid + 1,right );
// 归并两个有序的子序列
merge(arr, left, mid, right );
}
private static void merge(int[] arr, int low, int mid, int high) {
// temp[]是临时数组,包左不包右,所以要额外 + 1。
int[] temp = new int[high - low + 1];
int left = low; // 左侧指针从low开始。
int right = mid + 1; // 右侧指针从mid+1开始。
int index = 0; // 此索引用于temp[]
// 当两个子序列还有元素时,从小到大放入temp[]中。
while (left <= mid && right <= high) {
if (arr[left] < arr[right]) {
temp[index++] = arr[left++];
} else {
temp[index++] = arr[right++];
}
}
// 要么左边没有元素
while (left <= mid) {
temp[index++] = arr[left++];
}
// 要么右边没有元素
while (right <= high) {
temp[index++] = arr[right++];
}
// 重新赋值给arr对应的区间。
for (int i = 0; i < temp.length; i++) {
arr[low + i] = temp[i];
}
}
}
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists),本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
其基本思想是:通过一趟排序将待排的记录分割成独立的两个部分,其中一部分的关键字均比另一部分的记录的关键字小,则可分别对着两部分记录继续进行排序,以达到整个序列有序的目的。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log n) | O(n2) | O(log n) | 不稳定 |
LC 912. 排序数组
LC 169.多数元素
class QuickSort{
public static int[] QuickSortMain(int[] sourceArray) {
return quickSort(arr, 0, arr.length - 1);
}
private int[] quickSort(int[] arr, int left, int right) {
if (left < right) {
int partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
return arr;
}
private int partition(int[] arr, int left, int right) {
// 设定基准值(pivot)
int pivot = arr[left];
int index = left + 1;
for (int i = index; i <= right; i++) {
if (arr[i] < pivot) {
swap(arr, i, index);
index++;
}
}
swap(arr, left, index - 1);
return index - 1;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
以最大堆为例,其基本思想是:将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就跟将其与堆数组的末尾元素进行交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这就会得到n个元素中的次大值。如此反复执行,就会有一个长度为n的有序序列。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
LC 215. 数组中的第K个最大元素
LC 剑指Offer 40.最小的k个数
class HeapSort{
public static int[] HeapSortMain(int[] arr) {
int len = arr.length;
buildMaxHeap(arr, len); // 将数组整理成堆
for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i); // 把堆顶元素(当前最大)交换到数组末尾
len--; // 逐步减少堆有序的部分
heapify(arr, 0, len); // 下标 0 位置下沉操作,使得区间 [0, i] 堆有序
}
return arr;
}
//将数组整理成堆(堆有序)
private void buildMaxHeap(int[] arr, int len) {
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}
// i为当前下沉元素的下标
private void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
LC 912. 排序数组
LC 1122. 数组的相对排序
class CountingSort{
public static int[] countingSortMain(int[] arr) {
int maxValue = getMaxValue(arr);
return countingSort(arr, maxValue);
}
private int getMaxValue(int[] arr){
int maxValue = arr[0];c
for(int value : arr){
if(maxValue < value){
maxValue = value;
}
}
return maxValue;
}
private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
}
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
在额外空间充足的情况下,尽量增大桶的数量
使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
优点:实现简单,在数据量小的情况下性能良好,是稳定的排序算法
缺点:严重依赖于额外的存储空间
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n+k) | O(n+k) | O(n2) | O(n+k) | 稳定 |
LC 908.最小差值I
LC 164.最大间距
class BucketSort{
public static int[] BucketSortMain(int[] arr) {
if (arr.length == 0) {
return arr;
}
// 计算最大值与最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 利用映射函数将数据分配到各个桶中
// 将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
return arr;
}
}
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。所有对于字符串和文字排序不适合。
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n x k) | O(n x k) | O(n x k) | O(n + k) | 稳定 |
实现:将所有待比较数值(自然数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的两种方式:
LC 164.最大间距
LC 561.数组拆分I
// MSD 只考虑正整数的情况下的代码
class RadixSort{
public static int[] radixSortMain(int[] arr) {
// 高位优先法 MSD
//待排序列最大值
int max = arr[0];
int exp;//指数
//计算最大值
for (int anArr : arr) {
if (anArr > max) {
max = anArr;
}
}
//从个位开始,对数组进行排序
for (exp = 1; max / exp > 0; exp *= 10) {
//存储待排元素的临时数组
int[] temp = new int[arr.length];
//分桶个数
int[] buckets = new int[10];
//将数据出现的次数存储在buckets中
for (int value : arr) {
//(value / exp) % 10 :value的最底位(个位)
buckets[(value / exp) % 10]++;
}
//更改buckets[i],
for (int i = 1; i < 10; i++) {
buckets[i] += buckets[i - 1];
}
//将数据存储到临时数组temp中
for (int i = arr.length - 1; i >= 0; i--) {
temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
buckets[(arr[i] / exp) % 10]--;
}
//将有序元素temp赋给arr
System.arraycopy(temp, 0, arr, 0, arr.length);
return arr;
}
}
}