先来看看8种排序之间的关系:
下图是各种排序的比较:
1, 直接插入排序
(1)基本思想:在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排
好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数
也是排好顺序的。如此反复循环,直到全部排好顺序。
在插入算法中,如果有一个最小的数在数组的最后面,用插入算法就会从最后一个
位置移动到第一个。
(2)实例
package cglib;
public class StringNumber {
public static void insertSort(int[] a) {
if (a == null || a.length < 2) {
return;
}
int length=a.length; //数组长度
int j; //当前值的位置
int i; //指向j前的位置
int key; //当前要进行插入排序的值
//从数组的第二个位置开始遍历值
for(j=1;j<length;j++){
key=a[j];
i=j-1;
System.out.println(" 将i="+i);
//a[i]比当前值大时,a[i]后移一位,空出i的位置,好让下一次循环的值后移
while(i>=0 && a[i]>key){
System.out.println("进 i="+i);
a[i+1]=a[i]; //将a[i]值后移
i--; //i前移
System.out.println(" i="+i);
}//跳出循环(找到要插入的中间位置或已遍历到0下标)
System.out.println(" 退出while");
System.out.println(" i="+i);
a[i+1]=key; //将当前值插入
}
}
public static void main(String[] args) {
int[] array = { 3, -1, 0, -8, 2, 1 };
ArrayUtils.printArray(array);
insertSort(array);
ArrayUtils.printArray(array);
}
}
class ArrayUtils {
public static void printArray(int[] array) {
System.out.print("{");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
if (i < array.length - 1) {
System.out.print(", ");
}
}
System.out.println("}");
}
}
输出:
{3, -1, 0, -8, 2, 1}
将i=0
进 i=0
i=-1
退出while
i=-1
将i=1
进 i=1
i=0
退出while
i=0
将i=2
进 i=2
i=1
进 i=1
i=0
进 i=0
i=-1
退出while
i=-1
将i=3
进 i=3
i=2
退出while
i=2
将i=4
进 i=4
i=3
进 i=3
i=2
退出while
i=2
{-8, -1, 0, 1, 2, 3}
希尔排序(最小增量排序)
基本算法:
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元 素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。
算法最开始以一定的步长进行排序,然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一 定会被排序。Donald Shell 最初建议步长选择为\frac{n}{2}并且对步长取半直到步长达到 1。虽然这样取可以比\mathcal{O}(n^2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。
希尔排序示例:n=10的一个数组 58 27 32 93 65 87 58 46 9 65,步长为n/2。
第一次排序 步长为 10/2 = 5
58 27 32 93 65 87 58 46 9 65
1A 1B
2A 2B
3A 3B
4A 4B
5A 5B
首先将待排序元素序列分组,以5为步长,(1A,1B), (2A,2B),(3A,3B)等为分组标记,大写字母表示是该组的第几个元素,数字相同的表示在同一组,这样就分成5组,即(58,87), (27,58),(32,46),(93,9),(65,65),然后分别对各分组进行直接插入排序,排序后5组为(58,87),(27,58), (32,46),(9,93),(65,65),分组排序只是变得各个分组内的下表,下同。
第二次排序 步长为 5/2 = 2
58 27 32 9 65 87 58 46 93 65
1A 1B
2A 2B
3A 3B
.............................................
第三次排序 步长为 2/2 = 1
32 9 58 27 58 46 65 65 93 87
1A 1B 1C 1D 1E 1F 1G 1H 1I 1J
第四次排序 步长为 1/2 = 0 得到有序元素序列
9 27 32 46 58 58 65 65 87 93
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
增量序列的选择:Shell排序的执行时间依赖于增量序列。
好的增量序列的共同特征(查到的资料都这么讲):
① 最后一个增量必须为1;
② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
package cglib;
public class StringNumber {
public static void main(String[] args) {
int[] arr = new int[]{44,33,99,10,30,20,59,78,23,48};
System.out.print("排序前:");
for(int o: arr) {
System.out.print(o+" ");
}
System.out.println();
shellSort(arr);
System.out.print("排序后:");
for(int o: arr) {
System.out.print(o+" ");
}
System.out.println();
}
private static void shellSort(int[] arr) {
int j;
int len = arr.length;
for(int val=len>>1; val>0; val>>=1) {
//下面是对本次的所有分组做直接插入排序
for(int i=val; i<len; i++) {
System.out.println("for:i="+i);
System.out.println("for:arr[i]="+arr[i]);
System.out.println("for:val="+val);
int temp = arr[i];
/*
* 为什么每次都用temp比较呢?
* 因为直接插入就是找到temp的合适位置。
* 为什么temp<arr[j-val]这个条件可以放在for内呢?
* 因为原来的组内数据已经有序,找到位置就停止便是。
*
*/
for(j=i; j>=val&&temp<arr[j-val]; j-=val) {
System.out.println("er:j="+j);
System.out.println("er:arr[j]="+arr[j]);
System.out.println("er:j-val="+(j-val));
System.out.println("er:arr[j-val]="+arr[j-val]);
/*
* 为什么是arr[j-val]不是arr[j]呢?
* 因为j=i开始的,而且条件是j>=val&&temp<arr[j-val]
*/
arr[j] = arr[j-val];
System.out.println("赋值er:arr[j]="+arr[j]);
}
/*
* 注意不是arr[i] = temp
* 直接插入排序也是这样的。
* 为什么呢?
* 因为j是位置,i是待插入元素
*/
arr[j] = temp;
}
}
}
}
输出:
排序前:44 33 99 10 30 20 59 78 23 48
for:i=5
for:arr[i]=20
for:val=5
er:j=5
er:arr[j]=20
er:j-val=0
er:arr[j-val]=44
赋值er:arr[j]=44
for:i=6
for:arr[i]=59
for:val=5
for:i=7
for:arr[i]=78
for:val=5
er:j=7
er:arr[j]=78
er:j-val=2
er:arr[j-val]=99
赋值er:arr[j]=99
for:i=8
for:arr[i]=23
for:val=5
for:i=9
for:arr[i]=48
for:val=5
for:i=2
for:arr[i]=78
for:val=2
for:i=3
for:arr[i]=10
for:val=2
er:j=3
er:arr[j]=10
er:j-val=1
er:arr[j-val]=33
赋值er:arr[j]=33
for:i=4
for:arr[i]=30
for:val=2
er:j=4
er:arr[j]=30
er:j-val=2
er:arr[j-val]=78
赋值er:arr[j]=78
for:i=5
for:arr[i]=44
for:val=2
for:i=6
for:arr[i]=59
for:val=2
er:j=6
er:arr[j]=59
er:j-val=4
er:arr[j-val]=78
赋值er:arr[j]=78
for:i=7
for:arr[i]=99
for:val=2
for:i=8
for:arr[i]=23
for:val=2
er:j=8
er:arr[j]=23
er:j-val=6
er:arr[j-val]=78
赋值er:arr[j]=78
er:j=6
er:arr[j]=78
er:j-val=4
er:arr[j-val]=59
赋值er:arr[j]=59
er:j=4
er:arr[j]=59
er:j-val=2
er:arr[j-val]=30
赋值er:arr[j]=30
for:i=9
for:arr[i]=48
for:val=2
er:j=9
er:arr[j]=48
er:j-val=7
er:arr[j-val]=99
赋值er:arr[j]=99
for:i=1
for:arr[i]=10
for:val=1
er:j=1
er:arr[j]=10
er:j-val=0
er:arr[j-val]=20
赋值er:arr[j]=20
for:i=2
for:arr[i]=23
for:val=1
for:i=3
for:arr[i]=33
for:val=1
for:i=4
for:arr[i]=30
for:val=1
er:j=4
er:arr[j]=30
er:j-val=3
er:arr[j-val]=33
赋值er:arr[j]=33
for:i=5
for:arr[i]=44
for:val=1
for:i=6
for:arr[i]=59
for:val=1
for:i=7
for:arr[i]=48
for:val=1
er:j=7
er:arr[j]=48
er:j-val=6
er:arr[j-val]=59
赋值er:arr[j]=59
for:i=8
for:arr[i]=78
for:val=1
for:i=9
for:arr[i]=99
for:val=1
排序后:10 20 23 30 33 44 48 59 78 99
选择排序
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
package cglib;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
public class StringNumber {
public static void main(String[] args){
Random random = new Random();
int[] array = new int[2000];
for (int j = 0; j < 2000; j++) {
array[j] = random.nextInt(100000);
}
System.out.println(Arrays.toString(array));
selectSortTest(array);
System.out.println(Arrays.toString(array));
}
public static void selectSortTest(int a[]) {
Date dateStart = new Date();
selectSort(a);
Date dateEnd = new Date();
System.out.println("选择排序耗费时间:"
+ (dateEnd.getTime() - dateStart.getTime()));
}
public static void selectSort(int a[]){
int n = a.length;
for(int k=0; k<n-1; k++) {
int min = k;
for(int i=k+1; i<n; i++) {//找出最小值
if(a[i] < a[min]) {
min = i;
}
}
if(k != min) {
int temp = a[k];
a[k] = a[min];
a[min] = temp;
}
}
}
}
堆
堆排序和合并排序一样,是一种时间复杂度为O(nlgn)的算法,同时和插入排序一样,是一种就地排序算法(不需要额外的存储空间)。堆排序需要用到一种 被称为最大堆的数据结构,与java或者lisp的gc不一样,这里的堆是一种数据结构,他可以被视为一种完全二叉树,即树里面除了最后一层其他层都是填 满的。也正是因为这样,树里面每个节点的子女和双亲节点的序号都可以根据当前节点的序号直接求出。
Parent(i)=i/2
Left(i)=2*i
Right(i)=2*i+1
如 上图所示,1位置的子女节点分别为2,3 2节点的子女节点为4,5 2的双亲节点为1 考察其他节点也很容易发现上述关系。最大堆是一种特殊的堆,其特点是每个双亲节点的值都比子女节点大。他的这一特点使得他可以实现nlgn的就地排序。现 在我们先来看看怎么构建和保持一个最大堆。
最大堆的构建和保持
我们现在有一个数组A,大小是n,假设其中元素按照完全二叉树的方式排列。如何将其构造成一个最大堆?首先我们知道最大堆的每个子树都符合最大堆的性质 (根节点值大于所有子节点)。同时我们知道序号为(n/2+1)~n的元素都是叶子节点(因为其子女节点的序号都大于n,即说明没有子女节点),因此我们 构建最大堆的操作就在序号为1~n/2的元素内进行(其他元素已满足最大堆性质)。我们定义如下操作maxify(i):将以i位置节点为根的子树改造成 最大堆。其操作内容如下:对于每个节点i,我们考察他与子女节点的大小,如果他比某个子女节点小,则将他与子女节点中最大的那个互换位置,然后在相应的子 女节点位置重复操作,直到到达堆的叶子节点或者考察的位置比子女节点的值都要大为止。由此可知我们构造最大堆buildmaxheap的过程就是在每个内 部节点上调用maxify过程,依次到树的根部,此时其左右子树都是最大堆,现在
步骤&实例
实现堆排序需解决两个问题:
(1)如何将n 个待排序的数建成堆;
(2)输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。
建堆方法(小顶堆):
对初始序列建堆的过程,就是一个反复进行筛选的过程。
n 个结点的完全二叉树,则最后一个结点是第n/2个结点的子树。
筛选从第n/2个结点为根的子树开始(n/2是最后一个有子树的结点),使该子树成为堆。
之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程
无序序列:(49, 38, 65, 97, 76, 13, 27, 49)
(a) 无序序列,初始二叉树,97(第8/2=4个结点)为最后一个结点(49)的父结点。
(b) 97>=49,替换位置,接下来对n/2的上一个结点65进行筛选。
(c) 13<=27且65>=13,替换65和13的位置,接下来对38进行替换(都大于它,不需操作),对49进行筛选。
(d) 13<=38且49>=13,替换49和13的位置,49>=27,替换49和27的位置。
(e) 最终得到一个堆,13是我们得到的最小数。
调整堆的方法(小顶堆):
设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶,堆被破坏,其原因仅是根结点不满足堆的性质。
将根结点与左、右子树中较小元素的进行交换。
若与左子树交换:如果左子树堆被破坏,则重复方法(2).
若与右子树交换,如果右子树堆被破坏,则重复方法(2).
继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
调整堆只需考虑被破坏的结点,其他的结点不需调整。
package cglib;
public class StringNumber {
public static void main(String[] args) {
int[] array = { 49, 38, 65, 97, 76, 13, 27, 49 };
heapSort(array, true);
System.out.print("\n"+"排完:");
for (int i : array) {
System.out.print(i + " ");
}
}
/**
* 调整为小顶堆(排序后结果为从大到小)
*
* @param array是待调整的堆数组
* @param s是待调整的数组元素的位置
* @param length是数组的长度
*
*/
public static void heapAdjustS(int[] array, int s, int length) {
int tmp = array[s];
int child = 2 * s + 1;// 左孩子结点的位置
System.out.println("待调整结点为:array[" + s + "] = " + tmp);
while (child < length) {
// child + 1 是当前调整结点的右孩子
// 如果有右孩子且小于左孩子,使用右孩子与结点进行比较,否则使用左孩子
if (child + 1 < length && array[child] > array[child + 1]) {
child++;
}
System.out.println("将与子孩子 array[" + child + "] = " + array[child] + " 进行比较");
// 如果较小的子孩子比此结点小
if (array[s] > array[child]) {
System.out.println("子孩子比其小,交换位置");
array[s] = array[child];// 把较小的子孩子向上移动,替换当前待调整结点
s = child;// 待调整结点移动到较小子孩子原来的位置
array[child] = tmp;
child = 2 * s + 1;// 继续判断待调整结点是否需要继续调整
if (child >= length) {
System.out.println("没有子孩子了,调整结束");
} else {
System.out.println("继续与新的子孩子进行比较");
}
// continue;
} else {
System.out.println("子孩子均比其大,调整结束");
break;// 当前待调整结点小于它的左右孩子,不需调整,直接退出
}
}
}
/**
* 调整为大顶堆(排序后结果为从小到大)
*
* @param array是待调整的堆数组
* @param s是待调整的数组元素的位置
* @param length是数组的长度
*
*/
public static void heapAdjustB(int[] array, int s, int length) {
int tmp = array[s];
int child = 2 * s + 1;// 左孩子结点的位置
System.out.println("待调整结点为:array[" + s + "] = " + tmp);
while (child < length) {
// child + 1 是当前调整结点的右孩子
// 如果有右孩子且大于左孩子,使用右孩子与结点进行比较,否则使用左孩子
if (child + 1 < length && array[child] < array[child + 1]) {
child++;
}
System.out.println("将与子孩子 array[" + child + "] = " + array[child] + " 进行比较");
// 如果较大的子孩子比此结点大
if (array[s] < array[child]) {
System.out.println("子孩子比其大,交换位置");
array[s] = array[child];// 把较大的子孩子向上移动,替换当前待调整结点
s = child;// 待调整结点移动到较大子孩子原来的位置
array[child] = tmp;
child = 2 * s + 1;// 继续判断待调整结点是否需要继续调整
if (child >= length) {
System.out.println("没有子孩子了,调整结束");
} else {
System.out.println("继续与新的子孩子进行比较");
}
// continue;
} else {
System.out.println("子孩子均比其小,调整结束");
break;// 当前待调整结点大于它的左右孩子,不需调整,直接退出
}
}
}
/**
* 堆排序算法
*
* @param array
* @param inverse true 为倒序排列,false 为正序排列
*/
public static void heapSort(int[] array, boolean inverse) {
// 初始堆
// 最后一个有孩子的结点位置 i = (length - 1) / 2, 以此向上调整各结点使其符合堆
System.out.println("初始堆开始");
for (int i = (array.length - 1) / 2; i >= 0; i--) {
if (inverse) {
heapAdjustS(array, i, array.length);
} else {
heapAdjustB(array, i, array.length);
}
}
System.out.println("初始堆结束");
for (int i = array.length - 1; i > 0; i--) {
// 交换堆顶元素H[0]和堆中最后一个元素
int tmp = array[i];
array[i] = array[0];
array[0] = tmp;
// 每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
if (inverse) {
heapAdjustS(array, 0, i);
} else {
heapAdjustB(array, 0, i);
}
}
}
}
输出:
初始堆开始
待调整结点为:array[3] = 97
将与子孩子 array[7] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[2] = 65
将与子孩子 array[5] = 13 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[1] = 38
将与子孩子 array[3] = 49 进行比较
子孩子均比其大,调整结束
待调整结点为:array[0] = 49
将与子孩子 array[2] = 13 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[6] = 27 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
初始堆结束
待调整结点为:array[0] = 97
将与子孩子 array[2] = 27 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[6] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 97
将与子孩子 array[1] = 38 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[3] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 65
将与子孩子 array[1] = 49 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[4] = 76 进行比较
子孩子均比其大,调整结束
待调整结点为:array[0] = 76
将与子孩子 array[2] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 97
将与子孩子 array[1] = 65 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 76
将与子孩子 array[1] = 97 进行比较
子孩子均比其大,调整结束
待调整结点为:array[0] = 97
排完:97 76 65 49 49 38 27 13
5.冒泡排序
(1)基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
(2)实例:
package cglib;
public class StringNumber {
public static void main(String[] args){
int score[] = {67, 69, 75, 87, 89, 90, 99, 100};
for (int i = 0; i < score.length -1; i++){ //最多做n-1趟排序
for(int j = 0 ;j < score.length - i - 1; j++){ //对当前无序区间score[0......length-i-1]进行排序(j的范围很关键,这个范围是在逐步缩小的)
if(score[j] < score[j + 1]){ //把小的值交换到后面
int temp = score[j];
score[j] = score[j + 1];
score[j + 1] = temp;
}
}
System.out.print("第" + (i + 1) + "次排序结果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a] + "\t");
}
System.out.println("");
}
System.out.print("最终排序结果:");
for(int a = 0; a < score.length; a++){
System.out.print(score[a] + "\t");
}
}
}
输出:
第1次排序结果:69 75 87 89 90 99 100 67
第2次排序结果:75 87 89 90 99 100 69 67
第3次排序结果:87 89 90 99 100 75 69 67
第4次排序结果:89 90 99 100 87 75 69 67
第5次排序结果:90 99 100 89 87 75 69 67
第6次排序结果:99 100 90 89 87 75 69 67
第7次排序结果:100 99 90 89 87 75 69 67
最终排序结果: 100 99 90 89 87 75 69 67
快速排序
算法思想:基于分治的思想,是冒泡排序的改进 型。首先在数组中选择一个基准点(该基准点的选取可能影响快速排序的效率,后面讲解选取的方法),然后分别从数组的两端扫描数组,设两个指示标志(lo指 向起始位置,hi指向末尾),首先从后半部分开始,如果发现有元素比该基准点的值小,就交换lo和hi位置的值,然后从前半部分开始扫秒,发现有元素大于 基准点的值,就交换lo和hi位置的值,如此往复循环,直到lo>=hi,然后把基准点的值放到hi这个位置。一次排序就完成了。以后采用递归的方 式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就自然有序了。
排序过程:
public static int partition(int []array,int lo,int hi){ //固定的切分方式 int key=array[lo]; while(lo<hi){ while(array[hi]>=key&&hi>lo){//从后半部分向前扫描 hi--; } array[lo]=array[hi]; while(array[lo]<=key&&hi>lo){从前半部分向后扫描 lo++; } array[hi]=array[lo]; } array[hi]=key; return hi; } public static void sort(int[] array,int lo ,int hi){ if(lo>=hi){ return ; } int index=partition(array,lo,hi); sort(array,lo,index-1); sort(array,index+1,hi); }
快速排序的优化
对于基准位置的选取一般有三种方法:固定切分,随机切分和三取样切分。固定切分的效率并不是太好,随机切分是常用的一种切分,效率比较高,最坏情况下时间复杂度有可能为O(N2).对于三数取中选择基准点是最理想的一种。
三数取中切分:
快速排序的优化
对于基准位置的选取一般有三种方法:固定切分,随机切分和三取样切分。固定切分的效率并不是太好,随机切分是常用的一种切分,效率比较高,最坏情况下时间复杂度有可能为O(N2).对于三数取中选择基准点是最理想的一种。
三数取中切分:
public static int partition(int []array,int lo,int hi){ //三数取中 int mid=lo+(hi-lo)/2; if(array[mid]>array[hi]){//7,5,2 swap(array[mid],array[hi]);//7,2,5 } if(array[lo]>array[hi]){ swap(array[lo],array[hi]);//5,2,7 } if(array[mid]>array[lo]){ swap(array[mid],array[lo]); } int key=array[lo];//取第二大的 while(lo<hi){ while(array[hi]>=key&&hi>lo){ hi--; } array[lo]=array[hi]; while(array[lo]<=key&&hi>lo){ lo++; } array[hi]=array[lo]; } array[hi]=key; return hi; } public static void swap(int a,int b){ int temp=a; a=b; b=temp; } public static void sort(int[] array,int lo ,int hi){ if(lo>=hi){ return ; } int index=partition(array,lo,hi); sort(array,lo,index-1); sort(array,index+1,hi); }
快速排序在序列中元素很少时,效率将比较低,不然插入排序,因此一般在序列中元素很少时使用插入排序,这样可以提高整体效率。
public static void quick(int []array ,int lo,int hi){ if(hi-lo+1<10){ insertSort(array); }else{ quickSort(array,lo,hi); } }
归并排序的基本思想
将待排序序列R[0...n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。
综上可知:
归并排序其实要做两件事:
(1)“分解”——将序列每次折半划分。
(2)“合并”——将划分后的序列段两两合并后排序。
我们先来考虑第二步,如何合并?
在每次合并过程中,都是对两个有序的序列段进行合并,然后排序。
这两个有序序列段分别为 R[low, mid] 和 R[mid+1, high]。
先将他们合并到一个局部的暂存数组R2中,带合并完成后再将R2复制回R中。
为了方便描述,我们称 R[low, mid] 第一段,R[mid+1, high] 为第二段。
每次从两个段中取出一个记录进行关键字的比较,将较小者放入R2中。最后将各段中余下的部分直接复制到R2中。
经过这样的过程,R2已经是一个有序的序列,再将其复制回R中,一次合并排序就完成了。
基本排序:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
(2)实例:
核心代码
public void Merge(int[] array, int low, int mid, int high) {
int i = low; // i是第一段序列的下标
int j = mid + 1; // j是第二段序列的下标
int k = 0; // k是临时存放合并序列的下标
int[] array2 = new int[high - low + 1]; // array2是临时合并序列
// 扫描第一段和第二段序列,直到有一个扫描结束
while (i <= mid && j <= high) {
// 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
if (array[i] <= array[j]) {
array2[k] = array[i];
i++;
k++;
} else {
array2[k] = array[j];
j++;
k++;
}
}
// 若第一段序列还没扫描完,将其全部复制到合并序列
while (i <= mid) {
array2[k] = array[i];
i++;
k++;
}
// 若第二段序列还没扫描完,将其全部复制到合并序列
while (j <= high) {
array2[k] = array[j];
j++;
k++;
}
// 将合并序列复制到原始序列中
for (k = 0, i = low; i <= high; i++, k++) {
array[i] = array2[k];
}
}
掌握了合并的方法,接下来,让我们来了解 如何分解。
在某趟归并中,设各子表的长度为gap,则归并前R[0...n-1]中共有n/gap个有序的子表:R[0...gap-1], R[gap...2*gap-1], ... , R[(n/gap)*gap ... n-1]。
调用Merge将相邻的子表归并时,必须对表的特殊情况进行特殊处理。
若子表个数为奇数,则最后一个子表无须和其他子表归并(即本趟处理轮空):若子表个数为偶数,则要注意到最后一对子表中后一个子表区间的上限为n-1。
核心代码
public void MergePass(int[] array, int gap, int length) {
int i = 0;
// 归并gap长度的两个相邻子表
for (i = 0; i + 2 * gap - 1 < length; i = i + 2 * gap) {
Merge(array, i, i + gap - 1, i + 2 * gap - 1);
}
// 余下两个子表,后者长度小于gap
if (i + gap - 1 < length) {
Merge(array, i, i + gap - 1, length - 1);
}
}
public int[] sort(int[] list) {
for (int gap = 1; gap < list.length; gap = 2 * gap) {
MergePass(list, gap, list.length);
System.out.print("gap = " + gap + ":\t");
this.printAll(list);
}
return list;
}
package cglib;
import java.util.Arrays;
public class StringNumber {
/**
* 归并排序
* 简介:将两个(或两个以上)有序表合并成一个新的有序表 即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列
* 时间复杂度为O(nlogn)
* 稳定排序方式
* @param nums 待排序数组
* @return 输出有序数组
*/
public static int[] sort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左边
sort(nums, low, mid);
// 右边
sort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
return nums;
}
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
// 归并排序的实现
public static void main(String[] args) {
int[] nums = { 2, 7, 8, 3, 1, 6, 9, 0, 5, 4 };
StringNumber.sort(nums, 0, nums.length-1);
System.out.println(Arrays.toString(nums));
}
}
Java版本
1 package notes.javase.algorithm.sort;
2
3 public class MergeSort {
4 public void Merge(int[] array, int low, int mid, int high) {
5 int i = low; // i是第一段序列的下标
6 int j = mid + 1; // j是第二段序列的下标
7 int k = 0; // k是临时存放合并序列的下标
8 int[] array2 = new int[high - low + 1]; // array2是临时合并序列
9
10 // 扫描第一段和第二段序列,直到有一个扫描结束
11 while (i <= mid && j <= high) {
12 // 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
13 if (array[i] <= array[j]) {
14 array2[k] = array[i];
15 i++;
16 k++;
17 } else {
18 array2[k] = array[j];
19 j++;
20 k++;
21 }
22 }
23
24 // 若第一段序列还没扫描完,将其全部复制到合并序列
25 while (i <= mid) {
26 array2[k] = array[i];
27 i++;
28 k++;
29 }
30
31 // 若第二段序列还没扫描完,将其全部复制到合并序列
32 while (j <= high) {
33 array2[k] = array[j];
34 j++;
35 k++;
36 }
37
38 // 将合并序列复制到原始序列中
39 for (k = 0, i = low; i <= high; i++, k++) {
40 array[i] = array2[k];
41 }
42 }
43
44 public void MergePass(int[] array, int gap, int length) {
45 int i = 0;
46
47 // 归并gap长度的两个相邻子表
48 for (i = 0; i + 2 * gap - 1 < length; i = i + 2 * gap) {
49 Merge(array, i, i + gap - 1, i + 2 * gap - 1);
50 }
51
52 // 余下两个子表,后者长度小于gap
53 if (i + gap - 1 < length) {
54 Merge(array, i, i + gap - 1, length - 1);
55 }
56 }
57
58 public int[] sort(int[] list) {
59 for (int gap = 1; gap < list.length; gap = 2 * gap) {
60 MergePass(list, gap, list.length);
61 System.out.print("gap = " + gap + ":\t");
62 this.printAll(list);
63 }
64 return list;
65 }
66
67 // 打印完整序列
68 public void printAll(int[] list) {
69 for (int value : list) {
70 System.out.print(value + "\t");
71 }
72 System.out.println();
73 }
74
75 public static void main(String[] args) {
76 int[] array = {
77 9, 1, 5, 3, 4, 2, 6, 8, 7
78 };
79
80 MergeSort merge = new MergeSort();
81 System.out.print("排序前:\t\t");
82 merge.printAll(array);
83 merge.sort(array);
84 System.out.print("排序后:\t\t");
85 merge.printAll(array);
86 }
87 }
运行结果
排序前: 9 1 5 3 4 2 6 8 7
gap = 1: 1 9 3 5 2 4 6 8 7
gap = 2: 1 3 5 9 2 4 6 8 7
gap = 4: 1 2 3 4 5 6 8 9 7
gap = 8: 1 2 3 4 5 6 7 8 9
排序后: 1 2 3 4 5 6 7 8 9
基数排序
(1)基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
(2)实例:
桶式排序
1、概念:有限个数字m,每个数字的大小都在1与n之间,则我们可以假设有n个桶,遍历m个数字,将其存入对应的桶中(如数字的值为3,就存入3号桶,桶的值对应存入数字的个数)
2、例子
有数字3,3,5,1,2,大小均在0-5之间,所以我们假设有5个桶,分别标号1,2,3,4,5,遍历所有数字,将其存入桶中,则存储之后,所有桶的计数如下:
桶号 | 1 | 2 | 3 | 4 | 5 |
计数 | 1 | 1 | 2 | 0 | 1 |
我们按照桶的序号将数字倒出来,如下:
桶的倒出顺序 | 数字队列 |
5号桶倒出1个5 | 5 |
4号桶倒出0个4 | 5 |
3号桶倒出2个3 | 5,3,3 |
2号桶倒出1个2 | 5,3,3,2 |
1号桶倒出1个1 | 5,3,3,2,1 |
如上所示,我们成功将所给数列按照从大到小的排序,反之,如果从1号桶开始倒出,我们会得到从小到大排列的数字
3、代码实现(JAVA)
//桶式排序 public class BucketSort{ public static void main(String[] args){ int[] a = {2,4,15,11,6,3,7,19,8,5,4}; sort(a,19); } //桶式排序函数 //a是要排序的数组 //max是最大数字(这里我们默认数字最小为0) public static void sort(int[] a,int max){ //声明一个数组,这就是桶,编号从0到max的桶,一共max+1个 int[] count = new int[max + 1]; //遍历数组,用桶计数 for(int i = 0;i < a.length;i++){ count[a[i]]++; } //将桶里面的数字倒出 for(int i = max;i > 0;i--){ while(count[i] > 0){ System.out.print(i + " "); count[i]--; } } } }
4、弊端:如果我们的数字波动范围非常大,比如1到10000,那么我们需要一个10000元素数组的空间开销,而且在倒出数字的时候需要遍历10000个桶,这样效率是非常低的,于是我们有了基于桶式排序的基数排序
二、基数排序
1、基于桶式排序,将要排序的数字一位一位的比较,经历多次桶式排序,得出最终的序列
如果要排序的元素可以分成多位,并且每一位都在一个固定的范围内,则可以用这种排序方法,如对10进制数字的排序
2、例子
有数字23,35,9,73,3,314,11,1234,5,可以看出来,每一位数字的取值范围都是0到9,所以我们可以用10个桶来进行排序,分别编号0到9。
现在有数组:278,109,63,930,589,184,505,269,8,83
第一次根据各位数将数组划分为10个队列(当然其中的某些队列可能不含有元素)
0:930
1:
2:
3:63,83
4:184
5:505
6:
7:
8:278,8
9:109,589,269
然后收集成序列:
930,63,83,184,505,278,8,109,589,269
在进行第二次分组:
0:505,8,109
1:
2:
3:930
4:
5:
6:63,269
7:278
8:83,184,589
9:
第二次收集:
505,8,109,930,63,269,278,83,184,589
第三次分配:
0:8,63,83
1:109,184
2:278,269
3:
4:
5:505,589
6:
7:
8:
9:930
最后得到序列:
8,63,83,109,184,269,278,505,589,930
完成排序!
基数排序其实是利用多关键字先达到局部有序,再调整达到全局有序。
Java代码
- public static void radixSort(int[] array){
- //首先确定排序的趟数;
- int max=array[0];
- for(int i=1;i<array.length;i++){
- if(array[i]>max){
- max=array[i];
- }
- }
- int time=0;
- //判断位数;
- while(max>0){
- max/=10;
- time++;
- }
- //建立10个队列;
- LinkQueue<Integer>[] queue=new LinkQueue[10];
- for(int i=0;i<10;i++){
- queue[i]=new LinkQueue<Integer>();
- }
- //进行time次分配和收集;
- for(int i=0;i<time;i++){
- //分配数组元素;
- for(int j=0;j<array.length;j++){
- //得到数字的第time+1位数;
- queue[array[j]%(int)Math.pow(10, i+1)/(int)Math.pow(10, i)].enQueue(array[j]);
- }
- int count=0;//元素计数器;
- //收集队列元素;
- for(int k=0;k<10;k++){
- while(queue[k].size()>0){
- array[count]=(Integer) queue[k].deQueue().getElement();
- count++;
- }
- }
- }
- }
如果待排序列的关键字不是自然数,我们当然可以对其进行转化,然后利用类似的方式排序。
代码实现(JAVA)
- public class RadixSort {
- /**
- * 基于最低位优先方式对一个整型数组排序,数组中整数是d位数;
- * 以3位整数排序为例,基于最低位优先方式排序的步骤是:创建十个队列,用于临时存储整数;
- * 首先按个位排序,按照每个整数的个位数字把整数放进相应队列,比如120放进第0个队列,
- * 124放进第4个队列,当把所有整数都放进相应队列后,依次从第零个队列到第9个队列收集整数,
- * 收集得到的所有整数按照个位数大小从小到大排序了;
- * 然后按十位数排序,以同样的方式把每个整数按十位数放进相应队列,对于十位数相同的整数,
- * 个位数小的整数先放进队列,因为上一步按个位数的排序导致个位数小的数在前面,接下来,
- * 依次从第零个队列到第9个队列收集整数,整体上先收集十位数小的数,局部到具体队列中,
- * 也就是十位数相同的那些数,按照队列先进先出的特性,个位数小的数先被收集,于是,收集
- * 得到的所有整数是按照十位和个位组合后的大小从小到大排列的;
- * 最后按百位数排序,以同样的方式把每个整数按百位数放进相应队列,对于百位数相同的整数,
- * 十位数和个位数组合后小的整数先被放进队列,因为上一步的排序导致十位数和个位数组合后小
- * 的整数在前面,接下来,依次从第零个队列到第9个队列收集整数,整体上,先收集百位数小
- * 的数,局部到具体队列中,也就是百位数相同的那些数,按照队列的先进先出特性,十位数和
- * 个位数组合后小的整数先被收集,于是,收集得到的所有整数按从小到大排列。
- * 一句话总结排序过程,整体上从小到大,局部上也是从小到大。
- * @param integers 待排序整数数组
- * @param d 待排序数组中整数的位数
- */
- public void sort(Integer[] integers, int d) {
- /**
- * 创建队列集合并初始化,集合中的队列用于存放相应的元素,比如按最低为排序,234应该
- * 放在queues.get(4)这个队列中
- */
- LinkedList<LinkedList<Integer>> queues = new LinkedList<>();
- for(int i = 0; i < 10; i++){
- LinkedList<Integer> queue = new LinkedList<>();
- queues.add(queue);
- }
- for(int i = d - 1; i >= 0; i--) {
- // 根据排序key,把元素放进相应的队列里
- for (int j = 0; j < integers.length; j++) {
- Integer key = getValueByIndex(integers[j], i);
- queues.get(key).add(integers[j]);
- }
- /**
- * 把元素收回来;
- * for循环从头到尾便利集合
- */
- int index = 0;
- for (LinkedList<Integer> linkedList : queues) {
- for (Integer integer : linkedList) {
- integers[index] = integer;
- index++;
- }
- // 清空queues里的所有队列,为二次利用做准备
- linkedList.clear();
- }
- }
- }
- /**
- * 获得integer中第index + 1个数字
- * @param integer
- * @param index
- * @return
- */
- public Integer getValueByIndex(Integer integer, int index) {
- Objects.requireNonNull(integer);
- String iString = integer.toString();
- if (index < 0 || index >= iString.length()) {
- throw new IndexOutOfBoundsException();
- }
- String value = iString.substring(index, index + 1);
- return Integer.valueOf(value);
- }
- public static void main(String[] args) {
- Integer[] integers = new Integer[]{654, 122, 987, 123, 345, 234};
- new RadixSort().sort(integers, 3);
- for (int i = 0; i < integers.length; i++) {
- System.out.println(integers[i]);
- }
- }
- }
注: 正常情况下,我们是要告诉我们的排序方法,我们最高位的数字是几位的,这样在最高位也排序完成后就会停止排序,但是我们在这段代码中用了一个叫 hasNum的boolean型变量,用来表征我们的数组中是否还存在更高的位数,(PS:虽然省事了,方法更 通用了,但是也产生了额外的开销)
public class RadixSort{ public static void main(String[] args){ //声明要排序的数组 int[] data = {73,22,93,867494,43,55,123,8978,10000,14,28,65,39,81,33,100,567}; //调用基数排序函数 sort(data,10); //输出排序后的数组 for(int i = 0;i < data.length;i++){ System.out.print(data[i] + " "); } } ///基数排序函数 //a表示要排序的数组 //d表示每一位数字的范围(这里是10进制数,有0~9一共10种情况) public static void sort(int[] a,int d){ //n用来表示当前排序的是第几位 int n = 1; //hasNum用来表示数组中是否有至少一个数字存在第n位 boolean hasNum = false; //二维数组temp用来保存当前排序的数字 //第一维d表示一共有d个桶 //第二维a.length表示每个桶最多可能存放a.length个数字 int[][] temp = new int[d][a.length]; int[] order = new int[d]; while(true){ //判断是否所有元素均无比更高位,因为第一遍一定要先排序一次,所以有n!=1的判断 if(n != 1 && !hasNum){ break; } hasNum = false; //遍历要排序的数组,将其存入temp数组中(按照第n位上的数字将数字放入桶中) for(int i = 0;i < a.length;i++){ int x = a[i]/(n*10); if(x != 0){ hasNum = true; } int lsd = (x%10); temp[lsd][order[lsd]] = a[i]; order[lsd]++; } //k用来将排序好的temp数组存入data数组(将桶中的数字倒出) int k = 0; for(int i = 0;i < d;i++){ if(order[i] != 0){ for(int j = 0;j < order[i];j++){ a[k] = temp[i][j]; k++; } } order[i] = 0; } n++; } } }
三、基数排序(给英文字符串排序)
1、排序规则
①字符串更长的在前
②z在最前,a在最后
2、代码实现(JAVA)
public class RadixSort_Letter{ public static void main(String[] args){ //声明要排序的数组 String[] a = {"ac","ee","ef","b","z","f","ep","gaaa","azh","az","r"}; //调用基数排序函数 sort(a,4); //输出排序后的数组 for(int i = a.length - 1;i >= 0;i--){ System.out.print(a[i] + " "); } } ///基数排序函数 //a是要排序的数组 //m表示数组元素最高位数,如我们排序的数组中位数最高的元素为gaaa,有4位 public static void sort(String[] a,int m){ int n = 0; //27表示每一位字符分成27类,其中1~26对应'a'~'z' //第0位专门用来存放没有高位字符的数组元素,如在比较第二位字符时,b,z,f等没有第二位字符的元素就存在temp[0]中 //相对应的,ac存在temp[1]中,ef存在temp[5]中 String[][] temp = new String[27][a.length]; int[] order = new int[27]; while(n < m){ //这里声明String类型的数组b,将数组a中的每个元素倒序,然后放入数组b //如a[0]="abc",则b[0]="cba" //之所以这样做,是为了解决下面调用charAt方法时索引的问题,脑子太笨,没想到更好的方法 String[] b = new String[a.length]; for(int i = 0;i < a.length;i++){ if(a[i].length() > 1){ StringBuffer sb = new StringBuffer(a[i]); sb.reverse(); b[i] = new String(sb); }else{ b[i] = a[i]; } } for(int i = 0;i < a.length;i++){ if(a[i].length() > n){ int lsd = b[i].charAt(n) - 'a' + 1; temp[lsd][order[lsd]] = a[i]; order[lsd]++; }else{ temp[0][order[0]] = a[i]; order[0]++; } } int k = 0; for(int i = 0;i < 27;i++){ for(int j = 0;j < order[i];j++){ a[k] = temp[i][j]; k++; } order[i] = 0; } n++; } } }
字典排序
- import java.util.Comparator;
- import java.util.ArrayList;
- import java.util.Collections;
- public class Tester {
- public static void main(String[] args) {
- ArrayList list = new ArrayList();
- list.add("东海湾");
- list.add("傲来");
- list.add("东海湾-岩洞");
- list.add("傲来药店");
- /*
- * 运用Collections的sort()方法对其进行排序 sort()方法需要传 连个参数,一个是需要进行排序的Collection 另一个是一个Comparator
- */
- Collections.sort(list, new SpellComparator());
- for (int i = 0; i < list.size(); i++) {
- System.out.println(list.get(i));
- }
- }
- }
- /**
- * 汉字拼音排序比较器
- */
- class SpellComparator implements Comparator {
- public int compare(Object o1, Object o2) {
- try {
- // 取得比较对象的汉字编码,并将其转换成字符串
- String s1 = new String(o1.toString().getBytes("GB2312"), "ISO-8859-1");
- String s2 = new String(o2.toString().getBytes("GB2312"), "ISO-8859-1");
- // 运用String类的 compareTo()方法对两对象进行比较
- return s1.compareTo(s2);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return 0;
- }
- }
来源:oschina
链接:https://my.oschina.net/u/2822116/blog/783143