排序算法学习
=========================
排序算法众多,我这里主要学习了比较常见的几种排序算法,直接以代码形式给出,理解就以注释的形式给出了。每次运行时需要把前面的一个排序注释掉,因为数组传值时是传引用。经过前面的排序已经有序了,在运行后面的排序时就没有效果了。
package com.zcyfover.interview;
/**
* Created by zcy-fover on 2016/8/24.
*/
import java.util.Arrays;
/**
* 学习排序的各种算法
*/
public class Sort {
public static void main(String[] args) {
final int[] testArray = new int[]{5, 1, 43, 23, 24, 67, 6, 3, 788, 32};
Sort sort = new Sort();
// sort.bubbleSort(testArray);
// sort.selectSort(testArray);
// sort.insertSort(testArray);
// System.out.print("QuickSort Result: ");
// sort.quickSort(testArray, 0, testArray.length - 1);
// System.out.print("MergeSort Result: ");
// sort.mergeSort(testArray, 0, testArray.length-1);
// sort.binaryInsertSort(testArray);
// sort.shellSort(testArray, testArray.length);
for (int i = 0; i < testArray.length; i++){
System.out.print(testArray[i] + " ");
}
}
/**
* 冒泡排序:两两比较关键字,每次讲较大的关键字进行“下沉”处理;每一次排序都会使有序区增加一个气泡,
* 经过n-1次后,有序区就有n-1个气泡,此时有序区的气泡的值总是大于无序区的,所以冒泡排序至少要经过
* n-1次排序。
* 改进:在无序区的某一次排序中要是没有了气泡的交换,说明这个序列已经是有序得了。不用再执行后面的排序了,此时
* 需要一个交换的标志来监督程序的执行。每次排序开始前现将exchange置为false,如果在这一趟排序中发生了交换
* 则把exchange改编为true,如果没有进行交换说明在无序区的序列已经全部有序了,程序可以结束了。具体实现如下
* 算法分析:
* (1)最好时间复杂度:
* 若文件初始状态有序,即通过一次扫描即可结束排序,关键字的比较次数C和记录移动次数M都达到最小Cmin=n-1;Mmin=0
* (2)最坏时间复杂度:
* 若文件是倒序,要进行n-1次排序,每次都要进行n-i次关键字的比较(1< = i <= n-1),且每次都必须移动记录三次以达到交
* 换位置的目的,在这种情况下比较和交换次数都达到最大值Cmax=n(n-1)/2=O(n^2),Mmax=3n(n-1)/2=O(n^2);
* (3)平均时间复杂度:
* 在平常情况下,很少会有最好和最坏的情况出现,一般不一定会进行n-1次,但是由于他的记录移动次数较多,故平均时间性能
* 比直接插入排序要差很多。O(n^2)
* (4)冒泡排序时就地排序,所以它是稳定的。
* @param source
*/
public void bubbleSort(int[] source) {
boolean exchange;
for (int i = source.length - 1; i > 0; i--){
exchange = false;
for(int j = 0; j < i; j++){
if (source[j] > source[j + 1]){
int temp = source[j + 1];
source[j + 1] = source[j];
source[j] = temp;
exchange = true;
}
}
if (!exchange){
break;
}
}
System.out.print("BubbleSort Result: ");
}
/**
* 选择排序
* 算法思想:
* (1)从待排序序列中找到最小的元素;
* (2)将最小元素存放到未排序序列的起始位置;
* (3)从余下的n-i个元素中继续寻找最小元素
* 算法分析:
* 时间复杂度:假设排序序列有N个元素,比较次数总是在n(n-1)/2之间;
* 移动次数和序列的初始化顺序有关,当序列正序时,移动次数最小为0;当序列倒序时,移动次数最大为3n(n-1)/2;
* 综上,选择排序的时间复杂度为:O(n^2)
* 空间复杂度:每次交换时需要占用一个临时空间,空间复杂度为O(1)
* 稳定性:不稳定。比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面
* @param source
*/
public void selectSort(int[] source){
for (int i = 0; i < source.length; i++){
for (int j = i + 1; j < source.length; j++){
if (source[i] > source[j]){
int temp = source[j];
source[j] = source[i];
source[i] = temp;
}
}
}
System.out.print("SelectSort Result: ");
}
/**
* 插入排序:
* 算法思想:
* (1)从第一个元素开始,可以认为他已经是有序的
* (2)取出下一个元素,在已经排序的元素序列中从后向前扫描
* (3)如果该元素大于新元素,则将新元素与该元素交换位置
* (4)重复步骤3,直到新元素大于或等于已排序的元素
* (5)此时新元素已经找到自己合适的位置了
* (6)重复步骤2
* 时间复杂度:
* 如果目标是把n个元素的序列升序排列:
* 最好的情况:序列已经是升序,此时只需要进行n-1次比较即可,时间复杂度为O(n);
* 最坏的情况:序列是反序,此时需要进行n(n-1)/2次比较,插入操作为比较操作次数加n-1次,
* 此时复杂度为O(n^2)
* 空间复杂度:O(1)
* 稳定性:稳定
* 插入排序算法不适合于数据量比较大的排序应用,但如果数据量比较小的话,例如千级可以选用插入排序
* @param source
*/
public void insertSort(int[] source){
for (int i = 1; i < source.length; i++){
for (int j = i; (j > 0) && (source[j] < source[j - 1]); j--){
int temp = source[j];
source[j] = source[j - 1];
source[j -1] = temp;
}
}
System.out.print("InsertSort Result: ");
}
/**
* 快速排序
* 算法思想:
* 分治法思想:快速排序采用分治的策略,分治法是将原问题分解为若干个规模更小的但结构与原问题相似的子问题,
* 递归的解这些这些小问题,最后将子问题的解合并为原问题的解。
* 快速排序算法思想:设当前待排序的无序区为R[low...high]
* (1)分解:在R[low...high]中任选一个记录作为基准(Pivot),一次基准将当前无序区划分为左、右
* 两个较小的子区间R[low...Pivot-1]和P[Pivot+1...high],并使左边子区间中所有的记
* 录的关键字均小于或等于基准记录的关键字pivot.key,右边子区间的中所有的记录关键字均
* 大于或等于基准记录pivot.key,而基准记录pivot则位于正确的位置上(pivotPos),无需
* 参加后续的排序。划分的关键是要求出基准记录所在的位置pivotPos。划分可以简单的表示为
* R[low...pivotPos-1].keys <= R[pivotPos].key <= R[pivotPos+1...high].keys
* 其中必须满足low<pivotPos<high
* (2)求解:通过递归调用快速排序对左右子区间快速排序
* (3)组合:当求解的步骤结束时,左右两个子区间已经有序,对于快速排序而言,最后的“组合”步骤并不
* 需要做什么,可以看作是一个空操作。
* 算法复杂度:快速排序主要耗费在划分操作上,对长度为k的区间划分共需要k-1次比较
* 最坏情况:最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准
* 左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比
* 划分前的无序区中记录个数减少一个。时间复杂度为O(n^2)
* 最好情况:每次选取的基准位置都是无序区的中值,划分似的左右两个子区间长度大致相等,时间复杂度为O(nlgn)
* 平均时间复杂度为:O(nlgn)
* 稳定性:快速排序在进行交换时,是比较基数值判断是否交换,不是相邻元素来交换,在交换过程中可能改变相同元素的顺序,
* 因此是一种不稳定的排序算法。
* @param source
* @param low
* @param high
*/
public void quickSort(int[] source, int low, int high){
/*
* 排序过程,重点是基于基准位置的划分
* 参照程序一步一步来
*/
int i,j,pivot;
if (low < high){ //考虑如果数组是一个元素或者空数组,就已经有序了就不用进行排序了
i = low; //用i和j记录地位和高位的索引号,因为递归调用是还需要low和high他们的值指向不能改变
j = high;
pivot = source[i];//用pivot记录基准值,便于一次划分结束后恢复i或j位置的值。一般都是随机用区间的第一个
//元素的值作为基准值
while (i < j){ //从两端交替向中间扫描直至i=j,表示一次划分结束
while (i < j && source[j] >= pivot){//从后向前扫描,如果j位置的值比基准位置pivot的值大,则将j指
j--; //针继续向前移动,同时保证i < j
}
if (i < j){ //在j指针前移的时候,如果source[j]的值小于pivot,则需要将source[i]的值和source[j]的
source[i] = source[j]; //值交换
i++; //将i指针后移以为,下面开始从前向后(从低位向高位)扫描
} //此时基准位置移到了j上
while (i < j && source[i] <= pivot){
i++; //低位到高位扫描,如果source[i]<pivot,则指针后移。同时保证i<j
}
if (i < j){ //如果上面的循环是由于i=j而结束,这里不进入交换,说明一次划分已经结束了。
source[j] = source[i];//如果i<j,则将source[j]和source[i]的值交换,交换后
j--; //基准位置又移到了i上,将j指针前移一位,继续从后向前扫描
}
}//循环结束时,则一次划分结束,此时i = j
source[j] = pivot; //恢复source[i]=source[j]的值为最初的基准位置的值,并且他不参与下次的划分
quickSort(source, low, j-1);
quickSort(source, j+1, high);
}
}
/**
* 归并排序
* 算法思想:
* 归并算法是采用了分治的思想,主要分为两个步骤:“分解”-将序列每次折半划分;“归并”-将划分后的序列段两两合并后排序。
* @param source
* @param start
* @param end
*/
public void mergeSort(int[] source, int start, int end){
if(start < end){
int mid = (end + start)/2;
//两路归并,先进行划分,划分为一个一个的元素后,在执行归并
mergeSort(source, start, mid);
mergeSort(source, mid+1, end);
//归并
merge(source, start, mid, mid+1, end);
}
}
/**
* 序列的归并过程:在拆分的时候已经将序列划分为一个一个的元素了,再把这些元素两两归并到一起使之有序,不停的归并直至将所有的元素
* 都归并,并排好序。
* 例如:序列A:17 19 20 序列B:16 18 21
* (1)新建一个缓存序列C,为A和B大小的和;比较之后,将小的元素添加到缓存序列中去;然后小的元素序列的指向后移一位
* (2)A0和B0比较,A0>B0则C0=B0,B的指向后移一位为B1
* (3)A0和B1比较,A0<B1则C1=A0,A的指向后移一位为A1
* (4)A1和B1比较,A1>B1则C2=B1,B的指向后移一位为B2
* (5)A1和B2比较,A1<B2则C3=A1,A的指向后移一位为A2
* (6)A2和B2比较,A2<B2则C4=A2,如果有一个序列已经达到了尾部,则将另一个序列中剩下的元素全部拷贝到缓存序列中去
* @param array
* @param start1
* @param end1
* @param start2
* @param end2
*/
public void merge(int[] array, int start1, int end1, int start2, int end2){
int i, j; //定义为序列1和2的游标
{
i = start1; //将两个游标定位到两个序列的开始位置
j = start2;
}
int[] temp = new int[end2 - start1 + 1]; //定义一个缓存数组,大小是两个待归并数组的大小之和
int k = 0; //定义缓存数组的游标
while(i <= end1 && j <= end2){ //两个序列中的元素比较大小后将小的元素添加到缓存数组中
if (array[i] < array[j]){ //注意此处必须是小于等于,不然第一次归并时总是进入不了循环。导致后续归并无效
temp[k] = array[i]; //不能进行排序
k++;
i++;
}else {
temp[k] = array[j];
k++;
j++;
}
}
while (i <= end1){ //如果前一个序列还没有到达尾部,则将剩下的元素全部复制到缓存序列中
temp[k++] = array[i++]; //这里小于等于是为了保证两个序列长度相等时将最后一个元素较大的序列的最后一个元素复制进去
}
while (j <= end2){
temp[k++] = array[j++];
}
k = start1;
for (int element: temp){
array[k++] = element; //把归并好的序列复制给原序列
}
}
/**
* 二分查找:
* 算法思想:目前比较公认的二分排序就是折半插入排序,当直接插入排序进行到某一趟时,对于R[i].key,前面i-1个记录已经按关键字有序。
* 此时不再按照直接插入排序,而改为二分折半查找,找出R[i].key应该插入的位置,然后插入。
* 排序时是直接将前两个元素先折半排序,
* 算法复杂度:由于比较次数减半所以所以时间复杂度为O(nlogn),但是元素移动次数仍为O(n^2),故二分插入排序的时间复杂度为O(n^2)
* 稳定性:稳定
* @param source
*/
public void binaryInsertSort(int[] source){
int i, j;
int low, high, mid;
int temp;
for (i = 1; i < source.length; i++){
temp = source[i];
low = 0;
high = i - 1;
while (low <= high){
mid = (low + high) / 2;
if (source[mid] > temp){
high = mid - 1;
} else{
low = mid + 1;
}
}
for (j = i - 1; j > high; j--){
source[j+1] = source[j];
}
source[high + 1] = temp;
}
System.out.println("BinaryInsertSort: ");
}
/**
* 希尔排序
* 基本思想:先对数据分组,然后对每一组数据进行排序,在每一组数据都有序之后,就可以对所有的分组利用插入排序进行最后一次排序。
* 这样就减少了数据交换得次数
* 算法思想:先去一个小于n的整数d1作为第一个增量,把文件的全部记录分成di个组,所有距离为di的倍数的记录放在同一个组中。现在
* 各个组内进行直接插入排序;然后取第二个增量d2<d1,重复上述的分组和排序,直至所取得增量dt=1,即所有的记录放在同
* 一组中进行直接插入排序为止。
* 算法分析:
* (1)增量的选择:
* shell排序的执行时间依赖于增量序列。好的增量序列有如下特征:
* 最后一个增量必须是1;
* 应该尽量避免增量序列中的值互为倍数的情况
* (2)shell排序时间性能好于插入排序
* 当文件初态基本有序时,直接插入排序所需要的比较次数和移动次数均较少
* 当n值较少的时候,n和n^2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最差时间复杂度O(n^2)差别不大
* 当在希尔排序开始时,增量较大分组较多,每组的记录数少,故各组内直接插入较快,后来增量减小,分组减少,每组
* 记录数增多,但由于之前已经按其他增量排过序,文件较接近有序状态,所以最后的一次插入排序也会较快。希尔排序
* 在效率上对于直接插入排序有较大的改进。
* 稳定性:不稳定
* @param source
*/
public void shellSort(int[] source, int index){//index初始数组的长度
int i, j, k; //循环计数变量
int temp; //暂存变量
boolean change; //数据是否该改变
int dataLength; //分割集合的间隔长度
int pointer; //进行处理的位置
dataLength = (int) index / 2; //初始集合间隔长度
while (dataLength != 0){ //数列仍可分割
for (j = dataLength; j < index; j++){
change = false;
temp = source[j]; //暂存source[j]的值,在交换时使用
pointer = j - dataLength;//计算进行处理的位置
//进行集合内数值的比较与交换值
while (temp < source[pointer] && pointer >= 0 && pointer <= index){
source[pointer + dataLength] = source[pointer];
//计算下一个欲进行处理的位置
pointer = pointer - dataLength;
change = true;
if (pointer < 0 || pointer > index){
break;
}
}
//与最后的数值交换
source[pointer + dataLength] = temp;
if(change){
//打印目前排序结果
System.out.println("排序中: ");
for (k = 0; k < index; k++){
System.out.printf("%6s", source[k]);
}
System.out.println(" ");
}
}
dataLength = dataLength / 2;
}
}
}