并归排序与快速排序相似,靠分治思想突破了排序算法 O(n2) 的瓶颈。

  我们看回顾一下几大排序算法的时间、空间复杂度:

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 是否稳定
冒泡排序 O(n2) O(n2) O(1)
选择排序 O(n2) O(n2) O(1) 不是
直接插入排序 O(n2) O(n2) O(1)
归并排序 O(nlogn) O(nlogn) O(n)
快速排序 O(nlogn) O(n2) O(logn) 不是
堆排序 O(nlogn) O(nlogn) O(1) 不是
希尔排序 O(nlogn) O(ns) O(1) 不是
计数排序 O(n+k) O(n+k) O(n+k)
基数排序 O(N∗M) O(N∗M) O(M)

  早期的排序算法总是免不了元素间的一一比较,因此时间复杂度很难突破 O(n2) 。而并归排序采用分治的思想将问题的规模缩小,使用小问题的解来解决大问题,并由此突破了 n的诅咒。

  以冒泡排序为例,我们需要n次遍历,每次遍历将数组中最大或者最小的元素冒到顶端,而这样的遍历需要 n-1 次。本质上每次遍历等于从所有元素中找到最大或者最小的元素,这就要求我们需要遍历和比较到数组中未排序的每一个元素。

  所以冒泡排序的计算次数为 n-1 + n-2 + n-3 +...+1 = n(n+1)/2 ,时间复杂度表示为 O(n2)。

  那么我们想一下,如果我们不是对一个杂乱的序列进行排序,而是对两个有序的子序列进行排序的话情况会是怎样的:

  我们可以维护两个指针分别指向两个子序列的顶端,选择较小的元素放入新的序列,并向后移动指向拿走的元素的指针。这样我们从未排序的元素中选出一个最小或最大的数只要比较一次。

  我们可以不断的缩小排序序列的范围来构建有序的子序列,从下向上一层一层逐步完成对整个序列的排序。

  缩小的排序范围的过程是这样的,不断的将序列分解为俩个子序列,直到序列无法分解。比如一个序列长度为8:

  两个长度为4的子序列--->四个长度为2的子序列---->八个长度为1的子序列。

  分解过程就像一颗 B树 向下分裂(不同的是分裂时父节点不变),第 n 层的拥有 2 个节点,也就是说直到每个节点中只包含一个元素时共分裂 log2n 次。

  而每一层总的元素数不变,使该层所有序列变为有序数列需要 n 次比较。

  整个过程下来,我们需要比较 nlog2n 次。也就是并归排序的时间复杂度为 O( nlog2n ) 。

  (也不知道为什么,用小问题推导大问题总是比直接解决大问题来的快,可能是程序员的命吧。其实个人觉着不管什么问题,如果有办法用子问题来推导原问题,那么时间复杂度中一定包含log分解出的子问题数量问题规模,一旦觉着自己当前尝试的解法比该解法时间复杂度高,不妨尝试一下分治。)

  所以我们有两个关键步骤:分解为子序列、合并子序列为一个有序序列

  下面上代码,注释比较全,以下两种解法都已在leetcode提交通过:

    /**
* @Author Nxy
* @Date 2019/12/4
* @Param
* @Return
* @Exception
* @Description 数组并归排序
* 将begin、end间的数组分解为两个子序列并回归排序
*/
public static void mergeSort(int[] nums, int begin, int end) {
int length = nums.length;
//回归条件,子序列长度为一时返回
if (begin == end) {
return;
}
//序列中点
int mid = (begin + end) / 2;
//排序左边子序列
mergeSort(nums, begin, mid);
//排序右边子序列
mergeSort(nums, mid + 1, end);
//并归已排序的左右子序列
merge(nums, begin, mid, end); } /**
* @Author Nxy
* @Date 2019/12/4
* @Param
* @Return
* @Exception
* @Description 并归 begin--mid 与 mid+1--end 两个子序列
*/
public static void merge(int[] nums, int begin, int mid, int end) {
//临时数组大小
int length = end - begin + 1;
int[] temp = new int[length];
//临时数组将要填充的位置指针
int i = 0;
//左子序列将要拿出的位置指针
int left = begin;
//右子序列将要拿出的位置指针
int right = mid + 1;
while (i < length) {
//一个子序列为空,将另一个子序列余下的元素放入临时数组
if (left == mid + 1) {
System.arraycopy(nums, right, temp, i, end - right + 1);
break;
}
if (right == end + 1) {
System.arraycopy(nums, left, temp, i, mid - left + 1);
break;
}
//选择较小的元素放入临时数组
if (nums[left] >= nums[right]) {
temp[i] = nums[right];
right++;
i++;
} else {
temp[i] = nums[left];
left++;
i++;
}
}
System.arraycopy(temp, 0, nums, begin, length);
//手动为临时数组去掉引用,方便连续的内存空间被及时回收
temp=null;
}

  链表的并归排序与数组一个思路:

  /**
* @Author Nxy
* @Date 2019/12/4
* @Param
* @Return
* @Exception
* @Description 链表并归排序
* 递归分解序列为两个子序列,并向上并归排序,返回排序后的总链表
* 使用快慢指针法,快指针到终点时慢指针指向中点
*/
public static ListNode mergeSort(ListNode head) {
//回归条件
if (head.getNext() == null) {
return head;
}
//快指针,考虑到链表为2时的情况,fast比slow早一格
ListNode fast = head.getNext();
//慢指针
ListNode slow = head;
//快慢指针开跑
while (fast != null && fast.getNext() != null) {
fast = fast.getNext().getNext();
slow = slow.getNext();
}
//找到右子链表头元素,复用fast引用
fast = slow.getNext();
//将中点后续置空,切割为两个子链表
slow.setNext(null);
//递归分解左子链表,得到新链表起点
head = mergeSort(head);
//递归分解右子链表,得到新链表起点
fast = mergeSort(fast);
// System.out.println(head.getValue()+" "+fast.getValue());
//并归两个子链表
ListNode newHead = merge(head, fast);
// ListNode.print(newHead);
return newHead;
} /**
* @Author Nxy
* @Date 2019/12/4 14:48
* @Param
* @Return
* @Exception
* @Description 以left节点为起点的左子序列 及 以right为起点的右子序列 并归为一个有序序列并返回头元素;
* 传入的 left 及 right 都不可为 null
*/
public static ListNode merge(ListNode left, ListNode right) {
//维护临时序列的头元素
ListNode head;
if (left.getValue() <= right.getValue()) {
head = left;
left = left.getNext();
} else {
head = right;
right = right.getNext();
}
//两个子链表均存在剩余元素
ListNode temp = head;
while (left != null && right != null) {
//将较小的元素加入临时序列
if (left.getValue() <= right.getValue()) {
temp.setNext(left);
left = left.getNext();
temp = temp.getNext();
} else {
temp.setNext(right);
right = right.getNext();
temp = temp.getNext();
}
}
//左子序列用完将右子序列余下元素加入临时序列
if (left == null) {
temp.setNext(right);
}
//右子序列用完将左子序列余下元素加入临时序列
if (right == null) {
temp.setNext(left);
}
ListNode.print(head);
return head;
}

  

