这次学习下Java中几种经典的排序算法,参考十大经典排序算法最强总结(含JAVA代码实现),大致分为比较排序与非比较排序两种。
比较排序
在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
- 冒泡排序:重复地走访过要排序的数列,一次比较两个元素,并将这两个元素按顺序排列,不断重复地进行直到没有再需要交换即排序完成。
代码如下:
public class test {
public static void main(String[] args) {
int[] arr = {5,3,2,0,4,7,1,9,8};
int[] sorted = bubbleSort(arr);
System.out.print("result: ");
for(int i=0;i<sorted.length;i++) {
System.out.print(sorted[i]+" ");
}
}
public static int[] bubbleSort(int[] array) {
if(array.length == 0) {
return array;
}
for(int i=0;i<array.length;i++) {
for(int j=0;j<array.length-1-i;j++) {
if(array[j+1]<array[j]) {
int temp = array[j+1];
array[j+1] = array[j];
array[j] = temp;
}
}
for(int k=0;k<array.length;k++) {
System.out.print(array[k]+" ");
}
System.out.println();
}
return array;
}
}
运行结果如下:
上述程序中还有个小问题,就是缺少判断排序完成的标识,这个会导致算法最快的时间复杂度退化成O(n^2),可以设置一个判断交换标志:
...
public static int[] bubbleSort(int[] array) {
if(array.length == 0) {
return array;
}
for(int i=0;i<array.length;i++) {
boolean flag = true;
for(int j=0;j<array.length-1-i;j++) {
if(array[j+1]<array[j]) {
int temp = array[j+1];
array[j+1] = array[j];
array[j] = temp;
flag = false;
}
}
for(int k=0;k<array.length;k++) {
System.out.print(array[k]+" ");
}
System.out.println();
if(flag == true) {
break;
}
}
return array;
}
运行结果如下:
冒泡排序时间复杂度:最佳情况:T(n) = O(n);最差情况:T(n) = O(n^2
);平均情况:T(n) = O(n^2
);空间复杂度为S(n) = O(1)
- 选择排序:无论什么数据进去都是O(n^2)的时间复杂度。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
代码如下:
public class test {
public static void main(String[] args) {
int[] arr = {5,3,2,0,4,7,1,9,8};
int[] sorted = selectionSort(arr);
System.out.print("result: ");
for(int i=0;i<sorted.length;i++) {
System.out.print(sorted[i]+" ");
}
}
public static int[] selectionSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i+1; j < array.length; j++) {
if (array[j] < array[minIndex])
minIndex = j;
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
for(int k=0;k<array.length;k++) {
System.out.print(array[k]+" ");
}
System.out.println();
}
return array;
}
}
运行结果如下:
选择排序不稳定,时间复杂度最佳情况:T(n) = O(n^2
);最差情况:T(n) = O(n^2
);平均情况:T(n) = O(n^2
);空间复杂度为S(n) = O(1)
- 快速排序:在待排序的数组选取一个元素作为基准,将待排序的元素进行分区,比基准元素大的元素放在一边,比其小的放另一边,递归调用快速排序对两边的元素排序。选取基准元素并分区的过程采用双指针左右交换。。
代码如下:
public class test {
public static void main(String[] args) {
int[] arr = {5,3,2,0,4,7,1,9,8};
quickSort(arr);
System.out.print("result: ");
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
public static void quickSort(int[] arr){
quickSort(arr,0,arr.length-1);
}
private static void quickSort(int[] arr, int low, int high) {
if(low>=high) {
return;
}
int index = partition(arr,low,high);//将数组分为两部分
quickSort(arr,low,index-1); //递归排序左子数组
quickSort(arr,index+1,high);//递归排序右子数组
}
private static int partition(int[] arr, int low, int high) {
int index = arr[low];//指定基准为该次partition数组的第一个数字
while(low<high) {
while(low<high && arr[high]>=index) {
high--;
}
arr[low] = arr[high];//交换比基准大的记录到左端
while(low<high && arr[low]<=index) {
low++;
}
arr[high] = arr[low];//交换比基准小的记录到右端
}
arr[low] = index;//基准到位,此时基准左边均小于它,右边均大于它
return low;//返回基准的位置
}
}
执行效果如下:
快速排序不稳定,时间复杂度最佳情况下:T(n) = O(nlogn);最差情况(序列都已经有序或完全倒序):T(n) = O(n^2
) 此时选择排序就退化成了冒泡排序;平均情况:T(n) = O(nlogn),空间复杂度S(n) = O(logn)。
- 归并排序:分解待排序的数组成两个各具 n/2 个元素的子数组,递归调用归并排序两个子数组,合并两个已排序的子数组成一个已排序的数组。此外在归并时需要开辟一个临时数组来辅助归并操作,因此需要额外的内存空间。
代码如下:
public class test {
public static void main(String[] args) {
int[] arr = {5,3,2,0,4,7,1,9,8};
mergeSort(arr);
System.out.print("result: ");
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
private static void mergeSort(int[] arr) {
int[] temp = new int[arr.length];//开辟一个临时数组来辅助归并操作
internalMergeSort(arr,temp,0,arr.length-1);
}
private static void internalMergeSort(int[] arr, int[] temp, int left, int right) {
if(left<right) {
int mid = (left+right)/2;
internalMergeSort(arr,temp,left,mid);
internalMergeSort(arr,temp,mid+1,right);
mergeSortedArray(arr,temp,left,mid,right);
}
}
private static void mergeSortedArray(int[] arr, int[] temp, int left, int mid, int right) {
int i = left;
int j = mid+1;
int k = 0;
while(i<=mid&&j<=right) {
temp[k++] = arr[i]<arr[j]?arr[i++]:arr[j++];
}
while (i <= mid) {
temp[k++] = arr[i++];//左边数组还有剩余
}
while (j <= right) {
temp[k++] = arr[j++];//右边数组还有剩余
}
for (i = 0; i < k; i++) {
arr[left + i] = temp[i];// 把temp数据复制回原数组
}
}
}
运行结果为:
这里再看下归并的过程,其实就是开辟了一个新临时数组来辅助归并操作,采用双索引法,例如下图所示,将左边数组与右边数组归并,先同时指向第一个数并将两数中小的一方赋值到临时数组的第一个(这里是右边数组数字小):
接着右边数组索引++,指向4,比较2和4:
2比4小,那么2赋值到临时数组的下一个位置,左边数组索引++:
这就是大致思路,由于使用了额外的空间,因此空间复杂度较高,S(n)=O(n);时间复杂度最佳情况:T(n) = O(nlogn) ;最差情况:T(n) = O(nlogn);平均情况:T(n) = O(nlogn)。
线性排序(非比较排序)
非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
- 计数排序:根据待排序的数组中最大和最小的元素,统计数组中每个值为i的元素出现的次数,存入数组C的第i项,对所有的计数累加,然后反向填充目标数组。
public class test {
public static void main(String[] args) {
int[] arr = {5,3,2,0,4,7,1,9,8};
countSort(arr);
System.out.print("result: ");
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
public static void countSort(int[] arr) {
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
int[] b = new int[arr.length]; // 存储数组
int[] count = new int[max - min + 1]; // 计数数组
for (int num = min; num <= max; num++) {
// 初始化各元素值为0,数组下标从0开始因此减min
count[num - min] = 0;
}
for (int i = 0; i < arr.length; i++) {
int num = arr[i];
count[num - min]++; // 每出现一个值,计数数组对应元素的值+1
// 此时count[i]表示数值等于i的元素的个数
}
for (int i = min + 1; i <= max; i++) {
count[i - min] += count[i - min - 1];
// 此时count[i]表示数值<=i的元素的个数
}
for (int i = 0; i < arr.length; i++) {
int num = arr[i]; // 原数组第i位的值
int index = count[num - min] - 1; //加总数组中对应元素的下标
b[index] = num; // 将该值存入存储数组对应下标中
count[num - min]--; // 加总数组中,该值的总和减少1。
}
// 将存储数组的值替换给原数组
for(int i=0; i < arr.length;i++){
arr[i] = b[i];
}
}
}
执行结果为:
这里借鉴一文弄懂计数排序算法!中的图来解释上述代码,比如一组数据{101,109,108,102,110,107,103}:
首先找出数组中的最大值max、最小值min,并创建一个新数组count,其长度是max-min加1,其元素默认值都为0;接着遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。
......
接着对count数组变形,新元素的值是前面元素累加之和的值:
接着创建结果数组result,长度和原始数组一样,并遍历原始数组中的元素,当前元素A[j]减去最小值min,作为索引,在计数数组中找到对应的元素值count[A[j]-min],再将count[A[j]-min]的值减去1,就是A[j]在结果数组result中的位置,做完上述这些操作,count[A[j]-min]自减1:
...
计数排序时间复杂度最佳情况:T(n) = O(n+k) ;最差情况:T(n) = O(n+k) ;平均情况:T(n) = O(n+k),空间复杂度S(n) = O(k)
- 桶排序:计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中(数组ArrayList作为桶),然后找出待排序数组中的最大值max、最小值min,计算每个元素 arr[i] 放的桶,每个桶各自排序,遍历桶数组,把排序好的元素放进输出数组。
代码如下:
import java.util.ArrayList;
import java.util.Collections;
public class test {
public static void main(String[] args) {
int[] arr = {5,3,2,0,4,7,1,9,8};
bucketSort(arr);
System.out.print("result: ");
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
public static void bucketSort(int[] arr){
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
}
}
执行结果如下:
这里借鉴【排序】图解桶排序中的图解释上述代码:
桶排序时间复杂度最佳情况:T(n) = O(n+k) ;最差情况:T(n) = O(n+k) ; 平均情况:T(n) = O(n^2
),空间复杂度为O(n+k)。