(一)数据结构——排序

本文深入探讨了排序算法,包括插入排序、快速排序、选择排序、堆排序、归并排序等,详细解析了它们的时间复杂度和空间复杂度。此外,还介绍了计数排序、基数排序和桶排序等特殊排序方法。对于每种排序算法,提供了具体的Java实现,并分析了其最佳、最坏及平均情况下的性能。
摘要由CSDN通过智能技术生成


一、时间复杂度

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、空间复杂度

在这里插入图片描述

三、排序算法

在这里插入图片描述

1.插入排序(熟悉)

在这里插入图片描述

public class Solution {

    // 插入排序:稳定排序,在接近有序的情况下,表现优异

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 循环不变量:将 nums[i] 插入到区间 [0, i) 使之成为有序数组
        for (int i = 1; i < len; i++) {
            // 先暂存这个元素,然后之前元素逐个后移,留出空位
            int temp = nums[i];
            int j = i;
            // 注意边界 j > 0
            while (j > 0 && nums[j - 1] > temp) {
                nums[j] = nums[j - 1];
                j--;
            }
            nums[j] = temp;
        }
        return nums;
    }
}


输入:nums = [5,2,3,1]
输出:[1,2,3,5]
  • 最好情况下,即待排序序列已按关键字有序,每趟操作只需 1 次比较 0 次移动。
  • 最坏情况下,即待排序序列按关键字逆序排序,这时在第 j 趟操作中,为插入元素需要同前面的 j 个元素进行 j 次关键字比较,移动元素的次数为 j+1 次。
  • 平均情况下:即在第 j 趟操作中,插入记录大约需要同前面的 j/2 个元素进行关键字比较,移动记录的次数为 j/2+1 次。

2.希尔排序(不建议多花时间了解)

在这里插入图片描述

3.冒泡排序(了解)

在这里插入图片描述

public class Solution {
    public int[] sortArray(int[] nums) {
        int len = nums.length;
        for (int i = len - 1; i >= 0; i--) {
            // 先默认数组是有序的,只要发生一次交换,就必须进行下一轮比较,
            // 如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
            boolean sorted = true;
            for (int j = 0; j < i; j++) {
                if (nums[j] > nums[j + 1]) {
                    int temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                }
            }
        }
        return nums;
    }
}
  • 假设待排序的元素个数为 n,则总共要进行 n-1 趟排序,对 j 个元素的子序列进行一趟起泡排序需要进行 j-1 次关键字比较。

4.快速排序(重要)

在这里插入图片描述

  • 最坏情况是每次进行划分时,在所得到的两个子序列中有一个子序列为空。
  • 最好情况是在每次划分时,都将序列一分为二,正好在序列中间将序列分成长度相等的两个子序列。
class Solution {
    //利用快速排序解决
    public int[] sortArray(int[] nums) {
        //定义一个左指针,指向数组元素的第一个元素
        int left = 0;
        //定义一个右指针,指向数组元素的最后一个
        int right = nums.length-1;
        //定义一个快速排序的方法
        return quickSort(nums,left,right);
    }
    public int[] quickSort(int[] nums, int left,int right){
        //如果左指针大于右指针,怎退出循环
        if(left > right){
            return null;
        }
        //定一个基数,指向数组的最左边的元素
        int base = nums[left];
         //定义一个左指针,指向数组元素的第一个元素
        int i = left;
        //定义一个右指针,指向数组元素的最后一个
        int j = right;
        //当左右指针不相等时,就继续移动左右指针
        while(i != j){
            //从右往左遍历,当右指针指向的元素大于等于基数时,j--。右指针持续向左移动
            while(nums[j]>=base && i < j){
                j--;
            }
            //从左往右遍历,当左指针指向的元素小于等于基数时,i++。左指针持续向右移动
            while(nums[i]<=base && i < j){
                i++;
            }
            //当左右两个指针停下来时,交换两个元素
            swap(nums, i, j);

        }
        //当左右指针相遇时,将左右指针同时指向的元素和基数进行交换。
        swap(nums,i,left);//这个看着可能会变扭,等同于小面两行代码。
        //不过这下面的两行的代码的顺序不能相反,否则导致结果,都为第一个基数。
        //首先把基数要填入的位置空出来,然后在将基数填入。
        // nums[left] = nums[i];
        // nums[i] = base;
        quickSort(nums,left, i-1);
        quickSort(nums,i+1,right);
        return nums;
    }
    public void swap(int[] nums,int left, int right){
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
    }
}

5.选择排序(了解)

在这里插入图片描述

import java.util.Arrays;

public class Solution {

