基本思想:所谓交换,就是根据序列中两个记录键值的⽐较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较⼤的记录向序列的尾部移动,键值较⼩的记录向序列的前部移动。

/**
* 冒泡排序:
* 时间复杂度:O(N^2)
* 加上优化之后,最好情况下-》O(N)
* 空间复杂度:O(1)
* 稳定性:稳定
* @param array
*/
public static void bubbleSort(int[] array) {
//i趟数
for (int i = 0; i < array.length-1; i++) {
//
boolean flg = false;
for (int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]){
swap(array,j,j+1);
flg = true;
}
}
if(!flg){
break;
}
}
}
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
/**
* 快速排序
* 时间复杂度: 最坏(有序/逆序):O(n^2) 最好:O(N*logN)
* 空间复杂度: 最坏:O(N) 最好: O(logN)
* 稳定性:不稳定
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
if(end-start+1 <= 15) {
insertSort(array,start,end);
return;
}
//三数取中
int index = mid_three(array,start,end);
swap(array,index,start);
int pivot = partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}// 假设按照升序对array数组中[left, right)区间中的元素进⾏排序
void QuickSort(int[] array, int left, int right) {
if(right - left <= 1)
return;
// 按照基准值对array数组的[left, right)区间中的元素进⾏划分
int div = partion(array, left, right);
QuickSort(array, left, div);
}
// 划分成功后以div为边界形成了左右两部分 [left, div) 和[div+1, right)
// 递归排[left, div)
// 递归排[div+1, right)
QuickSort(array, div+1, right);
左哨兵:是找到比第一个的值(6)也就做基准值小的数,就停到这个数字这(7),然后与右哨兵找到的交换位置,左哨兵接着走,继续找到比第一个大的数,一直按着这个规则,直到俩人相遇结束。相遇点记作:pivot , 基准值跟相遇点的值换,但是看这个划分的话不是很有序,从这个相遇的这个点(3),就把3的左边的接着按着这个规则寻找,又相遇了接着按着这个规则,直到左右哨兵起点是同一个结束。
右哨兵:是从后往前找是找到比基准值(6)小的数,就停到这个数字这(3),然后与左哨兵找到的交换位置(左右交换一次就行,我只是写下来了),右哨兵接着走,继续找到比6小的数,一直按着这个规则,直到俩人相遇结束。后面跟左子树是一样的 简洁来说:
分区(Partition):通过 Hoare 划分法,将数组重新排列为两部分:
递归排序:对基准左侧和右侧的子数组分别重复上述步骤,直至子数组长度为 1 或 0(自然有序)。
这个有点像二叉树,左边只有左子树,右边只有右子树 下标分为两种情况: 第一次相遇的左边: L:0 起点 R:pivot-1 第一次相遇的右边: L:pivot +1 R : array.length -1 那为什么从后往前? 准值的初始位置(第一个或最后一个元素)会决定指针的初始移动方向,其核心逻辑是一致的:通过调整指针移动顺序,确保最终相遇点的元素性质与基准值的初始位置匹配,从而在交换后将基准值正确放置在分区的分界点。 说白了就是我们是以第一个数为准基值,我们先找比他小的,如果相遇这个值一定是班比他小的,然后他俩交换位置, 反之,我们将最后一个数为准基值,我们就从前往后,找比它大的,交换,最后找到的值是比这个准基值大的,然后进行交换 再用更直白的话补充两句:
为什么 array[right] >= tmp是>= 与 array[left] <= tmp要写< >?
既然他是不完整的子树,我们需不需要限制它呢? 比如左边的子树的子树没有右边的子树”(即某个左分区的子分区只有左子数组、没有右子数组)是正常现象,本质上是由数组元素的分布和基准值的选择决定的,并不需要刻意 “限制”—— 因为快速排序的递归逻辑本身就通过 “终止条件” 自然处理了这种情况。 终止条件是:当子数组长度≤1 时,不再对其进行分区
/**
* 划分待排序的序列
* @param array
* @param left
* @param right
* @return
*/
private static int partition1(int[] array, int left, int right) {
int i = left;
int tmp = array[left];
while (left < right) {
//= 存在的原因是:**
//如果左右两边的值是一样的,那就是啥也没有交换
while (left < right && array[right] >= tmp) {
right--;
}
while (left < right && array[left] <= tmp) {
left++;
}
swap(array,left,right);
}
//将基准值与相遇点交换,让基准值回到他应该在的位置
swap(array,left,i);
return left;
}挖坑法单趟排序动图

