一、概述
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。具体分类如图1-1(源自网上)
某些必要的定义:
- 时间复杂度:
时间复杂度是一个函数,它定量描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。 - 空间复杂度:
一个程序的空间复杂度是指运行完一个程序所需内存的大小。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。 - 稳定性:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的;否则称为不稳定的。
对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。
需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
二、具体算法
1、插入排序
1.基本思想:
插入排序由N-1趟排序组成。对于p=1到N-1趟,保证位置从0到p的元素处于已排序状态。即:第p次排序时,将位置p上的元素向前移动到合适的位置。
2.具体步骤及图解:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
下图详细描述了插入排序的排序过程(源自网上)。
3.代码实现
/**
* 插入排序(由小到大)
*
* @param a 排序数组
*/
public static <T extends Comparable<? super T>> void insertionSort(T[] a) {
int i;
for (int p = 1; p < a.length; p++) {
T temp = a[p];
for (i = p; i > 0 && temp.compareTo(a[i - 1]) < 0; i--) {
a[i] = a[i - 1];
}
a[i] = temp;
}
}
4.分析
- 稳定性:相同元素不互换次序,是稳定的。
- 时间复杂度:
- 最好情况:数组本就有序,则每趟排序只需进行一次比较,共需N-1趟,故为O(n)
- 最差情况:数组倒序,第p趟插入需要比较p-1次,故1+2+……N-1=N*(N-1)/2,故为O(N^2)
- 平均情况为O(N^2)
- 空间复杂度:所需空间为常数,O(1)
- 结论:数据量较小,较有序的情况下性能较好
2、冒泡排序
1.基本思想:
重复地走访过要排序的元素,一次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。
2.具体步骤及图解:
- 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
下图详细描述了冒泡排序的排序过程(源自网上)。
3.代码实现
/**
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void bubbleSort(T[] a) {
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a.length - i - 1; j++) {
if (a[j].compareTo(a[j + 1]) > 0) {
T temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
}
}
4.分析
- 稳定性:相同元素不互换次序,是稳定的。
- 时间复杂度:此种排序方式,数据顺序对算法影响不大,为O(N^2)
- 空间复杂度:所需空间为常数,O(1)
3、选择排序
1.基本思想:
在要排序的一组数中,选出最小的一个数与第1个位置的数交换;然后在剩下的数当中再找最小的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
2.具体步骤及图解:
- 第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
- 第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;
- 以此类推…..
- 第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,直到整个序列按关键码有序。
下图详细描述了选择排序的排序过程(源自网上)。
3.代码实现
/**
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void selectSort(T[] a) {
int minIndex;
for (int i = 0; i < a.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < a.length; j++) {
if (a[i].compareTo(a[j]) > 0) {
minIndex = j;
}
}
if (minIndex != i) {
T temp = a[minIndex];
a[minIndex] = a[i];
a[i] = temp;
}
}
}
4.分析
- 稳定性:选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。
- 时间复杂度:无论最好情况还是最差情况,都需要进行1+2+……N-1=N*(N-1)/2次比较找出最值,故为O(N^2)
- 空间复杂度:所需空间为常数,O(1)
4、希尔排序
1.基本思想:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
2.具体步骤及图解:
- 选择一个增量序列d[k],其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
增量的选择:
a. 希尔增量 d[k]的选择是 d[t]=N/2,d[k]=d[k+1]/2 此处即选择此种方式 最坏运行时间O(N^2)
b. Hibbard增量 {1, 3, ..., 2^k-1} 最坏运行时间O(N^3/2)
c. Sedgewick增量:{1, 5, 19, 41, 109...}该序列中的项或者是9*4^i - 9*2^i + 1或者是4^i - 3*2^i + 1。
下图详细描述了希尔排序的排序过程(源自网上)。
3.代码实现
/**
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void shellSort(T[] a) {
int j;
for (int gap = a.length / 2; gap > 0; gap /= 2) {//b[k]
for (int i = gap; i < a.length; i++) {
T temp = a[i];
for (j = i; j >= gap && temp.compareTo(a[j - gap]) < 0; j -= gap) {
a[j] = a[j - gap];
}
a[j] = temp;
}
}
}
4.分析
- 稳定性:希尔排序是不稳定的排序算法,因为相同元素可能被分割至不同的组,导致次序改变。
- 时间复杂度:与选择的增量有关
- 希尔增量:此种增量方式,当N=2^k时,会导致前后增量为倍数关系,有一直到步长为1之前都未涉及到的元素,极限情况下甚至还有可能之前所有的排序都在浪费时间,如{1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16}数组,步长为8,4,2时未进行任何处理。故最坏时间复杂度与插入排序相同为O(N^2)
- Hibbard增量 最坏运行时间O(N^3/2)
- Sedgewick增量:为O(N^4/3)
- 空间复杂度:所需空间为常数,O(1)
5、堆排序
1.基本思想:
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构(通常堆是通过一维数组来实现的),并同时满足堆的性质:即子结点的键值总是小于(或者大于)它的父节点。利用这种堆性质,可以轻易选出最值。
2.具体步骤及图解:
- 将数组构建为建一个堆(最大堆或者最小堆根据排序需求选择)
- 把堆顶元素(最大值)和堆尾元素互换
- 把堆的尺寸缩小1,并调整使得剩余元素成为一个新的堆。
- 重复步骤2,直到堆的尺寸为1
下图详细描述了堆排序的排序过程(源自网上)。
3.代码实现
/**
* 堆排序(第0位有值)
*
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void heapSort(T[] a) {
//将数组转化为堆
for (int i = a.length / 2 - 1; i >= 0; i--) {
percDown(a, i, a.length);
}
//排序
for (int i = a.length - 1; i >= 0; i--) {
swap(a, 0, i);
percDown(a, 0, i);
}
}
/**
* 最大堆的下滤操作
*
* @param a
* @param hole
* @param n 二叉堆中现有元素
* @param <T>
*/
private static <T extends Comparable<? super T>> void percDown(T[] a, int hole, int n) {
T temp = a[hole];
int child;
for (; (2 * hole + 1) < n; hole = child) {
child = hole * 2 + 1;//左儿子
if (child + 1 < n && a[child + 1].compareTo(a[child]) > 0) {
child += 1;
}
if (temp.compareTo(a[child]) < 0) {
a[hole] = a[child];
} else {
break;
}
}
a[hole] = temp;
}
/**
* 交换数组中两下标的元素
*
* @param a 待交换数组
* @param index1 数组下标1
* @param index2 数组下标2
* @param <T>
*/
private static <T extends Comparable<? super T>> void swap(T[] a, int index1, int index2) {
T temp = a[index1];
a[index1] = a[index2];
a[index2] = temp;
}
4.分析
- 稳定性:堆排序是不稳定的排序算法,不稳定发生在堆顶元素与a[i]交换的时刻。
- 时间复杂度:由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N^2)。
- 空间复杂度:所需空间为常数,O(1)
6、归并排序
1.基本思想:
归并的含义是将两个或两个以上的有序表合并成一个新的有序表。
假设我们有一个没有排好序的序列,那么首先我们使用分割的办法将这个序列分割成一个个已经排好序的子序列(每个序列一个元素)。然后再利用归并的方法将一个个的子序列合并成排序好的序列。
2.具体步骤及图解:
- 把 n 个记录看成 n 个长度为1的有序子表;
- 进行两两归并使记录关键字有序,得到 n/2 个长度为 2 的有序子表;
- 重复第2步直到所有记录归并成一个长度为 n 的有序表为止。
下图详细描述了堆排序的排序过程(源自网上)。
3.代码实现
/**
* 归并排序
*
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void mergeSort(T[] a) {
T[] tempArray = (T[]) new Comparable[a.length];
mergeSort(a, tempArray, 0, a.length - 1);
}
/**
* 分割
*
* @param a
* @param tempArray
* @param left
* @param right
* @param <T>
*/
private static <T extends Comparable<? super T>> void mergeSort(T[] a, T[] tempArray, int left, int right) {
if (left < right) {
int center = (left + right) / 2;
mergeSort(a, tempArray, left, center);
mergeSort(a, tempArray, center + 1, right);
merge(a, tempArray, left, center + 1, right);
}
}
/**
* 合并
*
* @param a
* @param tempArray
* @param leftPos
* @param rightPos
* @param rightEnd
* @param <T>
*/
private static <T extends Comparable<? super T>> void merge(T[] a, T[] tempArray, int leftPos, int rightPos, int rightEnd) {
int leftEnd = rightPos - 1;//左数组的界
int tempPos = leftPos;
int elementNums = rightEnd - leftPos + 1;//数组的总个数
while (leftPos <= leftEnd && rightPos <= rightEnd) {
if (a[leftPos].compareTo(a[rightPos]) > 0) {
tempArray[tempPos++] = a[rightPos++];
} else {
tempArray[tempPos++] = a[leftPos++];
}
}
while (leftPos <= leftEnd) {
tempArray[tempPos++] = a[leftPos++];
}
while (rightPos <= rightEnd) {
tempArray[tempPos++] = a[rightPos++];
}
for (int i = 0; i < elementNums; i++, rightEnd--) {
a[rightEnd] = tempArray[rightEnd];
}
}
4.分析
- 稳定性:归并排序是稳定的排序算法。
- 时间复杂度:
合并所耗时间是线性的O(N)
N=1时,花费常熟时间,故:T(1)=1
T(N)=2T(N/2)+NT(N)=2T(N/2)+N
将上式连续分解得:
T(N)=4T(N/4)+2N=……=2kT(N/2k)+k∗NT(N)=4T(N/4)+2N=……=2kT(N/2k)+k∗N
将数组分解到每个子表为1所需分解步骤k满足:2k=N2k=N,
故有:T(N)=NT(1)+N∗logN=N∗logN+NT(N)=NT(1)+N∗logN=N∗logN+N
故时间复杂度为O(N*logN) - 空间复杂度:
申请了一个大小为N的临时数组O(N)
递归logN次,每次递归使用需要栈空间记录递归O(logN)
故为O(N+logN)
7、快速排序
1.基本思想:
采用分治思想,从数组中挑出一个元素作为基准(pivot),使用此基准将数组一分为二,然后对两个数组分别继续采取上述做法,直至数组排序完成。
2.具体步骤及图解:
- 从序列中挑出一个元素,作为”基准”(pivot)。
- 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
- 对每个分区递归地进行步骤1~3,递归的结束条件是序列的大小是0或1,这时整体已经被排好序
下图详细描述了堆排序的排序过程(源自网上,图中选取pivot做法不可取!!!)。
基准的选取:
- 选取第一个元素或最后一个元素:这种做法是错误的,因为若输入数组是预排序或反序的,此种做法可能导致某个分区为空。
- 选取随机数:对于随机数组来说是安全的,但产生随机数开销较大,不建议。
- 三数中值法:采用头,尾以及中间三个数的中值(中位数)。
3.代码实现
采取三分法之后,最小值在最前,最大值在最后。在从后向前扫描(j)的过程中,第一位为了防止在扫描过程中,
/**
* 快速排序
*
* @param a 排序数组
* @param <T>
*/
public static <T extends Comparable<? super T>> void quickSort(T[] a) {
quickSort(a, 0, a.length - 1);
}
private static <T extends Comparable<? super T>> void quickSort(T[] a, int left, int right) {
if (right - left >= 3) {
T pivot = medianFromThree(a, left, right);//中值,标志
int i = left, j = right - 1;//开始扫描
while (true) {
while (a[++i].compareTo(pivot) < 0) {
}
while (a[--j].compareTo(pivot) > 0) {
}
if (i < j) {
swap(a, i, j);
} else {
break;
}
}
swap(a, i, right - 1);//将pivot放置合适的位置
quickSort(a, left, i - 1);//较小元素排序
quickSort(a, i + 1, right);//较大元素排序
} else {//对于小数组,直接使用插入排序,效率更佳
insertionSort(a);
}
}
/**
* 取三个数的中值数 并将三个数放至合适的位置,将中位数移至倒数第二位是为了防止数组越界
*
* @param a 数组
* @param left 左下标
* @param right 右下标
* @param <T> 返回的中值数
* @return
*/
private static <T extends Comparable<? super T>> T medianFromThree(T[] a, int left, int right) {
int center = (left + right) / 2;
if (a[left].compareTo(a[right]) > 0) {
swap(a, left, right);
}
if (a[left].compareTo(a[center]) > 0) {
swap(a, left, center);
}
if (a[center].compareTo(a[right]) > 0) {
swap(a, center, right);
}
swap(a, center, right - 1);//将中位数移动至倒数第二位
return a[right - 1];
}
/**
* 交换数组中两下标的元素
*
* @param a 待交换数组
* @param index1 数组下标1
* @param index2 数组下标2
* @param <T>
*/
private static <T extends Comparable<? super T>> void swap(T[] a, int index1, int index2) {
T temp = a[index1];
a[index1] = a[index2];
a[index2] = temp;
}
4.分析
- 稳定性:快速排序是不稳定的排序算法。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N*logN)
8、基数排序
1.基本思想:
桶排序:是将阵列分到有限数量的桶子里。每个桶子再个别排序。简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
基数排序:就是进行多次桶排序。
2.具体步骤及图解:
- 把 n 个记录看成 n 个长度为1的有序子表;
- 进行两两归并使记录关键字有序,得到 n/2 个长度为 2 的有序子表;
- 重复第2步直到所有记录归并成一个长度为 n 的有序表为止。
下图详细描述了桶排序的排序过程(源自网上)。
3.代码实现
/**
* 基数排序(字符串)
*
* @param a 待排序的整型数组
* @param maxLen 最长数字的长度
*/
public static void radixSort(String[] a, int maxLen) {
final int BUCKETS = 257;//所有字符的个数,第0位存放该位无字符的字符串
ArrayList<String>[] buckets = new ArrayList[BUCKETS];
for (int i = 0; i < BUCKETS; i++) {
buckets[i] = new ArrayList();
}
for (int i = maxLen - 1; i >= 0; i--) {//maxLen次桶排序
for (String s : a) {
if (s.length() > i) {
buckets[s.charAt(i)+1].add(s);
}else {
buckets[0].add(s);
}
}
int idx=0;
for(ArrayList<String> bucket:buckets){
for(String s:bucket){
a[idx++]=s;
}
bucket.clear();
}
}
}
/**
* 基数排序(正整数)
*
* @param a 待排序的字符串数组
* @param maxLen 最大数长度
*/
public static void radixSort(Integer[] a, int maxLen) {
ArrayList[] buckets = new ArrayList[10];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList();
}
for (int i = 0; i < maxLen; i++) {
for (Integer s : a) {
buckets[(s / (int) Math.pow(10, i)) % 10].add(s);
}
int idx = 0;
for (ArrayList<Integer> bucket : buckets) {
for (Integer s : bucket) {
a[idx++] = s;
}
bucket.clear();
}
}
}
4.分析
- 稳定性:基数排序是稳定的排序算法。
- 时间复杂度:O(d(r+n))
- 空间复杂度:O(rd+n)
d代表长度,r代表关键字的基数,d代表长度,n代表关键字。
三、所有代码
import java.util.ArrayList;
import java.util.Map;
/**
* Created by Administrator on 2017/6/26.
*/
public class Sort {
/**
* 插入排序(由小到大)
* 第p次排序时,将位置p上的元素向前移动到合适的位置
*
* @param a
*/
public static <T extends Comparable<? super T>> void insertionSort(T[] a) {
int i;
for (int p = 1; p < a.length; p++) {
T temp = a[p];
for (i = p; i > 0 && temp.compareTo(a[i - 1]) < 0; i--) {
a[i] = a[i - 1];
}
a[i] = temp;
}
}
/**
* 冒泡排序(由小到大)
* 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
* 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
* 针对所有的元素重复以上的步骤,除了最后一个。
* 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
*
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void bubbleSort(T[] a) {
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a.length - i - 1; j++) {
if (a[j].compareTo(a[j + 1]) > 0) {
T temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
}
}
/**
* 选择排序(由小到大)
* 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
*
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void selectSort(T[] a) {
int minIndex;
for (int i = 0; i < a.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < a.length; j++) {
if (a[i].compareTo(a[j]) > 0) {
minIndex = j;
}
}
if (minIndex != i) {
T temp = a[minIndex];
a[minIndex] = a[i];
a[i] = temp;
}
}
}
/**
* 希尔排序(由小到大)
* 希尔排序属于插入类排序,是将整个有序序列分割成若干小的子序列分别进行插入排序。
* <p/>
* 先取一个正整数d1<n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;
* 然后取d2<d1,重复上述分组和排序操作;
* 直至di=1,即所有记录放进一个组中排序为止。
* <p/>
* 希尔增量 d[k]的选择是 d[t]=N/2,d[k]=d[k+1]/2 此处即选择此种方式 最坏运行时间O(N^2)
* Hibbard增量 d[k]的选择是 d[k]=2^k-1 最坏运行时间O(N^3/2)
*
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void shellSort(T[] a) {
int j;
for (int gap = a.length / 2; gap > 0; gap /= 2) {//b[k]
for (int i = gap; i < a.length; i++) {
T temp = a[i];
for (j = i; j >= gap && temp.compareTo(a[j - gap]) < 0; j -= gap) {
a[j] = a[j - gap];
}
a[j] = temp;
}
}
}
/**
* 堆排序(第0位有值)
* 初始化操作:将a[1..n]构造为初始堆
* 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
* 重复此操作,直至结束
*
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void heapSort(T[] a) {
for (int i = a.length / 2 - 1; i >= 0; i--) {
percDown(a, i, a.length);
}
for (int i = a.length - 1; i >= 0; i--) {
swap(a, 0, i);
percDown(a, 0, i);
}
}
/**
* 最大堆的下滤操作
*
* @param a
* @param hole
* @param n 二叉堆中现有元素
* @param <T>
*/
private static <T extends Comparable<? super T>> void percDown(T[] a, int hole, int n) {
T temp = a[hole];
int child;
for (; (2 * hole + 1) < n; hole = child) {
child = hole * 2 + 1;//左儿子
if (child + 1 < n && a[child + 1].compareTo(a[child]) > 0) {
child += 1;
}
if (temp.compareTo(a[child]) < 0) {
a[hole] = a[child];
} else {
break;
}
}
a[hole] = temp;
}
/**
* 归并排序
* 归并的含义是将两个或两个以上的有序表合并成一个新的有序表。
* 假设我们有一个没有排好序的序列,那么首先我们使用分割的办法将这个序列分割成一个个已经排好序的子序列。
* 然后再利用归并的方法将一个个的子序列合并成排序好的序列。
*
* @param a
* @param <T>
*/
public static <T extends Comparable<? super T>> void mergeSort(T[] a) {
T[] tempArray = (T[]) new Comparable[a.length];
mergeSort(a, tempArray, 0, a.length - 1);
}
/**
* 分割
*
* @param a
* @param tempArray
* @param left
* @param right
* @param <T>
*/
private static <T extends Comparable<? super T>> void mergeSort(T[] a, T[] tempArray, int left, int right) {
if (left < right) {
int center = (left + right) / 2;
mergeSort(a, tempArray, left, center);
mergeSort(a, tempArray, center + 1, right);
merge(a, tempArray, left, center + 1, right);
}
}
/**
* 合并
*
* @param a
* @param tempArray
* @param leftPos
* @param rightPos
* @param rightEnd
* @param <T>
*/
private static <T extends Comparable<? super T>> void merge(T[] a, T[] tempArray, int leftPos, int rightPos, int rightEnd) {
int leftEnd = rightPos - 1;//左数组的界
int tempPos = leftPos;
int elementNums = rightEnd - leftPos + 1;//数组的总个数
while (leftPos <= leftEnd && rightPos <= rightEnd) {
if (a[leftPos].compareTo(a[rightPos]) > 0) {
tempArray[tempPos++] = a[rightPos++];
} else {
tempArray[tempPos++] = a[leftPos++];
}
}
while (leftPos <= leftEnd) {
tempArray[tempPos++] = a[leftPos++];
}
while (rightPos <= rightEnd) {
tempArray[tempPos++] = a[rightPos++];
}
for (int i = 0; i < elementNums; i++, rightEnd--) {
a[rightEnd] = tempArray[rightEnd];
}
}
/**
* 快速排序
* 通过一趟排序将待排序的记录分隔成独立的两个部分,其中一部分记录的关键字均比另一部分记录的关键字小,另一部分记录的关键字均比另一部分记录的关键字大。
* 接着分别对两部分分别进行同样的操作,最终得到有序的结果
* <p/>
* 关键字选取:三数中值法(取最左,最右和中间三个数的中值)
*
* @param a 排序数组
* @param <T>
*/
public static <T extends Comparable<? super T>> void quickSort(T[] a) {
quickSort(a, 0, a.length - 1);
}
private static <T extends Comparable<? super T>> void quickSort(T[] a, int left, int right) {
if (right - left >= 3) {
T pivot = medianFromThree(a, left, right);//中值,标志
int i = left, j = right - 1;//开始扫描
while (true) {
while (a[++i].compareTo(pivot) < 0) {
}
while (a[--j].compareTo(pivot) > 0) {
}
if (i < j) {
swap(a, i, j);
} else {
break;
}
}
swap(a, i, right - 1);//将pivot放置合适的位置
quickSort(a, left, i - 1);//较小元素排序
quickSort(a, i + 1, right);//较大元素排序
} else {//对于小数组,直接使用插入排序,效率更佳
insertionSort(a);
}
}
/**
* 取三个数的中值数 并将三个数放至合适的位置
*
* @param a 数组
* @param left 左下标
* @param right 右下标
* @param <T> 返回的中值数
* @return
*/
private static <T extends Comparable<? super T>> T medianFromThree(T[] a, int left, int right) {
int center = (left + right) / 2;
if (a[left].compareTo(a[right]) > 0) {
swap(a, left, right);
}
if (a[left].compareTo(a[center]) > 0) {
swap(a, left, center);
}
if (a[center].compareTo(a[right]) > 0) {
swap(a, center, right);
}
swap(a, center, right - 1);//将中位数移动至倒数第二位
return a[right - 1];
}
/**
* 交换数组中两下标的元素
*
* @param a 待交换数组
* @param index1 数组下标1
* @param index2 数组下标2
* @param <T>
*/
private static <T extends Comparable<? super T>> void swap(T[] a, int index1, int index2) {
T temp = a[index1];
a[index1] = a[index2];
a[index2] = temp;
}
/**
* 基数排序(字符串)
*
* @param a 待排序的整型数组
* @param maxLen 最长数字的长度
*/
public static void radixSort(String[] a, int maxLen) {
final int BUCKETS = 257;//所有字符的个数,第0位存放该位无字符的字符串
ArrayList<String>[] buckets = new ArrayList[BUCKETS];
for (int i = 0; i < BUCKETS; i++) {
buckets[i] = new ArrayList();
}
for (int i = maxLen - 1; i >= 0; i--) {//maxLen次桶排序
for (String s : a) {
if (s.length() > i) {
buckets[s.charAt(i)+1].add(s);
}else {
buckets[0].add(s);
}
}
int idx=0;
for(ArrayList<String> bucket:buckets){
for(String s:bucket){
a[idx++]=s;
}
bucket.clear();
}
}
}
/**
* 基数排序(正整数)
*
* @param a 待排序的字符串数组
* @param maxLen 最大数长度
*/
public static void radixSort(Integer[] a, int maxLen) {
ArrayList[] buckets = new ArrayList[10];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList();
}
for (int i = 0; i < maxLen; i++) {
for (Integer s : a) {
buckets[(s / (int) Math.pow(10, i)) % 10].add(s);
}
int idx = 0;
for (ArrayList<Integer> bucket : buckets) {
for (Integer s : bucket) {
a[idx++] = s;
}
bucket.clear();
}
}
}
}
四、总体分析
1 快速排序(QuickSort)
快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就地版本。快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。
2 归并排序(MergeSort)
归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。
3 堆排序(HeapSort)
堆排序适合于数据量非常大的场合(百万数据)。
堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。
4 Shell排序(ShellSort)
Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。平均效率是O(nlogn)。其中分组的合理性会对算法产生重要的影响。现在多用D.E.Knuth的分组方法。
Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。
5 插入排序(InsertSort)
插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对冒泡排序的改进。它比冒泡排序快2倍。一般不用在数据大于1000的场合下使用插入排序,或者重复排序超过200数据项的序列。
6 冒泡排序(BubbleSort)
冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。它是O(n^2)的算法。
7 选择排序(SelectSort)
这两种排序方法都是交换方法的排序算法,效率都是 O(n2)。在实际应用中处于和冒泡排序基本相同的地位。它们只是排序算法发展的初级阶段,在实际中使用较少。
8 基数排序(RadixSort)
基数排序和通常的排序算法并不走同样的路线。它是一种比较新颖的算法,但是它只能用于整数的排序,如果我们要把同样的办法运用到浮点数上,我们必须了解浮点数的存储格式,并通过特殊的方式将浮点数映射到整数上,然后再映射回去,这是非常麻烦的事情,因此,它的使用同样也不多。而且,最重要的是,这样算法也需要较多的存储空间。