Java数据结构(十三):排序

🌸雨落在了我的手上:个人主页

🐟个人仓库:Gitee仓库

❄️个人专栏:<<JaveSe>> <<C语言>> <<C语言数据结构>>**

🔥🔥🔥 人生格言:无人扶我青云志,我自踏雪至山巅

🎬 博主简介:


目录

一:排序的概念及引⽤

1.排序的概念

2.排序运⽤

3.常⻅的排序算法

二:常⻅排序算法的实现

1.插⼊排序

2.希尔排序(缩⼩增量排序)

3.选择排序

4.堆排序

5.冒泡排序

6.快速排序


内容大纲:

本文系统介绍了排序的基本概念,包括排序定义、稳定性、内部/外部排序分类及应用场景。重点阐述了六种常见排序算法:插入排序(稳定,O(n²))、希尔排序(优化插入)、选择排序(不稳定,O(n²))、堆排序(不稳定,O(nlogn))、冒泡排序(稳定,O(n²))和快速排序(不稳定,平均O(nlogn))。通过代码实现与过程分析,对比了各算法的时间复杂度、空间复杂度及稳定性特性,为算法选型提供依据。


一:排序的概念及引⽤

1.排序的概念

排序:所谓排序,就是使⼀串记录,按照其中的某个或某些关键字的⼤⼩,递增或递减的排列起来的 操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的 相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,⽽在排序后的序列中,ri仍在rj之 前,则称这种排序算法是稳定的;否则称为不稳定的

内部排序:数据元素全部放在内存中的排序

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断的在内外存之间移动数据的 排序。

2.排序运⽤

3.常⻅的排序算法

二:常⻅排序算法的实现

1.插⼊排序

想象你在打扑克牌,手里已经有一些排好序的牌,现在摸到一张新牌,你会把它插入到合适的位置,使得手里的牌依然有序。插入排序就是模拟这个过程:

  1. 把数组分成两部分:已排序区未排序区

  2. 初始时,已排序区只有第一个元素(因为一个元素本身是有序的)。

  3. 然后依次从未排序区取出第一个元素,在已排序区从后往前扫描,找到合适的位置插入。

  4. 重复直到未排序区为空。

代码:

java 复制代码
//插入排序(这里我们默认是升序)
public class InsertionSort {
    //插入方法的实现
    public static void insertionSort(int[] arr){
        //默认从第二个元素开始,即下标为1的位置
        for (int i =1;i< arr.length;i++){
            //先把我们要插入的元素保存起来
            int cur = arr[i];
            //用j 来表示已经排序的最后一个元素
            int j =i - 1;

            //用j去和插入元素比较大小,如果j大,就把j往后移动一格
            while (j>=0&&arr[j]>cur){
                arr[j+1]=arr[j];
                j--;
            }
            //此时j+1 就是我们需要插入的位置cur
            arr[j+1]= cur;
        }
    }
}  

以数组 {5, 2, 9, 1, 5, 6} 为例,排序过程如下:

轮次 已排序区 未排序区 操作
初始 5 2, 9, 1, 5, 6 -
1 2, 5 9, 1, 5, 6 2 插入到 5 前面
2 2, 5, 9 1, 5, 6 9 比前面都大,直接放最后
3 1, 2, 5, 9 5, 6 1 插入到最前面
4 1, 2, 5, 5, 9 6 第二个 5 插入到 5 后面
5 1, 2, 5, 5, 6, 9 \[\] 6 插入到 5 和 9 之间
  • 时间复杂度

    • 最好情况:数组已经有序,只需要比较 n-1 次,时间复杂度为 O(n)

    • 最坏情况:数组完全逆序,需要比较和移动 n(n-1)/2 次,时间复杂度为 O(n²)

    • 平均情况:O(n²)

  • 空间复杂度 :只用了几个临时变量,O(1),属于原地排序。

  • 稳定性 :插入排序是稳定 的,因为当 arr[j] == current 时不会交换,保持原有顺序。

2.希尔排序(缩⼩增量排序)

基本思想:

普通插入排序每次只能把元素往前移动一位,如果元素离正确位置很远,就要移动很多次。

希尔排序的做法是:

  1. 选择一个增量 gap,把数组分成 gap 个组。

  2. 每组内进行插入排序。

  3. 然后缩小 gap,再分组、再排序。

  4. 当 gap 缩小到 1 时,就是普通插入排序。但此时数组已经基本有序,所以最后一步很快。

常用的增量序列是:gap = 数组长度 / 2,然后每次 gap = gap / 2,直到 gap = 1

代码:

java 复制代码
//希尔排序(升序)
public class ShellSort {
    //希尔排序方法的实现
    public static void shellSort(int[]arr){
        //设置一个gap来作为划分组的中间值,初始gap为数组的/2
        //之后每次都/2,一直到gap=1
        for (int gap = arr.length/2;gap>0;gap/=2){
            //内层循环对每个分组插入排序
            // 为什么从 i = gap 开始?因为每个分组的前 gap 个元素是各组的第一个元素,默认有序
            for (int i =gap;i<arr.length;i++){
                //当前要插入的元素
                int cur = arr[i];
                //同一分组中,前一个元素的下标
                int j =i - gap;


                // 在同一个分组内,从后往前找插入位置
                while (j>=0&&arr[j]>cur){
                    //先把大的元素往后移动gap个位置
                    arr[j+gap] = arr[j];
                    //继续往前找
                    j-=gap;
                }

                //此时要插入的位置找到了
                arr[j+gap] =cur;
            }
        }
    }
}

