排序算法五:归并排序

归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用,将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2-路归并。

算法描述:

  • 把长度为 n 的输入序列分为两个长度为 n / 2 的子序列
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的序列合并成一个最终的排序序列

动画演示:在这里插入图片描述
代码实现:

function mergeSort(arr) {   //mergeSort函数,
    var len = arr.length;
    if (len < 2) {     // 数组长度小于2 的情况分析
        return arr;
    }
    var middle = Math.floor(len / 2),  //先找出分界值,即数组的中间值,
        left = arr.slice(0, middle),    //slice()选取middle之前的数
        right = arr.slice(middle);  //同理选择middle后面的数
    return merge(mergeSort(left), mergeSort(right)); //
}
 
function merge(left, right) {      //merge函数,合并两个有序序列
    var result = [];
 
    while (left.length>0 && right.length>0) {  
        if (left[0] <= right[0]) {
            result.push(left.shift());  //shift()方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。 push()方法可向数组的末尾添加一个或多个元素,并返回新的长度。
        } else {
            result.push(right.shift());
        }
    }
 
    while (left.length)
        result.push(left.shift());
 
    while (right.length)
        result.push(right.shift());
 
    return result;
}

https://www.cnblogs.com/chengxiao/p/6194356.html也有讲解,用的是 java,讲的通俗易懂。

package sortdemo;

import java.util.Arrays;

/**
 * Created by chengxiao on 2016/12/8.
 */
public class MergeSort {
    public static void main(String []args){
        int []arr = {9,8,7,6,5,4,3,2,1};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void sort(int []arr){        // public sort函数,仅仅为了重新定义一个临时数组
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(arr,0,arr.length-1,temp);
    }
    private static void sort(int[] arr,int left,int right,int []temp){ // private sort函数,嵌套完成归并算法,输入{序列的长度,序列的最左值,序列的最右值,}
        if(left<right){     //很关键的一句条件判断,因为最终像二叉树一样会分成两个值之间的比较,那么比如0和1,则它们的middle是0,left == middle,条件判断不成立,递归终止,就像我们以前做的斐波那契数 递归后要有条件判断语句 if(n<=2)来停止递归,然后自内向外计算。比如
            int mid = (left+right)/2;
            sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){ 
        int i = left;//左序列指针 
        int j = mid+1;//右序列指针
        int t = 0;//临时数组指针
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){       //比较左序列的第一个数和右序列的第一个数,哪个数小,把哪个数先放到临时数组中。 在这个过程中,因为 i++,j++ 的原因,所以相当于两个序列一直在从第一个值到最后一个值迭代,直到其中的一个序列全部放入,while条件不成立了,跳出。因为比小的话不可能两个序列同时到达最后一个值,总有一个先到右边的临界点。
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//将左边剩余元素填充进temp中    
            temp[t++] = arr[i++];
        }
        while(j<=right){//将右序列剩余元素填充进temp中  //这两步就是将剩下的数全部放入
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){   //代表着从left 到 right 这个区间,因为多个区间left是不同的,所以最下面的arr{left]改变的值不会影响到其他区间,实际上就是这个区间排序的过程。
            arr[left++] = temp[t++];
        }
    }
}

归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性都不会太差。jav中,Array.sort()采用了一种名为TimSort 的排序算法,就是归并排序的优化版本,从上文的图中可以看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为log2n ,总的平均时间复杂度为 O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。

一大进步:即使是最坏的时间复杂度也是 O(nlogn)。,比希尔排序好了很多。

归并排序链表

作者:jyd
链接:https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/

通过递归实现链表归并排序,有以下两个环节:

- 分割 cut 环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
我们使用fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。 找到中点 slow 后,执行 slow.next = None 将链表切断。 递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow切断的)。 cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点。

  • 合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。 双指针法合并,建立辅助ListNode h 作为头部。 设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。返回辅助ListNode h 作为头部的下个节点 h.next。 时间复杂度 O(l + r),l, r 分别代表两个链表长度。当题目输入的 head == None 时,直接返回None。
    在这里插入图片描述
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode fast = head.next, slow = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode tmp = slow.next;
        slow.next = null;
        ListNode left = sortList(head);
        ListNode right = sortList(tmp);
        ListNode h = new ListNode(0);
        ListNode res = h;
        while (left != null && right != null) {
            if (left.val < right.val) {
                h.next = left;
                left = left.next;
            } else {
                h.next = right;
                right = right.next;
            }
            h = h.next;
        }
        h.next = left != null ? left : right;
        return res.next;
    }
}

作者:jyd
链接:https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/

注意点(1):寻找节点中点(节点数量为奇数情况)以及寻找节点中点靠左(节点数量为偶数情况)。

ListNode fast = head.next, slow = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
  • 首先定义了 slow 节点为头节点, fast节点为头节点之后的节点;
  • 判断 slow 指针和 fast 指针移动结束的条件(当fast节点指向链表中最后一个节点或者超出链表界限)
  • 移动的过程中,fast指针每次走两步,slow指针每次走一步

最终可以保证 slow 指针能够走到指定的位置。即链表的中点或者中点的左边。

注意点(2): 左右链表的比较过程, 归并排序的特点在这里会将链表不断二分,最终剩下两个或者一个,然后排序好之后合并。

这里的排序要对左右子链表中的所有节点进行排序

 while (left != null && right != null) {
        if (left.val < right.val) {
            h.next = left;
            left = left.next;
        } else {
            h.next = right;
            right = right.next;
        }
        h = h.next;
    }
    h.next = left != null ? left : right;
    return res.next;
}

对于非递归的情况

对于非递归的归并排序,需要使用迭代的方式替换cut环节:
我们知道,cut环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。

每一轮合并merge操作针对的单元都有固定长度 intv, 例如:

  • 第一轮合并时intv = 1,即将整个链表切分为多个长度为1的单元,并按顺序两两排序合并,合并完成的已排序单元长度为2。

  • 第二轮合并时intv = 2,即将整个链表切分为多个长度为2的单元,并按顺序两两排序合并,合并完成已排序单元长度为4。

以此类推,直到单元长度intv >= 链表长度,代表已经排序完成。
根据以上推论,我们可以仅根据intv计算每个单元边界,并完成链表的每轮排序合并,例如:

  • 当intv = 1时,将链表第1和第2节点排序合并,第3和第4节点排序合并,……。
  • 当intv = 2时,将链表第1-2和第3-4节点排序合并,第5-6和第7-8节点排序合并,……。
  • 当intv =4时,将链表第1-4和第5-8节点排序合并,第9-12和第13-16节点排序合并,……。

此方法时间复杂度O(nlogn),空间复杂度O(1)。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值