本节学习内容全览
- 排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
- 平时的上下文中,如果提到排序,通常指的是排升序(非降序)。
- 通常意义上的排序,都是指的原地排序(in place sort).
两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。
整个区间被分为
- 有序区间(已排序区间)
- 无序区间(待排序区间)
每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入
插入排序和前面所学的顺序表插入非常相似。如将这组数据进行排序:9 5 2 7 3 6 8
思路分析:还是画图
代码实现:
package java2021_1013;
import java.util.Arrays;
/**
* Created by Sun
* Description:插入排序
* User:Administrator
* Date:2021-10-19
* Time:10:31
*/
public class TestSort {
//按照升序排序
public static void insertSort(int[] array){
//通过bound划分出两个区间
//[0,bound)已排序区间
//[bound,size)待排序区间
//这里bound从1开始
for(int bound=1;bound < array.length;bound++){
int v=array[bound]; //把bound位置的值先给他取出来保存到一个单独的变量v里面
int cur=bound-1;//cur表示已排序区间的最后一个元素的下标,通过这个下标,进行比较和搬运操作
for(;cur>=0;cur--){//cur从后往前走,cur从bound-1开始,如果一直大于等于0,就--
if(array[cur]>v){//从后往前依次进行比较和搬运,判断cur位置的下标和v的值谁大
array[cur+1]=array[cur];//如果cur大,就搬运,即向后走一步,此时就完成了一次搬运
}else{
break;//如果v大,说明已经找到了合适的位置,就不需要搬运了
}
}
array[cur+1]=v;//把保存到一个单独的变量v里面的值赋值到对应的位置上,
}
}
//测试排序效果
public static void main(String[] args) {
int[] arr={9,5,2,7,3,6,8};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
}
打印结果:
[2, 3, 5, 6, 7, 8, 9]
插入排序的时间复杂度:代码中有两重循环,第一重循环循环了length次,即有多少个元素他就循环多少次,也就是O(N),而每一个大循环里面又要循环若干次,即第二重循环也可能循环N次,也就是O(N),所以它整体的复杂度就是O(N^2)。
当数据有序的时候它的时间复杂度是最优的,为O(N);
平均时间复杂度为O(N^2);
最坏时间复杂度是数据逆序时,为O(N^2)。
插入排序的空间复杂度:看在上面的代码中创建了多少个临时变量,即bound、v、cur这三个临时变量,可以知道,无论数组中有多少个元素,始终是这三个临时变量,所以当前所占用的额外空间和数组(即问题规模)的大小并没有直接关系,由于并没有引入过多额外的空间,所以空间复杂度为O(1).
稳定性:当前的这个插入排序是一个稳定排序。
什么是稳定?:如果一个数组中有两个元素的值一样,那么这两个元素相对之间的顺序么决定呢?如果排序之后能保证和原来的先后顺序是一致的,就叫稳定排序,否则就是不稳定排序。
如下面这个例子:
本来5a在前5b在后,排完序之后,5a还在5b的前面,仍然符合原来的相对顺序,这时就说明它是一个稳定排序。
插入排序的两个重要特点
1、当排序区间元素比较少的时候,排序效率很高;
2、当整个数组比较接近有序的时候,排序效率也很高,即初始数据越接近有序,时间效率越高。
希尔排序,相当于进阶版本的插入排序,它是将原始先分组,针对每个组进行插入排序(插排),逐渐缩小组的个数,最终整个数组就接近有序了。
如果你还不知道希尔排序的过程,那么请耐心点一步步看下去,看完就会觉得好像还挺好理解的哈!
下面我们通过举例并画图来了解一下希尔排序的过程:
疑问:希尔为什么要这样排序呢?
看看上面写的插入的两个重要特点你就明白了,算了,再写一遍吧
插入排序的两个重要特点
1、当排序区间元素比较少的时候,排序效率很高;**希尔排序将一组数据很多的数组分成了n组,减少了排序区间的元素,提高了效率;
2、当整个数组比较接近有序的时候,排序效率也很高,即初始数据越接近有序,时间效率越高。希尔排序最终使整个数组就接近有序,在进行插入排序,同时也提高了效率。
注意:希尔排序一般实现的时候不能按照3,2, 1来设定gap,上面的例子只是为了方便分析所设的,常见的gap取值为:size,size/2, size/4…1
代码实现:
//《二、希尔排序》
public static void shellSort(int[] array){
int gap= array.length/2;//初始情况下,让gap从length/2开始进行分组
while(gap>1){//如果大于1,就需要进行分组插排
//构造一个插排方法,创建一个被分组的插排方式
insertSortGap(array,gap);
gap=gap/2;//每次让gap依次除以2,
}
//一直到gap为1的时候,按照gap为1,再来一次插排(插入排序)
insertSortGap(array,1);//当gap为1的时候
}
private static void insertSortGap(int[] array, int gap) {
//在这个插排方法中写一个插入排序
//通过bound划分出两个区间
//[0,bound)已排序区间
//[bound,size)待排序区间
//这里bound从gap开始,gap为几,表示bound就从几开始
for(int bound=gap;bound < array.length;bound++){
//这个循环的含义是:
/*先处理第一组的第一个元素和第二个元素
* 再处理第二组的第一个元素和第二个元素
* 再处理第三组的第一个元素和第二个元素
* 再处理第一组的第二个元素和第三个元素
* .....直到把所有组都处理完毕*/
int v=array[bound]; //即无序区间的第一个数,先给他取出来保存到一个单独的变量v里面,
int cur=bound-gap;//这个操作是在找同组中的上一个元素
for(;cur>=0;cur-=gap){//这个循环只从处理当前组的比较和插入过程。cur-=gap表示找同组中的相邻元素,同组元素的下标差值就是gap
if(array[cur]>=v){//从后往前依次进行比较和搬运,判断cur位置的下标和v的值谁大
array[cur+gap]=array[cur];//如果cur大,就搬运,即向后走一步,此时就完成了一次搬运
}else{
break;//如果v大,说明已经找到了合适的位置,就不需要搬运了
}
}
array[cur+gap]=v;//cur--之后,cur的位置发生了变化,此时把保存到一个单独变量v里面的值赋值到对应的位置上,即cur+1的位置
}
}
//测试排序效果
public static void main(String[] args) {
int[] arr={9,5,2,7,3,6,8,10,7,12,6,4};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
}
打印结果:
[2, 3, 4, 5, 6, 6, 7, 7, 8, 9, 10, 12]
希尔排序的时间复杂度:理论上能达到O(N^1.3),如果按照size,size/2, size/4…1的这种取值方式设定gap序列,那它的时间复杂度仍然是O(N^2).
当数据有序的时候它的时间复杂度是最优的,为O(N);
平均时间复杂度为O(N^1.3);
最坏时间复杂度是为O(N^2),比较难构造。
希尔排序的空间复杂度:只比插入排序多了一个gap变量,仍然没有引入过多额外的空间,所以空间复杂度为O(1).
希尔排序的稳定性:不稳定,因为分组的时候可能把相同的值分到不同组中,也就无法保证相对顺序了,所以不稳定。
原理:每次从待排序区间中找到一个最小值,放到已排序区间末尾。
基于打擂台的思想,将当前元素与擂主元素比较交换,每次从数组中找最小值,然后把最小值放到合适的位置上。
举一个具体的例子来了解基本思路和过程吧,如下图:
代码实现
public static void selectSort(int[] array){
for(int bound=0;bound<array.length;bound++){
//以bound位置的元素作为擂主,循环从待排序区间中取出元素和擂主进行比较,如果打擂成功就和擂主交换
for(int cur=bound+1;cur< array.length;cur++){
if(array[cur]<array[bound]){//当前值如果小于擂主的话
//打擂成功,交换
int tmp=array[cur];
array[cur]=array[bound];
array[bound]=tmp;
}
}
}
}
//测试排序效果
public static void main(String[] args) {
int[] arr={9,5,2,7,3,6,8};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
}
打印结果:
[2, 3, 5, 6, 7, 8, 9]
选择排序的时间复杂度:外层循环循环N次,里层循环循环N次,所以时间复杂度为O(N^2);
选择排序的空间复杂度:临时变量的数量是固定的,和数组大小没有关系,所以空间复杂度为O(1);
选择排序的稳定性:不稳定排序。
如:int[] a = { 9, 2, 5a, 7, 4, 3, 6, 5b };
交换中该情况无法识别,保证 5a 还在 5b 前边
- 基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
- 每次从待排序区间中找到一个最小值,放到已排序区间末尾;
- 或者每次从待排序区间中找到一个最大值,放到已排序区间最前面(推荐找最大值(建大堆)的方式)
堆排序的风格:排升序要建大堆;排降序要建小堆;
方案一:先把数组建立成一个小堆,此时堆顶元素就是最小值,取出最小值放到另外一个数组中,循环取堆顶元素尾插到新数组即可。其实这相当于是一个升级版本的选择排序。这是一种解决方案,但这种方案有一个小缺陷,因为它要最小值放到另外一个数组中,需要额外**O(N)**的空间。这种方式思路较简单,但是空间稍微多了点。
方案二:先把数组建立成一个大堆,把堆顶元素和数组的最后一个元素互换,把最后一个元素删除,再从堆顶来向下调整。这种方式不需要浪费额外的空间,就在当前数组上进行折腾,所以它的空间复杂度为O(1)
代码实现:
//《四、堆排序》
private static void swap(int[] array,int i,int j){
//交换i和j
int tmp=array[i];
array[i]=array[j];
array[j]=tmp;
}
public static void heapSort(int[] array){
//先建立堆
//单独写一个建堆的方法
creatHeap(array);
//循环把堆顶元素交换到最后,并进行调整堆
for(int i=0;i<array.length-1;i++){//当堆中只剩一个元素的时候也就一定是有序的了,不需要再循环了,所以循环length-1次即可
//交换堆顶元素和堆的最后一个元素,可以写一个单独的swap方法
swap(array,0,array.length-1-i);//调用swap
//堆的元素个数相当于array.length-i,i多循环一次,堆里面的元素就少一个
//堆的最后一个元素下标是array.length-i-1;
//交换完成之后,要把最后一个元素从堆中删掉,堆的长度就又进一步缩水了array.length-i-1
//数组中
//[0,array.length-i-1)是待排序区间
//[array.length-i-1,array.length)是排序区间
//向下调整
//注意这个代码中的边界条件,看是-1,还是不减,还是+1,最好代入数值验证一下,如:i=0的时候,看逻辑是否合理
shiftDown(array,array.length-i-1,0);
//从下标为0一直到array.length-i-1进行调整,注意边界条件
}
}
private static void creatHeap(int[] array) {
//建堆:从最后一个非叶子节点出发向前循环,依次进行向下调整
for(int i=(array.length-1-1)/2;i>=0;i--){
//length-1是最后一个节点的下标,再-1/2就是得到最后一个节点的父节点
shiftDown(array,array.length,i);
}
}
private static void shiftDown(int[] array, int heapLength, int index) {
//这里是升序排序,建立的是大堆,大堆就需要找出左右子树中的较大值再和根节点比较
int parent = index;
int child=2 * parent+1;
while(child < heapLength){
if(child+1 < heapLength && array[child+1] > array[child]){
child=child+1;
}
//条件结束就意味着,child就已经是左右子树比较大的值的下标了
if(array[child] > array[parent]){
//需要交换两个元素
swap(array,child,parent);
}else{
break;
}
parent=child;
child=2 * parent +1;
}
}
//测试排序效果
public static void main(String[] args) {
//测试堆排序
int[] arr={9,5,6,7,3,6,2};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
}
打印结果:
[2, 3, 5, 6, 6, 7, 9]
堆排序的时间复杂度:建堆:(O(N)), 循环:N次,循环里面的shifDown向下调整:logN, 所以 O(N)+O(N*logN)=O(N *logN), 可以把logN近似看成O(1);
所以O(N *logN)
堆排序的空间复杂度:O(1)
堆排序的稳定性:不稳定排序;
冒泡排序:它的核心目标和堆排序/选择排序都很像,每次从待排序区间找到一个最小值或者最大值,并放到合适的位置上(最小值放到已排序区间的末尾,最大值放到已排序区间的最前面)。通过借助相邻元素比较交换的方式来找。
以升序排序为例:进行画图分析
代码实现:上图的过程是以升序排列为例(找最大值)的过程,这里的代码是以降序排列(找最小值)的过程为例的。
public static void bubbleSort(int[] array){
//按照每次找最小的方式来进行排序,(从后往前比较交换)
//[0,bound)已排序区间
//[bound,size)待排序区间
for(int bound = 0;bound < array.length; bound++){
for(int cur = array.length-1;cur>bound; cur--){
//为什么是cur>bound而不是>=呢?可以带入一个数验证一下,当bound为0时,如果>=,cur也为0,cur-1=-1,也就下标越界了
if(array[cur-1] > array[cur]){//此处为什么是cur-1而不是cur+1呢? cur-1是因为cur初始值是array.length-1,如果取cur+1下标的元素,就越界了。
swap(array,cur-1,cur);
}
}
}
}
打印结果:
[2, 3, 5, 6, 7, 8, 9]
冒泡排序的时间复杂度:第一个循环是O(N),第二个循环也是O(N),所以时间复杂度为O(N^2).
冒泡排序的空间复杂度:O(1).
冒泡排序的稳定性:稳定排序.
基本思路:依赖递归,递归的是左右区间。
1、现在待排序区间中,找到一个基准值(常见的可以取区间的第一个元素或者最后一个元素,都可以把它作为基准值)。
2、以基准值为中心,把整个区间整理成三个部分:左侧部分的元素都小于等于基准值,右侧部分的元素都大于等于基准值,中间部分为基准值;
3、然后针对左侧整理好的区间和右侧整理好的区间,进一步进行递归,重复刚才的整理过程。完成左右的整理之后,排序也就排好了。
举一个例子,给定一组数,将这组数进行快速排序。
9 5 2 7 3 6 8 10 -1 4
画图分析排序过程:以取右侧元素为基准值为例
注意:快速排序的效率和基准值之间的关系?
快速排序的效率和基准值取的得好坏密切相关,基准值是一个接近数组中位数的元素,划分出的左右区间比较均衡,此时效率就比较高,如果当前取得的基准值是最大或最小值,此时划分的区间不均衡,效率就低。
如果数组正好逆序,此时的快排就变成了慢排,此时的快速排序效率很低,时间复杂度是O(N^2),也是最坏时间复杂度;
如果是在交换的比较平均的情况下(如上面举的例子),平均时间复杂度是O(NlogN);
取右侧元素为基准值和取左侧元素为基准值是有差异的
取最右侧元素为基准值是:从左往右找一个大于基准值的元素为left,再从右往左找一个小于基准值的元素为right;
取最左侧元素为基准值是:从右往左找一个大于基准值的元素为left,再从左往右找一个小于基准值的元素为rigth;
结论
如果是先从左往右找,再从右往左找,left和right重合位置的元素,一定大于等于基准值,所以要把这个用于交换的基准值放到数组的最后
如果是先从右往左找,再从左往右找,left和right重合位置的元素,一定小于等于基准值,所以要把这个用于交换的基准值放到数组的最前
快速排序代码实现的核心过程:
1、先针对整个区间进行整理,整理成左侧小于等于基准值,右侧大于等于基准值;
2、再递归针对左侧区间和右侧区间分别进行递归整理。
代码实现:这是没有详细注释的
package java2021_1015;
import java.util.Arrays;
public class TestSort2 {
public static void quickSort(int[] array){
quickSortHelper(array,0,array.length-1);
}
//快速排序的递归过程
private static void quickSortHelper(int[] array, int left, int right) {
if(left>=right){
return ;
}
int index = partition(array,left,right);//同时整理完毕之后,返回index下标
quickSortHelper(array,left,index-1);
quickSortHelper(array,index+1,right);
}
// 通过左右指针的方式进行partition(整理)操作
private static int partition(int[] array, int left, int right) {
int begin=left;//左侧元素的下标
int end=right;//右侧元素的下标
int base=array[right];//基准值
while(begin<end){
while(begin < end && array[begin]<=base){// array[begin]<=base:表示当前元素比基准值小的话
begin++;//找下一个元素
}
while(begin < end && array[end]>=base){// array[end]>=base当前元素比基准值大的话
end--;//找下一个元素
}
swap(array,begin,end);
}
swap(array,begin,right);//right是一个序列中最后的位置的下标
return begin;
}
// 交换操作
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void main(String[] args) {
int[] array={9,5,2,7,3,6,8};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
代码实现:这是有详细注释的,头大!!
package java2021_1015;
import java.util.Arrays;
/**
* Description:快速排序
*/
public class TestSort2 {
public static void quickSort(int[] array){
//创建一个辅助完成递归的方法,来辅助完成递归过程
quickSortHelper(array,0,array.length-1);
//参数array:表示针对数组中的哪一个部分进行递归
//0,array.length-1:给定一个区间范围,这个递归就由0,array.length-1共同划分了一个前闭后闭的区间
// 划分前闭后闭的区间主要是为了代码简单
}
//快速排序的递归过程
private static void quickSortHelper(int[] array, int left, int right) {
//在这个方法中进行判定
if(left>=right){
//此时表示区间中有0个或1个元素,由于是前闭后闭,所以当left=right是,也是有一个元素的,此时不需要排序
return ;//直接返回即可
}
//针对[left,right]区间进行整理
//单独创建一个整理的方法partition
//index返回值就是整理完毕后,left和right的重合位置。知道了这个位置,才能分出左右区间,进而进一步进行递归
int index = partition(array,left,right);//同时整理完毕之后,返回index下标
quickSortHelper(array,left,index-1);
quickSortHelper(array,index+1,right);
}
// 通过左右指针的方式进行partition(整理)操作
private static int partition(int[] array, int left, int right) {
int begin=left;//左侧元素的下标
int end=right;//右侧元素的下标
//取最右侧元素为基准值
int base=array[right];//基准值
while(begin<end){
//从左往右找比基准值大的元素
//循环条件:左侧元素的下标 < 右侧元素的下标 && 当前左侧下标对应的元素的值 <= 基准值
while(begin < end && array[begin]<=base){// array[begin]<=base:表示当前元素比基准值小的话
begin++;//找下一个元素
}
//当上面的循环结束时,此时begin要么和end重合,要么i就指向一个大于base的值
//从右往左找比基准值小的元素,初始情况下,end = right.array[end] 就和base相等。
//此时要把这个基准值直接跳过,始终保持基准值位置就在原位。
while(begin < end && array[end]>=base){// array[end]>=base当前元素比基准值大的话
end--;//找下一个元素
}
//当上面的循环结束时,此时i要么和j重合,要么j就指向一个小于base的值
//下一个swap方法,执行交换操作
swap(array,begin,end);
}
//【思考】为啥下面交换了之后,仍然能满足快排的顺序要求呢?
//当begin和end重合的时候,最后一步要把重合位置的元素再和基准值进行交换
//right 这是一个序列中最后的位置的下标,就要求begin,end重合位置的元素必须是大于等于基准值的元素,才可以放到最后面
//如何证明找到的 begin 位置的元素一定是 >= 基准值的呢?
/*a)如果是 begin++ 导致的和 end 重合
此时最终的值取决于上次循环中 end 指向的值,上次循环中,end 应该是找到了一个小于基准值的元素,然后和一个大于基准值的元素交换了。
此处最终的 end 一定是大于基准值的元素
b)如果是end-- 导致的和 begin 重合
此时上面 begin++ 的循环退出就一定是因为 begin 位置找到了一个比基准值大的元素, end 和 begin 重合最终元素也一定大于等于基准值*/
swap(array,begin,right);//right是一个序列中最后的位置的下标
//这个交换为甚麽是合理的,要时刻考虑到重合位置的元素和基准值之间的大小关系,符合要求的才能够进行交换,推导过程可以结合代码进行
return begin;
}
// 交换操作
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void main(String[] args) {
int[] array={9,5,2,7,3,6,8};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
打印结果:
[2, 3, 5, 6, 7, 8, 9]
前面在解答快速排序的效率和基准值之间的关系时已经对时间复杂度有了分析。
时间复杂度
最好 | 平均 | 最坏 |
---|---|---|
O(N*(logN)) | O(N*(logN)) | O(N^2) |
空间复杂度:快排的空间复杂度主要在递归这里,递归的深度越身,占用的空间也就越多,平均来看递归的深度就是logN,最坏来看,递归的深度就是N(当基准值恰好是最大值或最小值的时候)。
最好 | 平均 | 最坏 |
---|---|---|
O(log(N)) | O(log(N)) | O(N) |
快速排序的稳定性:不稳定排序(无法确定相等情况)。
非递归方式实现快速排序,借助栈来模拟递归过程。
代码实现:
//非递归方式实现快速排序,借助栈来模拟递归过程。
public static void quickSortByLoop(int[] array){
//stack用来存放数组下标,通过下标来表示接下来要处理的区间是啥
Stack<Integer> stack = new Stack<>();
//初始情况下,先把右侧边界下标入栈,再把左侧边界下标入栈,左右边界仍然构成前闭后闭区间
stack.push(array.length-1);//右侧区间入栈
stack.push(0);//左侧区间入栈
while(!stack.isEmpty()){
//这个取元素的顺序要和push的顺序正好相反,因为栈是先进后出
int left=stack.pop();
int right=stack.pop();
if(left>=right){
//如果条件满足,说明区间中只有1个或0个元素,不需要整理
continue;
}
//通过调用partition把区间整理成以基准值为中心,左侧小于等于基准值,右侧大于等于基准值的形式
int index = partition(array,left,right);
//准备处理下个区间,将基准值右侧区间和基准值左侧区间入栈
//[index+1,right]基准值右侧区间
stack.push(right);
stack.push(index+1);
//[left,index-1]基准值左侧区间
stack.push(index-1);
stack.push(left);
}
}
public static void main(String[] args) {
int[] array={9,5,2,7,3,6,8};
quickSortByLoop(array);
System.out.println(Arrays.toString(array));
}
}
//打印结果
[2, 3, 5, 6, 7, 8, 9]
自己结合代码画一下递归过程会更好。
1、优化基准值的取法。选择基准值很重要,通常使用几数取中法;
- 即三个元素取中(最左侧元素,中间位置元素,最右侧元素,取中间值作为基准值,把确认的基准值swap到数组末尾或者开始,为了后面的整理动作做铺垫)。
2、当区间已经比较小的时候,再去递归其实效率就不高了,所以不再继续递归而是直接进行插入排序,就可以提高快排效率;
3、如果区间特别大,递归深度也会非常深,当递归深度达到一定程度的时候,再去递归就可能会占用大量的空间,此时可以把当前区间的排序使用堆排序来进行优化。
如果你不想看图,或者看不太懂图,那就看一个归并排序的舞蹈来理解一下吧,也许更有意思!
归并排序舞蹈
代码实现:
//《归并排序》
//写一个合并的方法,表示两个数组合并
//low ,mid ,high表示下标
//[low,mid):是一个有序区间
//[mid,high):是一个有序区间
//要把这两个有序区间合并成一个有序区间。
public static void merge(int[] array,int low,int mid,int high) {
//设置一个临时区间
int[] output = new int[high-low];
int outputIndex=0;//记录当前output数组中被放入多少个元素,方便后面进行尾插
int cur1 = low;//第一个区间的起始下标
int cur2 = mid;//第二个区间的起始下标
//进入循环
while(cur1 < mid && cur2 < high){//两个区间的结束条件
if(array[cur1] <= array[cur2]){//判断条件这里写成小于等于才能保证稳定性
//分别比较cur1和cur2谁大谁小
//如果cur1小,就把cur1放入临时区间
output[outputIndex] = array[cur1];
outputIndex++;//箭头往后走
cur1++;
}else {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
}
//当上面的循环结束的时候,肯定是cur1 或 cur2有一个先到达末尾,另一个还剩下一些内容
//把剩下的内容一股脑拷贝到output中
while(cur1 < mid){
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
}
//这两个循环只执行一个
while(cur2 < high){
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
//把output中的元素再搬运回原来的数组
for(int i = 0; i < high - low;i++){
array[low+i] = output[i];
}
}
//写一个归并排序操作,划分区间
public static void mergeSort(int[] array){
//写一个辅助方法,用于递归
mergeSortHelper(array,0,array.length);
}
//[low,high)是前闭后开区间,如果两者差值小于等于1,区间中就只有0个元素或1个元素
private static void mergeSortHelper(int[] array, int low, int high) {
//递归过程
//处理1个元素的情况
if(high - low <= 1){
//[low,high)是前闭后开区间,如果两者差值小于等于1,区间中就只有0个元素或1个元素,此时就无需合并直接返回
return;
}
//处理多个元素的情况
int mid=(low+high)/2;//取一个中间值mid,有了mid就可以把它分成两个区间了
//这个方法执行完,就认为low,mid 已经排序好了
mergeSortHelper(array,low,mid);//low开始,mid结束,进行归并
//这个方法执行完,就认为mid ,high已经排序好了
mergeSortHelper(array,mid,high);//mid开始,high结束,进行归并
//当把左右区间已经归并排序完了,说明左右区间已经是有序区间了
//接下来就可以针对两个有序区间进行合并了
merge(array,low,mid,high);
}
public static void main(String[] args) {
int[] array={9,5,2,7,3,6,8};
mergeSort(array);
System.out.println(Arrays.toString(array));
}
}
//打印结果
[2, 3, 5, 6, 7, 8, 9]
下面试着根据代码把递归走一部分吧
归并排序的时间复杂度:O(NlogN);
归并排序的空间复杂度:他有一个额外的临时空间output,所空间为O(N),它还有一个递归占的空间logN,所以空间复杂度为O(N)+O(logN)=O(N),不过这是针对数组归并时的空间复杂度,如果是对链表,它的空间复杂度可以是O(1).
通过合理的分组方式就可以完成非递归实现,不需要使用栈。
//《归并排序的非递归实现》
public static void mergeSortByLoop(int[] array){
//引入一个gap变量进行分组,使用gap变量来表示分组, gap是两个相邻组之间的下标之差,同时gap也表示每一组的长度
//当gap为1的时候,[0] [1]进行合并,[2] [3]进行合并,[4] [5] 进行合并,[6] [7]进行合并,以此类推...这里的数字表示的是下标
//当gap为2的时候,[0,1]和[2,3]进行合并,[4,5]和 [6,7]进行合并,以此类推
//当gap为4的时候,[0,1,2,3]和[4,5, 6,7]进行合并,以此类推
for(int gap = 1; gap < array.length;gap *= 2){
//接下来进行具体的分组合并
//下面这个循环执行一次,就完成了一次相邻两个组的合并
for(int i = 0;i < array.length;i += 2*gap){
//当前相邻组是[begin,mid) [mid ,end)
int begin = i;
int mid = i + gap;
int end = i + 2*gap;
//如果超出返回,就赋值,防止下标越界
if(mid > array.length){//如果mid超出范围
mid = array.length;
}
if(end > array.length){//如果mid超出范围
end = array.length;
}
merge(array,begin,mid,end);//调用merges实现begin,mid,end的区间的归并
}
}
}
public static void main(String[ ] args) {
int[] array={9,5,2,7,3,6,8};
mergeSortByLoop(array);
System.out.println(Arrays.toString(array));
}
}
//打印结果:
[2, 3, 5, 6, 7, 8, 9]
归并排序有两个重要的特点,可以适用外部排序,也适合针对链表排序
如:它可以解决海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
- 先把文件切分成 200 份,每个 512 M
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
- 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了。
各个排序的性能也要掌握,背会!!!
排序方法 | 时间复杂度–最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
希尔排序 | O(n) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(1) | 不稳定 |
快速排序 | O(n * log(n)) | O(n * log(n)) | O(n^2) | O(log(n))-O(n) | 不稳定 |
归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(n) | 稳定 |
重点掌握每种排序的代码实现和性能分析,原理和过程也要结合代码深入理解每种排序的基本思路。