讲排序前,先来了解排序中一个不重要但又很重要的点:稳定性
如果排序中存在两个值相同的元素,排序前后,保证这两个值的相对位置不变,则说明此排序是稳定的
例:9 5 2(a) 8 7 2(b) 4
即排序后2(a)依旧在2(b)前面,则此排序算法为稳定排序
友情提示:若需要排序的动画演示,十分推荐B站UP主:"有个知识",个人认为其动画演示十分清晰易懂
大致链接:【两分钟认识插入排序】https://www.bilibili.com/video/BV1W841197rx?vd_source=9f4130957b41e56dead3e5dea7cdb26c
现在开始介绍各类排序方法
一,选择排序
public class SelectSort {
/**
* 选择排序算法实现
* 基本思想:将数组分为已排序区间(初始为0)和未排序区间,
* 通过比较的方式将未排序区间中的最小值(或最大值)放到已排序区间中去
* 当bound达到length-1时,整个数组就排序完毕了
*
* 时间复杂度:O(n²)
* 空间复杂度:O(1)
* 不稳定排序(因为交换可能改变相等元素的相对位置)
*
*/
private static void selectSort(int[] arr) {
// 外层循环控制已排序区间的边界
// 当bound达到arr.length-1时,排序完成(最后一个元素无需比较)
for (int bound = 0; bound < arr.length - 1; bound++) {
// 内层循环遍历未排序区间
for (int cur = bound + 1; cur < arr.length; cur++) {
// 如果发现未排序区间中有比当前bound位置更小的元素
if (arr[cur] < arr[bound]) {
// 交换这两个元素的位置
int temp = arr[cur];
arr[cur] = arr[bound];
arr[bound] = temp;
}
}
// 每轮循环结束后,bound位置的元素就是未排序区间中的最小值
// 已排序区间扩大1
}
}
}
二,插入排序
public class InsertSort {
/**
* 插入排序算法实现
* 基本思想:将数组分为有序区间和无序区间,每次从无序区间取出第一个元素,
* 在有序区间中找到合适的位置插入
*
* 时间复杂度:
* - 最好情况(已有序):O(n)
* - 最坏情况(逆序):O(n²)
* - 平均:O(n²)
* 空间复杂度:O(1)
* 稳定排序(相等元素不会改变相对位置)
*
*/
private static void insertSort(int[] arr) {
// bound表示有序区间和无序区间的分界
// 初始时有序区间只有第一个元素(arr[0]),所以bound从1开始
for (int bound = 1; bound < arr.length; bound++) {
// 保存当前待插入的元素值
int value = arr[bound];
// cur用于在有序区间中从后往前查找插入位置
int cur = bound - 1;
// 在有序区间中寻找合适的插入位置
for (; cur >= 0; cur -= 1) {
// 如果当前元素比待插入元素大,则向后移动
if (arr[cur] > value) {
arr[cur + 1] = arr[cur];
} else {
// 找到合适位置(前一个元素<=value),跳出循环
break;
}
}
// 将待插入元素放到正确位置
arr[cur + 1] = value;
}
}
}
三,希尔排序
注意:希尔排序属于插入排序的变种或者说优化版
倘若只是为了方便记忆,可以直接把插入排序中的bound - 1,cur -= 1,cur + 1中的1全部改为gap即可
package Sort;
import java.util.Arrays;
public class ShellSort {
/**
* 希尔排序(缩小增量排序)
* 基本思想:将数组按照增量gap分组,对每组进行插入排序,
* 然后逐步缩小gap直到1,最后进行一次完整的插入排序
*
* 时间复杂度:取决于增量序列,最好可达O(n log²n)
* 空间复杂度:O(1)
* 不稳定排序(分组插入可能改变相等元素的相对位置)
*
*/
private static void shellSort(int[] arr) {
// 初始化gap为数组长度,使用希尔增量序列(gap = gap/2)
int gap = arr.length;
// 当gap >= 1时继续排序
while (gap >= 1) {
// 对当前gap值进行分组插入排序
insertSortGap(arr, gap);
// 缩小gap值
gap /= 2;
}
}
/**
* 按指定gap值进行分组插入排序
*
*/
private static void insertSortGap(int[] arr, int gap) {
// bound从gap开始,表示每个分组的第二个元素(第一个元素视为已排序)
for (int bound = gap; bound < arr.length; bound++) {
// 保存当前待插入的元素值
int value = arr[bound];
// cur指向同组前一个元素
int cur = bound - gap;
// 在组内向前查找插入位置
for (; cur >= 0; cur -= gap) {
if (arr[cur] > value) {
// 前移较大的元素
arr[cur + gap] = arr[cur];
} else {
// 找到合适位置,停止查找
break;
}
}
// 插入元素到正确位置
arr[cur + gap] = value;
}
}
}
四,堆排序
/**
* 堆排序算法实现
* 基本思想:利用堆这种数据结构进行排序,分为建堆和排序两个阶段
*
* 时间复杂度:O(n log n)
* 空间复杂度:O(1)
* 不稳定排序(堆调整过程可能改变相等元素的相对位置)
*/
public static void heapSort(int[] arr) {
// 1. 构建初始大顶堆(将数组调整为大顶堆结构)
createHeap(arr);
// 2. 排序阶段:循环将堆顶元素与待排序区末尾元素交换,并调整堆
// [0, bound]为待排序区间
int bound = arr.length - 1; // bound表示待排序区间的末尾边界
// 共需要n-1次交换和调整操作
for (int i = 0; i < arr.length - 1; i++) {
// 将堆顶元素(当前最大值)与待排序区末尾交换
swap(arr, 0, bound);
// 对新的堆顶元素进行下沉操作,重新调整堆结构
// 调整范围是[0, bound),bound位置已经是当前最大值
shiftDown(arr, bound, 0);
// 缩小待排序区间
bound--;
}
}
/**
* 构建大顶堆
* @param arr 待构建的数组
*/
private static void createHeap(int[] arr) {
// 最后一个叶子节点的下标
int lastLeaf = arr.length - 1;
// 最后一个非叶子节点的下标(完全二叉树性质)
int lastNonLeaf = (lastLeaf - 1) / 2;
// 从最后一个非叶子节点开始,向前依次进行下沉操作
for (int i = lastNonLeaf; i >= 0; i--) {
shiftDown(arr, arr.length, i);
}
}
/**
* 下沉操作(调整堆结构)
*/
private static void shiftDown(int[] arr, int length, int index) {
//length为当前堆的有效长度,index为需要下沉的节点下标
int parent = index; // 当前父节点
int child = 2 * parent + 1; // 左孩子节点
// 当孩子节点在堆范围内时循环
while (child < length) {
// 如果右孩子存在且比左孩子大,则选择右孩子
if (child + 1 < length && arr[child + 1] > arr[child]) {
child = child + 1;
}
// 如果孩子节点大于父节点,则交换
if (arr[child] > arr[parent]) {
swap(arr, child, parent);
// 继续向下检查
parent = child;
child = 2 * parent + 1;
} else {
// 如果父节点已经比孩子大,则调整结束
break;
}
}
}
五,快速排序
/**
* 快速排序算法实现(递归版本)
* 基本思想:通过一趟排序将待排记录分隔成独立的两部分,
* 其中一部分记录的关键字均比另一部分的关键字小,
* 然后分别对这两部分记录继续进行排序,以达到整个序列有序
*
* 时间复杂度:
* - 平均:O(n log n)
* - 最坏:O(n²)(当数组已经有序时)
* 空间复杂度:O(log n)(递归调用栈)
* 不稳定排序(交换操作可能改变相等元素的相对位置)
*/
private static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
/**
* 快速排序递归辅助方法
* @param arr 待排序数组
* @param left 当前处理区间的左边界(包含)
* @param right 当前处理区间的右边界(包含)
*/
private static void quickSort(int[] arr, int left, int right) {
// 递归终止条件:区间长度小于等于1
if (left >= right) {
return;
}
// 分区操作,返回基准值最终位置
int index = partition(arr, left, right);
// 递归处理左半部分
quickSort(arr, left, index - 1);
// 递归处理右半部分
quickSort(arr, index + 1, right);
}
/**
* 分区操作(挖坑填数法)
* @param arr 待排序数组
* @param left 区间左边界
* @param right 区间右边界(基准值位置)
* @return 基准值的最终位置
*/
private static int partition(int[] arr, int left, int right) {
// 选择最右侧元素作为基准值
int pivot = arr[right];
int l = left; // 左指针
int r = right; // 右指针
while (l < r) {
// 从左往右找第一个大于基准值的元素
while (l < r && arr[l] <= pivot) {
l++;
}
// 从右往左找第一个小于基准值的元素
while (l < r && arr[r] >= pivot) {
r--;
}
// 交换这两个元素
swap(arr, l, r);
}
// 将基准值放到最终位置(l和r相遇的位置)
swap(arr, l, right);
// 返回基准值的最终位置
return l;
}
特别:非递归,用栈实现的快排
/**
* 区间范围类(用于非递归实现)
*/
static class Range {
public int left;
public int right;
public Range(int left, int right) {
this.left = left;
this.right = right;
}
}
/**
* 快速排序非递归实现(使用栈模拟递归)
* @param arr 待排序数组
*/
public static void quickSortByLoop(int[] arr) {
Stack<Range> stack = new Stack<>();
// 初始压入整个数组范围
stack.push(new Range(0, arr.length - 1));
while (!stack.isEmpty()) {
Range range = stack.pop();
// 区间长度小于等于1时跳过
if (range.left >= range.right) {
continue;
}
// 分区操作
int index = partition(arr, range.left, range.right);
// 将右半区间先压栈(保证左半区间先处理)
stack.push(new Range(index + 1, range.right));
// 将左半区间压栈
stack.push(new Range(range.left, index - 1));
}
}
六,归并排序
/**
* 归并排序(递归版本)
* 基本思想:采用分治法,将数组分成两半分别排序,然后将排序好的两部分合并
*
* 时间复杂度:O(n log n)
* 空间复杂度:O(n)(需要额外临时数组)
* 稳定排序(合并时相等元素不会改变相对位置)
*/
private static void mergeSort(int[] arr) {
mergeSort(arr, 0, arr.length - 1);
}
/**
* 递归归并排序辅助方法
* @param arr 待排序数组
* @param left 当前处理区间的左边界(包含)
* @param right 当前处理区间的右边界(包含)
*/
private static void mergeSort(int[] arr, int left, int right) {
// 递归终止条件:区间长度小于等于1
if (left >= right) {
return;
}
// 计算中间位置
int mid = (left + right) / 2;
// 递归排序左半部分
mergeSort(arr, left, mid);
// 递归排序右半部分
mergeSort(arr, mid + 1, right);
// 合并两个有序区间
merge(arr, left, mid, right);
}
/**
* 合并两个有序子数组
* @param arr 原始数组
* @param left 左区间起始位置
* @param mid 左区间结束位置(右区间起始位置为mid+1)
* @param right 右区间结束位置
*/
private static void merge(int[] arr, int left, int mid, int right) {
// 1. 创建临时数组保存合并结果
int[] result = new int[right - left + 1];
int resultSize = 0; // 记录已插入元素个数
// 2. 设置两个指针分别指向两个区间的起始位置
int cur1 = left; // 左区间指针
int cur2 = mid + 1; // 右区间指针
// 3. 比较两个区间的元素,按顺序放入临时数组
while (cur1 <= mid && cur2 <= right) {
if (arr[cur1] <= arr[cur2]) {
result[resultSize++] = arr[cur1++];
} else {
result[resultSize++] = arr[cur2++];
}
}
// 4. 处理剩余元素(左区间或右区间可能还有剩余元素)
while (cur1 <= mid) {
result[resultSize++] = arr[cur1++];
}
while (cur2 <= right) {
result[resultSize++] = arr[cur2++];
}
// 5. 将合并结果拷贝回原数组
System.arraycopy(result, 0, arr, left, result.length);
// 相当于for(int i = 0;i< result.length;i++){
// arr[left+i] = result[i];
// }
}
特别:非递归版本的归并排序
/**
* 归并排序(非递归版本)
* 基本思想:自底向上,先两两合并,再四四合并,直到整个数组有序
*
* @param arr 待排序数组
*/
private static void mergeSortByLoop(int[] arr) {
// 外层循环控制合并的数组大小,从1开始,每次翻倍
for (int size = 1; size < arr.length; size *= 2) {
// 内层循环处理每个合并对
for (int i = 0; i < arr.length; i += size * 2) {
// 计算当前合并的两个子数组边界
int left = i;
int mid = i + size - 1;
// 处理边界情况,防止数组越界
if (mid >= arr.length - 1) {
mid = arr.length - 1;
}
int right = i + size * 2 - 1;
if (right >= arr.length - 1) {
right = arr.length - 1;
}
// 合并这两个子数组
merge(arr, left, mid, right);//merge方法重用上面的就行
}
}
}

被折叠的 条评论
为什么被折叠?



