
1.排序的概念及引用
1.1 排序的概念
**排序:**所谓排序,就是使⼀串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性: 假定在待排序的记录序列中,存在多个具有相同 的关键字的记录,若经过排序,这些记录的相对次序 保持不变,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序: 数据元素全部放在内存 中的排序。(排序的数据量不大 大多数 为此 重点)
外部排序: 数据元素太多不能同时放在内存中,根据排序过程的要求不断的在内外存之间移动数据的排序。(排序的数据量太大 内存存不下 借助硬盘 外存)

(100w的数据,若为int,一个int四个字节,400w字节 = 4MB)


1.2 排序运用

1.3 常见的七大排序算法

2. 常见排序算法的实现
2.1 插入排序
基本思想:把待排序的记录按其关键码值的**大小逐个插⼊**到⼀个已经排好序的有序序列中,直到所有的记录插入完为止,得到⼀个新的有序序列。(类似于整理扑克牌的时候)
(1) 直接插入排序

9 5 2 7 3 6 8 有 序区间 [0,0) 无 序区间 [0,7) 【每次选择无序区间的一个元素,把这个元素往前插入到有序区间的合适位置上】

代码实现
java
import java.util.Arrays;
public class Sort {
//实现直接插入排序 (insert--插入 boundary--边界 order--有序 disorder--无序)
//思路:把整个数组划分为有序与无序俩相连区间,每次取出无序区间的第一个元素将其插入到有序区间的合适位置上。
//【整个过程循环N次,或者说N-1次,因为第一次不需要做什么】
private static void insertSort(int[] arr)
{
//boundary--边界 有序区间-[0,boundary) 无序区间-[boundary,arr.length)
//1、外层循环:每次循环 插入一个元素 有序+1! 无序-1!
for(int boundary = 1; boundary < arr.length; boundary++)
{
//2、内层循环:进行一次插入操作,先从无序区间 [boundary,arr.length) 中取出待插入元素 !! 取出后那个位置为空
int disorder = arr[boundary]; //disorder--无序
//3、拿上述元素与有序区间 [0,boundary) 的元素从后往前进行比较,即又需要一次for循环
//boundary-1 就是有序区间的最后一个元素 从 boundary-1 处开始循环 (从后往前遍历)
int a = boundary - 1;
for(; a>=0; a--)
{ int order = arr[a]; //order--有序
//拿无序disorder第一个元素与有序order最后一个元素比较
if(order > disorder)
{
//这个情况下就需要搬运,把order位置的元素挪到后面a+1的位置
arr[a + 1] = order;
}
else
{
//此刻说明已经找到位置了,刚好
break;
}
}
//此时就可以把 disorder 放到 order之后也就是 a+1 位置了,此处的a+1指有序区间的最后那个位置
arr[a+1] = disorder;
}
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 3, 6, 8};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
}

特性总结
(1)时间复杂度:O(N^2) 【搬运需要O(N) 整个排序需要插入N次】
(2)空间复杂度:O(1) 【int[ ] arr虽然也占空间,但不算在空间复杂度中;而在代码中的order、disorder、a变量在循环开始时创建,循环结束就销毁了】
(3)稳定性:稳定 【遇到相同的记录 排序不变】
(4)元素集合越接近有序,直接插⼊排序算法的时间效率越高。
(2) 希尔排序

【gap为同一组的下标差值****进行若干次 分组插排 分组排序 最后gap=1 大火收汁】


代码实现:
java
import java.util.Arrays;
public class Sort1 {
//实现希尔排序
//分组进行排序,根据gap值把整个数组分成多个组,针对每个组进行插入排序,此处使用希尔序列,gap为size/2,size/4,size/8
private static void shellSort(int[] arr){
//先指定一个gap值
int gap = arr.length/2;
while(gap >= 1){
//针对每个组进行插入排序 打印出来看看
insertSortGap(arr, gap);
System.out.println("gap = " + gap + ":" + Arrays.toString(arr));
gap = gap/2;
}
}
//实现intsertSortGap方法 分组插入排序
private static void insertSortGap(int[] arr, int gap){
//每个组中的元素,下标差值为gap。 如:gap为3时,组为[0,3),[3,6),[6,9)
//直接使boundary边界 = gap 即第0组的第一个元素,跳过第0个元素 因为第0个不用比 都一样
//此处同一个组内部元素下标差值是gap,所以当取下一个元素的时候看起来是 boundary += gap
//但此处的处理,其实是针对所有组,同时处理
//比如 gap 为3,则有3个组,比如是0,1, 2 boundary的循环过程如下
//先处理第 0 组的第 1 个元素的插入,再处理第 1 组第 1 个元素的插入,再处理第 2 组的第 1 个元素的插入;
//再处理第 0 组第 2 个元素的插入,再处理第 1 组第 2 个元素的插入,再出来第 2 组第 2 个元素的插入
//也就是boundary++
for(int boundary = gap; boundary < arr.length; boundary ++){
//先取出待插入元素
int value = arr[boundary];
//再取出 value 所在组的前一个元素(下标差值是gap)
int cur = boundary - gap;
//从后往前比较
for(;cur >= 0; cur -= gap){
if(arr[cur] > value){
//需要搬运,搬到后面的位置
arr[cur + gap] = arr[cur];
}
else{
break;
}
}
//找到了插入的位置--将value插入到cur之后 也就是 cur + gap 的位置
arr[cur + gap] = value;
}
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 3, 6, 8, 1, 4, 10, 11, 12};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
}
特性总结:
(1)时间复杂度:最坏 O(N^2) 平均 不确定。
(2)空间复杂度:O(1)
(3)稳定性:不稳定排序
2.2 选择排序
基本思想:每⼀次从待排序的数据元素中选出最小(或最大)的⼀个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
(1) 直接选择排序


代码实现:
java
import java.lang.reflect.Array;
import java.util.Arrays;
public class Sort2 {
//实现直接选择排序
//每次从待排序区间值找到最小值,放到已排序区间的末尾
//这种 "找最小值" 的过程呢,就类似于 "打擂台",以待排序区间第一个元素作为擂台,拿待排序区间后面的元素与它比,
//若比其小,就交换位置,接着拿剩下的比,知道找到最小的。
private static void selectSort(int[] arr){
//[0,boundary) 已排序区间 初始为空
//[boundary,arr.length) 待排序区间
//当 boundary = arr.length-1 的时候,整个数组就排序完了。
for(int boundary = 0; boundary < arr.length; boundary++){
//针对每一趟进行打擂台操作 以boundary位置的元素作为擂台,从boundary+1开始,拿后面每个元素与此处元素比较
for(int cur = boundary+1; cur < arr.length; cur++){
if(arr[cur] < arr[boundary]){
int temp = arr[cur];
arr[cur] = arr[boundary];
arr[boundary] = temp;
}
}
//此时 boundary 位置的元素,就是待排序区间的最小值了
//随着后续进行 boundary++,此时 boundary 位置的元素就已经 "自动并入" 到 "已排序区间"了!!
//打印日志 打印每次结果看看
System.out.println("boundary = " + boundary + ":" + Arrays.toString(arr));
}
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 3, 6, 8, 1, 4, 10, 11, 12};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
}

特性总结:
(1)时间复杂度:O(N^2)
(2)空间复杂度:O(1)
(3)稳定性:不稳定排序
(2) 堆排序(实用)
堆排序(Heapsort)是指利用堆积树(堆 )这种数据结构所设计的⼀种排序算法,它是通过堆来进行选择数据。 【排升 序要建大堆 ,排降 序建小堆】


代码 实现【笔试面试时也写成3个方法】
java
import java.util.Arrays;
public class Sort3 {
//实现堆排序 HeapSort
//核心思路:建大堆(升序) 把堆顶元素和最后一个元素交换,再重新向下调整。
private static void heapSort(int[] arr){
//1.建堆
creatHeap(arr);
//2.依次把堆顶元素和待排序区间的最后一个元素交换
//此处设定 [0,boundary] 为待排序区间,堆顶元素为arr[0]
int boundary = arr.length - 1;
for(int i = 0; i < arr.length; i++){
//把堆顶元素和待排序区间的最后一个元素交换
int temp = arr[0];
arr[0] = arr[boundary];
arr[boundary] = temp;
//重新向下调整堆,通过 boundary 表示堆的元素个数
shiftDown(arr, boundary, 0);
//把最后一个元素归入到 "已排序区间"
boundary--;
}
}
private static void creatHeap(int[] arr) {
//找最后一个非叶子节点,其实就是最后一个叶子结点的父节点,最后一个叶子结点下标就是 arr.length - 1
//(叶子结点下标- 1) / 2 得到的就是父节点
for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(arr, arr.length, i);
}
}
//创建方法 length表示堆的元素个数,把堆从index 的位置往下调整
private static void shiftDown(int[] arr, int length, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < length) {
if (child + 1 < length && arr[child + 1]> arr[child]) {
//注意此处是建立大堆。需要取出左右子树中,较大的值,和父节点比较.
child += 1;
}
//比较 child 和 parent 元素大小.
if (arr[child] > arr[parent]) {
// 交换 child 和 parent 的元素
int temp = arr[child];
arr[child] = arr[parent];
arr[parent] = temp;
}
else {
// parent 已经是比 child 大了。此时不需要继续向下调整了。
break;
}
// 更新 parent 和 child
parent = child;
child = 2 * parent + 1;
}
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 3, 6, 8, 1, 4, 10, 11, 12};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
}

特性总结:
(1)时间复杂度:O(NlogN)
(2)空间复杂度:O(1)
(3)稳定性:不稳定排序
2.3 交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置; 特点是:将键值较大 的记录向序列的尾 部移动,键值较小 的记录向序列的前部移动。
(1) 冒泡排序

比较交换相邻元素,从前往后遍历,一趟下来可以把最大 值放最后;从后往前遍历,可以把最小值放到最前。

从前往后遍历 【10和5需要交换】

【10和3需要交换】 以此类推......
代码实现:
java
// 比较两个相邻元素. 不符合升序要求, 就交换.
// 此处按照从后往前的方式来遍历, 每次就能把最小值放到最前面.
// 此时就可以约定 [0, bound) 为已排序区间, [bound, arr.length) 为待排序区间.
private static void bubbleSort(int[] arr) {
// 通过 bound 确定好循环次数.
for (int bound = 0; bound < arr.length - 1; bound++) {
// 注意边界, cur 初始值为 arr.length - 1, 此时意味着下面的 if 就是 cur - 1 和 cur 比较.
// 另外, cur > bound 不需要写作 >= . 如果写作 >= 意味着接下来会比较 arr[bound - 1] 和 arr[bound].
// 但是 bound 前面的区间都已经排好序了, 不需要.
for (int cur = arr.length - 1; cur > bound; cur--) {
if (arr[cur - 1] > arr[cur]) {
// 交换
int temp = arr[cur - 1];
arr[cur - 1] = arr[cur];
arr[cur] = temp;
}
}
}
}
特性总结:
(1)时间复杂度:O(N^2)
(2)空间复杂度:O(1)
(3)稳定性:稳定排序

(2) 快速排序(常用 常考)

1选基准值 2分为左侧、基准值、右侧三部分(从小到大) 3再进行左右递归(左 侧和右 侧内 部还未 排序**)**
当把所有 "小的 区间" 全部整理完毕后 ,整个数组的顺序就排完了。
举例如下:

【这就是上文中 基础的 左侧 基准值 右侧 三部分】 下面再对 左 区间 与 右 区间 分别反复递归进行此过程


代码实现:
java
import com.sun.tools.javac.Main;
import java.util.Arrays;
public class Sort5 {
//实现快速排序
//总的入口
private static void quickSort(int[] arr){
quickSort(arr, 0, arr.length-1);
}
//快速排序,辅助递归的方法(递归方法的具体实现)
//此处约定 [left,right] 闭区间为待处理区间 (这的 left right 代表下标 0 1 2 3这种)
//想约定成前闭后开区间也行 只不过代码会更复杂一点 要一直考虑右边界要不要-1 会不会引起什么问题之类的(如下面的index-1)
private static void quickSort(int[] arr, int left, int right){
//①待处理区间为空:当 left > right 时,表示待处理区间没有元素。如果 left = 5 且 right = 4,这意味着区间 [5, 4] 是一个无效的区间,因为没有元素在这个范围内。
//②待处理区间只有一个元素:当 left == right 时,表示待处理区间只有一个元素。
//例如:初始数组:arr = [5, 3, 8, 4, 2],left = 0,right = 4。
//假设选择基准值(pivot)为 5,经过分区操作后,数组变为 [2, 3, 4, 5, 8]
//此时:左子区间:[2, 3, 4],left = 0,right = 2。 右子区间:[8],left = 4,right = 4。
//对于右子区间 [8]:left = 4,right = 4。此时 left == right,表示该区间只有一个元素,无需排序。
//③对于左子区间 [2, 3, 4]: left = 0,right = 2。此时 left < right(因为 0 < 2),表示该区间中有多个元素,需要继续排序。
if(left < right){
//待处理区间为非空 或者 有很多元素,就需要针对其进行整理排序 partition--分隔 index--指数 索引
//用partition方法进行整理初始 [left,right] 闭区间 → 【左侧 基准值 右侧】
//并返回基准值 index = 基准值所在的位置,明确了基准值的位置,才能进一步对它的左右区间 进行递归
int index = partition(arr, left, right);
//递归左侧区间
quickSort(arr, left, index - 1);
//递归右侧区间
quickSort(arr, index + 1, right);
}
else {
//left >= right 待处理区间为空 或者 只有一个元素,就无需排序
return;
}
}
//实现 partition 方法 整理初始 [left,right] 闭区间
//此处不用再判断left right大小了 上面判断过了 ; 但如果多人协作、跨模块调用 为了保险一点要进行 再次判断 (即double check)
private static int partition(int[] arr, int left, int right) {
//1. 设定最右侧元素为基准值 【此处选最右侧为基准值 先从左往右走大的;若选最左侧为基准值,要先从右往左找最小】
int value = arr[right];
//2. 设定两个下标 一个从左往右 一个从右往左 同时进行
int l = left;
int r = right;
//通过以下循环 找到重合的位置
while (l < r) {
//先从左往右 ++ ,找到比基准值大的元素 或者 重合,即 arr[r] > value 或 l = r 才结束循环
while (arr[l] <= value && l < r) {
l++;
}
//此循环结束时,l就会停在一个比基准值大的元素上。
//先从右往左 -- ,找到比基准值小的元素 或者 重合,即 arr[r] < value 或 l = r 才结束循环
while (arr[r] >= value && l < r) {
r--;
}
//上述循环结束,r就会停在一个比基准值小的元素上。
//如果为重合 直接进行交换;如果重合 自己换自己 交换后也无影响
//交换 l 和 r 的位置 用个swap方法 方便点
swap(arr, l, r);
}
//3. 当上述循环完成 需要把重合位置的元素 和基准值进行交换 l r 相同
swap(arr, l, right);
//4. 返回 l 位置 作为基准值的位置
return l;
}
//实现交换 swap 方法
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 3, 6, 8, 1, 4, 10, 11, 12};
quickSort(arr);
System.out.println(Arrays.toString(arr));
}
}

特性总结:
(1)时间复杂度:

(2)空间复杂度:

(3)稳定性:不稳定排序
(3) 快速排序策略优化
**面试常见问题 【提供思路即可 不用实现】**为了避免极端情况出现(基准值正好取得是最大/最小值),要让基准值更接近平均值。

第一种

第二种

第三种

(4) 快速排序 非递归实现

java
// [left, right] 闭区间为待处理区间.
static class Range {
public int left;
public int right;
public Range(int left, int right) {
this.left = left;
this.right = right;
}
}
// 非递归版本的快速排序. 使用栈维护待排序的子区间.
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();
if (range.left >= range.right) {
// 区间为空, 或者只有一个元素, 不需要排序
continue;
}
// 区间不为空, 使用 partition 进行整理
int index = partition(arr, range.left, range.right);
// 把左侧区间和右侧区间, 入栈. 下次循环出栈, 取出的当前这样的子区间了.
stack.push(new Range(index + 1, range.right));
stack.push(new Range(range.left, index - 1));
}
// 整个栈为空了, 说明排序完成了.
}
2.4 归并排序(实用)
基本思想: 归并排序(MERGE-SORT)是建立在归并操作上的⼀种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将已有序的子序列合并 ,得到完全有序的序列;**即先使每个子序列有序,再使子序列段间有序。**若将两个有序表合并成一个有序表,称为二路归并。