    // 选择排序:每一轮选择最小元素交换到未排定部分的开头

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 循环不变量:[0, i) 有序,且该区间里所有元素就是最终排定的样子
        for (int i = 0; i < len - 1; i++) {
            // 选择区间 [i, len - 1] 里最小的元素的索引,交换到下标 i
            int minIndex = i;
            for (int j = i + 1; j < len; j++) {
                if (nums[j] < nums[minIndex]) {
                    minIndex = j;
                }
            }
            swap(nums, i, minIndex);
        }
        return nums;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }

    public static void main(String[] args) {
        int[] nums = {5, 2, 3, 1};
        Solution solution = new Solution();
        int[] res = solution.sortArray(nums);
        System.out.println(Arrays.toString(res));
    }
}

  • 在简单选择排序中,所需移动元素的次数较少,在待排序序列已经有序的情况下,简单选择排序不需要移动元素,在最坏的情况下,即待排序序列本身是逆序时,则移动元素的次数为 3(n-1)。然而无论简单选择排序过程中移动元素的次数是多少,在任何情况下,简单选择排序都需要进行n(n-1)/2 次比较操作,因此简单选择排序的时间复杂度为Ο(n2)。

6.堆排序(堆很重要,堆排序根据个人情况掌握)

在这里插入图片描述

class Solution{
    
public int[] sortArray(int[] nums) {
        heapSort(nums);
        return nums;
    }

    /**
     * 堆排序(宇宙无敌的JAVA)
     *  第一步: 构建大顶堆
     *  第二步: 交换堆点元素(堆点元素与当前处理的二叉树最后一个元素交换)
     *  第三步: 去除二叉树最后一个节点, 对二叉树根节点堆化(heapify)
     *  第四步: 重复第二、第三步直至结束
     */
    private void heapSort(int[] nums) {
        int len = nums.length;
        //第一步: 构建大顶堆
        buildMaxHead(nums, len);
        //第四步: 重复第二、第三步直至结束
        for (int i = len - 1; i >= 1; i--) {
            //第二步: 交换堆点元素(堆点元素与当前处理的二叉树最后一个元素交换)
            swap(nums, i, 0);
            //第三步: 去除二叉树最后一个节点, 对二叉树根节点堆化(heapify)
            //  元素少于两个没有必要再处理, 这里不特殊判断处理
            heapify(nums, i, 0);
        }
    }

    /**
     * 构建大顶堆
     */
    private void buildMaxHead(int[] nums, int len) {
        //从倒数第二层数的节点开始, 一直到二叉树根节点, 进行堆化(heapify)
        //  求取最后一个节点的父节点, 父节点索引为 int parentIndex = (i - 1) / 2
        int lastNodeIndex = ((len - 1) - 1) / 2;
        for (int i = lastNodeIndex; i >= 0; i--) {
            heapify(nums, len, i);
        }
    }

    private void heapify(int[] nums, int len, int cur) {
        if (cur >= len){
            return;
        }
        //第一个子节点的索引位置 int c1 = 2 * i + 1
        //第二个子节点的索引位置 int c2 = 2 * i + 2
        int c1 = 2 * cur + 1;
        int c2 = 2 * cur + 2;
        //求最大值的索引
        int maxIndex = cur;
        if (c1 < len && nums[c1] > nums[maxIndex]){
            maxIndex = c1;
        }
        if (c2 < len && nums[c2] > nums[maxIndex]){
            maxIndex = c2;
        }
        //当前节点cur不是该堆的最大值, 交换元素值, 并递归被交换点进行堆化(heapify)
        if (cur != maxIndex){
            swap(nums, cur, maxIndex);
            heapify(nums, len, maxIndex);
        }
    }

    /**
     * 交换元素值
     */
    private void swap(int[] nums, int x, int y) {
        int temp = nums[x];
        nums[x] = nums[y];
        nums[y] = temp;
    }
}

7.归并排序(重点)

在这里插入图片描述

public class Solution {
    /**
     * 列表大小等于或小于该大小,将优先于 mergeSort 使用插入排序
     */
    private static final int INSERTION_SORT_THRESHOLD = 7;

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        int[] temp = new int[len];
        mergeSort(nums, 0, len - 1, temp);
        return nums;
    }

    /**
     * 对数组 nums 的子区间 [left, right] 进行归并排序
     * @param temp  用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁
     */
    private void mergeSort(int[] nums, int left, int right, int[] temp) {
        // 小区间使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) {
            insertionSort(nums, left, right);
            return;
        }

        int mid = left + (right - left) / 2;
        // Java 里有更优的写法,在 left 和 right 都是大整数时,即使溢出,结论依然正确
        // int mid = (left + right) >>> 1;

        mergeSort(nums, left, mid, temp);
        mergeSort(nums, mid + 1, right, temp);
        // 如果数组的这个子区间本身有序,无需合并
        if (nums[mid] <= nums[mid + 1]) {
            return;
        }
        mergeOfTwoSortedArray(nums, left, mid, right, temp);
    }

    /**
     * 对数组 arr 的子区间 [left, right] 使用插入排序
     */
    private void insertionSort(int[] arr, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int temp = arr[i];
            int j = i;
            while (j > left && arr[j - 1] > temp) {
                arr[j] = arr[j - 1];
                j--;
            }
            arr[j] = temp;
        }
    }

    /**
     * 合并两个有序数组:先把值复制到临时数组,再合并回去
     * @param mid   [left, mid] 有序,[mid + 1, right] 有序
     * @param temp  全局使用的临时数组
     */
    private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
        System.arraycopy(nums, left, temp, left, right + 1 - left);

        int i = left;
        int j = mid + 1;

        for (int k = left; k <= right; k++) {
            if (i == mid + 1) {
                nums[k] = temp[j];
                j++;
            } else if (j == right + 1) {
                nums[k] = temp[i];
                i++;
            } else if (temp[i] <= temp[j]) {
                // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
                nums[k] = temp[i];
                i++;
            } else {
                // temp[i] > temp[j]
                nums[k] = temp[j];
                j++;
            }
        }
    }
}

