【数据结构】八大排序(超详解+附动图+源码)_数据结构排序-CSDN博客
看这个学思路
一 归并排序介绍:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
二 实现思路
-
分解(Divide):
- 将待排序的序列分割成若干个子序列,这些子序列可以是顺序的,也可以是随机的。
- 分解的标准通常是按照序列的中间位置或者某个特定的划分点来划分序列。
-
解决(Conquer):
- 递归地对每个子序列进行排序。
- 递归的基本情况是当子序列足够小,可以直接排序或者已经有序,不需要进一步分解。
-
合并(Combine):
- 将排序好的子序列合并成一个有序的完整序列。
- 合并操作的复杂度通常取决于具体的算法,例如归并排序中的合并操作需要将两个有序序列合并成一个有序序列。
三 代码实现
实现1
public class Main {
public static void main(String[] args) {
int[] arr = {11, 44, 23, 67, 88, 65, 34, 48, 9, 12}; // 待排序数组
int[] tmp = new int[arr.length]; // 新建一个临时数组,用于在归并过程中存放元素
mergeSort(arr, 0, arr.length - 1, tmp); // 调用归并排序方法,传入数组、起始索引、结束索引和临时数组
// 输出排序后的数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
// 归并排序方法,采用递归实现
private static void mergeSort(int[] arr, int left, int right, int[] tmp) {
if (left < right) { // 如果左索引小于右索引,说明当前区间内有多个元素,需要排序
int mid = (left + right) / 2; // 计算中间索引,将当前区间分为两半
// 对左边序列进行归并排序
mergeSort(arr, left, mid, tmp);
// 对右边序列进行归并排序
mergeSort(arr, mid + 1, right, tmp);
// 合并两个有序序列
merge(arr, left, mid, right, tmp);
}
}
// 归并方法,用于合并两个有序序列
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int tempBeginIndex = 0; // 临时数组的起始索引
int i = left; // 左边序列的起始索引
int j = mid + 1; // 右边序列的起始索引
// 合并两个有序序列到临时数组temp中
while (i <= mid && j <= right) { // 当左右序列都还有元素时
if (arr[i] <= arr[j]) { // 如果左边序列的元素小于等于右边序列的元素
temp[tempBeginIndex++] = arr[i++]; // 将左边序列的元素加入临时数组,并移动索引
} else {
temp[tempBeginIndex++] = arr[j++]; // 将右边序列的元素加入临时数组,并移动索引
}
}
// 拷贝左边序列剩余的元素到temp
while (i <= mid) {
temp[tempBeginIndex++] = arr[i++];
}
// 拷贝右边序列剩余的元素到temp
while (j <= right) {
temp[tempBeginIndex++] = arr[j++];
}
// 将temp中的元素拷贝回原数组arr,完成合并
for (int k = 0; k < tempBeginIndex; k++) {
arr[left + k] = temp[k];
}
}
}
实现2
主要就是合并的时候有区别。可以确保每次合并只用一次。
public class Main {
public static void main(String[] args) {
// 初始化一个整型数组,包含一些无序的元素
int[] arr = {1, 3, 2, 3, 1};
// 创建一个临时数组,用于在归并排序过程中临时存放数据
int[] tmp = new int[arr.length];
mergeSort(arr, 0, arr.length - 1, tmp);
// 打印排序后的数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
// 归并排序的主要方法,递归地将数组分为两半,直到无法再分
private static void mergeSort(int[] arr, int left, int right, int[] tmp) {
// 如果左边界小于右边界,说明还有元素可以继续划分
if (left < right) {
// 计算中间索引,将数组分为两半
int mid = (left + right) / 2;
// 递归地对左半部分进行归并排序
mergeSort(arr, left, mid, tmp);
// 递归地对右半部分进行归并排序
mergeSort(arr, mid + 1, right, tmp);
// 合并两个已排序的半部分
Merge(arr, left, mid, right, tmp);
}
}
// 合并两个有序数组的方法
public static void Merge(int record[], int left, int mid, int right, int temp[]) {
// 初始化两个指针,分别指向左右两个子数组的开始位置
int i = left;
int j = mid + 1;
// 将原数组中的元素复制到临时数组中,以便在合并过程中使用
for (int k = left; k <= right; k++) {
temp[k] = record[k];
}
// 合并过程,将两个有序子数组合并到原数组中
for (int k = left; k <= right; k++) {
// 如果左指针已经到达子数组的末尾,将右子数组的元素拷贝到原数组
if (i == mid + 1) {
record[k] = temp[j++];
}
// 如果右指针已经到达子数组的末尾,或者左子数组的当前元素小于等于右子数组的当前元素
// 则将左子数组的元素拷贝到原数组
else if (j == right + 1 || temp[i] <= temp[j]) {
record[k] = temp[i++];
}
// 如果右子数组的当前元素小于左子数组的当前元素,则将右子数组的元素拷贝到原数组
else {
record[k] = temp[j++];
}
}
}
}
实现3
这个是对单个链表的归并排序的案例
class Solution {
public ListNode sortList(ListNode head) {
// 如果链表为空或只有一个节点,直接返回,无需排序
if (head == null || head.next == null) {
return head;
}
// 使用快慢指针找到链表的中点
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针每次移动一步
fast = fast.next.next; // 快指针每次移动两步
}
// 将链表从中间断开,分成左右两部分
ListNode tmp = slow.next; // 保存右半部分的头节点
slow.next = null; // 断开链表
// 递归对左右两部分链表进行排序
ListNode left = sortList(head); // 对左半部分排序
ListNode right = sortList(tmp); // 对右半部分排序
// 合并两个已排序的链表
ListNode h = new ListNode(0); // 创建一个虚拟头节点,方便操作
ListNode res = h; // 保存合并后链表的头节点
while (left != null && right != null) {
// 比较左右链表的当前节点值,将较小的节点接到合并链表的后面
if (left.val < right.val) {
h.next = left; // 将左链表的当前节点接到合并链表
left = left.next; // 左链表指针后移
} else {
h.next = right; // 将右链表的当前节点接到合并链表
right = right.next; // 右链表指针后移
}
h = h.next; // 合并链表指针后移
}
// 如果其中一个链表还有剩余节点,直接接到合并链表的后面
h.next = left != null ? left : right;
// 返回合并后链表的头节点(去掉虚拟头节点)
return res.next;
}
}
实现4
合并 K 个升序链表
这个是对多个链表的归并排序的案例
/**
* Definition for singly-linked list.
* public class ListNode {
* int val; // 节点的值
* ListNode next; // 指向下一个节点的指针
* ListNode() {} // 默认构造函数
* ListNode(int val) { this.val = val; } // 带值的构造函数
* ListNode(int val, ListNode next) { this.val = val; this.next = next; } // 带值和下一个节点的构造函数
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 主函数,用于合并K个有序链表
// 使用分治法,递归地将链表数组分成两部分,分别合并后再合并结果
return mergeList(lists, 0, lists.length - 1);
}
ListNode mergeList(ListNode[] lists, int left, int right) {
// 分治法的递归函数,用于合并链表数组中从left到right的链表
if (left > right) {
// 如果left大于right,说明没有链表需要合并,返回null
return null;
}
if (left == right) {
// 如果left等于right,说明只有一个链表,直接返回该链表
return lists[left];
}
// 计算中间位置,将链表数组分成两部分
int mid = (left + right) >> 1;
// 递归合并左边部分和右边部分
ListNode res = mergeTwoLists(
mergeList(lists, left, mid), // 合并左边部分
mergeList(lists, mid + 1, right) // 合并右边部分
);
// 返回合并后的结果
return res;
}
ListNode mergeTwoLists(ListNode left, ListNode right) {
// 合并两个有序链表的函数
if (left == null || right == null) {
// 如果其中一个链表为空,直接返回另一个链表
return left == null ? right : left;
}
// 创建一个虚拟头节点,用于简化合并操作
ListNode head = new ListNode(0);
ListNode temp = head; // 用于遍历合并后的链表
ListNode leftPtr = left; // 指向左边链表的当前节点
ListNode rightPtr = right; // 指向右边链表的当前节点
// 遍历两个链表,直到其中一个链表为空
while (leftPtr != null && rightPtr != null) {
if (leftPtr.val >= rightPtr.val) {
// 如果左边链表的当前节点值大于等于右边链表的当前节点值
temp.next = rightPtr; // 将右边链表的当前节点接到合并链表的后面
rightPtr = rightPtr.next; // 移动右边链表的指针
temp = temp.next; // 移动合并链表的指针
} else if (leftPtr.val < rightPtr.val) {
// 如果左边链表的当前节点值小于右边链表的当前节点值
temp.next = leftPtr; // 将左边链表的当前节点接到合并链表的后面
leftPtr = leftPtr.next; // 移动左边链表的指针
temp = temp.next; // 移动合并链表的指针
}
}
// 如果左边链表或右边链表还有剩余节点,直接接到合并链表的后面
temp.next = leftPtr == null ? rightPtr : leftPtr;
// 返回合并后的链表(从虚拟头节点的下一个节点开始)
return head.next;
}
}
四 总结
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定