希尔排序的特性总结:

  1. 希尔排序是对直接插⼊排序的优化。

  2. 当gap>1时都是预排序,⽬的是让数组更接近于有序。当gap==1时,数组已经接近有序的了,这 样就会很快。这样整体⽽⾔,可以达到优化的效果。我们实现后可以进⾏性能测试的对⽐。

3.选择排序

基本思想:

每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待 排序的数据元素排完

把数组分成两部分:

  • 已排序区:在左边,一开始为空。

  • 未排序区:在右边,一开始是整个数组。

每一轮:

  1. 在未排序区中找到最小的元素。

  2. 把它和未排序区的第一个元素交换。

  3. 这样已排序区就多了一个元素,未排序区少了一个元素。

重复 n - 1 轮,整个数组就有序了。

代码:

java 复制代码
//选择排序
public class SelectionSort {
    public static void selectionSort(int[]arr){
        // 外层循环:i 表示当前已排序区的末尾,也是未排序区的第一个位置
        for (int i =0;i< arr.length-1;i++){
            //先设置一个未排序最小元素
            int min = i;
            //通过循环找到最小元素
            for (int j =i+1;j< arr.length;j++){
                if (arr[j]<arr[min]){
                    min =j;
                }
            }
            
            //如果最小元素不在i上,就交换
            if (min!=i){
                int temp = arr[i];
                arr[i]=arr[min];
                arr[min]=temp;
            }
        }
    }
}
  • 时间复杂度

    • 比较次数是固定的:n(n-1)/2,所以最好、最坏、平均都是 O(n²)

    • 交换次数最多 n-1 次,比冒泡排序少。

  • 空间复杂度O(1),只需要几个临时变量。

  • 稳定性 :选择排序是不稳定的。

    • 例如 {5a, 5b, 2},第一轮最小的是 2,和 5a 交换后变成 {2, 5b, 5a},两个 5 的相对顺序改变了。

4.堆排序

基本思想:

是一种特殊的完全二叉树,通常用数组来存储。

  • 大顶堆 :每个父节点的值都 大于等于 它的左右子节点。

  • 小顶堆 :每个父节点的值都 小于等于 它的左右子节点。

堆排序(升序)的做法:

  1. 先把整个数组调整成一个大顶堆。

  2. 此时堆顶(数组第 0 个元素)就是最大值。

  3. 把堆顶和数组末尾元素交换,最大值就放到了最终位置。

  4. 然后把剩下的元素重新调整成大顶堆。

  5. 重复以上步骤,直到所有元素有序。

用数组表示完全二叉树时,下标关系很重要:

  • 父节点下标 i

  • 左孩子下标 2 * i + 1

  • 右孩子下标 2 * i + 2

代码:

java 复制代码
//堆排序(升序)
public class HeapSort {
    //堆排序方法的实现
    public static void heapSort(int[]arr){
        int n = arr.length;
        //建堆
        // 从最后一个非叶子节点开始,依次向前调整
        for (int i = n/2-1;i>=0;i--){
            heapify(arr, arr.length, i);
        }

        //此时建堆完成
        //排序:每次把堆顶(最大值)交换到末尾,然后调整剩余部分
        for (int i=n-1;i>0;i--){
            //交换堆顶和当前末尾元素
            swap(arr,0,i);

            // 对剩下的 i 个元素重新调整为大顶堆
            heapify(arr, i, 0);
        }
    }

    //建立大堆的辅助方法
    private static void heapify(int[] arr, int heapSize, int root) {
        int largest = root;            // 先假设根节点最大
        int left = 2 * root + 1;       // 左孩子
        int right = 2 * root + 2;      // 右孩子

        // 如果左孩子比当前最大还大,更新 largest
        if (left < heapSize && arr[left] > arr[largest]) {
            largest = left;
        }

        // 如果右孩子比当前最大还大,更新 largest
        if (right < heapSize && arr[right] > arr[largest]) {
            largest = right;
        }

        // 如果最大值不是根节点,交换,并继续向下调整
        if (largest != root) {
            //交换

            heapify(arr, heapSize, largest);
        }
    }

    //交换元素辅助方法
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
  • 时间复杂度

    • 建堆:O(n)

    • 每次调整堆:O(log n) ,共 n - 1

    • 总时间复杂度:O(n log n),最好、最坏、平均都是这个量级。

  • 空间复杂度O(1) (递归栈是 O(log n),但通常忽略)。

  • 稳定性 :堆排序是不稳定的。交换时可能改变相同元素的相对顺序。

5.冒泡排序

想象水里的气泡,越大的气泡越先浮到水面。

冒泡排序也是这样:

