并归排序与快速排序相似,靠分治思想突破了排序算法 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. Worker Services的新项目模板

    .NET Core3.0创建Worker Services2019-10-24 09:05  成天  阅读(1438)  评论(20)  编辑收藏 .NET CORE 3.0新增了Worker Ser ...

  2. jquery ajax怎么使用jsonp跨域访问

    在项目中使用接口的比较多,在客户端跨域访问,jquery中只能使用jquery ajax的jsonp方法. 值得注意的是,jQuery.ajax()只支持get方式的跨域,post的方式是不支持的.& ...

  3. Prometheus 监控K8S集群资源监控

    Prometheus 监控K8S集群中Pod 目前cAdvisor集成到了kubelet组件内,可以在kubernetes集群中每个启动了kubelet的节点使用cAdvisor提供的metrics接 ...

  4. 1、Ext.NET 1.7官方示例笔记-事件

    官网参考地址:https://examples1.ext.net/#/Events/DirectEvents/Overview/ 先了解一下“事件” Ext.NET包括3种事件机制 DirectEve ...

  5. Java学习——单元测试JUnit

    Java学习——单元测试JUnit 摘要:本文主要介绍了什么是单元测试以及怎么进行单元测试. 部分内容来自以下博客: https://www.cnblogs.com/wxisme/p/4779193. ...

  6. Laravel 创建指定表 migrate

    解决方案:打开创建表的那个 migration 文件,在创建表的方法执行之前加一个判断条件 if (!Schema::hasTable('password_resets')) { Schema::cr ...

  7. android studio学习----gradle多渠道打包

    由于国内Android市场众多渠道,为了统计每个渠道的下载及其它数据统计,就需要我们针对每个渠道单独打包,如果让你打几十个市场的包岂不烦死了,不过有了Gradle,这再也不是事了. 友盟多渠道打包 废 ...

  8. Github使用教程图文详解

    最近几天发现有些人对Github网站很好奇,但是无奈自己不会用,因为是外国人的网站,首先自己的英文就不过关.对于这个,其实可以用谷歌浏览器去浏览Github,它有一键翻译的功能.但还是有必要介绍一下关 ...

  9. springBoot添加日志管理

    一. 近期自己的项目想要一个记录日志的功能,而springboot本身就内置了日志功能,然而想要输入想要的日志,并且输出到磁盘,然后按天归档,或者日志的切分什么的,自带的日志仅仅具有简单的功能,百度了 ...

  10. 变量、数据类型、python内存管理

    pycharm快捷键 ctrl + c 复制, 默认复制整行 ctrl + v 粘贴 ctrl + x 剪切 ctrl + a 全选 ctrl + z 撤销 ctrl + f 查找 ctrl + sh ...