数据结构与算法的游戏从简单排序开始
简单排序是排序算法中基础的部分,这部分算法都是属于O(n2)的算法,虽然从数量级上看时间消耗要比后续的O(nlog(n))级别的算法要慢,但实际表现却不见得;特别是优化过后的插入排序,在对基本有序序列的排序的时耗很低,甚至可以超越Onlog(n)级别的算法;怎么实现呢,我们逐个来看看(各排序耗时PK在最后)~~
选择排序
选择排序的主体实现思路:从数组的第一个成员开始逐个遍历,找到最小那个放到第一个位置,以此类推;这里放的是优化版本,一次循环直接找到最大值和最小值,减少算法运行时间~~
// 选择排序
public class SelectionSort {
private SelectionSort() {}
public static void sort(Comparable[] arr) {
int n=arr.length-1;
int right=n; // 最右边序号
int left=0; // 最左边序号
if(n <= 0)
return;
while(left<right) {
int maxIndex = right;
int minIndex = left;
if(arr[right].compareTo(arr[left]) < 0) {
swap(arr, right, left);
}
// 一次性找到未排序数据的最大值和最小值,然后左右边界均向中间缩进一位
for (int i = left+1; i < right; i++) {
if (arr[i].compareTo(arr[left]) < 0) {
minIndex = i;
}
if (arr[i].compareTo(arr[right]) > 0) {
maxIndex = i;
swap(arr, i, right--);
}
}
swap(arr, maxIndex, right--);
swap(arr, minIndex, left++);
}
}
// 用Object可普遍适用于各种数据类型
public void swap(Object[] arr, int a, int b) {
Object temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
冒泡排序
冒泡排序的主体实现思路:从第一个数组成员开始,从左到右两两比较,如果出现前面的成员数据比后面的小则交换数据,遍历一次就能在左边界处放置最小值,然后左边界加1,继续遍历直到数组有序;这里有一个小技巧,就是记录上一次排序最后出现有成员数据交换的位置作为左边界,因为左边界前面的序列已有序,那么就可以从不再对标记前的元素进行排序,加快了排序速度~~
// 冒泡排序
public class BubbleSort {
// 不允许创建实例,只允许调用执行
private BubbleSort() {}
public static void sort(Comparable[] arr) {
if(arr.length <= 1)
return;
// 用jump进行记录,从哪里开始是最后一次换位,之后没有进行换位,说明已经有序
int jump;
// 记录已经有序位的后一位
int mark=0;
do {
jump = arr.length-1;
for(int i=arr.length-1; i>mark; i--) {
// 如果后面位置的数比前面位置的数小则换位
if(arr[i].compareTo(arr[i-1]) < 0) {
.swap(arr, i, i - 1);
jump = i-1;
}
}
mark = jump;
}while(jump<arr.length-1);
}
// 用Object可普遍适用于各种数据类型
public void swap(Object[] arr, int a, int b) {
Object temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
插入排序
插入排序的主体实现思路:来来来,打牌!这就是一个打牌排序法,打牌的时候,一般习惯就是把同花色的牌放一起,而且会从左到右按序拿着,回想一下这个过程,会发现是摸一张就将这张插入到该在的位置;插入排序也就是从左到右遍历,一边遍历,一边将右边遍历到序列成员插入到左边已经排好序的序列中
//插入排序法
public class InsertionSort {
private InsertionSort() {}
public static void sort(Comparable[] arr) {
if(arr.length <= 1)
return;
for(int i=1; i<arr.length; i++) {
Comparable currentMin = arr[i];
int j = i;
// 如果已经遍历完或被插入的序列已经有序,如果新插入的数值大于最右边的元素则结束这一循环
for(; j > 0 && currentMin.compareTo(arr[j-1]) < 0; j--) {
arr[j] = arr[j-1];
}
arr[j] = currentMin;
}
}
}
希尔排序
希尔排序的主体实现思路:插入算法的黄金马甲,以低耗时的遍历查询次数换取更为耗时的数据赋值次数的减少;前面的三种排序,序列中一个成员要到有序的位置必须进行逐个比较,比较后,如果发现位置要变,则必定会出现数据的交换或至少是赋值(挪位);然而如果知道最后一个(或靠后)数据是最小的(或几乎最小的),那么最好的做法肯定就是直接跳过和中间序列成员的比较,将后面这个成员跳到前面去比较,这样就减少了很多不必要的挪位;希尔算法就是通过先以大间隔进行比较,让远离有序位置的成员跳跃性地进行比较到所需位置的附近,从大间隔的比较然后逐渐到间隔为1的比较,从而达到序列有序;这个过程虽然增加了查询次数,但是因为查询比赋值耗时要低很多,所以实际意义很大;但需要注意的时候在面对基本有序序列时,插入排序仍然最优,因为面对基本有序序列,插入排序的机制让插入排序只需比较,很少赋值,而希尔排序增加的查询时间在这个时候就不够给力了,那基本有序到什么程度呢,我肯定不会告诉你大概是100000个数据中只有200个数据未排好序时,超过了时还是希尔牛;
// 希尔排序法
public class ShellSort {
private ShellSort() {}
public static void sort(Comparable[] arr) {
if(arr.length <= 1)
return;
// Sedgewick序列,值演示了17个,适用于最多100万个数据的序列
int[] SedgewickSeq = new int[]{1,5,19,41,109,209,505,929,2161,3905,8929,16001,36289,64769,146305,260609,587521};
int n=arr.length-1;
int startIndex=0;
for(int i = 0; i < 17; i++) {
if(n / SedgewickSeq[i] < 2) {
startIndex = i-1;
break;
}
}
// 搭配插入排序玩法
do{
int w = SedgewickSeq[startIndex];
for(int i = w; i < n; i++) {
Comparable currentMin = arr[i];
int j = i;
for(; j >= w && currentMin.compareTo(arr[j-w]) < 0; j -= w) {
arr[j] = arr[j-w];
}
arr[j] = currentMin;
}
startIndex--;
}while(startIndex>=0);
}
}
简单排序性能PK赛
比赛规则:
- 对10万个数据进行排序;
- 分随机数序列和基本有序序列两场比赛;
- 基本有序序列的未有序数据数量是200个;
- 所有排序法均已优化;
- 从上到下是选择排序、冒泡排序、插入排序、希尔排序;
随机数序列:
BubbleSort : 41099ms
InsertionSort : 14809ms
SelectionSort : 25ms
ShellSort : 45ms
基本有序序列:
BubbleSort : 11557ms
InsertionSort : 28ms
SelectionSort : 1455ms
ShellSort : 12ms
比赛结果:
- 综合来看希尔排序优势最大;
- 原始三大简单排序优化后的选择排序最优;
- 插入排序则在基本有序上表现突出;
- 插入排序无论是在查询还是赋值的次数上都少于另外两种原始简单排序;选择排序不管是否基本有序都要进行那么多的查询,所以耗时稳定;冒泡排序数据交换很多,查询较少,所以对随机序列略显乏力,对基本有序序列因数据交换大量减少,性能大幅提升;
- 比赛结果供菜鸟参考,大神点评;