并归排序与快速排序相似,靠分治思想突破了排序算法 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. 宝塔面板+djiango+mod wsgi +apache 配置多项目站点

    在一台服务器上同时有多个djiango项目,那么就需要配置多站点,利用不同的域名不同的端口  同时存在多个项目. 环境,centos + 宝塔面板+apache+django 1.在centos环境下 ...

  2. 无聊系列 - C#中一些常用类型与java的类型对应关系

    昨天在那个.NET转java群里,看到一位朋友在问C#的int 对应java的哪个对象,就心血来潮,打算写一下C#中一些基础性的东西,在java中怎么找. 1. 基础值类型 如:int,long,do ...

  3. 解决 Github 图片加载慢的问题

    一.前言 本文主要介绍一种解决 Github 图片加载慢的方法,亲测有效. 笔者博客是使用 Github 作为图床,每次打开博客时的图片加载很慢或者根本加载不出来.这是因为 GitHub 的 CDN ...

  4. Javal连载4-注释&class与public class区别

    一.Java注释 1.作用:不会编译倒.class文件之中:增强可读性 2.分类: (1)单行注释(只注释当前行):// (2)多行注释: /* 注释 注释 注释 */ (3)javadoc注释 /* ...

  5. redis命令之 ----SortedSed(有序集合)

    ZADD ZADD key score member [[score member] [score member] ...] 将一个或多个 member 元素及其 score 值加入到有序集 key  ...

  6. 云原生生态周报 Vol.9| K8s v1.15 版本发布

    本周作者 | 衷源.心贵 业界要闻 1.Kubernetes Release v1.15 版本发布,新版本的两个主题是持续性改进和可扩展性.(https://github.com/kubernetes ...

  7. C++回调,函数指针

    想要理解回调机制,先要理解函数指针 函数指针 函数指针指向的是函数而非对象,和其他指针一样,函数指针指向某种特定的类型 函数的类型由他的返回类型和参数类型共同决定,与函数名无关,如: bool len ...

  8. Kafka学习笔记之Kafka High Availability(上)

    0x00 摘要 Kafka在0.8以前的版本中,并不提供High Availablity机制,一旦一个或多个Broker宕机,则宕机期间其上所有Partition都无法继续提供服务.若该Broker永 ...

  9. MVC三层架构搭建

    MVC三层架构搭建 项目主要是用三层来搭建项目,三层分为表现层,数据层和业务层.项目用了目前比较流行的IOC架构.目前流行的IoC 框架有AutoFac,Unity,Spring.NET等,项目中选用 ...

  10. 【spring】自定义注解 custom annotation

    自定义注解 custom annotation 使用场景 类属性自动赋值. 验证对象属性完整性. 代替配置文件功能,像spring基于注解的配置. 可以生成文档,像java代码注释中的@see,@pa ...