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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构