JAVA并归排序(数组+链表)
并归排序与快速排序相似,靠分治思想突破了排序算法 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) 。而并归排序采用分治的思想将问题的规模缩小,使用小问题的解来解决大问题,并由此突破了 n2 的诅咒。
以冒泡排序为例,我们需要n次遍历,每次遍历将数组中最大或者最小的元素冒到顶端,而这样的遍历需要 n-1 次。本质上每次遍历等于从所有元素中找到最大或者最小的元素,这就要求我们需要遍历和比较到数组中未排序的每一个元素。
所以冒泡排序的计算次数为 n-1 + n-2 + n-3 +...+1 = n(n+1)/2 ,时间复杂度表示为 O(n2)。
那么我们想一下,如果我们不是对一个杂乱的序列进行排序,而是对两个有序的子序列进行排序的话情况会是怎样的:
我们可以维护两个指针分别指向两个子序列的顶端,选择较小的元素放入新的序列,并向后移动指向拿走的元素的指针。这样我们从未排序的元素中选出一个最小或最大的数只要比较一次。
我们可以不断的缩小排序序列的范围来构建有序的子序列,从下向上一层一层逐步完成对整个序列的排序。
缩小的排序范围的过程是这样的,不断的将序列分解为俩个子序列,直到序列无法分解。比如一个序列长度为8:
两个长度为4的子序列--->四个长度为2的子序列---->八个长度为1的子序列。
分解过程就像一颗 B树 向下分裂(不同的是分裂时父节点不变),第 n 层的拥有 2n 个节点,也就是说直到每个节点中只包含一个元素时共分裂 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并归排序(数组+链表)的更多相关文章
- 数据结构java(一)数组链表
链表是数据结构中最基础的内容,链表在存储结构上分成两种:数组形式储存,链式存储. 相比c语言需要的结构体,在java中由于有了面向对象编程,将指针‘藏’了起来,不需要分配内存. 所以只需要创建一个对象 ...
- 使用排序数组/链表/preorder构建二叉搜索树
2018-08-13 11:29:05 一.Convert Sorted Array to Binary Search Tree 问题描述: 问题求解: public TreeNode sortedA ...
- 算法练习之合并两个有序链表, 删除排序数组中的重复项,移除元素,实现strStr(),搜索插入位置,无重复字符的最长子串
最近在学习java,但是对于数据操作那部分还是不熟悉 因此决定找几个简单的算法写,用php和java分别实现 1.合并两个有序链表 将两个有序链表合并为一个新的有序链表并返回.新链表是通过拼接给定的两 ...
- 【Java】 剑指offer(25) 合并两个排序的链表
本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集 题目 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照 ...
- 【Java】 剑指offer(53-1) 数字在排序数组中出现的次数
正文 本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集 题目 统计一个数字在排序数组中出现的次数.例如输入排序数组{1, ...
- 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 ...
- C++:探究纯虚析构函数以及实现数组的高速排序与链表的归并排序
C++:探究纯虚析构函数以及实现数组的高速排序与链表的归并排序 标签: 数据结构 数组 链表 高速排序 归并排序 抽象类 虚继承 by 小威威 1.介绍 本篇博文将通过课后作业的(15 C++ Hom ...
- 剑指Offer-36.数字在排序数组中出现的次数(C++/Java)
题目: 统计一个数字在排序数组中出现的次数. 分析: 给定一个已经排好序的数组,统计一个数字在数组中出现的次数. 那么最先想到的可以遍历数组统计出现的次数,不过题目给了排序数组,那么一定是利用了排序这 ...
- 用java刷剑指offer(数字在排序数组中出现的次数)
题目描述 统计一个数字在排序数组中出现的次数. 牛客网链接 java代码 //看见有序就用二分法 public class Solution { public int GetNumberOfK(int ...
- Java基础语法(8)-数组中的常见排序算法
title: Java基础语法(8)-数组中的常见排序算法 blog: CSDN data: Java学习路线及视频 1.基本概念 排序: 是计算机程序设计中的一项重要操作,其功能是指一个数据元素集合 ...
随机推荐
- CentOS6.9安装SonarQube7.6
1 安装前准备 Java (Oracle JRE 8 or OpenJDK 8) MySQL5.6 or MySQL5.7,具体可参考Centos6.9安装MySQL5.6 SonarQube7.6, ...
- Unreal Engine 4 系列教程 Part 6:动画教程
.katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...
- Vue全选和全不选
HTML代码: <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script> ...
- mysql8 安装
准备工作: 首先安装这些依赖 yum install -y flex yum install gcc gcc-c++ cmake ncurses ncurses-devel bison libaio ...
- spring的@primary和@qualifier注解解决一个接口多个实现的注入问题
Spring中提供了@Primary和@Qualifier注解来解决一个接口多个实现的注入问题. @Primary注解 Spring中有提供一个@Primary注解,具体的作用是在一个接口有多个实现类 ...
- Autoware 培训笔记 No. 1——构建点云地图
1. 首记 相信许多刚开始玩无人驾驶的人都用过Autoware,对runtime manager都比较熟悉,虽然可以通过各种渠道了解到有些设置,甚至有些设置的app下参数的含义,但是,在真车的使用过程 ...
- 统一批处理流处理——Flink批流一体实现原理
实现批处理的技术许许多多,从各种关系型数据库的sql处理,到大数据领域的MapReduce,Hive,Spark等等.这些都是处理有限数据流的经典方式.而Flink专注的是无限流处理,那么他是怎么做到 ...
- js和C#互相调用
快速上手 js和C#互相调用. C#调用js比较容易.JS调用C#代码,现有两种方法.老方法的缺点是只支持单页,如果切换页面,原有创建的变量就失效了.新方法没有这些问题. 老方法: Cefsharp ...
- 夜神模拟器怎么连接adb
夜神模拟器连接不了adb的原因主要是adb的版本与夜神模拟器adb版本不一致造成的,具体的解决办法请看下面的操作步骤. 工具/原料 电脑 安装了夜神模拟器 方法/步骤 1 使用快捷键win + ...
- colmap编译过程中出现,无法解析的外部符号错误 “__cdecl google::base::CheckOpMessageBuilder::ForVar1(void)”
错误提示: >colmap.lib(matching.obj) : error LNK2019: 无法解析的外部符号 "__declspec(dllimport) public: cl ...