注:此图借鉴博主是此链接的https://blog.csdn.net/gfdxx/article/details/126826128 选择基准值 以数组左侧第一个元素为基准值(tmp = array[left]),目标是将数组中所有 ≤ tmp 的元素移到左侧,≥ tmp 的元素移到右侧。 2. 双向指针移动与元素覆盖 使用 left(左指针)和 right(右指针)从数组两端向中间逼近,通过覆盖操作逐步划分区域:
while (left < right && array[right] >= tmp) {
right--; // 右指针左移,跳过所有≥基准的元素
}
array[left] = array[right]; // 将找到的≤基准的元素放到左指针位置右指针从最右侧出发,不断左移,直到找到第一个小于等于基准值的元素,然后将该元素 “搬运” 到左指针当前位置(此时左指针位置的原始值已被存为 tmp,不会丢失)。
while (left < right && array[left] <= tmp) {
left++; // 左指针右移,跳过所有≤基准的元素
}
array[right] = array[left]; // 将找到的≥基准的元素放到右指针位置左指针从左侧出发(已被覆盖为右指针找到的元素),不断右移,直到找到第一个大于等于基准值的元素,然后将该元素 “搬运” 到右指针当前位置。 循环重复:上述两步不断交替,直到 left == right(左右指针相遇),此时数组已被划分为两部分(左侧≤基准,右侧≥基准)。 3. 基准值归位 当 left == right 时,相遇位置就是基准值 tmp 在排序后应处的位置,将 tmp 放入该位置:
array[left] = tmp;
return left; // 返回基准值的最终位置,用于后续递归划分左右子数组代码如下:
* 快速排序
* 时间复杂度: 最坏(有序/逆序):O(n^2) 最好:O(N*logN)
* 空间复杂度: 最坏:O(N) 最好: O(logN)
* 稳定性:不稳定
private static int partition(int[] array, int left, int right) {
int tmp = array[left];
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= tmp) {
left++;
}
array[right] = array[left];
}
array[left] = tmp;
return left;
}该方法存在缺陷:
1、递归层数过多有爆栈风险 2、面对有序或者接近有序的待排序数据,时间复杂度就变成了O()
所以需要作如下优化:
1.4三数取中,优化选key 1、随机选key(听着很随机,虽然不靠谱,但有的场景还是可以使用随即选key的方法)
2、针对有序情况,选正中间数据做key(前提是知道有序)
3、三数取中(选出左中右三数中间大小的做key)(三数取中后,对于缺陷2,直接由最坏情况变成最好情况)


