一. 基本思想
归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。把长度为n的输入序列分成两个长度为n/2的子序列;对这两个子序列分别采用归并排序;将两个排序好的子序列合并成一个最终的排序序列。
二. 代码实现
- 版本一
package com.hong.sort;
import java.util.Arrays;
/**
* 归并排序 O(Nlog(N))
* 需要将当前的数组拷贝一份出来,因此相比之前的排序,需要更多的存储空间
*/
public class MergeSort {
/**
* 将arr[l...mid]和arr[mid+1...r]两部分进行归并
*
* @param arr
* @param l 左半部元素起始点
* @param mid 左半部元素终点
* @param r 右半部元素终点
*/
private static void merge(int[] arr, int l, int mid, int r) {
int[] aux = Arrays.copyOfRange(arr, l, r + 1);
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) { // 如果左半部分元素已经全部处理完毕
arr[k] = aux[j - l];
j++;
} else if (j > r) { // 如果右半部分元素已经全部处理完毕
arr[k] = aux[i - l];
i++;
} else if (aux[i - l] < aux[j - l]) { // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i - l];
i++;
} else { // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j - l];
j++;
}
}
}
/**
* 递归使用归并排序,对arr[l...r]的范围进行排序
*
* @param arr
* @param l
* @param r
*/
public static void sort(int[] arr, int l, int r) {
if (l >= r) {
return;
}
// int mid = (l + r) / 2;
int mid = l + (r-l)/2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public static void sort(int[] arr) {
int n = arr.length;
// [0,n-1],这里注意区间的开闭
sort(arr, 0, n - 1);
}
}
- 版本二
package com.hong.sort;
import java.util.Arrays;
/**
* 归并排序优化
*/
public class MergeSort2 {
public static void sort(int[] arr) {
sort(arr, 0, arr.length - 1);
}
public static void sort(int[] arr, int left, int right) {
/**
* 优化2:
*对于小规模数组, 使用插入排序
*/
if (right - left <= 15){
InsertionSort.sort(arr,left,right);
return;
}
// int mid = (left + right) / 2;
int mid = left + (right - left)/2;
sort(arr, left, mid);
sort(arr, mid + 1, right);
/**
* 优化1
* 对于arr[mid] <= arr[mid+1]的情况,不进行merge
* 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
*/
if (arr[mid] > arr[mid+1]){
merge(arr, left, mid, right);
}
}
public static void merge(int[] arr, int l, int mid, int r) {
int[] aux = Arrays.copyOfRange(arr, l, r + 1);
int i = l;
int j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) {
arr[k] = aux[j - l];
j++;
} else if (j > r) {
arr[k] = aux[i - l];
i++;
} else if (aux[i - l] < aux[j - l]) {
arr[k] = aux[i - l];
i++;
} else {
arr[k] = aux[j - l];
j++;
}
}
}
}
public class InsertionSort {
/**
* 对arr[l...r]的区间使用InsertionSort排序
* @param arr
* @param l
* @param r
*/
public static void sort(int[] arr, int l, int r) {
for (int i = l + 1; i <= r; i++) {
int e = arr[i];
int j = i;
for (; j > l && arr[j - 1] > e; j--) {
arr[j] = arr[j - 1];
}
arr[j] = e;
}
}
}
上面的两个版本都是使用递归实现自顶向下的归并排序。
- 版本三
package com.hong.sort;
import java.util.Arrays;
/**
* <br>自底向上的归并排序</br>
* 之前的归并排序中是自顶向下,扩散式逐步拆分成小的组,排好序后,再逐步向上收敛整合成一个最终有序的序列
*
*/
public class MergeSort3 {
public static void merge(int[] arr, int l, int mid, int r) {
int[] aux = Arrays.copyOfRange(arr, l, r + 1);
int i = l;
int j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) {
arr[k] = aux[j - l];
j++;
} else if (j > r) {
arr[k] = aux[i - l];
i++;
} else if (aux[i - l] < aux[j - l]) {
arr[k] = aux[i - l];
i++;
} else {
arr[k] = aux[j - l];
j++;
}
}
}
/**
* Merge Sort Bottom Up 无优化版本
*
* @param arr
*/
public static void sort0(int[] arr) {
int n = arr.length;
// sz 表示对需要进行merge的元素个数进行遍历
// 第一轮 看 1个元素。之后看2个元素。。。
for (int sz = 1; sz < n; sz *= 2) {
// 每一轮在归并的过程中,起始元素位置
for (int i = 0; i < n - sz; i += sz + sz) {
// 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并,注意越界问题
merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1));
}
}
}
/**
* Merge Sort Button Up优化
*
* @param arr
*/
public static void sort(int[] arr) {
int n = arr.length;
// 对于小数组, 使用插入排序优化
for (int i = 0; i < n; i += 16) {
InsertionSort.sort(arr, i, Math.min(i + 15, n - 1));
}
for (int sz = 16; sz < n; sz += sz) {
for (int i = 0; i < n - sz; i += sz + sz) {
// 对于arr[mid] <= arr[mid+1]的情况,不进行merge
if (arr[i + sz - 1] > arr[i + sz]) {
merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1));
}
}
}
}
}
三. 归并排序在链表中的应用
LeetCode 148. 排序链表。
Merge Sort Bottom Up :使用 nlog(n) 的复杂度为链表排序
package com.hong.leetcode;
import com.hong.linked.ListNode;
/**
* Created by wanghong
* Date 2019-05-10 19:18
* Description:148. 排序链表
*/
public class Solution148 {
/**
* 自顶向下
* 参考:Sort List——经典(链表中的归并排序) https://www.cnblogs.com/qiaozhoulin/p/4585401.html
*
* 归并排序法:在动手之前一直觉得空间复杂度为常量不太可能,因为原来使用归并时,都是 O(N)的,
* 需要复制出相等的空间来进行赋值归并。对于链表,实际上是可以实现常数空间占用的(链表的归并
* 排序不需要额外的空间)。利用归并的思想,递归地将当前链表分为两段,然后merge,分两段的方
* 法是使用 fast-slow 法,用两个指针,一个每次走两步,一个走一步,知道快的走到了末尾,然后
* 慢的所在位置就是中间位置,这样就分成了两段。merge时,把两段头部节点值比较,用一个 p 指向
* 较小的,且记录第一个节点,然后 两段的头一步一步向后走,p也一直向后走,总是指向较小节点,
* 直至其中一个头为NULL,处理剩下的元素。最后返回记录的头即可。
*
* 主要考察3个知识点,
* 知识点1:归并排序的整体思想
* 知识点2:找到一个链表的中间节点的方法
* 知识点3:合并两个已排好序的链表为一个新的有序链表
*
* 一切的前提就是搞清为啥归并排序可以排序链表,原因如下
* 包括选择、插入、堆排序、快排等等都需要随机读取进行元素的交换,
* 这正是链表所缺乏的,归并排序是为数不多不需要随机读取的排序算法
*/
public ListNode sortList(ListNode head) {
return head == null ? null : mergeSort(head);
}
private ListNode mergeSort(ListNode head) {
if (head.next == null) {
return head;
}
// p 最后表示中间节点,pre 最后表示 中间节点的前一个节点
ListNode p = head, q = head, pre = null;
while (q != null && q.next != null) {
pre = p;
p = p.next;
q = q.next.next;
}
// 将整个链表一分为二
pre.next = null;
ListNode l = mergeSort(head);
ListNode r = mergeSort(p);
return merge(l, r);
}
ListNode merge(ListNode l, ListNode r) {
// 哑链表头。临时创建的一个链表头,把边界情况和普通情况做统一处理
ListNode dummyHead = new ListNode(0);
ListNode cur = dummyHead;
while (l != null && r != null) {
if (l.val <= r.val) {
cur.next = l;
cur = cur.next;
l = l.next;
} else {
cur.next = r;
cur = cur.next;
r = r.next;
}
}
if (l != null) {
cur.next = l;
}
if (r != null) {
cur.next = r;
}
return dummyHead.next;
}
/****************************************************/
/**
*要求空间复杂度是 O(1),因此不能使用递归。因此这里使用 bottom-to-up 的算法来解决
* bottom-to-up 的归并思路是这样的:先两个两个的 merge,完成一趟后,再 4 个4个的 merge,直到结束。举个简单的例子:[4,3,1,7,8,9,2,11,5,6].
*
* step=1: (3->4)->(1->7)->(8->9)->(2->11)->(5->6)
* step=2: (1->3->4->7)->(2->8->9->11)->(5->6)
* step=4: (1->2->3->4->7->8->9->11)->5->6
* step=8: (1->2->3->4->5->6->7->8->9->11)
* 链表里操作最难掌握的应该就是各种断链啊,然后再挂接啊。在这里,我们主要用到链表操作的两个技术:
*
* merge(l1, l2),双路归并,我相信这个操作大家已经非常熟练的,就不做介绍了。
* cut(l, n),可能有些同学没有听说过,它其实就是一种 split 操作,即断链操作。不过我感觉使用 cut 更准确一些,它表示,将链表 l 切掉前 n 个节点,并返回后半部分的链表头。
* 额外再补充一个 dummyHead 大法,已经讲过无数次了,仔细体会吧。
* @param head
* @return
*/
public ListNode sortList2(ListNode head) {
return null;
}
public static void main(String[] args) {
Solution148 s = new Solution148();
int[] nums = {4,2,1,3};
ListNode head = new ListNode(nums);
System.out.println(head);
head = s.sortList(head);
System.out.println(head);
}
}
package com.hong.linked;
/**
* @author wanghong
* @date 2019/04/08 22:57
**/
public class ListNode {
public int val;
public ListNode next;
public ListNode(int x) {
val = x;
}
// 链表节点的构造函数
// 使用arr为参数,创建一个链表,当前的ListNode为链表头结点
public ListNode(int[] arr){
if(arr == null || arr.length == 0)
throw new IllegalArgumentException("arr can not be empty");
this.val = arr[0];
ListNode cur = this;
for(int i = 1 ; i < arr.length ; i ++){
cur.next = new ListNode(arr[i]);
cur = cur.next;
}
}
// 以当前节点为头结点的链表信息字符串
@Override
public String toString(){
StringBuilder s = new StringBuilder();
ListNode cur = this;
while(cur != null){
s.append(cur.val + "->");
cur = cur.next;
}
s.append("NULL");
return s.toString();
}
public int getVal() {
return val;
}
}