排序算法总览
排序算法
冒泡排序(Bubble Sort)
从前往后两两比较相邻元素的值,若为逆序则交换他们,直到(n-1+i),若某次遍历未发现有逆序情况证明已经有序则应该直接返回。
Bubble Sort
/**
* 1.冒泡排序
* 每次在0~(n-1-i)中将最大的一步一步冒泡到最后
*
* @param arr
*/
private static void bubbleSort(int arr[]) {
int n = arr.length;
boolean flag = false;//是否已经正序
for (int i = 0; !flag; i++) {
flag = true;
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
flag = false;
}
}
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:由于对于相等元素不会交换所以 => 稳定
选择排序 (Selection Sort)
每次找出i~(n-1) 最小值 与第i位交换
Selection Sort
/**
* 2.选择排序
* 每次从i~(n-1)找出最小元素放在最前面
*
* @param arr
*/
private static void selectSort(int arr[]) {
int n = arr.length;
int minIndex = 0;
for (int i = 0; i < n; i++) {
minIndex = i;//假定最小元素是第i个
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex])
minIndex = j;
}
swap(arr, minIndex, i);//把i~n中最小元素和第i位交换
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:由于最小值元素会直接与i位元素互换所以可能造成相同元素次序改变 => 不稳定
同样适用于链式存储结构
直接插入排序(Insertion Sort)
从第二个元素开始每次从向前 0~(i-1) 有序序列倒叙依次尝试自己合适的位置,直到比前一个元素大
Insertion Sort
/**
* 3.直接插入排序
* 从第二个元素开始每次从向前 0~(i-1) 有序序列倒叙依次尝试自己合适的位置,直到比前一个元素大
*
* @param arr
*/
private static void insertionSort(int arr[]) {
int n = arr.length;
int temp;
for (int i = 1; i < n; i++) {
temp = arr[i];//待插入的元素
int j = i;
for (; j > 0 && temp < arr[j - 1]; j--) {
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:由于都是向前先比较在插入小于等于当前元素不会移动 => 稳定
希尔排序(Shell Sort)
希尔排序,又称缩小增量排序,是插入排序变形,基于插入排序适用于基本有序和数据量小的基本思想:按照增量序列函数每次按增量提取出相差h的元素组成待排序列进行直接插入排序,然后缩小增量后在排序知道增量变为1完成最后一次排序
Shell Sort
private static void shellSort2(int arr[]) {
int n = arr.length;
int h = n / 2;//初始增序起始
while (h >= 1) {//获得增序序列并不断缩小h
int temp;
for (int i = h; i < n; i++) {//从第一个h位元素开始,每次和所有前面与它相差h的元素序列做插入排序
temp = arr[i];//待插入的元素
int j = i;
for (; j >= h && temp < arr[j - h]; j -= h) {//这里的h是 最大值而不是准确数字但该序列第二位一定大于等于h
arr[j] = arr[j - h];
}
arr[j] = temp;
}
h = h / 2;
}
}
空间复杂度:O(1)
时间复杂度:O(n^2)
稳定性:由于每次划分子序列再排序可能改变相同元素的相对次序 => 不稳定
归并排序(Merge Sort)
基本思想:将多个有序序列组合成一个新的有序表,自底向上归并应该是先2个元素合并,然后2个长度为2的元素合并,然后2个长度为4的元素合并直到直到子序列长度>=n
package basicAlgorithm.sort;
/**
* 归并排序
*/
public class MergeSort {
private static int[] aux;//辅助函数
/**
* 将[l...r-1]与[r..rEnd]归并
* @param arr
* @param l
* @param r
* @param rEnd
*/
private static void merge(int arr[], int l, int r, int rEnd) {
int temp = l;
int lEnd = r - 1;
//先将待排序两组子序列复制到辅助数组中
for (int i = l; i <= rEnd; i++) {
aux[i] = arr[i];
}
while (l <= lEnd && r <= rEnd) {
if (aux[l] < aux[r]) {
arr[temp++] = aux[l++];
} else {
arr[temp++] = aux[r++];
}
}
//将另一边剩下的复制到原数组
while (l <= lEnd) {
arr[temp++] = aux[l++];
}
// (右边的不用复制吧,本来就在原数组)
while (r<=rEnd){
arr[temp++]=aux[r++];
}
}
/**
* 自底向上的归并排序(递归写法)
* @param arr
* @param L
* @param R
*/
private static void mergeSort(int arr[], int L, int R) {
if (L >= R) {
return;
}
int mid = (L + R) / 2;
mergeSort(arr, L, mid);
mergeSort(arr, mid + 1, R);
if (arr[mid] > arr[mid + 1]) {//如果左右已经有序则不归并
merge(arr, L, mid + 1, R);
}
}
/**
* 自底向上的归并排序(迭代写法)
* @param arr
*/
private static void mergeSortBU(int arr[]) {
int n = arr.length;
for (int size = 1; size < n; size = 2*size) { //size 表示每次将2个size大小的序列归并成一个序列
for (int i = 0; i + size < n; i = i + (size * 2)) {
merge(arr, i, i + size, Math.min(i + (size * 2) - 1, n - 1));
}
}
}
public static void main(String[] args) {
int array[] = {9, 8, 19, 6, 5, 3, 4, 2, 1};
//初始化辅助数组
aux = new int[array.length];
// mergeSort(array, 0, array.length - 1);
mergeSortBU(array);
for (int a : array) {
System.out.println(a);
}
}
}
空间复杂度:O(n) 辅助数组
时间复杂度:O(nlogn)
稳定性:因为依此合并没有交换操作所以 => 稳定
快速排序(Quick Sort)
号称21世纪最优化的算法来了,快排也是分治思想的应用,基本思想就是先设一个基准点然后将序列小于基准点的部分放到基准点左面小于基准点的部分放到基准点右面,然后在递归的处理左面和右面。快排有很多版本我下面代码的是最常见双路快排版
Quick Sort
//双路快排
private static void quickSort2(int[] array, int l, int r) {
if (l >= r) {
return;
}
swap(array, l, ((int) (Math.random() * (r - l)) + l));
int pivote = array[l];//基点放到最左边
int i = l + 1;//左边比基点小的标志位
int j = r;//右边比基点大的标志位
while (true) {
while (i <= r && array[i] < pivote) {
i++;
}
while (j >= l + 1 && array[j] > pivote) {
j--;
}
if (i >= j) break;
else swap(array, i++, j--);
}
swap(array, l, j);
quickSort2(array, l, j - 1);
quickSort2(array, j + 1, r);
}
private static void quickSort(int[] array) {
quickSort2(array, 0, array.length - 1);
}
空间复杂度:O(logn) 由于快排要借助递归实现所以要消耗额外系统栈空间平均情况下栈深log2(n)
时间复杂度:O(nlogn)
稳定性:因为选取基准点的过程中可能会改变相同元素次序 => 不稳定
另外序列越无序越随机快排效率越高,而现实中的序列大多是随机分布的所以快排广泛应用
堆排序(Heap Sort)
堆是一种树形结构,是一颗完全二叉树,并满足任意节点都大于他的子结点(大根堆)
image.png
Heap Sort
private static void sink(int[] array, int n, int k) {//下沉
while (2 * k + 1 <= n - 1) {
int j = 2 * k + 1;//左孩子索引
if (j + 1 <= n - 1 && array[j + 1] > array[j]) j++;//如果有右孩子并且右孩子比左孩子大 j 换成右孩子索引
if (array[j] > array[k]) {
swap(array, j, k);
k = j;
} else {
break;
}
}
}
private static void heapSort(int[] array) {
int n = array.length;
//heapify 大根堆建成
for (int i = (n - 1) / 2; i >= 0; i--) {
sink(array, n, i);
}
//排序
for (int j = n - 1; j > 0; j--) {
swap(array, 0, j);//把最大的和最后以为交换
sink(array, j, 0);//缩小对的
}
}
BuildHeap 时间复杂度O(nlogn)
heapify 时间复杂度O(n)
稳定性:=> 不稳定
空间复杂度:O(1)
计数排序(Counting Sort)
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
Insertion Sort
桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的N个数据均匀的分配到K个桶中
基数排序(Radix Sort)
分为 MSD(最高位优先) 和 LSD(最低位优先)
LSD
基数排序 vs 计数排序 vs 桶排序
image.png
其中, d 表示位数, k 在基数排序中表示 k 进制,在桶排序中表示桶的个数, maxV 和 minV 表示元
素最大值和最小值。
- 首先,基数排序和计数排序都可以看作是桶排序。
- 计数排序本质上是一种特殊的桶排序,当桶的个数取最大( maxV-minV+1 )的时候,就变成了计数排序。
- 基数排序也是一种桶排序。桶排序是按值区间划分桶,基数排序是按数位来划分;基数排序可以看做是多轮桶排序,每个数位上都进行一轮桶排序。
- 当用最大值作为基数时,基数排序就退化成了计数排序。
- 当使用2进制时, k=2 最小,位数 d 最大,时间复杂度 O(nd) 会变大,空间复杂度 O(n+k) 会变小。当用最大值作为基数时, k=maxV 最大, d=1 最小,此时时间复杂度 O(nd) 变小,但是空间复杂度 O(n+k) 会急剧增大,此时基数排序退化成了计数排序。
总结
排序算法