归并排序和快速排序都适合大规模的数据排序,它们的平均时间复杂度都是O()。这两种排序方式解决问题的方式都是将一个大问题分解成若干个小问题来解决的,小问题解决了。大问题自然也就解决了。
归并排序
归并排序的核心思想:对于一个要排序的数组,先把数组从中间分解为前后两部分,然后对前后两部分进行排序,再将排序好的数组合并在一起,这样的数组就有序了。(摘自:数据结构之美)
归并排序的主要思想:把待排序的记录序列分成若干个子序列,先将每一个子序列的记录进行排序,再将已排序的子序列合并,得到一个完全排序的记录序列。归并排序可分为多路归并排序和两路归并排序(摘自:数据结构-java语言描述)
不同大牛表述上虽然略有区别,但其表述的思想是一致的。接下来只针对两路归并排序就行学习和总结。
归并排序示意图:
归并排序采用的分治思想,使用递归算法来实现。所以我们需要根据递归算法推导出递归公式和边界条件。如下:
//递归公式
merge_sort(left...right) =merge(merge_sort(left...mid),merge_sort(mid+1...right));
//终止条件
left>=right
分析:merge_sort(left...right)表示,给下标从left到right的数组进行排序,把问题转化为了两个子问题,merge_sort(left...mid)和merge_sort(mid+1...right)。其中下标mid为left和right的中间位置,即mid=(left+right)/2。当下标left到mid和下标从mid+1到right的子数组排序好序后,再将两个子数组进行合并,这样从下标left到right之间的数组就排好序了
转化为Java代码:
public void merge_sort(int[] array,int left,int right){
if(left>=right) return; //递归终结条件
int mid=(left+right)/2;
//分治思想,把要排序数组从中间分解为两个子数组的排序
merge_sort(array,left,mid);
merge_sort(array,mid+1,right);
//把排序好的子数组进行合并
merge(array,left,mid,right);
}
merge()函数就是对两个排好序的子数组进行合并。并把合并后的数据重新拷贝到array数组中从下标left到下标right的位置。
注:两个子数组的处于array数组下标left到right的位置
这里的merge()方法提供两种解决方案。一种是没有使用哨兵的方案,另一种则使用了哨兵方案。
方案一:没有使用哨兵。
新建一个临时数组temp,临时数组长度为两个子数组的长度之和,即right-left+1。再设置三个指针,分别x,y,k。分别指向两个子数组的第一个数据元素以及临时数组temp的第一个数据元素。当array[x]<array[y]时,则把array[x]放入临时数组中,指针x和k后移一位。反之,把array[y]的数据元素放入临时数组中,指针y和k后移一位。当其中一个子数组先一步把数据元素放入临时数组中后,另外一个子数组必然还有一部分数据元素没有放入临时数组中。所以接下来要完成的就是把数据元素还没有完全放入临时数组的子数组中的数据元素放入临时数组中。最后把临时数组中的数据元素拷贝到array数组的下标从left到right的位置。
转化为Java代码如下:
private void merge(int[] array, int left, int mid, int right) {
int[] temp=new int[right-left+1]; //创建临时数组
int x=left; //x指向其中一个子数组的第一个数据元素
int y=mid+1; //y指向另一个子数组的第一个数据元素
int z=0; //z指向临时数组的第一个数据元素
//把子数组中的数据元素放入临时数组中
while (x<=mid && y<=right){
if(array[x]<array[y]){
temp[z++]=array[x++];
}else {
temp[z++]=array[y++];
}
}
//获取数据元素还没有拷贝到临时数组的索引范围
int index_s=x;
int index_e=mid;
if(y<=right){
index_s=y;
index_e=right;
}
//把未放入临时数组的数据元素放入临时数组
while (index_s<=index_e){
temp[z++]=array[index_s++];
}
//把临时数组中的数据拷贝到array中
for(int i=0;i<=right-left;i++){
array[left+i]=temp[i];
}
}
方案二:带有哨兵的解决方案
新建两个临时数组,长度等于子数组的长度加上1,用来存放哨兵。把子数组拷贝到临时数组,拷贝完成后,在往临时数组中的最后位置添加哨兵。再把临时数组中的值拷贝到array数组下标从left到right的位置
转化为java代码:
private void merge(int[] array, int left, int mid, int right) {
//新建两个临时数组,临时数组的长度为子数组的长度加上1,用来存放哨兵
int[] sub_left=new int[mid-left+2];
int[] sub_right=new int[right-mid+1];
//把子数组中的数据拷贝到临时数组并添加哨兵
for(int i=0;i<=mid-left;i++){
sub_left[i]=array[left+i];
}
sub_left[mid-left+1]=Integer.MAX_VALUE;
for(int i=0;i<right-mid;i++){
sub_right[i]=array[mid+1+i];
}
sub_right[right-mid]=Integer.MAX_VALUE;
//把临时数组中的数据拷贝到array数组下标值为left到right的位置
int x=0;
int y=0;
int z=left;
while (z<=right){
if(sub_left[x]<sub_right[y]){
array[z++]=sub_left[x++];
}else {
array[z++]=sub_right[y++];
}
}
}
哨兵的作用是用来解决边界问题,简化开发
经分析:
归并排序:最好时间复杂度为O(),最坏时间复杂度为O(
),平均时间复杂度为O(
)。空间复杂度为O(n),稳定性为稳定排序算法
快速排序
快速排序的核心思想:如果要排序数组中下标从p到r之间的一组数据,会在p到r之间选择任意一个节点作为分区点(pivot),并遍历从p到r中的数据,把小于pivot的数据元素放到pivot左侧,把大于pivot的数据元素放到pivot右侧。将pivot放到中间。经过这一步骤后,从p到r的数组分成三个部分,假设pivot的下标为q,小于pivot的部分,下标从p到q-1。大于pivot的部分,下标从q+1到r。以及中间的pivot部分。(摘自:数据结构与算法之美)
如下图所示:
快速排序的基本做法:任取待排数组的n个记录中的某个记录作为基准,通过排序,将待排序记录分成左右两个子序列。左子序列的关键字均小于或等于该基准记录的关键字,右子序列的关键字均小于或等于该基准记录的关键字,从而得到该记录位置最终排序的位置,然后该记录不再参加排序,此一趟排序称为第一趟快速排序,然后对所有左右序列重复上述方法(摘自:数据结构--java语言描述)
虽然表述的意思差不多,但我还是喜欢上面那一种的表述,通俗易懂。
快速排序示意图:
快速排序同归并排序一样,采用分治思想,使用递归算来实现。所以需要通过递归公式推导出递归公式和边界条件。
递归公式
递推公式:
quick_sort(left…right) = quick_sort(left…q-1) + quick_sort(q+1… right)
终止条件:
letf >= right
转化为java代码:
public void quickSort(int[] array,int left,int right){
if(left>=right) return;
//获取分区点的下标
int p=getPivot(array,left,right); //分区点
quickSort(array,left,p-1); //小于分区点的数据元素
quickSort(array,p+1,right); //大于分区点的数据元素
}
获取分区点的下标。同样提供两种解决方案。
方案一:这种方案有点类似选择排序
这个方案有点类似选择排序,会把要排序的数组分为已处理区域和未处理区域。依次选取未处理区域的数据元素与分区点比较。选取最后一个数据元素作为分区点,设置指针x,y指向要快排数组的第一个数据元素,通过y的不断后移,让y指向的数据元素一一与分区点数据元素比较。如果y指向的数据元素大于分区点,则让y指向的数据元素与x指向的数据元素进行位置交换,x指针后移一位。
private int getPivot(int[] array, int left, int right) {
int pivot=array[right];
int x=left;
for (int y=left;y<right;y++){ //让数组中的数据元素依次与分区点比较
if(array[y]<pivot){
if(x==y){
x++;
}else {
int temp=array[y];
array[y]=array[x];
array[x]=temp;
x++;
}
}
}
//交互array[x]和分区点的位置
int temp=array[x];
array[x]=array[right];
array[right]=temp;
return x;
}
方案二:
上面那种方案,是从一端开始探查。逐一与分区点比较大小。而方案二则是从两端开始向中间探查,从左端开始探查,如果探查到的数据大于分区点,则暂停;从右端开始探查的,如果探查到的数据小于分区点,则暂停,交互二者数据,再继续往中间探查,直至二者相遇。二者相遇的位置则是分区点所在位置
private int getPivot(int[] array, int left, int right) {
int low=left;
int hight=right;
int pivot=array[right];
while (hight>low){
while (array[low]<pivot && hight!=low){
low++;
}
while (array[hight]>=pivot && hight!=low){ //从右边逐一向左探测
hight--;
}
//如果两指针还每相遇,则交互二者的位置
if(hight!=low){
int temp=array[hight];
array[hight]=array[low];
array[low]=temp;
}
}
//交互分区点到中间位置
int temp=array[hight];
array[hight]=array[right];
array[right]=temp;
return hight;
}
快速排序:最好时间复杂度为O(),最坏时间复杂度为O(
),平均时间复杂度为O(
)。空间复杂度为O(1),稳定性为不稳定排序算法
小结:
归并和快速排序都是分治思想,使用递归来实现。过程非常相似。理解归并排序,需要理解递归公式和merge()方法。理解快速排序,需要理解快排的递归公式以及getPivot()方法
归并排序算法是一种再任何情况下时间复杂度都比较稳定的算法,相比快排,缺陷就是空间复杂度比较高为O(n)。
快速排序的缺陷就是时间复杂度可能会退化到O(n),虽然这种机率比较小。但我们可以合理的选取分区点来避免这种情况。
这里推荐两个网址,可以通过这两个网址观察排序算法的动态图,加深对排序算法的理解
https://mp.weixin.qq.com/s/HQg3BzzQfJXcWyltsgOfCQ
参考:数据结构与算法之美 -- 王争
《数据结构--java语言描述》