代码实现:
java
package Sort_2024_2_10;
import java.lang.reflect.Array;
import java.util.Arrays;
public class Sort6 {
//实现归并排序 merge--合并
//归并排序的入口方法
private static void mergeSort(int[] arr){
mergeSort(arr, 0, arr.length-1);
}
//归并排序的辅助方法
//参数中引入子区间, 通过子区间来决定当前是要针对哪个部分的数组进行归并排序
//约定 [left,right] 为待处理区间
private static void mergeSort(int[] arr, int left, int right){
if(left < right){
//待处理区间为非空 或者 有很多元素,就需要针对其进行整理排序
//1. 把当前区间分成两个等长的区间,分别进行递归 奇偶差不多
int mid = (left + right) / 2;
//2. 递归左侧区间 ,再递归右侧区间
//快速排序中,共三部分,基准值是单独一部分,左右递归时是要排除的;
//归并排序中,就两部分,左右递归时 mid 这个位置的元素也要参与
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
//3.上述递归完成后,说明左右两个区间已经是有序的了,接下来针对这两区间进行合并即可
merge(arr, left, mid, right);
}
else{
//如果子区间中没有元素为空 或者只有一个元素 就无需排序
return;
}
}
//实现merge 合并方法
//此时相当于左侧区间 [left,mid] 和 右侧区间 [mid+1,right] 已经是单个 也就是有序的了; 将其进行合并即可
//合并后,把结果再放到 [left,right] 区间中
//此处不用再判断left right大小了 上面判断过了 ; 但如果多人协作、跨模块调用 为了保险一点要进行 再次判断 (即double check)
private static void merge(int[] arr, int left, int mid, int right){
//1. 先创建一个临时数组,保存合并的结果 长度应该是 right-left+1
int[] result = new int[right - left + 1];
//后续合并的时候,要把对应的元素进行尾插到 result 中的,使用 resultSize 表示 result 中已经插入的元素个数
int resultSize = 0;
//2. 设定两个下标指向每个区间的开头位置
int cur1 = left;
int cur2 = mid + 1;
while(cur1 <= mid && cur2 <= right){
if(arr[cur1] <= arr[cur2]){
//将 cur1 位置的元素尾插到 result中
result[resultSize++] = arr[cur1++];
//先用后加 可以这样写
}
else {
//将 cur2 位置的元素尾插到 result中
result[resultSize++] = arr[cur2++];
}
}
//3. 处理剩余的元素 由于这两个区间的长度不一定完全相同,只需要把多余的部分,整体尾插到result即可
while(cur1 <= mid){
result[resultSize++] = arr[cur1++];
}
while(cur2 <= right){
result[resultSize++] = arr[cur2++];
}
//4. 把 result 临时数组的结果写回到 arr的数组 [left,right] 中
for(int i = 0; i < resultSize; i++){
arr[left + i] = result[i];
}
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 3, 6, 8, 1, 4, 10, 11, 12};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
}
特性总结:

非递归实现

java
// 归并排序, 非递归实现
private static void mergeSortByLoop(int[] arr) {
// 创建一个变量, 表示当前要合并的数组的大小.
for (int size = 1; size < arr.length; size *= 2) {
// 在循环内部, 进行两两合并.
for (int i = 0; i < arr.length; i += size * 2) {
// 这个循环的意思就是, 先取两个长度为 size 的数组, 进行合并; 再取两个长度为 size 的数组, 再合并;
// 依次类推, 直到把整个数组取完.
// 每次循环, i 就对应着两个数组
// 这俩数组的区间 [i, i + size - 1] 和 [i + size, i + size * 2 - 1]
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);
}
}
}
其它特点


3.排序算法复杂度及稳定性分析

