排序算法
排序也称排序算法,排序是将一组数据,按照指定的顺序进行排列的过程
- 内部排序:将需要处理的所有数据都家长赛到内部存储器中进行排序
- 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
冒泡排序
基本思想
通过对待排序的序列从前往后,依次比较相邻的两个元素的值,如果发现逆序则交换,使的最大(降序则最小)的元素逐渐从前移到后部,像水底的气泡逐渐向上冒;每一轮排序结束,就保证了当前无序的序列中的最大的元素已经到了序列尾部
代码实现
/**
* 冒泡排序
* @author laowa
*
*/
public class BubbleSort {
public static void main(String args[]) {
int arr[]= {3,9,-1,10,-2};
bubbleSort(arr);
System.out.println("最终排序结果为");
print(arr);
}
/**
* 冒泡排序
* @param arr 待排序的数组
*/
private static void bubbleSort(int[] arr) {
//使用一个标志量来优化算法,当某趟冒泡结束后,没有发生数字的交换,说明当前任意两个相邻的数都是有序的,则该数组已有序
boolean isSorted = true;
//临时变量用户交换前后变量
int temp;
for(int i=0;i<arr.length-1;i++) {
for(int j=0;j<arr.length-1-i;j++) {
if(arr[j]>arr[j+1]) {
//当前后数据逆序,则进行交换
//标志量改为false,表示当前趟的冒泡发生了交换
isSorted=false;
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
//如果标志量没有被改变,即序列已有序,直接返回
if(isSorted) {
return;
}
isSorted=true;
System.out.printf("第%d轮排序的结果为\n",i+1);
print(arr);
}
}
/**
* 打印数组
* @param arr 待打印的数组
*/
private static void print(int []arr) {
for(int i:arr) {
System.out.print(i+" ");
}
System.out.println();
}
}
速度测试
代码中使用了循环嵌套,冒泡的时间复杂度为O(n2),在十万级的数据量下,冒泡排序耗时高达16秒
选择排序
基本思想
选择排序也属于内部排序,它的基本思想是:在arr[n]数组中,第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换;第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换。。。进行n-1次,得到一个有序序列
选择排序每一轮排序下来,会得到无序序列中的最小值,这点和冒泡类似,但是选择排序不会每一次比较之后都进行交换,而是一轮排序只进行一次交换,所以效率要高许多
代码实现
/***
* 选择排序
* @author laowa
*
*/
public class SelectSort {
public static void main(String[] args) {
int arr[]= {101,34,119,1};
selectSort(arr);
System.out.println("最终排序结果为");
print(arr);
timeTest();
}
/**
* 排序时间检查
*/
private static void timeTest() {
int count = 100000;
int arr[] = new int[count];
for(int i=0;i<count;i++) {
arr[i]=new Random().nextInt(count);
}
long before = System.currentTimeMillis();
selectSort(arr);
long after = System.currentTimeMillis();
System.out.printf("排序%d个数用时%d毫秒", count,after-before);
}
/**
* 选择排序
* @param arr 待排数组
*/
private static void selectSort(int[] arr) {
//存储当前轮找到的最小值
int min;
//当前轮找到的最小值的索引
int minIndex;
for(int i=0;i<arr.length-1;i++) {
//初始化最小值为当前轮的第一个元素
min = arr[i];
minIndex = i;
//从i+1开始,因为第i个已经取得了,依次找最小值
for(int j=i+1;j<arr.length;j++) {
//依次将无序列中的数与最小值比较,取最小值
if(min>arr[j]) {
min = arr[j];
minIndex = j;
}
}
//将最小值和当前轮的1个元素交换
arr[minIndex]=arr[i];
arr[i]=min;
}
}
/**
* 打印数组
* @param arr 待打印的数组
*/
private static void print(int []arr) {
for(int i:arr) {
System.out.print(i+" ");
}
System.out.println();
}
}
速度测试
代码使用了循环嵌套,时间复杂度为O(n2),但是选择排序每一轮排序只进行一次交换,省去了大量的交换时间,所以比冒泡排序用户要低很多,在十万级的数据下耗时约两秒
插入排序
基本思想
把n个待排序的元素看成一个有序表和一个无序表,开始时有序表只包含一个元素,无序表包含n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码一次与有序表中元素的排序码进行比较,将他插入到有序表中的适当位置,使之成为新的有序表
代码实现
/***
* 插入排序
* @author laowa
*
*/
public class InsertSort {
public static void main(String[] args) {
int []arr= {101,34,119,1,-1,89};
insertSort(arr);
System.out.println("插入排序之后的结果为");
print(arr);
}
/**
* 插入排序
* @param arr 待排数组
*/
private static void insertSort(int []arr) {
//当前需要插入的值
int insertVal;
//表示插值的位置
int insertIndex;
//最初以第一个元素作为有序序列,所以从i=1开始遍历
for(int i=1;i<arr.length;i++) {
//当前遍历到的值即需要插入的值
insertVal=arr[i];
//从当前值的前一个位置开始,往前遍历
insertIndex=i-1;
//for循环形式
//从i-1,即有序序列的最后一个元素开始往前遍历
for(insertIndex=i-1;insertIndex>=0;insertIndex--) {
//如果遍历到的元素比待插的值大,说明要插在这个元素前面,将这个元素向后移动
if(arr[insertIndex]>insertVal) {
arr[insertIndex+1]=arr[insertIndex];
}else {
//否则表示这个值要插的位置已经找到了,跳出循环,将待插值插在这个值的后面
break;
}
}
arr[insertIndex+1]=insertVal;
//while循环形式
//一直往前查找,直到找到第一个位置或者需要插入的值大于某个位置的值
while(insertIndex>=0&&insertVal<arr[insertIndex]) {
//将当前位置的值往后移动
arr[insertIndex+1]=arr[insertIndex];
//位置向前移动
insertIndex--;
}
//当while循环结束后,表示都找到了当前值需要插入的位置
//要么当前的insertIndex=-1跳出,那么将值插在0位置上
//要么当前arr[inserIndex]<=insertVal那么将这个值插在arr[insertIndex]的后面
arr[insertIndex+1]=insertVal;
}
}
/**
* 打印数组
* @param arr 待打印的数组
*/
private static void print(int []arr) {
for(int i:arr) {
System.out.print(i+" ");
}
System.out.println();
}
}
速度测试
插入排序也是双重循环,时间复杂度为O(n2),因为每次内层循环都会有多次赋值操作,相对于冒泡的交换要少,但相对于选择的一个循环一次要多,十万级的数量用户约四秒
希尔排序
插入排序的缺点分析
假设这样一个数组arr={2,3,4,5,6,1},这里经过4四轮排序之后轮到了1(最小),那么接下来这轮排序过程是:
- {2,3,4,5,6,6}
- {2,3,4,5,5,6}
- {2,3,4,4,5,6}
- {2,3,3,4,5,6}
- {2,2,3,4,5,6}
- {1,2,3,4,5,6}
因为当前要插入的数是最小的一个数,所以在有序列从后往前遍历的时候会将有序列所有元素都后移一次,影响效率(当需要插入的数是较小的数的时候,后移次数明显增多,对效率有影响)
希尔排序介绍
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也成为缩小增量排序
基本思想
将记录按照下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法终止
代码实现
/***
* 希尔排序
* @author laowa
*
*/
public class ShellSort {
public static void main(String[] args) {
int[] arr= {8,9,1,7,2,3,5,4,6,0};
shellSort(arr);
print(arr);
// timeTest();
}
/**
* 排序时间检查
*/
private static void timeTest() {
int count = 10000000;
int arr[] = new int[count];
for(int i=0;i<count;i++) {
arr[i]=new Random().nextInt(count);
}
long before = System.currentTimeMillis();
shellSort(arr);
long after = System.currentTimeMillis();
System.out.printf("排序%d个数用时%d毫秒", count,after-before);
}
/**
* 希尔排序
* @param arr 待排数组
*/
private static void shellSort(int []arr) {
int step=arr.length/2;//表示当前的增量步长,初始化为length/2
int temp;//临时变量,在交换法中,用于辅助两个值的交换;在移位法中,用于保存当前需要插入的值
//开始排序,直到步长小于1退出
while(step>=1) {
//从步长的位置开始,因为将第一位看作了有序序列,遍历每一组后面的无序序列
for(int i=step;i<arr.length;i++) {
//交换法,交换法中,待插的值会在有序序列中从后到前不断的于逆序的数交换,每次交换有三次赋值操作
//从有序序列的最后一位开始往前遍历,将无序序列的第一位插入到有序序列中;第一次循环时,arr[j+step]就是arr[i]即无序序列的第一位
for(int j=i-step;j>=0;j-=step) {
//当前位比后一位大,则两个数交换位置
//arr[i]位置待插入的数,会经过不断的交换,一直到前一个数比它小或序列第一
if(arr[j]>arr[j+step]) {
temp = arr[j+step];
arr[j+step]=arr[j];
arr[j]=temp;
}else {
//这里一定要加上break,表示如果当前的值比后面值小则不在向前交换,因为待插值已经确定好位置了,前面的序列都是有序的,不用遍历了
break;
}
}
// //移位法,移位法中,出现逆序只会将当前数往后移动一位,每次移位只有一次赋值操作
// int j=i-step;
// temp = arr[i];//将待插值存入临时变量中
// //从最后一位往前遍历,一直到当前位小于待插值或已经到了序列头部
// while(j>=0&&arr[j]>temp) {
// //将当前位往后移动一位
// arr[j+step]=arr[j];
// //当前位前移
// j-=step;
// }
// //循环结束后,当前位置就是待插值的前一位,将待插值插在后一位即可
// arr[j+step]=temp;
}
step=step/2;
}
}
/**
* 打印数组
* @param arr 待打印的数组
*/
private static void print(int []arr) {
for(int i:arr) {
System.out.print(i+" ");
}
System.out.println();
}
}
时间测试
希尔排序在十万级数据用时仅29毫秒,在千万级数据用时也只有3秒左右
快速排序
基本思想
通过一趟排序,将要排的数据分割成独立的两部分,其中一部分数据都比另外一部分的所有数据都小,然后再对每一部分按照相同的规则进行排序,直到每一部分元素的个数为1 ;即每一趟排序下来,指定数左边的部分都比该数小,右边的部分都比该数大
代码实现
/***
* 快速排序
* @author laowa
*
*/
public class QuickSort {
public static void main(String[] args) {
// int []arr= {-9,78,0,23,-567,70,70,70,70,70,70,70,780};
// quickSort(arr,0,arr.length-1);
// print(arr);
timeTest();
}
/**
* 排序时间检查
*/
private static void timeTest() {
int count = 10000000;
int arr[] = new int[count];
for(int i=0;i<count;i++) {
arr[i]=new Random().nextInt(count);
}
long before = System.currentTimeMillis();
quickSort(arr,0,arr.length-1);
long after = System.currentTimeMillis();
System.out.printf("排序%d个数用时%d毫秒", count,after-before);
}
/**
* 快速排序
* @param arr 待排序列
*/
private static void quickSort(int []arr,int left,int right) {
//创建变量l存储当前需要进行排序的序列最左
int l = left;
//创建变量r存储当前需要排序的序列最右
int r = right;
//临时变量,用于两个数进行交换
int temp;
//支点数,每一次递归都会根据这个支点元素,让小于它的在他左边,大于他的在他右边
int pivot = arr[(left+right)/2];
//当左边的指针超过了右边的指针,表示左边没有了比右边大的数
while(l<r) {
//从left开始找,一直到找到和支点相等或大于支点的数
while(arr[l]<pivot) {
l++;
}
//从right向左找,一直找到和支点相等或小于支点的数
while(arr[r]>pivot) {
r--;
}
//如果此时左边的指针已经和右边指针相同(同时指向了支点),或者左边的指针超过了右边,表示已经达到目的,直接break
if(l>=r) {
break;
}
//将l和r指向的元素进行交换,如果其中有一个是支点元素,则表示支点元素某一边存在了与支点逆序的数,将支点与该数交换
temp = arr[l];
arr[l]=arr[r];
arr[r]=temp;
//1.将当前满足条件的值省去可以省去同一值的重复比较
//2.出现重复的数据的时候,不进行移位会导致arr[l]==arr[r]==pivot造成死循环
//如果左指针指向的数已经满足条件,则指向下一位,(如果当前指向的值和支点相等,他和支点的前后顺序是无所谓的,所以也可以当作满足条件)
if(arr[l]<=pivot) {
l++;
}
//如果右指针指向的数已经满足条件,则指向下一位,(如果当前指向的值和支点相等,他和支点的前后顺序是无所谓的,所以也可以当作满足条件)
if(arr[r]>=pivot) {
r--;
}
}
//如果左右指针相等,则两者都往远处移动一位,放置下一次递归重复,造成栈溢出
if(l==r) {
l++;
r--;
}
//如果left>=r说明左递归已经到最左边了,则不用继续递归
if(left<r) {
quickSort(arr,left,r);
}
//如果right<=l说明右递归已经到最右边了,则不用继续递归
if(right>l) {
quickSort(arr,l,right);
}
}
/**
* 打印数组
* @param arr 待打印的数组
*/
private static void print(int []arr) {
for(int i:arr) {
System.out.print(i+" ");
}
System.out.println();
}
}
速度测试
千万级的数据在快速排序下只用到了不到2秒
归并排序
基本思想
归并排序采取了分治策略,先将序列分割成最小的子序列,然后子序列之间按照顺序进行合并,最后合并成有序序列
代码实现
/***
* 归并排序
* @author laowa
*
*/
public class MergeSort {
public static void main(String[] args) {
// int arr[] = { 8, 4, 5, 7, 1, 3, 6, 2 };
// int temp[] = new int[arr.length];
// mergeSort(arr,0,arr.length-1,temp);
// print(arr);
timeTest();
}
/**
* 排序时间检查
*/
private static void timeTest() {
int count = 10000000;
int arr[] = new int[count];
for(int i=0;i<count;i++) {
arr[i]=new Random().nextInt(count);
}
long before = System.currentTimeMillis();
mergeSort(arr,0,arr.length-1,new int[arr.length]);
long after = System.currentTimeMillis();
System.out.printf("排序%d个数用时%d毫秒", count,after-before);
}
/**
* 归并排序,将数组进行分解与合并
* @param arr 待排序数组
* @param left 待分解部分的最左边索引
* @param right 待分解部分的最右边索引
* @param temp 辅助合并的数组
*/
private static void mergeSort(int[] arr,int left,int right,int []temp) {
//当最左边小于最右边的时候进行分解与合并
//当递归到最左边或最右边只有一个元素时,不能在拆分了,此时left==right不会再进行分解与合并
if(left<right) {
//取当前待分解部分的中间,将它分开
int mid=(left+right)/2;
//取左边~中间向左递归分解
mergeSort(arr,left,mid,temp);
//取中间~右边向右递归分解
mergeSort(arr,mid+1,right,temp);
//将当前部分合并
//合并的执行是在递归分解的下面,所以合并会在所有递归分解结束后进行,最后只剩一个数的时候方法不会继续递归,然后从栈顶往栈底依次从2,4...个元素开始合并
merge(arr,left,right,mid,temp);
}
}
/**
* 合并,left~mid是分解后的左边部分,mid~right是分解后的右边部分,这里就是依次从左右两边按顺序取数放到temp中
*
* @param arr
* 待排数组
* @param left
* 左边有序序列的初始值,左边部分的第一个位置
* @param right
* 最右边索引
* @param mid
* 中间索引,左边部分最后一个位置
* @param temp
* 辅助合并数组
*/
private static void merge(int[] arr, int left, int right, int mid, int[] temp) {
//左边序列第一个数索引
int i = left;
//右边序列第一个数索引
int j = mid+1;
//辅助数组当前待插部分索引
int t = 0;
//当左边元素取完或者右边元素取完,表示比较结束,左边和右边的序列都是有序的,剩余部分直接放入temp中即可
while (i <= mid && j <= right) {
//如果左边更小,则将左边数插入temp中,左边索引后移
if (arr[i] < arr[j]) {
temp[t] = arr[i];
i++;
} else {
//否则将右边数插入temp中,右边索引后移
temp[t] = arr[j];
j++;
}
t++;
}
//将左边剩余元素插入temp中
while (i <= mid) {
temp[t] = arr[i];
i++;
t++;
}
//将右边剩余元素插入temp中
while (j <= right) {
temp[t] = arr[j];
j++;
t++;
}
//将当前合并部分复制到原始数组中,原始数组该部分九有序了
t = 0;//指向temp数组
int tempLeft = left;//指向原始数组left~right就是当前部分在原始数组中的片段
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
tempLeft++;
t++;
}
}
/**
* 打印数组
*
* @param arr
* 待打印的数组
*/
private static void print(int[] arr) {
for (int i : arr) {
System.out.print(i + " ");
}
System.out.println();
}
}
速度测试
归并排序在千万级数据量下,时间在两秒内
基数排序
基本介绍
- 基数排序属于“分配式排序”,又称“桶子法”,它是通过键值的各个位的值,将要排序的元素分配至某些桶中,达到排序的作用
- 基数排序属于稳定型的排序,且效率高,但耗费空间大
- 基数排序是桶排序的扩展
基本思想
将所有待比较数值统一位同样长度,数位较短的位置数字补0,然后从最低位开始,依次进行依次排序,从最低位到最高位排序完成后,就变成一个有序序列
代码实现
/***
* 基数排序
*
* @author laowa
*
*/
public class RadixSort {
public static void main(String[] args) {
// int arr[] = { 53, 3, 542, 748, 14, 214 };
// radixSort(arr);
// print(arr);
timeTest();
}
/**
* 排序时间检查
*/
private static void timeTest() {
int count = 10000000;
int arr[] = new int[count];
for(int i=0;i<count;i++) {
arr[i]=new Random().nextInt(count);
}
long before = System.currentTimeMillis();
radixSort(arr);
long after = System.currentTimeMillis();
System.out.printf("排序%d个数用时%d毫秒", count,after-before);
}
private static void radixSort(int arr[]) {
// 使用一个二维数组来模拟十个桶,二维数组的行号是每个桶的编号,二维数组的列是用来存储每一轮放入同种元素的一维数组
int[][] bucket = new int[10][arr.length];
//用一个一维数组记录当前轮次的排序,存入到每个桶中的数据数量
//这个一维数组的下标表示二维数组bucket的行
int[] bucketElementCount = new int[10];
//开始遍历位数
for (int i = 1; i <= maxLength(arr); i*=10) {
// 开始遍历数组
for (int j = 0; j < arr.length; j++) {
// 获得当前位的数字
int digitOfElement = arr[j]/i % 10;
//将当前数字放在对应的digitOfElement的桶中,根据bucketElementCount来获得当前应该放的位置
//因为bucketElementCount存的是目标桶存放的个数,所以bucketElementCount存的值就可以当作当前需要插入的下标来使用
//例如,bucket[0]中存了一个元素,那么bucketElementCount[0]=1;接下来下一个元素要存的位置也正是bucket[0][1]
bucket[digitOfElement][bucketElementCount[digitOfElement]] = arr[j];
//存放之后,将当前桶数据个数+1
bucketElementCount[digitOfElement]++;
}
//将桶中的数据复制到原数组中
int index=0;
//外层循环遍历存储每个同种数字个数的数组,将每个桶中的数据依次复制给原序列
for(int k=0;k<bucketElementCount.length;k++) {
//从0到当前轮次当前桶的数量个数,进行循环,将这一轮放入这个桶的数据复制到序列
for(int l=0;l<bucketElementCount[k];l++) {
arr[index]=bucket[k][l];
index++;
}
//复制结束后,将个数清零,为下一轮排序作准备
//桶中的数据是脏的,获取桶中的有效数据是通过bucketElementCount来进行的
bucketElementCount[k]=0;
}
}
}
/**
* 获取最长数字数量级
* @param arr
* @return
*/
private static double maxLength(int arr[]) {
int res = 0;
int current;
for (int i : arr) {
current = String.valueOf(i).length();
res = res > current ? res : current;
}
return Math.pow(10, res);
}
/**
* 打印数组
* @param arr 待打印的数组
*/
private static void print(int []arr) {
for(int i:arr) {
System.out.print(i+" ");
}
System.out.println();
}
}
速度测试
排序千万级数据用时约6秒,但是花费了极大量的内存
排序算法对比
- 稳定性:对于两个大小相同的数a,b如果排序前后两者相对位置不变,则表示稳定
- 内排序与外排序:所有操作都在内存中进行,为内排序;由于数据量过大,部分在内存中,部分在磁盘中进行
- 时间复杂度:一个算法执行所耗费的时间
- n:数据规模
- In-place:不占用额外内存
- Out-place:占用额外内存