Internal Sorting Algorithms Part 1/2: Elementary Sorts
目录
概述
InsertSort
插入排序,是这样一种排序算法: 它把待排序的元素分成两部分,前一部分已经排好序,后一部分尚未排序,每一次从未排序的元素中选取一个,插入到已经排好序的元素里。
说一万句,也不如看图来的快。
i是待插入元素的位置,j是的范围是从0到i-1(这些元素已经排好序)
1、初始排好序的数组元素个数为0,
2、i插入后,i后移。此时排好序的数组中有个9,后面的数组尚未排序。i指向了8,待插入。
3、想要把8插入到绿色区域,且保证绿色区域有序,那么9必须后移,8插入到9的位置。i后移到7。
4、想要插入7,那么8和9都必须后移,7插到8的位置。i后移到6。
5、以此类推,i来到0的位置。
6、此时绿色区域内后移比0大的元素,0插入到合适的位置(1的位置)。
完成排序。
怪不得叫插入排序呢。
ShellSort
希尔排序,本质上也是一种插入排序,但是它将插入排序进行了增强。在插入排序中我们可以看到,数组中元素的后移是很影响效率的。希尔排序会减少这种数据挪动的次数。
希尔排序使用增量序列这一辅助来完成排序。排序只会在有一定间隔的元素之间进行。
假设增量序列为{5, 3, 1}
1、相隔为5,这里只会在相同颜色的元素之间进行插入排序
2、5-ShellSort排完后,得到如下结果:
3、现在我们得到了这样一个数组(与上图一样)
4、以3为增量,重新着色
5、3-ShellSort排完后,得到如下结果:
6、最后进行1-ShellSort,其实也就是InsertSort了,观察到上图的数组已经基本有序,挪动次数相对也就少很多了。
理解了InsertSort,ShellSort也就很好理解了。
SelectSort
选择排序是一种非常直观的排序方法。
1、第一次选择整个数组中最小的元素,与第1个元素交换。
2、第二次在去除第1位的数组中选择最小的元素,与第2个元素交换
3、第三次在去除第1、2位的数组中选择最小的元素,与第3个元素交换
……
最终实现整个排序。
BubbleSort
最后来说一下BubbleSort,名字还是很形象的,元素像小泡泡一样从下到上,也算是很经典的排序算法了。
冒泡算法是这样的一种算法:
第一趟,数组中全部元素经过两两比较,把最小的”冒”到了第一位
第二趟,数组中除第一位外的全部元素经过两两比较,把第二小的”冒”到了第二位
…
最终实现排序。
1、第一趟,比较若干次,0浮到第一位
2、第二趟,比较若干次,1浮到第二位
3、第三趟,比较若干次,2浮到第三位
4、以此类推,可以得到最终的排序结果。
可见,冒泡排序就是两两之间一直比较,小的就往上移动,所以每一趟都可以获得最小值,并移到合适的位置。但是,也可以看到,冒泡排序的比较次数实在太多了。
代码实现
// Sort.java
package com.stephen.sortalgorithms;
public interface Sort {
void sort(int[] array);
}
// SortUtils.java
package com.stephen.sortalgorithms;
public class SortUtils {
/**
* 插入排序
*/
public static class InsertSort implements Sort {
@Override
public void sort(int[] array) {
int i, j;
for(i = 1; i < array.length; ++i) { // i是待插入元素的下标
int toBeInserted = array[i];
/**
* 0到j属于已经排好序的元素下标
* 如果array[j] > toBeInserted,那么j到i - 1得元素都要后移
* 然后插入元素array[i]。
*/
for(j = i - 1; j >= 0 && array[j] > toBeInserted; --j) {
array[j + 1] = array[j];
}
array[j + 1] = toBeInserted;
}
}
}
/**
* 选择排序
*/
public static class SelectSort implements Sort {
@Override
public void sort(int[] array) {
int min, minPos = 0;
for(int i = 0; i < array.length; ++i) {
min = array[i];
for(int j = i + 1; j < array.length; ++j) { // 找到最小的值和对应的索引
if(array[j] < min) {
min = array[j];
minPos = j;
}
}
if(i != minPos) { // 把最小值交换到数组的前面去
int temp = array[i];
array[i] = array[minPos];
array[minPos] = temp;
}
}
}
}
/**
* 冒泡排序
*/
public static class BubbleSort implements Sort {
@Override
public void sort(int[] array) {
boolean exchange = true;
/* 如果某一趟没有发生数据交换,那么就没必要再冒下去了... */
for(int i = array.length - 1; i >=0 && exchange; --i) {
exchange = false;
for(int j = array.length - 1; j > array.length - 1 - i; --j) {
if(array[j - 1] > array[j]) { // 相邻的数据两两比对
exchange = true;
int temp = array[j - 1];
array[j - 1] = array[j];
array[j] = temp;
}
}
}
}
}
/**
* 希尔排序
*/
public static class ShellSort implements Sort {
@Override
public void sort(int[] array) {
int[] intervals = {5, 3, 1}; // 增量序列
for(int interval: intervals) {
shellSort(array, interval);
}
}
/*
* 与插入排序如出一辙,唯一的区别,就在于数据每次跳跃interval,而不是1
*/
private void shellSort(int[] array, int interval) {
int i, j;
for(i = 0; i < array.length; i += interval) {
int toBeInserted = array[i];
for(j = i - interval; j >= 0 && array[j] > toBeInserted; j -= interval) {
array[j + interval] = array[j];
}
array[j + interval] = toBeInserted;
}
}
}
}
// SortTest.java
package com.stephen.sortalgorithms;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Date;
public class SortTest{
private static final int SIZE = 10000;
private static int[] array;
static {
array = new int[SIZE];
for(int i = 0; i < SIZE; ++i) {
array[i] = (int)(Math.random() * SIZE);
}
}
public static void main(String[] args) {
benchmark();
}
public static void benchmark() {
int[] arraycopy = null;
Sort[] sorts = {
new SortUtils.InsertSort(),
new SortUtils.SelectSort(),
new SortUtils.BubbleSort(),
new SortUtils.ShellSort(),
};
for(Sort sort: sorts) {
InvocationHandler handler=new SortHandler(sort);
Sort proxy=(Sort)Proxy.newProxyInstance(
sort.getClass().getClassLoader(),
sort.getClass().getInterfaces(),
handler);
arraycopy = Arrays.copyOf(array, SIZE);
proxy.sort(arraycopy);
}
}
}
class SortHandler implements InvocationHandler{
private Object target;
public SortHandler(Object target) {
super();
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = new Date().getTime();
Object result=method.invoke(target, args);
long end = new Date().getTime();
System.out.println(String.format("%-10s %s: %d", target.getClass().getSimpleName(),"consuming", (end - start)));
return result;
}
}
Summary
以上各算法在同一台机器上的运行时间以及对应的元素数目如下所示:
/*
* Time consuming of each sort, in millisecond
*/
/*
* 10,000 random numbers
*/
InsertSort consuming: 58
SelectSort consuming: 84
BubbleSort consuming: 247
ShellSort consuming: 32
/*
* 100,000 random numbers
*/
InsertSort consuming: 5053
SelectSort consuming: 8218
BubbleSort consuming: 22564
ShellSort consuming: 2965
/*
* 500,000 random numbers, unbearable
*/
InsertSort consuming: 134790
SelectSort consuming: 214202
BubbleSort consuming: 582737
ShellSort consuming: 91378
- 当待排列的元素数目在10,000时,这四种算法表现还可以,除了冒泡之外,100ms之内可以完成。
- 当待排列的元素数目上升到100,000时,冒泡已经力不从心,其他三种10秒之内均可完成。相对来说,希尔排序更胜一筹。
- 但是当待排列的元素数目上升到500,000时,四种算法均筋疲力竭,难当大任。
- 这与四种算法的时间复杂度有关,如下表所示:
算法 | 插入排序 | 希尔排序 | 选择排序 | 冒泡排序 |
---|---|---|---|---|
时间复杂度 | O(N^2) | O(N^(7/6)(?)) | O(N^2) | O(N^2) |
- 下一篇将会介绍几种高级的排序算法
日常使用这几种基本算法,数量不大时,均可完成任务。但是当数目变得很大时,几种算法已经不行了。这个时候就要交给另外几种比较高级,同时也比较迅速的算法,例如快排,归并和堆排序。可以看见即使元素数量达到1,000,000,排序依然可以在100ms左右完成,当元素数量达到10,000,000时,依然可以在几秒之内完成。