归并排序(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)。