准备
面试中经常出现的排序算法,先放一个总表,便于查询
待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。
研究排序算法的成本模型时,计算的是比较和交换的次数。
使用辅助函数 less() 和 swap() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。
public abstract class Sort <T extends Comparable<T>>{
public abstract void sort(T[] nums);
protected boolean less(T a, T b)
{
return a.compareTo(b)<0;
}
protected void swap(T[] nums, int i, int j)
{
T temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
选择排序(不稳定的排序)
选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。
选择排序需要 ~N2/2 次比较和 ~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。
public class SelectionSort<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
for(int i = 0;i<nums.length;i++)
{
int min = i;//注意这边记录的是下标
for(int j = i+1;j<nums.length;j++)
{
if(less(nums[j],nums[min]))
min = j;
}
swap(nums,min,i);
}
}
}
冒泡排序(稳定的算法)
从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。
在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。
以下演示了在一轮循环中,将最大的元素 5 上浮到最右侧。
public class BubbleSort<T extends Comparable<T>> extends Sort<T>{
@Override
public void sort(T[] nums) {
boolean isSorted = false;
for (int i = nums.length-1;i>0&&!isSorted;i--)
{
isSorted = true;//这边加了一个判断,避免重复计算
for(int j = 0; j<i;j++)
{
if(less(nums[j+1],nums[j]))
{
swap(nums,j+1,j);
isSorted = false;
}
}
}
}
}
插入排序(稳定的算法)
PS:笔试中非常常见的一种排序
每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左侧数组依然有序。
对于数组 {3, 5, 2, 4, 1},它具有以下逆序:(3, 2), (3, 1), (5, 2), (5, 4), (5, 1), (2, 1), (4, 1),插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。
插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,逆序较少,那么插入排序会很快。
- 平均情况下插入排序需要 ~N2/4 比较以及 ~N2/4 次交换;
- 最坏的情况下需要 ~N2/2 比较以及 ~N2/2 次交换,最坏的情况是数组是倒序的;
- 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。
以下演示了在一轮循环中,将元素 2 插入到左侧已经排序的数组中。
PS:看图还是很清晰的,记住是两两交换的,所以很清晰
public class InsertionSort<T extends Comparable<T>> extends Sort<T>{
@Override
public void sort(T[] nums) {
int n = nums.length;
for (int i=1;i<n;i++)
{
for(int j = i;j>0&&less(nums[j],nums[j-1]);j--)
swap(nums,j,j-1);
}
}
}
希尔排序(不稳定的排序)
对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。
希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。
希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。
public class Shell<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort(T[] nums) {
int N = nums.length;
int h = 1;
while (h < N / 3) {
h = 3 * h + 1; // 1, 4, 13, 40, ...
}
while (h >= 1) {
for (int i = h; i < N; i++) {
for (int j = i; j >= h && less(nums[j], nums[j - h]); j -= h) {
swap(nums, j, j - h);
}
}
h = h / 3;
}
}
归并排序(稳定的排序)
归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。
public abstract class MergeSort<T extends Comparable<T>> extends Sort<T> {
protected T[] aux;
protected void merge(T[] nums, int l, int m, int h) {
int i = l, j = m + 1;
for (int k = l; k <= h; k++) {
aux[k] = nums[k]; // 将数据复制到辅助数组
}
for (int k = l; k <= h; k++) {
if (i > m) {
nums[k] = aux[j++];
} else if (j > h) {
nums[k] = aux[i++];
} else if (aux[i].compareTo(nums[j]) <= 0) {
nums[k] = aux[i++]; // 先进行这一步,保证稳定性
} else {
nums[k] = aux[j++];
}
}
}
}
自顶向下
将一个大数组分成两个小数组去求解。
因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlogN),因此该归并排序方法的时间复杂度也为 O(NlogN)。
public class Up2DownMergeSort<T extends Comparable<T>> extends MergeSort<T> {
@Override
public void sort(T[] nums) {
aux = (T[]) new Comparable[nums.length];
sort(nums, 0, nums.length - 1);
}
private void sort(T[] nums, int l, int h) {
if (h <= l) {
return;
}
int mid = l + (h - l) / 2;
sort(nums, l, mid);
sort(nums, mid + 1, h);
merge(nums, l, mid, h);
}
}
自底向上归并排序
先归并那些微型数组,然后成对归并得到的微型数组。
public class Down2UpMergeSort<T extends Comparable<T>> extends MergeSort<T> {
@Override
public void sort(T[] nums) {
int N = nums.length;
aux = (T[]) new Comparable[N];
for (int sz = 1; sz < N; sz += sz) {
for (int lo = 0; lo < N - sz; lo += sz + sz) {
merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}
}