注:声明这个图还是借鉴博主是此链接的https://blog.csdn.net/gfdxx/article/details/126826128
// 递归排序方法
private static void quickSortRecursive(int[] array, int left, int right) {
// 终止条件:当子数组范围无效(left >= right)时,停止递归
if (left >= right) {
return;
}
// 调用 partition3 进行分区,获取基准值的最终位置
int pivotPos = partition3(array, left, right);
// 递归排序基准值左侧子数组(left 到 pivotPos - 1)
quickSortRecursive(array, left, pivotPos - 1);
// 递归排序基准值右侧子数组(pivotPos + 1 到 right)
quickSortRecursive(array, pivotPos + 1, right);
}
private static int partition3(int[] array, int left, int right) {
int prev = left ;
int cur = left+1;
//防止cur越界超出范围
while (cur <= right) {
//cur 寻找比基准值小的数停下来
//prev找到了prev+1
//prev 与 cur 之间一定都是比他大的数,然后交换即可
//array[++prev] != array[cur] 然后交换
//array[++prev] != array[cur])相等的话有可能就是循环了
if(array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
//此时prev 在与left交换
return prev;
}那这三种方法搞出来的排序一般都是不一样的,如果出选择题优先用挖坑法 ,Hoare , 前后指针法,一般会以挖坑法去考察你,

优化快速排序的核心目的是提升其在实际场景中的稳定性和效率
1.避免极端情况导致性能退化
快速排序的 “优化” 本质上是扬长避短:保留其平均 O(n log n) 时间复杂度、原地排序(空间效率高)的优点,同时通过针对性改进,解决基准选择不当、小规模数据低效、重复元素处理差等问题
快速排序优化核心思路:
private static void quick(int[] array, int start, int end) {
if (start >= end) {
return;
}
// 三数取中
int index = mid_three(array, start, end);
swap(array, index, start);
int pivot = partition(array, start, end);
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}
private static int mid_three(int[] array, int left, int right) {
int mid = (left+right)/2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if(array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if(array[mid] > array[left]) {
return left;
}else if(array[mid] < array[right]) {
return right;
}else {
return mid;
}
}
}但是这个优化的深度还是不够,如果他是[9,8,7,6,5],你在交换的完,右指针会进行很多的交换,还是有一定的问题的
1.对外接口 quickSort(int[] array) 作为公共方法,接收待排序数组,调用内部递归方法 quick 并传入数组的全区间(从索引 0 到 array.length-1),隐藏了排序的细节实现。 2.核心递归方法 quick(int[] array, int start, int end)负责快速排序的主要逻辑,处理数组中 [start, end] 区间的排序:
3.插入排序辅助方法 insertSort(int[] array, int left, int end)专门处理小规模区间 [left, end] 的排序: 通过 “逐个将元素插入到已排序部分的正确位置” 实现排序,适合小规模数据(因逻辑简单、无递归开销,效率高于快速排序)。
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
if(end-start+1 <= 15) {
insertSort(array,start,end);
return;
}
//三数取中
int index = mid_three(array,start,end);
swap(array,index,start);
int pivot = partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
//插入排序
public static void insertSort(int[] array,int left,int end) {
for (int i = left+1; i <= end; i++) {
int tmp = array[i];
int j = i-1;
for (; j >= left; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = tmp;
}
}点击这里,进行把栈扩大

出现这个界面


if(pivot > start + 1){
左边有两个及以上的数据
}
if(pivot < end - 1){
右边有两个及以上的数据
} private static int partition(int[] array, int left, int right) {
int tmp = array[left];
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= tmp) {
left++;
}
array[right] = array[left];
}
array[left] = tmp;
return left;
}
public static void quickSortNonR(int[] array) {
int start = 0;
int end = array.length-1;
Deque<Integer> stack = new LinkedList<>();
int pivot = partition(array,start,end);
if(pivot > start + 1) {
stack.push(start);
stack.push(pivot-1);
}
if(pivot < end - 1) {
stack.push(pivot+1);
stack.push(end);
}
while (!stack.isEmpty()) {
end = stack.pop();
start = stack.pop();
pivot = partition(array,start,end);
if(pivot > start + 1) {
stack.push(start);
stack.push(pivot-1);
}
if(pivot < end - 1) {
stack.push(pivot+1);
stack.push(end);
}
}
}
归并排序(MERGE-SORT)是建立在归并操作上的⼀种有效的排序算法,该算法是采用分治法(Divide andConquer)的⼀个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成⼀个有序表,称为二路归并。 思路:
1.不断的分割数据,让数据的每一段都有序(一个数据相当于有序)
2.当所有子序列有序的时候,在把子序列归并,形成更大的子序列,最终整个数组有序。

该图借鉴:https://www.cnblogs.com/MarisaMagic/p/16908457.html
/**
* 时间复杂度:O(N*logN) 和数据是否有序无序没有关系
* 空间复杂度:O(N)
* 稳定性:稳定的
* 直接插入 冒泡排序 归并排序
* @param array
*/
public static void mergeSort(int[] array) {
mergeChild(array, 0, array.length - 1);
}
private static void mergeChild(int[] array, int left, int right) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
mergeChild(array, left, mid); // 递归排序左半部分
mergeChild(array, mid + 1, right); // 递归排序右半部分
merge(array, left, mid, right); // 合并两个有序子数组
}
// 合并两个有序子数组:array[left..mid] 和 array[mid+1..right]
private static void merge(int[] array, int left, int mid, int right) {
int[] temp = new int[right - left + 1]; // 临时数组,存储合并后的结果
int i = left; // 左子数组的起始指针
int j = mid + 1; // 右子数组的起始指针
int k = 0; // 临时数组的指针
// 比较左右子数组的元素,按从小到大放入临时数组
while (i <= mid && j <= right) {
if (array[i] <= array[j]) {
//左右两个数组都是前边小,后边大,所以两边比较,谁小谁先存
temp[k++] = array[i++];
} else {
temp[k++] = array[j++];
}
}
//因为两边的数不一定相等,需要我们判断到底是那一边长,就执行哪边
// 处理左子数组剩余的元素
while (i <= mid) {
temp[k++] = array[i++];
}
// 处理右子数组剩余的元素
while (j <= right) {
temp[k++] = array[j++];
}
// 将临时数组的结果拷贝回原数组
for (k = 0; k < temp.length; k++) {
array[left + k] = temp[k];
}
} /**
* 非递归的归并排序
* @param array
*/
public static void mergeSortNor(int[] array) {
int gap = 1;
while (gap < array.length) {
// 用于遍历每个分组的起始位置
//i + 2*gap 表示跳到 “下一个同类型分组的起始位置”(因为每个分组内部的元素间隔为 gap,相邻分组的起始位置间隔为 2*gap)
// 每一对 “待合并的相邻子数组” 的第一个子数组的起始下标
//这一步就是把每两个小数组合并成大一点的大数组,所以中间大数组的起点间隔就是i+* gap,小数组是2 * gap
for (int i = 0; i < array.length; i = i + 2*gap) {
int left = i;
int mid = left+gap - 1;
//因为他们会越界 mid ,right .所以在他们要越界的时候,放他们等于数组最后一个
if(mid >= array.length) {
mid = array.length-1;
}
int right = mid+gap;
if(right >= array.length) {
right = array.length-1;
}
merge(array,left,mid,right);
}
gap *= 2;
}
}外部排序:排序过程需要在磁盘等外部存储进⾏的排序 前提:内存只有1G,需要排序的数据有100G 因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序

