本文统统是升序。
一、十一种排序算法
1、排序算法间的比较
排序算法名称 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|
最好 | 平均 | 最坏 | |||
冒泡排序 | O(N) | O(N2) | O(N2) | O(1) | 稳定 |
选择排序 | O(N2) | O(N2) | O(N2) | O(1) | 不稳定 |
排堆序 | O(N)+O(N logN) | O(N)+O(N logN) | O(N)+O(N logN) | O(1) | 不稳定 |
插入排序 | O(N) | O(N2) | O(N2) | O(1) | 稳定 |
希尔排序 | O(N) | 取决于步长序列 | 取决于步长序列 | O(1) | 不稳定 |
归并排序 | O(N)=2O(N/2)+O(N) 即 O(N logN) | O(N logN) | O(N logN) | O(N)---(递归+N/2空间的新数组) | 稳定 |
快速排序 | O(N)=2O(N/2)+O(N) 即 O(N logN) | O(N logN) | O(N)=O(N-1)+O(N) 即 O(N2) | O(logN)---(递归) | 不稳定 |
计数排序 | O(N+K)---(K=max-min+1) | O(N+K) | O(N+K) | O(N+K) | 稳定 |
基数排序 | dxO(N+K)---(d是整数位数) | dxO(N+K)---(K既是整数范围也是进制数) | dxO(N+K) | O(N+K)---(已复用计数数组和目标数组) | 稳定 |
桶排序 | O(N+K)---(大概) | O(N+K) | O(N+K) | O(N+M)---(M是桶的数量) | 稳定 |
休眠排序 | O(N)---(这种耗时舍我其谁) | O(N)---(滑稽脸) | O(N)---(滑稽脸) | O(N) | 不稳定 |
2、排序算法的分类
① 基于比较的排序
冒泡排序、选择排序、推排序、插入排序、希尔排序、归并排序、快速排序
② 非比较的排序
计数排序、基数排序、桶排序
③ 史上最强排序
休眠排序当之无愧稳居第一。
二、冒泡排序
1、思路
冒泡其实就是将前一个元素和后一个元素比较,如果前面元素比后面大就交换两个位置,那一轮比较后其实最大的数就在数组末尾了,执行n-1次循环。今天问了不少人冒泡排序怎么写,外面两层for循环都是清一色这样的:
for(int count =0;count<array.length-1;count++) {
for(int i=0;i<array.length-1-count;i++) {
这样其实就不方便进行尾部优化了。
如何优化?①最简单的优化方式其实就是设个flag,如果一次循环中没有发生交换,那就不需要再执行下一轮循环了,数组已经有序了。
② 还一种优化方式就是,每次都记录下最后一次冒泡的索引,下次执行循环的时候只需要比较到那个索引位置就可以了
2、代码
未优化代码:
public class BubbleSort1<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1 - i; j++) {
if (compareByIndex(i, i + 1) > 0) {
swapByIndex(i, i + 1);
}
}
}
}
}
优化代码①:
public class BubbleSort2<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int i = 0; i < array.length - 1; i++) {
boolean isAscOrder = true;
for (int j = 0; j < array.length - 1 - i; j++) {
if (compareByIndex(i, i + 1) > 0) {
swapByIndex(i, i + 1);
isAscOrder = false;
}
}
if (isAscOrder) //如果冒泡的时候发现数组已经有序了就不再执行循环
return;
}
}
}
优化代码②:
public class BubbleSort3<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
int lastIndex = 0;
for (int begin = 0; begin < end; begin++) {
if (compareByIndex(begin, begin + 1) > 0) {
swapByIndex(begin, begin + 1);
lastIndex = begin + 1; //记录最后进行交换的较大索引(外层循环还要减减)
}
}
end = lastIndex;
}
}
}
可以看到实现的时候如果值相等并没有交换,冒泡排序完全可以实现为稳定的排序。
三、选择排序
1、思路
思路很简单,每次循环从数组中选出最大值,将最大值和数组尾部元素交换位置,执行n-1次循环。
2、代码
public class SelectionSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
int maxIndex = 0;
for (int begin = 1; begin <= end; begin++) {
if (compareByIndex(maxIndex, begin) < 0)
maxIndex = begin;
}
swapByIndex(maxIndex, end); //每次都将最大值和数组尾部元素交换
}
}
}
选择排序是不稳定的排序,当最大值正好是索引为0的元素,并且此时数组中有多个最大值的时候,第一次排序直接改变了相对位置。
四、堆排序
1、思路
很简单,既然是升序直接建个最大堆,每次都取出堆顶元素和数组末尾元素交换,堆顶进行下滤,并将堆的末尾元素剔除,直到堆中只剩下一个元素为止。和选择排序思路一样,可以看成是选择排序的升级版。
2、代码
public class HeapSort<E extends Comparable> extends Sort<E> {
private int size = 0;
/**
* 建立堆
*
* @return
*/
private E[] heapBatch() {
size = array.length;
for (int i = (size >> 1) - 1; i >= 0; i--) //从最后个非叶子结点开始下滤
slipDown(i);
return array;
}
/**
* 下滤
*
* @param index 执行下滤的索引
*/
private void slipDown(int index) {
E element = array[index];
while (index < (size >> 1)) {
int maxChildIndex = (index << 1) + 1;
E maxChild = array[maxChildIndex];
if (maxChildIndex + 1 < size && compare(maxChild, array[maxChildIndex + 1]) < 0) {
maxChildIndex++;
maxChild = array[maxChildIndex];
}
if (compare(maxChild, element) > 0)
array[index] = maxChild;
else
return;
index = maxChildIndex;
}
array[index] = element;
}
@Override
protected void sort() {
heapBatch();
do {
swapByIndex(0, --size);
slipDown(0);
} while (size > 1);
}
}
堆排序和选择排序思想一样,所以也是不稳定的排序算法。
五、插入排序
1、思路
和扑克牌差不多,尾部插入。一轮循环摸一次牌,摸到新牌的时候需要依次和前面已经排好顺序的牌进行比较,如果新的牌比前面的牌大就交换,否则就摸下一张牌,直到结束。
如何优化?①一张张牌一次交换可以优化为牌的挪动,先从已经排好序的牌中遍历找到第一张比摸到的新牌大的牌并记录其位置,将从新牌位置到记录位置上的所有牌都往后挪动一个位置,将新牌直接放到之前记录的位置上。
②之前查找位置的时候是逐个遍历的,因为遍历的数组已经是有序了,可用二分查找查出新牌插入的位置,之后也是挪动,优化了查询次数,再将新牌放到二分查找查出的位置上。
2、代码
未优化代码:
public class InsertionSort1<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
for (int index = begin - 1; index >= 0; index--) {
if (compareByIndex(index, index + 1) > 0) //前面的数比后面数大就不断交换
swapByIndex(index, index + 1);
else
break;
}
}
}
}
优化代码①:
public class InsertionSort2<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
E v = array[begin];
int index;
for (index = begin - 1; index >= 0; index--) {
if (compare(array[index], v) > 0) //前面的数比待插入的数大就不断往后挪
array[index + 1] = array[index];
else
break;
}
array[index + 1] = v; //跳出循环的时候已经自减过一了, 需要加回来
}
}
}
优化代码②:
public class InsertionSort3<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
E v = array[begin];
int index = searchIndex(begin, v);
for (int temp = begin - 1; temp >= index; temp--)
array[temp + 1] = array[temp];
array[index] = v;
}
}
/**
* [begin, end]
*/
public int indexOf1(E[] integers, E element) {
int begin = 0, end = integers.length - 1;
while (begin <= end) {
int mid = (begin + end) / 2;
if (compare(integers[mid], element) < 0)
begin = mid + 1;
else if (compare(integers[mid], element) > 0)
end = mid - 1;
else
return mid;
}
return -1;
}
/**
* [begin, end)
*/
public int indexOf2(E[] integers, E element) {
int begin = 0, end = integers.length;
while (begin < end) {
int mid = (begin + end) / 2;
if (compare(integers[mid], element) < 0)
begin = mid + 1;
else if (compare(integers[mid], element) > 0)
end = mid;
else
return mid;
}
return -1;
}
/**
* 从左边已经排好序的数组中查找元素的插入位置
*
* @param index 尾索引
* @return 元素的插入位置
*/
private int searchIndex(int index, E element) {
int begin = 0, end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (compare(array[mid], element) > 0)
end = mid;
else //如果中间元素小于等于待查找元素就去右边查找
begin = mid + 1;
}
return begin;
}
}
二分查找的时候用的是左闭右开区间,参考代码中给的indexOf2函数
,这样方便优化。可以看到插入排序也可以实现为稳定的排序,二分搜索的时候两元素一旦相等就还要去右边寻找,元素的相对位置并没有变化。
六、希尔排序
1、思路
根据步长递减序列,依次将数组分为n列,对每一列元素进行排序,最后步长必须是1,再对步长为1的序列排序后即希尔排序算法结束。其实每次根据步长对数组进行排序之后会减少数组中逆序对的数量,因此希尔排序底层可以用插入排序来实现。当然希尔排序也可以看成是插入排序的升级版。
2、代码
public class ShellSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
List<Integer> stepSequence = generateSedgewickStepSequence();
for (Integer step : stepSequence) {
sort(step);
}
}
private void sort(int step) {
for (int i = 0; i < step; i++) {
//新牌索引 = 所在行索引 * step + i
for (int begin = i + step; begin < array.length; begin += step) { //从第二个数开始插入
int index = begin;
E v = array[begin];
for (index = begin - step; index >= 0; index -= step) {
if (compare(array[index], v) > 0)
array[index + step] = array[index];
else
break;
}
array[index + step] = v;
}
}
}
/**
* 希尔步长
*/
private List<Integer> generateShellStepSequence() {
List<Integer> stepSequence = new LinkedList<>();
Integer i = array.length;
do {
i = (i >> 1);
stepSequence.add(i);
} while (i > 0);
return stepSequence;
}
/**
* 最优步长
*/
private List<Integer> generateSedgewickStepSequence() {
List<Integer> stepSequence = new LinkedList<>();
int k = 0, step = 0;
do {
if (k % 2 == 0) {
step = 9 * ((1 << k) - (1 << (k >> 1))) + 1;
} else {
step = (8 << k) - (6 << ((k + 1) >> 1)) + 1;
}
if (step > array.length)
break;
stepSequence.add(0, step);
k++;
} while (true);
return stepSequence;
}
}
希尔排序虽然底层用的是插入排序,但是因为根据步长序列分了组,相等的值无法保证顺序一致性,因此希尔排序是不稳定的排序算法。
七、归并排序
1、思路
不断将待排序的数组平均拆分成两个子序列,直到只有一个元素,再将相邻的子序列不断合并成有序的序列,直到最后只剩下一个序列,排序结束。明显需要用到递归,写递归要搞清楚递归结束的条件!这里很简单,明显就是子序列中只剩下一个元素则直接返回了。
2、代码
public class MergeSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
sort(0, array.length);
}
private void sort(int begin, int end) {
if (end - begin < 2) //拆分的序列中只有一个元素直接返回
return;
int mid = (begin + end) >> 1;
sort(begin, mid); // [begin, mid)
sort(mid, end); // [mid, end)
merge(begin, mid, end);
}
/**
* 将两个有序的子序列合并成一个大序列
*/
private void merge(int begin, int mid, int end) {
E[] leftPart = (E[]) new Comparable[mid - begin];
for (int i = 0; i < leftPart.length; i++)
leftPart[i] = array[begin + i];
int leftBegin = begin, leftEnd = mid, rightBegin = mid, rightEnd = end, nowIndex = begin;
while (leftBegin < leftEnd) {
if (rightBegin < rightEnd && compareByIndex(leftBegin, rightBegin) > 0)
array[nowIndex++] = array[rightBegin++];
else //左边元素小于等于右边元素就取左边元素
array[nowIndex++] = array[leftBegin++];
}
}
}
可从代码看出归并排序完全可以实现为稳定的排序,即只有右边元素大于左边元素的时候,才合并右边的元素,否则都是取左边的元素(包括相等)。
八、快速排序
1、思路
思想很简单,每次从数组中选取一个轴点(不一定非要是第一个元素),第一次排序后比轴点元素小的都在轴点左边,比轴点元素大的都在轴点右边,相等随意放(因为到最后结束的时候肯定每个元素都曾经轴点了,因此保持不了快排算法的稳定性)。紧接着对轴点左边的进行快排,右边的进行快排。递归可以实现,还是要先搞清楚递归结束的条件,其实还是数组中只有一个元素就返回。
2、代码
public class QuickSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
sort(0, array.length);
}
private int pivotIndex(int begin, int end) {
end--; //选取其他元素作为轴点元素只需要和begin上的元素交换位置即可
E v = array[begin];
boolean flag = true; //默认先从右边遍历
while (begin < end) {
if (flag) {
if (compare(array[end], v) > 0) //右边的元素比轴点元素大
end--;
else {
array[begin++] = array[end];
flag = false;
}
} else { //需要添加else不然数组可能会越界
if (compare(array[begin], v) < 0) //左边的元素比轴点元素小
begin++;
else {
array[end--] = array[begin];
flag = true;
}
}
}
array[begin] = v;
return begin;
}
private void sort(int begin, int end) {
if (end - begin < 2) //所有元素都是轴点就返回
return;
int mid = pivotIndex(begin, end);
sort(begin, mid);
sort(mid + 1, end);
}
}
快速排序本质就是将所有元素都变成轴点,所以不可能保证排序算法的稳定性。
九、计数排序
1、思路
用一个能包含最大值索引的数组记录每个元素出现的次数(直接将数字当作索引),再根据次数对数组元素进行排序。
如何优化?不仅计算数组最大值还是计算最小值,new数组的时候将空间压缩,而且记录元素出现次数的数组中不仅包括自己出现的次数还要加上前面所有元素出现过的次数,最后从右往左遍历起始的数组,根据记录次数的数组计算出索引,再放到数组的相应位置即可。
2、代码
未优化代码:
public class CountSort1<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
}
int[] newArray = new int[max + 1]; //int数组new出来默认都是0
for (index = 0; index < array.length; index++)
newArray[array[index]]++;
int current = 0;
for (index = 0; index < newArray.length; index++) {
if (newArray[index] != 0) {
int count = newArray[index];
while (count-- > 0)
array[current++] = index;
}
}
this.array = (E[]) array;
}
}
优化代码:
public class CountSort2<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int min = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
if (compare(min, array[index]) > 0)
min = array[index];
}
int[] counts = new int[max - min + 1]; //int数组new出来默认都是0
for (index = 0; index < array.length; index++)
counts[array[index] - min]++;
for (index = 1; index < counts.length; index++)
counts[index] += counts[index - 1];
Integer[] newArray = new Integer[array.length];
for (index = array.length - 1; index >= 0; index--) {
int newIndex = --counts[array[index] - min];
newArray[newIndex] = array[index];
}
this.array = (E[]) newArray;
}
}
十、基数排序
1、思路
依次对个位数、十位数、百位数、千位数、万位数等等(从低位到高位)进行计数排序即可。
2、代码
public class RadixSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
}
for (int radix = 1; radix <= max; radix *= 10) {
sortByRadix(radix);
}
}
private void sortByRadix(int radix) {
Integer[] array = (Integer[]) this.array;
int index = 0;
int[] counts = new int[10]; //直接用0-9计数
for (index = 0; index < array.length; index++)
counts[array[index] * radix % 10]++;
for (index = 1; index < counts.length; index++)
counts[index] += counts[index - 1];
Integer[] newArray = new Integer[array.length];
for (index = array.length - 1; index >= 0; index--) {
int newIndex = --counts[array[index] * radix % 10];
newArray[newIndex] = array[index];
}
this.array = (E[]) newArray;
}
}
十一、桶排序
1、思路
有点分而治之的思想。首先创建一定数量的桶(可用数组、链表作为桶),按照一定的规则将数组中的元素均匀分配到对应的桶里面,然后分别对每个桶进行单独排序,最后将所有非空桶的元素合并成有序序列,桶排序结束。
2、代码
这里以将整形数据除以10得到的值作为索引放入桶里面。
public class BucketSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
}
List<Integer>[] buckets = new ArrayList[array.length];
int current = 0;
for (index = 0; index < array.length; index++) {
List<Integer> bucket = buckets[array[index] / 10];
if (bucket == null) {
bucket = new ArrayList<>();
}
bucket.add(array[index]);
buckets[array[index] / 10] = bucket;
}
for (index = 0; index < buckets.length; index++) {
if (buckets[index] == null)
continue;
buckets[index].sort(null);
for (Integer i : buckets[index])
array[current++] = i;
}
}
}
十二、休眠排序
1、思路
每个元素都新开一条线程。。。通过线程休眠时间的长短来排序。
2、代码
public class ThreadSort implements Runnable {
private static int current = 0;
@Override
public void run() {
return;
}
public static void main(String[] args) {
Integer[] array = {2400, 500, 201, 489, 897, 320};
Integer[] newArray = new Integer[array.length];
int max = array[0];
for (int index = 0; index < array.length; index++) {
final int indexFinal = index;
if (max < array[index])
max = array[index];
new Thread(() -> {
try {
Thread.sleep(array[indexFinal]);
newArray[ThreadSort.current++] = array[indexFinal];
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start(); //start方法才是才是开启新线程
}
try {
Thread.sleep(max); //需等待开启的线程关闭后在遍历数组
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int index = 0; index < newArray.length; index++)
System.out.print(newArray[index] + " ");
}
}