1. 准备工作
为方便排序算法工作,首先编写了一段代码,用于随机生成一个指定大小的数组。具体代码如下:
import java.util.Random;
public class BuildArray {
/**
* 调用此方法以得到一个随机的整型数组
*
* @param N 随机数组的大小
* @return 返回指定大小的随机数组
*/
public static int[] getArray(int N) {
if(N < 1) {
return null;
}
int[] array = new int[N];
int bound = 10 * N; // 随机数组元素大小的上界
for (int i = 0; i < array.length; i++) {
array[i] = new Random().nextInt(bound);
}
return array;
}
/**
* 调用此方法以打印整型数组array
*
* @param array 需要打印的数组
*/
public static void printArray(int[] array) {
System.out.print("[");
for (int i : array) {
System.out.print(i + " ");
}
System.out.println("]");
}
}
2. 排序算法
为方便描述,以下的排序算法均按照从小到大排序。
2.1 冒泡排序
算法描述:
冒泡排序需要逐个比较相邻的未排序的元素,如果顺序错误,则交换它们的位置。每次遍历可以得到一个已排序的元素,并将它放置在未排序数组末端,未排序数组元素减一,当不存在未排序元素时,排序结束,具体操作如下。
1.从头开始比较相邻的元素,如果前着大于后者,则交换它们的位置。
2.依次向后对每一组相邻的元素执行步骤1,执行结束后,最大的元素应该在数组的末尾,将其归为“已排序”元素。
3.针对剩下的未排序的元素,重复执行上述1~2的步骤,直至所有元素排序完毕。
代码实现:
public class BubbleSort {
/**
* 冒泡排序算法
*
* @param array 待排序数组
*/
public static void bubbleSort(int[] array) {
if (null == array) {
return;
}
int size = array.length;
for (int i = 0; i < size-1; i++) { // 重复以完成所有元素的排序
for (int j = 0; j < size-i-1; j++) { // 遍历所有未排序的元素对
if(array[j] > array[j+1]) { // 如果前面的元素大于后面的元素,就交换它们
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] array = BuildArray.getArray(15);
System.out.print("排序前数组:");
BuildArray.printArray(array);
bubbleSort(array);
System.out.print("排序后数组:");
BuildArray.printArray(array);
}
}
执行结果:
排序前数组:[81 8 102 77 48 17 78 92 84 33 59 71 1 13 42 ]
排序后数组:[1 8 13 17 33 42 48 59 71 77 78 81 84 92 102 ]
2.2 选择排序
算法描述:
选择排序的方法比较直观,只要遍历一次未排序的数组,就能找出其中的最小值,未排序元素-1。重复此操作,就可以完成对数组的排序,具体操作如下。
1.遍历数组,找出其中的最小值,并将其与首元素交换位置,至此未排序元素-1。
2.对未排序的数组重复操作1,直至所有元素排序完毕。
代码实现:
public class SelectionSort {
/**
* 选择排序算法
*
* @param array 待排序数组
*/
public static void selectionSort(int[] array) {
if (null == array) {
return;
}
int size = array.length;
for (int i = 0; i < size-1; i++) { // 重复以完成所有元素的排序
int min = i; // 最小元素的下标
for (int j = i+1; j < size; j++) { // 遍历以找到最大的元素
if (array[min] > array[j]) {
min = j;
}
}
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
}
public static void main(String[] args) {
int[] array = BuildArray.getArray(15);
System.out.print("排序前数组:");
BuildArray.printArray(array);
selectionSort(array);
System.out.print("排序后数组:");
BuildArray.printArray(array);
}
}
执行结果:
排序前数组:[68 63 9 3 121 51 100 39 55 92 141 136 106 66 123 ]
排序后数组:[3 9 39 51 55 63 66 68 92 100 106 121 123 136 141 ]
2.3 插入排序
算法描述:
2.1和2.2中的两种方法都是基于未排序数组的比较进行排序的,而插入算法与它们略有不同。插入算法首先会选择一个未排序的元素,通过将其与已排序的元素对比,找到合适的位置将其插入,并重复此操作以完成排序,具体操作如下。
1.首先认为数组的首元素是已排序元素;
2.向后选择第一个未排序的元素(在这是指第二个元素);
3.记住此元素(记为A)的值
4.让其与前面的元素(记为B)进行比较,如果A较小,则将B后移一位,然后A继续向前比较;
5.直到比较结果为A较大,则已经找到合适的位置,比较结束,将A插入。
6.依此向后选择第一个未排序的元素,执行步骤3~5,直至所有元素排序完毕。
代码实现:
public class InsertionSort {
/**
* 插入排序算法
*
* @param array 待排序数组
*/
public static void insertionSort(int[] array) {
if (null == array) {
return;
}
int size = array.length;
for (int i = 1; i < size; i++) { // 重复以完成所有元素的排序
int temp = array[i];
int j = i;
while (j > 0 && temp < array[j-1]) { // 如果小于前面的元素,就将前面的元素后移一位
array[j] = array[j-1];
j--;
}
array[j] = temp; // 正确的位置插入元素
}
}
public static void main(String[] args) {
int[] array = BuildArray.getArray(15);
System.out.print("排序前数组:");
BuildArray.printArray(array);
insertionSort(array);
System.out.print("排序后数组:");
BuildArray.printArray(array);
}
}
执行结果:
排序前数组:[95 24 3 118 2 62 81 110 51 146 82 2 25 63 108 ]
排序后数组:[2 2 3 24 25 51 62 63 81 82 95 108 110 118 146 ]
2.4 希尔排序
算法描述:
希尔排序是简单插入排序的改进算法,该算法是冲破二次时间屏障的第一批算法之一,它通过比较相距一定间隔的元素来工作,因此也被称作缩小增量排序。希尔排序算法的核心在于增量序列的选取,增量序列里的元素依次代表了每轮比较的数据之间的间隔,这里选择以gap = gap/2的方式(即增量序列为{n, n/2, n/4, …, 1})实现希尔排序,具体操作如下。
1.设定初始增量gap = array.length / 2;
2.以增量gap对整个数组进行插入排序;
3.修改增量gap = gap/2;
4.重复操作2~3,直至gap = 0;
代码实现:
public class ShellSort {
/**
* 希尔排序算法
*
* @param array 待排序数组
*/
public static void shellSort(int[] array) {
if (null == array) {
return;
}
int size = array.length;
for (int gap = size/2; gap > 0; gap /= 2) { // 构建增量序列
for (int i = gap; i < size; i++) { // 以增量gap对整个数组进行插入排序
int temp = array[i];
int j = i;
while (j >= gap && temp < array[j-gap]) {
array[j] = array[j-gap];
j -= gap;
}
array[j] = temp;
}
}
}
public static void main(String[] args) {
int[] array = BuildArray.getArray(15);
System.out.print("排序前数组:");
BuildArray.printArray(array);
shellSort(array);
System.out.print("排序后数组:");
BuildArray.printArray(array);
}
}
执行结果:
排序前数组:[121 141 81 141 46 56 142 105 68 145 72 81 48 94 13 ]
排序后数组:[13 46 48 56 68 72 81 81 94 105 121 141 141 142 145 ]
2.5 归并排序
算法描述:
在谈归并排序之前,先来了解一个概念——分治策略:为了解决一个规模较大的问题,我们可以将其分解为两个子问题,并借助递归分别得到它们的解,然后将子问题的解合并成原问题的解。
归并排序就采用了分治策略,它首先将需要排序的序列分割成两个子序列,并对它们分别进行归并排序,最后将得到的有序子序列进行合并,得到最终已排序的序列,具体操作如下。
1.把长度为n的原序列分成两个长度为n/2的子序列。
2.对两个子序列进行归并排序,得到已排序的两个子序列。
3.合并已排序的子序列。
代码实现:
import java.util.Arrays;
public class MergeSort {
/**
* 归并排序算法
*
* @param array 待排序数组
* @return 排序后的数组
*/
public static int[] mergeSort(int[] array) {
if (null == array) {
return null;
}
int size = array.length;
if (1 == size) {
return array;
}
// 分割数组
int[] left = Arrays.copyOfRange(array, 0, size/2);
int[] right = Arrays.copyOfRange(array, size/2, size);
// 递归调用归并排序算法
left = mergeSort(left);
right = mergeSort(right);
return merge(left, right); // 合并已排序的子数组
}
/**
* 合并已排序的两个子序列
*
* @param left 左子序列
* @param right 右子序列
* @return 合并后的序列
*/
private static int[] merge(int[] left, int[] right) {
int leftIndex = 0;
int rightIndex = 0;
int leftBound = left.length;
int rightBound = right.length;
int[] mergedArray = new int[leftBound+rightBound];
for (int i = 0; i < mergedArray.length; i++) {
if (leftIndex == leftBound) { // 左子数组的元素已经合并完毕
while (rightIndex < rightBound) {
mergedArray[i++] = right[rightIndex++];
}
break;
}
if (rightIndex == rightBound) { // 右子数组的元素已经合并完毕
while (leftIndex < leftBound) {
mergedArray[i++] = left[leftIndex++];
}
break;
}
if (left[leftIndex] < right[rightIndex]) { // 至此,左右子数组的元素均未合并完毕
mergedArray[i] = left[leftIndex++];
} else {
mergedArray[i] = right[rightIndex++];
}
}
return mergedArray;
}
public static void main(String[] args) {
int[] originalArray = BuildArray.getArray(15);
System.out.print("排序前数组:");
BuildArray.printArray(originalArray);
int[] sortedArray = mergeSort(originalArray);
System.out.print("排序后数组:");
BuildArray.printArray(sortedArray);
}
}
执行结果:
排序前数组:[96 122 108 92 28 26 51 91 57 57 80 73 124 119 66 ]
排序后数组:[26 28 51 57 57 66 73 80 91 92 96 108 119 122 124 ]
2.6 快速排序
算法描述:
快速排序是分治策略的又一典型应用,与归并排序不同的是,归并排序算法的主要计算量集中于有序子序列的归并,而快速排序算法正好相反,它的主要计算量集中于将原问题划分为两个子问题,具体操作如下。
1.选取数组的某一位置作为轴点。
2.调整数组,以使轴点左边的元素均小于轴点的元素,轴点右边的元素均大于轴点的元素。
3.递归的对轴点左右的数组进行快速排序。
轴点的选取对快速排序算法性能的影响较大,如果只是单纯的选择数组的第一个元素作为轴点,再输入非随机的情况下,可能很难获得较好的性能。轴点最好的选择是数组中值所在的位置,但不幸的是,这很难算出,且会拉低整个排序算法的性能。基于此,我们一般会选取左端、右端、中心位置的元素,以它们的中值所在的位置作为轴点。在代码实现中,构造带有指定轴点的数组是关键,其具体操作如下。
1.选取轴点,将其放置在最右端;
2.设最左端下标为left,最右端为right;
3.令i=left,j=right-1;
4.如果array[1]大于轴点处的元素,交换i和j处的元素,j–;
5.如果array[1]小于轴点处的元素,i++;
6.重复操作5~6,直至i>j(同样的,j也可以按类似的规则向前扫描);
7.交换i和right处的元素。
代码实现:
public class QuickSort {
/**
* 快速排序算法
*
* @param array 待排序数组
* @param left 待排序元素的左边界
* @param right 待排序元素的右边界
*/
public static void quickSort(int[] array, int left, int right) {
if (null == array || left >= right) {
return;
}
median(array, left, right); // 至此,轴点元素已经被放置在末尾,且array[0]小于等于轴点的元素。
int i = left + 1;
int j = right -1;
while (i <= j) {
if (array[i] > array[right]) {
swap(array, i, j);
j--;
} else {
i++;
}
}
swap(array, i, right); // 至此,数组被轴点分成左右两部分
quickSort(array, left, i-1);
quickSort(array, i+1, right);
}
/**
* 选取数组轴点的元素,并将其放置在数组的末端
*
* @param array 待排序数组
* @param left 左边界
* @param right 右边界
*/
private static void median(int[] array, int left, int right) {
int center = (left + right) / 2;
if (array[center] < array[left]) {
swap(array, center, left);
} // 至此,array[left] <= array[center]
if (array[left] > array[right]) {
swap(array, right, left);
} // 至此,array[right]为三者的中值,且array[left]小于等于此中值
}
/**
* 给定指定数组array和下标index0、index1, 交换给定下标处的数组元素
*
* @param array 待交换的数组
* @param index0 数组下标1
* @param index1 数组下标2
*/
private static void swap(int[] array, int index0, int index1) {
int temp = array[index0];
array[index0] = array[index1];
array[index1] = temp;
}
public static void main(String[] args) {
int[] array = BuildArray.getArray(15);
System.out.print("排序前数组:");
BuildArray.printArray(array);
quickSort(array, 0, array.length-1);
System.out.print("排序后数组:");
BuildArray.printArray(array);
}
}
执行结果:
排序前数组:[44 139 15 12 4 146 37 9 27 51 53 27 100 5 140 ]
排序后数组:[4 5 9 12 15 27 27 37 44 51 53 100 139 140 146 ]
3. 复杂度分析
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 |
---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) |
选择排序 | O(n2) | O(n2) | O(1) |
插入排序 | O(n2) | O(n2) | O(1) |
希尔排序 | 因增量序列不同而有所不同 | 因增量序列不同而有所不同(如Hibbard增量为O(n3/2)) | O(1) |
归并排序 | O(n*long2n) | O(n*long2n) | O(n) |
快速排序 | O(n*long2n) | O(n2) | O(long2n) |