排序的稳定性:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
选择排序
排序思想:不断选择剩余元素之中的最小(或大)者
每一次从待排序的数据元素中找出最小(或最大)的一个元素,将它和待排序的元素中第一个位置的元素进行交换,直到全部待排序的数据元素排完。
代码如下:
public class SelectionSort<T extends Comparable<T>> implements Sort<T>{
//核心算法
public void sort(T[] a) {
int N = a.length;
for(int i=0;i<N;i++) {
int min = i;
for(int j=i+1;j<N;j++) {
if(less(a[j],a[min])) min=j;
swap(a,i,min);
}
}
}
//比较两个元素大小
public boolean less(T a,T a2) {
return a.compareTo(a2)<0;
}
//交换两个元素
public void swap(T[] a,int i,int j) {
T temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//测试
public static void main(String[] args) {
Integer[] a = {3,2,1,4,5};
SelectionSort<Integer> ss = new SelectionSort<Integer>();
ss.sort(a);
for(int s:a) {
System.out.println(s);
}
}
}
分析:
选择排序的交换操作介于0~(n-1)之间,数据移动最少,比较操作n(n-1)/2次,所以时间复杂度位O(n^2)
选择排序运行时间只和输入数组的长度N有关,和输入的状态(是否有序)无关。
选择排序是不稳定的
插入排序
排序思想:每次将一个元素,插入已经排好序的数组中,插入后数组依然有序。
直接插入排序
把待排序的元素按其值的大小逐个插入到一个已经排好序的有序序列中,直到所有的纪录插入完为止,得到一个新的有序序列。
代码如下:
public class InsertionSort<T extends Comparable<T>> implements Sort<T>{
//核心代码
public void sort(T[] a) {
int N = a.length;
for (int i = 1; i < N; i++) {
for (int j = i; j >= 1 && less(a[j], a[j-1]); j--) {
swap(a, j, j-1);
}
}
}
//比较两个元素大小
public boolean less(T a,T a2) {
return a.compareTo(a2)<0;
}
//交换两个元素
public void swap(T[] a,int i,int j) {
T temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//测试
public static void main(String[] args) {
String[] a={"hello","world","hella","mi"};
InsertionSort<String> ss = new InsertionSort<String>();
ss.sort(a);
for(String s:a) {
System.out.println(s);
}
}
}
分析:
- 插入排序需要交换的次数和数组中倒置的次数相同,如:数组3,2,1, 元素倒置次数为3,分别为(3,2),(3,1),(2,1)。需要比较次数大于倒置次数,小于倒置次数+数组大小N。所以插入排序的时间复杂度:最差(逆序数组,交换次数为n(n-1)/2)为O(n^2),最优(有序数组)为O(n)。
- 插入排序不会移动比被插入元素要小的元素,它所需的比较次数平均只有选择排序的一半
- 插入排序是稳定的。
- 插入排序适用于数组基本有序(或已经有序)的情形,如:数组中每个元素距离他的最终位置都不远,一个有序的大数组接一个小数组,数组中只有几个元素位置不正确
改进:在内循环中将较大的元素一次性向右移动而不是总是交换两个元素,这样访问数组的次数就能够减半
核心代码如下:
public void sort(T[] a) {
int N = a.length;
for (int i = 1; i < N; i++) {
T temp = a[i];
int index=0; //要插入的位置
for (int j = i; j >= 1; j--) {
if (less(temp, a[j-1])) {
a[j] = a[j-1];
}else {
index = j;
break;
}
}
a[index] = temp;
}
}
二分插入排序
排序思想:
将直接插入排序中寻找a[i]的插入位置的方法改为采用折半比较,即可得到折半插入排序算法。
核心代码:
public void sort(T[] a) {
int N = a.length;
for (int i = 1; i < N; i++) {
int num = binaryFind(a,a[i],0,i-1);
T temp = a[i];
//num后的元素向后移动
for (int j = i; j > num; j--) {
a[j] = a[j-1];
}
a[num] = temp;
}
}
//找出元素应在数组中插入的位置
public int binaryFind(T[] a, T temp, int down, int up) {
if(up<down || up>a.length || down<0) {
System.out.println("下标错误");
}
if(temp.compareTo(a[down]) < 0) return down;
if(temp.compareTo(a[up]) > 0) return up+1;
int mid = (up-down)/2 + down;
if(temp.compareTo(a[mid]) == 0) {
return mid + 1;
}else if(temp.compareTo(a[mid])<0) {
up = mid-1;
}else if(temp.compareTo(a[mid])>0) {
down = mid+1;
}
return binaryFind(a,temp,down,up);
}
分析:二分插入排序减少了比较次数,特别是当要排序是数据很大时,这个效果将更加明显。
测试
代码如下:
public class Test {
public static final int NUM = 100000;
public static <T> double testSort(T[] data, Sort<T> sort) {
StopWatch timer = new StopWatch();
sort.sort(data);
return timer.elapsedTime();
}
public static void main(String[] args) {
Double[] data1 = new Double[NUM];
for(int i=0; i < NUM; i++) {
data1[i] = Math.random();
}
Double[] data2 = new Double[NUM];
for(int i=0; i < NUM; i++) {
data2[i] = data1[i];
}
InsertionSort<Double> its = new InsertionSort<Double>();
SelectionSort<Double> sts = new SelectionSort<Double>();
double time1 = Test.testSort(data1,sts);
double time2 = Test.testSort(data2,its);
System.out.println("SelectionSort---->" + time1);
System.out.println("InsertionSort---->" + time2);
}
}
class StopWatch {
public final long start;
public StopWatch() {
start = System.currentTimeMillis();
}
public double elapsedTime() {
long now = System.currentTimeMillis();
return (now-start)/1000.0;
}
}
结果如下: