八大排序算法
常用的排序算法的对比
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 第i趟结束后的特点 | 适用场景 |
---|---|---|---|---|---|
插入排序 | O(N^2) | O(1) | 稳定 | 前i+1个元素是相对有序序列 | 大部分已排序较好 |
希尔排序 | O(N^2) | O(1) | 不稳定 | 对于任意x保证A[x] <= A[x+d] | N大时较好 |
冒泡排序 | O(N^2) | O(1) | 稳定 | 保证i个元素放入了最终位置 | N小时较好 |
快速排序 | N*O(logN) | O(N) | 不稳定 | 确定i个枢值放在最终位置 | N大时较好 |
选择排序 | O(N^2) | O(1) | 稳定 | 保证i个元素放入了最终位置 | N小时较好 |
堆排序 | N*O(logN) | O(1) | 不稳定 | 保证i个元素放入了最终位置 | N大时较好 |
归并排序 | N*O(logN) | O(N) | 稳定 | 每2^i个元素为一组,组内相对有序 | N大时较好 |
基数排序 | O(d(n+r)) | O(d) | 稳定 | 只看最低的i位,已完成排序 |
冒泡排序
:只有当 arr[i] > arr[i+1] 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法。
选择排序
:选择排序是给每个位置选择当前元素最小的,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了,所以选择排序是一种不稳定的排序算法。
插入排序
:比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
希尔排序
:希尔排序是按照不同步长对元素进行插入排序 ,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
归并排序
:归并排序在归并的过程中,只有 arr[i] < arr[i+1] 的时候才会交换位置,如果两个元素相等则不会交换位置,所以它并不会破坏稳定性,归并排序是稳定的。
快速排序
:快速排序需要一个基准值,在基准值的右侧找一个比基准值小的元素,在基准值的左侧找一个比基准值大的元素,然后交换这两个元素,此时会破坏稳定性,所以快速排序是一种不稳定的算法。
冒泡排序
排序原理:最值往一端移动
- 比较相邻的元素。如果前一个元素比后一个元素大,就交换这两个元素的位置。
- 对每一对相邻元素做同样的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大值。
/**
* 冒泡排序
*
* @author 文轩
* @create 2023-12-30 17:53
*/
public class BubbleSort {
public static void sortArray(int[] nums) {
// 数据校验
if(nums == null || nums.length == 0 || nums.length == 1) return;
for (int i = 0; i < nums.length - 1; i++) {
for (int j = 0; j < nums.length - i - 1; j++) {
if(nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
}
选择排序
排序原理:找出各个位置上合适的值
- 每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索引处的值为最小值,最后可以找到最小值所在的索引。
- 交换第一个索引处和最小值所在的索引处的值。
/**
* 选择排序
*
* @author 文轩
* @create 2023-12-30 18:01
*/
public class ChoiceSort {
public static void sortArray(int[] nums) {
if(nums == null || nums.length == 0 || nums.length == 1) return;
for (int i = 0; i < nums.length - 1; i++) {
int minIndex = i;
for (int j = i; j < nums.length; j++) {
if(nums[j] < nums[minIndex]) {
minIndex = j;
}
}
int temp = nums[minIndex];
nums[minIndex] = nums[i];
nums[i] = temp;
}
}
}
插入排序
排序原理:无序插入有序
- 把所有的元素分为两组,已经排序的和未排序的;
- 找到未排序的组中的第一个元素,向已经排序的组中进行插入;
- 倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位。
/**
* 插入排序
*
* @author 文轩
* @create 2024-01-02 9:13
*/
public class InsertSort {
public static void sortArray(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int curValue = nums[i];
int curIndex = i - 1;
while(curIndex >= 0 && curValue < nums[curIndex]) {
nums[curIndex + 1] = nums[curIndex];
curIndex--;
}
nums[curIndex + 1] = curValue;
}
}
}
希尔排序
排序原理:分组插入
- 选定一个增长量 gap ,按照增长量 gap 作为数据分组的依据,对数据进行分组;
- 对分好组的每一组数据完成插入排序;
- 减小增长量,直到增量减为 1,重复第二步操作
/**
* 希尔排序
*
* @author 文轩
* @create 2024-01-02 11:21
*/
public class ShellSort {
public static void sortArray(int[] nums) {
int gap = nums.length / 2;
while (gap > 0) {
for (int i = 0; i < nums.length; i++) {
int curIndex = i - gap;
int curValue = nums[i];
while (curIndex >= 0 && nums[curIndex] > curValue) {
nums[curIndex + gap] = nums[curIndex];
curIndex -= gap;
}
nums[curIndex + gap] = curValue;
}
gap /= 2;
}
}
}
归并排序
排序原理:分治法,先分后治
- 尽可能的一组数据拆分成两个元素相同的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是 1 为止。
- 将相邻的两个子组进行合并成一个有序的大组;
- 不断的重复步骤2,直到最终只有一个组为止。
/**
* 归并排序
*
* @author 文轩
* @create 2024-01-03 9:11
*/
public class MergeSort {
public static void sortArray(int[] nums) {
int[] temp = new int[nums.length];
mergeSort(nums, 0, nums.length - 1, temp);
}
public static void mergeSort(int[] nums, int start, int end, int[] temp) {
if(start >= end) return;
int mid = (start + end) / 2;
// 将数组分成两份
mergeSort(nums, start, mid, temp);
mergeSort(nums, mid + 1, end, temp);
// 将两个有序数组合并
int i = start;
int j = mid + 1;
int index = start;
while(i <= mid && j <= end) {
temp[index++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
}
while(i <= mid) {
temp[index++] = nums[i++];
}
while(j <= end) {
temp[index++] = nums[j++];
}
for (int k = start; k <= end; k++) {
nums[k] = temp[k];
}
}
}
快速排序
排序原理:冒泡排序的改进,多次比较和排序
- 首先设定一个分界值,通过该分界值将数组分成左右两部分;
- 将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
- 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
- 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
/**
* 快速排序
*
* @author 文轩
* @create 2024-01-02 10:40
*/
public class QuickSort {
public static void sortArray(int[] nums, int start, int end) {
if(start >= end) return;
int pointValue = nums[start];
int left = start, right = end;
while(left != right) {
while(left < right && nums[right] >= pointValue) right--;
swap(nums, left, right);
while(left < right && nums[left] <= pointValue) left++;
swap(nums, left, right);
}
// 递归实现左右分组
sortArray(nums, start, left - 1);
sortArray(nums, right + 1, end);
}
public static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
堆排序
堆排序的基本思想:
- 将待排序的序列构造成一个大顶堆
- 从最后一个非叶子节点开始,以该节点为根节点形成的二叉树,将子节点和该节点中的最大值放到该节点处,这样子就形成了一个小的大顶堆;
- 一直到二叉树的根节点循环第1步操作,这样子大值一直在往上走,小值一直在往下走
- 等根节点也完成了第1步操作后,就形成了一个大顶堆
- 让第一个值【根节点的值】和最后一个元素的值交换,这样子就能让最大的值放到最后
- 将剩余 n - 1 个元素重新构造成一个大顶堆,重复 1、2步骤,就能得到一个升序的序列
注意:升序采用大顶堆,降序采用小顶堆
/**
* 堆排序
*
* @author 文轩
* @create 2024-01-03 14:52
*/
public class HeapSort {
/**
* 堆排序
* @param nums 要排序的数组
*/
public static void sortArray(int[] nums) {
// 构建大顶堆
build(nums, nums.length - 1);
for (int i = nums.length - 1; i > 0; i--) {
// 交换堆顶元素和末尾元素
swap(nums, 0, i);
// 重新调整对结构
adjust(nums, 0, i - 1);
}
}
/**
* 将数组构建成一个大顶堆
* @param nums 数组
* @param end 末尾范围
*/
public static void build(int[] nums, int end) {
// 最后一个非叶子节点开始从上往下进行构建
for (int i = (end - 1) / 2; i >= 0 ; i--) {
adjust(nums, i, end);
}
}
/**
* 调整指定范围构成的二叉树为大顶堆,前提是只有根元素不满足大顶堆的定义
* @param nums 原始数组
* @param start 开始范围
* @param end 结束范围
*/
public static void adjust(int[] nums, int start, int end) {
int maxIndex, left, right;
// 最后一个非叶子节点下标:(end - 1) / 2
for (int i = start; i <= (end - 1) / 2; i++) {
maxIndex = i;
// 找出i节点的左右子节点的下标
left = maxIndex * 2 + 1;
right = maxIndex * 2 + 2;
// 将左右子节点中较大值和父节点值交换
if(left <= end && nums[maxIndex] < nums[left]) maxIndex = left;
if(right <= end && nums[maxIndex] < nums[right]) maxIndex = right;
if(maxIndex != i) swap(nums, maxIndex, i);
}
}
/**
* 交换数组两个下标的值
* @param nums 数组
* @param i 下标
* @param j 下标
*/
public static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
基数排序
基数排序原理:非比较型整数排序算法
- 准备好十个桶【二维数组】,用于存放数组,按各个位的进行存放;
- 计算数组中各个数的个位,按个位【0~9】放到十个桶对应的桶中;
- 按桶的顺序将桶中的数字依次放回数组中;
- 重复 2~3 步骤,直到数组中最多数组的最多位数为止。
/**
* 基数排序
*
* @author 文轩
* @create 2024-01-04 10:59
*/
public class RadixSort {
public static void sortArray(int[] nums) {
if(nums == null || nums.length == 0 || nums.length == 1) return;
int length = nums.length;
// 计算所有数中最大值的长度
int maxValue = Arrays.stream(nums).max().getAsInt();
int maxLength = String.valueOf(maxValue).length();
// 准备十个桶
int[][] buckets = new int[10][length];
// 用于记录各个桶的数字数量
int[] bucketLen = new int[10];
for (int i = 0; i < maxLength; i++) {
int divisor = (int) Math.pow(10, i);
for (int j = 0; j < length; j++) {
// 求余数
int bucketIndex = (nums[j] / divisor) % 10;
// 将数字根据余数放到对应桶中
buckets[bucketIndex][bucketLen[bucketIndex]++] = nums[j];
}
// 将十个桶中的数据放回数组中
int index = 0;
for (int j = 0; j < buckets.length; j++) {
while(bucketLen[j] > 0) {
nums[index++] = buckets[j][--bucketLen[j]];
}
}
// 将桶长度清0
for (int j = 0; j < bucketLen.length; j++) {
bucketLen[j] = 0;
}
}
}
}