希尔排序写的不是很准确,上面有介绍

注意:图片来源https://blog.csdn.net/k1234hxh/article/details/134631590
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。操作步骤: a. 统计相同元素出现次数 b. 根据统计的结果将序列回收到原来的序列中 【计数排序的特性总结】 a. 计数排序在数据范围集中时,效率很高,但是使用范围及场景有限。 b. 时间复杂度:O(MAX(N,范围)) c. 空间复杂度:O(范围) d. 稳定性:稳定

注意:该图借鉴https://blog.csdn.net/m0_46975599/article/details/112174826 核心原理 确定最大位数 d:找到数组中最大的数,确定其位数(如最大数是 123,则 d=3)。 按位排序:从最低位(个位)到最高位(百位),对每一位进行 “分配 - 收集” 操作:
import java.util.Arrays;
public class RadixSort {
// 基数排序入口
public static void radixSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
// 1. 找到数组中的最大值,确定最大位数 d
int max = array[0];
for (int num : array) {
if (num > max) {
max = num;
}
}
int d = 0; // 最大位数
while (max > 0) {
max /= 10;
d++;
}
// 2. 按每位进行排序(从个位到高位)
int radix = 1; // 用于提取当前位(1:个位,10:十位,100:百位...)
int[][] buckets = new int[10][array.length]; // 10个桶,每个桶最多存放array.length个元素
int[] bucketCounts = new int[10]; // 记录每个桶中元素的数量
for (int i = 0; i < d; i++) { // 循环 d 次,处理每一位
// 清空桶计数
Arrays.fill(bucketCounts, 0);
// 分配:将元素按当前位放入对应桶中
for (int num : array) {
int digit = (num / radix) % 10; // 提取当前位的值(0-9)
buckets[digit][bucketCounts[digit]++] = num;
}
// 收集:按桶顺序(0-9)将元素取出,重新组成数组
int index = 0;
for (int j = 0; j < 10; j++) { // 遍历每个桶
for (int k = 0; k < bucketCounts[j]; k++) { // 取出桶中所有元素
array[index++] = buckets[j][k];
}
}
radix *= 10; // 处理下一位(个位→十位→百位...)
}
}
// 测试
public static void main(String[] args) {
int[] array = {170, 45, 75, 90, 802, 24, 2, 66};
System.out.println("排序前:" + Arrays.toString(array));
radixSort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
}特点与适用场景 优点:效率高(非比较排序),适合大规模整数排序。 缺点:依赖数据的位数,不适用于浮点数(需特殊处理)或长度差异大的字符串,且需要额外空间(桶)。 适用场景:整数排序、固定长度的字符串排序(如手机号、邮编)等。