8.计数排序(了解)

在这里插入图片描述

public class Solution {
    private static final int OFFSET = 50000;

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 由于 -50000 <= A[i] <= 50000
        // 因此"桶" 的大小为 50000 - (-50000) = 10_0000
        // 并且设置偏移 OFFSET = 50000,目的是让每一个数都能够大于等于 0
        // 这样就可以作为 count 数组的下标,查询这个数的计数
        int size = 10_0000;

        // 计数数组
        int[] count = new int[size];
        // 计算计数数组
        for (int num : nums) {
            count[num + OFFSET]++;
        }

        // 把 count 数组变成前缀和数组
        for (int i = 1; i < size; i++) {
            count[i] += count[i - 1];
        }

        // 先把原始数组赋值到一个临时数组里,然后回写数据
        int[] temp = new int[len];
        System.arraycopy(nums, 0, temp, 0, len);

        // 为了保证稳定性,从后向前赋值
        for (int i = len - 1; i >= 0; i--) {
            int index = count[temp[i] + OFFSET] - 1;
            nums[index] = temp[i];
            count[temp[i] + OFFSET]--;
        }
        return nums;
    }
}

9.基数排序(了解)

在这里插入图片描述

public class Solution {
    private static final int OFFSET = 50000;

    public int[] sortArray(int[] nums) {
        int len = nums.length;

        // 预处理,让所有的数都大于等于 0,这样才可以使用基数排序
        for (int i = 0; i < len; i++) {
            nums[i] += OFFSET;
        }

        // 第 1 步:找出最大的数字
        int max = nums[0];
        for (int num : nums) {
            if (num > max) {
                max = num;
            }
        }

        // 第 2 步:计算出最大的数字有几位,这个数值决定了我们要将整个数组看几遍
        int maxLen = getMaxLen(max);

        // 计数排序需要使用的计数数组和临时数组
        int[] count = new int[10];
        int[] temp = new int[len];

        // 表征关键字的量:除数
        // 1 表示按照个位关键字排序
        // 10 表示按照十位关键字排序
        // 100 表示按照百位关键字排序
        // 1000 表示按照千位关键字排序
        int divisor = 1;
        // 有几位数,外层循环就得执行几次
        for (int i = 0; i < maxLen; i++) {

            // 每一步都使用计数排序,保证排序结果是稳定的
            // 这一步需要额外空间保存结果集,因此把结果保存在 temp 中
            countingSort(nums, temp, divisor, len, count);

            // 交换 nums 和 temp 的引用,下一轮还是按照 nums 做计数排序
            int[] t = nums;
            nums = temp;
            temp = t;

            // divisor 自增,表示采用低位优先的基数排序
            divisor *= 10;
        }

        int[] res = new int[len];
        for (int i = 0; i < len; i++) {
            res[i] = nums[i] - OFFSET;
        }
        return res;
    }

    private void countingSort(int[] nums, int[] res, int divisor, int len, int[] count) {
        // 1、计算计数数组
        for (int i = 0; i < len; i++) {
            // 计算数位上的数是几,先取个位,再十位、百位
            int remainder = (nums[i] / divisor) % 10;
            count[remainder]++;
        }

        // 2、变成前缀和数组
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        // 3、从后向前赋值
        for (int i = len - 1; i >= 0; i--) {
            int remainder = (nums[i] / divisor) % 10;
            int index = count[remainder] - 1;
            res[index] = nums[i];
            count[remainder]--;
        }

        // 4、count 数组需要设置为 0 ,以免干扰下一次排序使用
        for (int i = 0; i < 10; i++) {
            count[i] = 0;
        }
    }

    /**
     * 获取一个整数的最大位数
     *
     * @param num
     * @return
     */
    private int getMaxLen(int num) {
        int maxLen = 0;
        while (num > 0) {
            num /= 10;
            maxLen++;
        }
        return maxLen;
    }
}

10.桶排序(了解)

基本思路:一个坑一个萝卜,也可以一个坑多个萝卜,对每个坑排序,再拿出来,整体就有序。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>