排序算法总览
自己总结的一个判断排序算法稳定性的方法:如果存在跨下标的数据交换,则算法不稳定。如选择排序、希尔排序、快速排序都存在跨下标的数据交换,它们都是不稳定的。而如果不存在跨下标的数据交换的算法不一定是稳定的,如将冒泡算法的大小判断条件加上等号,就是不稳定的,去掉等号是稳定的。
举个例子:在下面的表格中,如果下标为0的数据和下标为2的的数据进行交换,则第二个3就会跑到第一个3前面,这样的算法肯定是不稳定的。
数据 | 4 | 3 | 3 | 0 | 9 |
---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 |
以下Java代码实现的都是从小到大的排序。
算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 稳定 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 不稳定 |
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 稳定 |
希尔排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log 2 n ) O(n\log^2 n^) O(nlog2n) | O ( n log 2 n ) O(n\log^2 n) O(nlog2n) | O ( 1 ) O(1) O(1) | In-place | 不稳定 |
归并排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n ) O(n) O(n) | Out-place | 稳定 |
快速排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( log n ) O(\log n) O(logn) | In-place | 不稳定 |
堆排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( 1 ) O(1) O(1) | In-place | 不稳定 |
基数排序 | O ( n × k ) O(n\times k) O(n×k) | O ( n × k ) O(n\times k) O(n×k) | O ( n × k ) O(n\times k) O(n×k) | O ( n + k ) O(n+k) O(n+k) | Out-place | 稳定 |
桶排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n 2 ) O(n^2) O(n2) | O ( n + k ) O(n+k) O(n+k) | Out-place | 稳定 |
计数排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( k ) O(k) O(k) | Out-place | 稳定 |
1. 冒泡排序
//冒泡排序及优化算法
public int[] bubble(int[] arr) {
int len = arr.length;
boolean flag;
int temp;
for(int i=1; i<len; i++) {
flag = false;
for(int j=0; j<len-i; j++) {
if(arr[j] > arr[j+1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
//如果在一趟排序中没有交换过数据的位置,说明已完成排序,跳出循环
if(!flag) {
break;
}
}
return arr;
}
2. 选择排序
//选择排序
public static int[] select(int[] arr) {
int len = arr.length;
int k,temp;
for(int i=1; i<len; i++) {
k = 0;
for(int j=0; j<len-i; j++) {
if(arr[k] < arr[j+1]) {
k = j+1;
}
}
if(k != len-i) {
temp = arr[k];
arr[k] = arr[len-i];
arr[len-i] = temp;
}
}
return arr;
}
3. 直接插入排序
//直接插入排序
public static int[] insert(int[] arr) {
int len = arr.length;
int k,temp;
for(int i=1; i<len; i++) {
k = i-1;
temp = arr[i];
while(k>=0 && arr[k]>temp) {
arr[k+1] = arr[k];
k--;
}
arr[k+1] = temp;
}
return arr;
}
4. 希尔排序
要点1:值得注意的一点是第二个for循环的意思是每个组轮流排序,而不是排完一组再排下一组。
要点2:while循环中要隔一个gap的距离移位。
//希尔排序,移位式
public static int[] shell2(int[] arr) {
int len = arr.length;
int temp,j;
for(int gap = len/2; gap>0; gap /= 2) {
for(int i=gap; i<len; i++) {
j = i - gap;
temp = arr[i];
while(j>=0 && arr[j] > temp) {
arr[j+gap] = arr[j];
j -= gap;
}
arr[j+gap] = temp;
}
}
return arr;
}
5. 快速排序
要点1:从小到大排序时,当选择数组中第一个数为基点时,要先移动右指针再移动左指针,这样可以保证两个指针相交时的数据小于基点数据。相反,以最后一个数据为基点时,先移动左指针再移动右指针,可以保证两个指针相交时的数据大于基点数据。总之,选择两端数据为基点时,要先移动离基点远的数据,从大到小排序是也一样。
要点2:第二层两个while循环中判断语句不要忘记等号,因为不需要对基点数据进行判断,加上等号可以跳过基点数据。
//快速排序
public static void quick(int[] arr, int left, int right) {
if(left >= right) {
return ;
}
int t;
int i = left;
int j = right;
int temp = arr[left];
while(i < j) {
while(i < j && arr[j] >= temp) {
j--;
}
//不要忘记等号
while(i < j && arr[i] <= temp) {
i++;
}
if(i < j) {
t = arr[i];
arr[i] = arr[j];
arr[j] = t; }
}
arr[left] = arr[i];
arr[i] = temp;
quick(arr, left, i-1);
quick(arr, i+1,right);
}
6. 归并排序
//分
public static void merge(int[] arr, int left, int right, int[] temp) {
if(left < right) {
int mid = (left + right) / 2;
merge(arr, left, mid, temp);
merge(arr, mid+1, right, temp);
mergeSort(arr, left, mid, right, temp);
}
}
//两个有序数列的合并,
public static void mergeSort(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;
int j = mid + 1;
int t = 0;
while(i <= mid && j<=right) {
if(arr[i] <= arr[j]) {
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i <= mid) {
temp[t++] = arr[i++];
}
while(j <= right) {
temp[t++] = arr[j++];
}
t = 0;
while(left <= right) {
arr[left++] = temp[t++];
}
}
7. 堆排序
//堆排序
public static void heapSort(int[] arr) {
//经过这一轮循环后,调整为大顶堆
for(int i=arr.length/2-1; i>=0; i--) {
//从最后一个非叶子节点开始,从右向左,从下到上
ajustHeap(arr, i, arr.length);
}
int temp;
//将堆顶元素放到数组最后
for(int i=arr.length-1; i>0; i--) {
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
ajustHeap(arr, 0, i);
}
}
/**
* 将以i为根节点的二叉子树调整为大顶堆
* @param arr 目标数组
* @param i 要调整的位置
* @param length 要调整的数组长度
*/
public static void ajustHeap(int[] arr, int i, int length) {
int temp = arr[i];
for(int k=i*2+1; k<length ;k=k*2+1) {
if(k+1 < length && arr[k+1] > arr[k]) {
k++;
}
if(arr[k] > temp) {
arr[i] = arr[k];
i = k;
}else {
//如果arr[i]比两个子节点的值要大,说明已完成调整
break;
}
}
arr[i] = temp;
}
8. 基数排序
//基数排序
public static void radix(int[] arr) {
int len = arr.length;
int max = arr[0];
for(int i=1; i<len; i++) {
if(arr[i] > max) {
max = arr[i];
}
}
int maxLen = (max + "").length();
int[][] bucket = new int[10][len];
//记录每个桶中数据的个数
int[] bucketCount = new int[10];
int temp;
int index = 0;
for(int i=0, n=1; i<maxLen; i++, n *= 10) {
//将数据放到桶里
for(int j=0; j<len; j++) {
temp = arr[j] / n % 10;
bucket[temp][bucketCount[temp]] = arr[j];
bucketCount[temp]++;
}
index = 0;
//将桶中数据放到数组中
for(int j=0; j<10; j++) {
for(int k=0; k<bucketCount[j]; k++) {
arr[index++] = bucket[j][k];
}
bucketCount[j] = 0;
}
}
}