写在前面
如果说各种编程语言是程序员的招式,那么数据结构和算法就相当于程序员的内功。
想写出精炼、优秀的代码,不通过不断的锤炼,是很难做到的。
八大排序算法
排序算法作为数据结构的重要部分,系统地学习一下是很有必要的。
1、排序的概念
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。
排序分为内部排序和外部排序。
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。
反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
2、排序分类
八大排序算法均属于内部排序。如果按照策略来分类,大致可分为:交换排序、插入排序、选择排序、归并排序和基数排序。如下图所示:
以上内容摘抄自:https://cuijiahua.com/blog/2018/01/alogrithm_9.html
还有一些性能分析之类,就不贴图,这些都不是今天的重点,今天的重点是
归
并
排
序
\color{red}{归并排序}
归并排序
归并排序
之前也在b站上看过韩顺平老师讲的视频《图解数据结构与算法》,可能由于本人愚笨,其中的归并排序,看了几遍都没看懂(其它的很多也没看懂),无奈放弃了。但是再次《算法》第4版中,看到归并排序的时候,豁然贯通了。
归并排序是算法中分治思想的一种体现,将一个问题分解一个个小的问题,将这些小的问题解决,将小问题的答案整理合并,得到最后这个问题的答案。当然,这些小的问题可能还可以再分。
其实我觉得归并排序的理解重点,不是排序,而是归并。如果只看代码,连排序的逻辑都看不到,那归并排序到底是怎么排序的呢?让我们先来看归并。
两数组归并
什么是归并?归并是将两个有序的数组,合并成一个新的有序数组,注意注意:前提是两个数组已经是有序的,才可以进行归并,否则即使归并了也没什么意义,因为归并之后的数组可能是无序的。
思考一下:如果是自己实现,如何将两个有序的数组合并成一个有序的数组呢?
思路
先创建一个数组,用于存储合并后的元素。
遍历这个新数组,遍历的时候,用两个指针,分别指向两个需要合并的小数组的头,每次都比较这个两个数,将小的放进目标数组,放了之后就将相应的指针向前移动一位。
当然,在比较之前,需要先判断指针是否越界,如果某个数组的指针已经越界了,那么直接将另一个数组里的所有数都添加进目标数组里。这样,遍历完成之后,新得到的数组一定是两个数组合并而成,并且有序的。
代码
public static int[] merge(int[] a, int[] b)
{
int len1 = a.length,len2 = b.length;
int [] target = new int[len1+len2];
int i = 0, j = 0;
for (int k = 0; k < len1+len2; k++)
{
//如果a中的元素已经取完了,那么就直接将b中的所有元素加到目标集合中
if (i >= len1) target[k] = b[j++];
//如果b中的元素已经取完了,那么就直接将a中的元素加到目标集合中,
else if (j >= len2) target[k] = a[i++];
//否则就比较两个数字,将小的那个添加进目标集合
else if(a[i] < b[j]) target[k] = a[i++];
else target[k] = b[j++];
}
return target;
}
没什么难点,主要是理解遍历那中所做的事情。
原地归并
理解了上面的两个数组归并之后,我们就可以继续思考了,可否对同一个数组进行归并呢?因为我们最终是要对一个数组排序的。
其实是可以的,我们可以将一个数组从中间“分开”,把左右两边各当成一个数组,对左右两边进行归并。
代码
public static void merge(int[] a)
{
int len = a.length,mid = len/2;
int[] temp = new int[len];
//将原数组复制到目标数组里
for (int m = 0; m < len; m++)
{ temp[m] = a[m]; }
int i = 0,j = mid;
//进行原地归并
for (int k = 0; k < len; k++)
{
if (i >= mid) a[k] = temp[j++];
else if(j >= len) a[k] = temp[i++];
else if (temp[i] < temp[j]) a[k] = temp[i++];
else a[k] = temp[j++];
}
}
这样做了,就可以对一个数组本身进行归并,但是有个问题:需要左右两边都是有序的才可以,那岂不是很鸡肋?
实现排序
仔细思考一下:如果一个数组只有两个,那么不就可以排序了?我们可以将一个数组看个两个数组,再将两个小数组再看成两个小的数组,直到这个小数组只有两个数的时候,就可以排序了,然后在将小数组归并,再一步一步的归并,最后就是有序的了。
所以,我们可以使用递归,直到一个数组只有一个元素时,再往回归并,最后就实现了排序。
代码
public class IntArrayMerge
{
private static int[] temp;
public static void sort(int[] a)
{
temp = new int[a.length];
sort(a,0,a.length-1);
}
//用于递归调用的排序方法
private static void sort(int[] a, int lo, int hi)
{
if (lo >= hi) return;
int mid = lo+(hi-lo)/2;
sort(a,lo,mid);
sort(a,mid+1,hi);
merge(a,lo,mid,hi);
}
//原地归并的方法
private static void merge(int[] a, int lo, int mid, int hi)
{
for (int k = lo; k <= hi; k++)
{ temp[k] = a[k]; }
int i = lo,j = mid+1;
for (int k = lo; k<= hi; k++)
{
if (i > mid) a[k] = temp[j++];
else if (j > hi) a[k] = temp[i++];
else if (temp[i] > temp[j]) a[k] = temp[j++];
else a[k] = temp[i++];
}
}
}
这是相当于是一个工具类,整合成一个方法,应该也能实现,只是略显臃肿,并且不好理解。
自我提升
上面的工具类,只能实现对数组进行排序,因为参数只能传入int类型的数组,但其实字符也是能排序的,那应该怎么实现了?
可以自己先思考一下,提示:可以往Comparable接口方向思考。
代码
public class MyMerge
{
//临时容器
private static Comparable[] temp;
//对外暴露的排序方法
public static void sort(Comparable[] a)
{
temp = new Comparable[a.length];
sort(a,0,a.length-1);
}
//内部使用,用于递归的排序方法
private static void sort(Comparable[] a, int lo, int hi)
{
if (lo >= hi) return;
int mid = lo+(hi-lo)/2;
sort(a,lo,mid);
sort(a,mid+1,hi);
merge(a,lo,mid,hi);
}
//原地归并的方法
private static void merge(Comparable[] a, int lo, int mid, int hi)
{
//将原数组复制到临时数组中
int i = lo,j = mid+1;
for (int k = lo; k <= hi ; k++)
{ temp[k] = a[k]; }
//原地归并
for (int k = lo; k <= hi; k++)
{
if (i > mid) a[k] = temp[j++];
else if (j > hi) a[k] = temp[i++];
else if (less(temp[i],temp[j])) a[k] = temp[i++];
else a[k] = temp[j++];
}
}
//比较v是否小于w
private static boolean less(Comparable v, Comparable w)
{ return v.compareTo(w) < 0; }
}
实现Comparable 这个接口,就必须实现其内的compareTo方法,所以就可以根据这个元素的比较逻辑进行排序。