核心原理
import java.util.*;
public class BucketSort {
// 桶排序入口
public static void bucketSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
// 1. 确定数据范围(假设数据在 [0, 100) 之间)
int min = 0;
int max = 100;
int bucketCount = 10; // 创建10个桶,每个桶存放10个范围的数据(0-9, 10-19, ..., 90-99)
int bucketSize = (max - min) / bucketCount; // 每个桶的范围大小
// 2. 初始化桶(使用链表存储桶内元素,方便动态添加)
List<List<Integer>> buckets = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
buckets.add(new LinkedList<>());
}
// 3. 将元素分配到对应的桶中
for (int num : array) {
// 计算元素应放入的桶索引(确保索引在0~bucketCount-1之间)
int bucketIndex = Math.min((num - min) / bucketSize, bucketCount - 1);
buckets.get(bucketIndex).add(num);
}
// 4. 对每个桶内的元素排序,并合并结果
int index = 0;
for (List<Integer> bucket : buckets) {
if (!bucket.isEmpty()) {
// 桶内排序(使用Collections.sort,底层是归并排序的变种)
Collections.sort(bucket);
// 将排序后的桶元素放入原数组
for (int num : bucket) {
array[index++] = num;
}
}
}
}
// 测试
public static void main(String[] args) {
int[] array = {45, 22, 88, 35, 56, 12, 77, 9, 63};
System.out.println("排序前:" + Arrays.toString(array));
bucketSort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
}特点与适用场景 优点:效率高(数据均匀时接近线性时间),可并行处理(每个桶独立排序)。 缺点:依赖数据分布(分布不均时效率下降),需要额外空间存储桶。 适用场景:数据范围明确且分布均匀的场景(如学生成绩、用户年龄、商品价格等)。 桶排序是 “分治思想” 的典型应用,通过将大问题拆分为小问题(每个桶的排序),再合并结果,实现高效排序。

总结:基数排序是 “按位拆分,多轮排序”,桶是固定的,不依赖数据分布;桶排序是 “按范围分桶,一次排序”,桶是灵活的,依赖数据均匀分布。