  • 从数组第一个元素开始,相邻两个元素两两比较

  • 如果前面比后面大,就交换它们。

  • 这样一轮下来,最大的元素就"冒"到了数组末尾

  • 然后对剩下的元素重复这个过程,直到整个数组有序

代码:

java 复制代码
//冒泡排序
public class BubbleSort {
    //冒泡排序方法的实现
    public static void bubbleSort(int[] arr){
        int n = arr.length;

        // 外层循环:控制一共需要几轮
        for (int i= 0;i<n-1;i++){
            boolean swapped = false; // 标记本轮是否发生了交换
            // 内层循环:比较相邻的两个元素
            // 每轮结束后,末尾已经有 i 个元素排好了,所以是 n - 1 - i
            for (int j=0;j<n-1-i;j++){
                if (arr[j]>arr[j+1]){
                    // 如果前面比后面大,就交换
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                }
            }
            
            // 如果本轮没有发生任何交换,说明数组已经有序,提前结束
            if (!swapped) {
                break;
            }
        }
    }
}
  • 时间复杂度

    • 最好情况(数组已经有序):O(n),因为一轮比较后就会提前结束。

    • 最坏情况(数组完全逆序):O(n²)

    • 平均情况:O(n²)

  • 空间复杂度O(1) ,只用了一个临时变量 temp

  • 稳定性 :冒泡排序是稳定 的。因为只有 arr[j] > arr[j + 1] 时才交换,相等时不交换,不会改变相同元素的相对顺序

6.快速排序

基本思想:

  1. 从数组中选一个元素作为基准(pivot)

  2. 把数组重新排列:小于等于基准的放左边,大于基准的放右边 。这个过程叫分区(partition)

  3. 分区后,基准元素就处于它的最终位置。

  4. 然后对基准左边和右边的子数组分别重复以上步骤,直到子数组长度为 0 或 1。

这样整个数组就有序了。

代码:

java 复制代码
//快速排序
public class QuickSort {
    /**
     * 快速排序方法
     * @param arr  待排序的数组
     * @param low  当前子数组的起始下标
     * @param high 当前子数组的结束下标
     */
    //快速排序方法的实现
    public static void quickSort(int[]arr,int low,int high){
        if (low<high){
            // 分区,并返回基准元素的最终位置
            int pivotIndex = partition(arr, low, high);

            // 递归排序左半部分
            quickSort(arr, low, pivotIndex - 1);
            // 递归排序右半部分
            quickSort(arr, pivotIndex + 1, high);
        }
    }

    /**
     * 分区方法:把小于等于基准的放左边,大于基准的放右边
     * @param arr  数组
     * @param low  起始下标
     * @param high 结束下标
     * @return 基准元素的最终位置
     */
    //分区方法
    public static int partition(int[]arr,int low,int high){
        // 选择最后一个元素作为基准
        int pivot = arr[high];

        // i 指向小于等于基准的最后一个元素
        int i = low - 1;

        // 遍历 low 到 high-1
        for (int j = low; j < high; j++) {
            // 如果当前元素小于等于基准,就把它换到左边
            if (arr[j] <= pivot) {
                i++;
                swap(arr, i, j);
            }
        }

        // 最后把基准放到正确的位置(i+1)
        swap(arr, i + 1, high);
        return i + 1; // 返回基准的下标
    }

    //交换辅助方法
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
  • 时间复杂度

    • 最好情况:每次分区都很均匀,时间复杂度 O(n log n)

    • 平均情况:O(n log n)

    • 最坏情况:数组已经有序,且每次选最后一个元素为基准,分区极不均匀,退化为 O(n²)

  • 空间复杂度 :递归调用需要栈空间,平均 O(log n) ,最坏 O(n)

  • 稳定性 :快速排序是不稳定的。分区交换时可能改变相同元素的相对顺序。

以上就是我们的全部内容了!!!

相关推荐
Logic1012 小时前
C语言/数据结构位运算题解:异或XOR找出独特卡片数字——只出现一次的元素
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
郝学胜-神的一滴2 小时前
C++20模板元编程 02:吃透模板基础核心四大核心知识点
开发语言·数据结构·c++·程序人生·软件工程·visual studio
我不会起名字3223 小时前
万字总结Golang入门项目
数据结构·经验分享·算法·golang·项目实战·复盘
不会就选b13 小时前
数据结构之排序
数据结构
我不会起名字32216 小时前
一天一道算法题(34):回溯法的经典例题(子集)
java·数据结构·python·算法·golang·深度优先·力扣
Logic10118 小时前
C语言/数据结构位运算题解:异或XOR找出货船中的“独特载货量“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
彧azz19 小时前
DFS与BFS:图遍历的两大核心算法
数据结构·学习·算法·深度优先·广度优先
青山木20 小时前
Hot 100 --- 最长递增子序列
java·数据结构·算法·leetcode·动态规划
土司大王21 小时前
LeetCode hot100——33.搜索旋转排序数组:Java 二分模板与 O(log n) 实现
数据结构·算法·leetcode