JAVA并归排序(数组+链表)的更多相关文章

  1. 数据结构java(一)数组链表

    链表是数据结构中最基础的内容,链表在存储结构上分成两种:数组形式储存,链式存储. 相比c语言需要的结构体,在java中由于有了面向对象编程,将指针‘藏’了起来,不需要分配内存. 所以只需要创建一个对象 ...

  2. 使用排序数组/链表/preorder构建二叉搜索树

    2018-08-13 11:29:05 一.Convert Sorted Array to Binary Search Tree 问题描述: 问题求解: public TreeNode sortedA ...

  3. 算法练习之合并两个有序链表, 删除排序数组中的重复项,移除元素,实现strStr(),搜索插入位置,无重复字符的最长子串

    最近在学习java,但是对于数据操作那部分还是不熟悉 因此决定找几个简单的算法写,用php和java分别实现 1.合并两个有序链表 将两个有序链表合并为一个新的有序链表并返回.新链表是通过拼接给定的两 ...

  4. 【Java】 剑指offer(25) 合并两个排序的链表

    本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集   题目 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照 ...

  5. 【Java】 剑指offer(53-1) 数字在排序数组中出现的次数

    正文 本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集   题目 统计一个数字在排序数组中出现的次数.例如输入排序数组{1, ...

  6. LeetCode第[4]题(Java):Median of Two Sorted Arrays (俩已排序数组求中位数)——HARD

    题目难度:hard There are two sorted arrays nums1 and nums2 of size m and n respectively. Find the median ...

  7. C++:探究纯虚析构函数以及实现数组的高速排序与链表的归并排序

    C++:探究纯虚析构函数以及实现数组的高速排序与链表的归并排序 标签: 数据结构 数组 链表 高速排序 归并排序 抽象类 虚继承 by 小威威 1.介绍 本篇博文将通过课后作业的(15 C++ Hom ...

  8. 剑指Offer-36.数字在排序数组中出现的次数(C++/Java)

    题目: 统计一个数字在排序数组中出现的次数. 分析: 给定一个已经排好序的数组,统计一个数字在数组中出现的次数. 那么最先想到的可以遍历数组统计出现的次数,不过题目给了排序数组,那么一定是利用了排序这 ...

  9. 用java刷剑指offer(数字在排序数组中出现的次数)

    题目描述 统计一个数字在排序数组中出现的次数. 牛客网链接 java代码 //看见有序就用二分法 public class Solution { public int GetNumberOfK(int ...

  10. Java基础语法(8)-数组中的常见排序算法

    title: Java基础语法(8)-数组中的常见排序算法 blog: CSDN data: Java学习路线及视频 1.基本概念 排序: 是计算机程序设计中的一项重要操作,其功能是指一个数据元素集合 ...

随机推荐

  1. 小白专场-堆中的路径-c语言实现

    目录 一.题意理解 二.堆的表示及其操作 三.主程序 更新.更全的<数据结构与算法>的更新网站,更有python.go.人工智能教学等着你:https://www.cnblogs.com/ ...

  2. python-2-条件判断

    前言 python3当中的条件语句是非常简单简洁的,说下这两种:if 条件.while 条件. 一.if 条件语句 1.if 语句: # 如果条件成立,打印666 if True: print(666 ...

  3. Eureka服务注册中心错误:com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused: connect

    报错信息 14:43:45.484 [main] INFO com.netflix.discovery.DiscoveryClient - Getting all instance registry ...

  4. linux中dd命令详解

    本文转自:https://www.cnblogs.com/yuanqiangfei/p/9138625.html 一.dd命令的解释 dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换. ...

  5. axios 源码解析(下) 拦截器的详解

    axios的除了初始化配置外,其它有用的应该就是拦截器了,拦截器分为请求拦截器和响应拦截器两种: 请求拦截器    ;在请求发送前进行一些操作,例如在每个请求体里加上token,统一做了处理如果以后要 ...

  6. Mysql 常用数据类型 占用字节数 [转]

    数据类型是定义列中可以存储什么数据以及该数据实际怎么存储的基本规则.Mysql的常用数据类型主要有: 串数据类型:最常用的数据类型,有两种基本的串类型:分别为定长串和不定长串.定长串结束长度固定的字符 ...

  7. jmeter入门操作 = 接口

    1.没安装工具的朋友,不熟悉菜单插件使用的朋友,请先看:https://www.cnblogs.com/beile/p/11007754.html 2.开始调用http请求 测试地址:http://h ...

  8. PHP工作岗位要求

    初级PHP 企业对初级PHP的要求是,在日常工作中,保证编码质量,对一般问题具有解决能力. 1.团队合作:经常是Git或者SVN.主要是为了能够融入敏捷开发团队2.前端:HTML.CSS.JS要精通. ...

  9. CSS animation属性

    定义和用法 animation属性是下列属性的一个缩写属性: animation-name animation-duration animation-timing-function animation ...

  10. 前端开发JS——数组

    25.数组 1)声明数组: ①构造函数创建数组 var arr = new Array(); console.log(arr):        //[]   var arr = new Array(2 ...