蓝桥杯——不就是几个排序嘛!

一、前言

二、排序

2.1 快速排序

2.1.1 快排之单向扫描分区法

  • 虽然是单向扫描,但是我们仍然需要两个指针进行操作。
  • 单向扫描,我们将左指针定义为主指针,以左指针为主进行的扫描。
  • 当遇到左指针扫描的数比哨兵值大时,我们就将左右指针的数进行交换,将大的数换到后面,然后继续用左指针扫描。
	public static int partition(int[] A, int l, int r) {
        int pivot = A[l];
        // 左侧扫描指针
        int left = l + 1;
        // 右侧指针
        int right = r;
        while(left <= right) {
            // 扫描元素小于哨兵,左指针移动
            if(A[left] <= pivot) {
                left++;
            }else {
                // 扫描元素大于哨兵,则交换双指针指向的元素,将大的元素换到右边,右指针左移
                Utils.swap(A,left,right);
                right--;
            }
        }
        Utils.swap(A,left,right);
        return right;
    }

2.1.2 快排之双向扫描分区法

  • 双向扫描就是我们平时数据结构学习的正常快排算法
  • 同样我们需要利用两个指针进行扫描
  • 这里我使用两者快排写法,但是思想相同

写法一:

    /**
     * 快速排序一
     * @param arr
     * @param start
     * @param end
     * @return
     */
    public static void quickSort(int[] arr, int start, int end) {
        // 出口
        if(start > end) return;
        int low = start;
        int high = end;
        // 1.找哨兵
        int temp = arr[start];
        while(low < high) {
            // 2.从最右侧开始扫描到比 哨兵 小的值,记录high
            while(low < high && temp <= arr[high]) {
                high--;
            }
            // 3.再从左侧扫描到比 哨兵 大的值,记录low
            while(low < high && temp >= arr[low]) {
                low++;
            }
            if(low < high) {
                // 4.如果左指针仍然小于右指针,证明还没结束,将两个值交换位置
                Utils.swap(arr, low,high);
            }
        }
        // 5. 最后low小于high,low的位置是 小于哨兵中的最后一个元素,所以我们将low指针的值 和 哨兵的值进行交换。
        // 交换后就达到了哨兵左侧的值小,右侧的值大
        arr[start] = arr[low];
        arr[low] = temp;
        // 6.哨兵左侧继续排好序
        quickSort(arr, start,low-1);
        // 7.哨兵右侧继续排好序
        quickSort(arr, low+1, end);
    }

写法二:

    /**
     * 快速排序二:划分区域
     * @param arr
     * @param low
     * @param high
     * @return
     */
    public static int partition(int[] arr, int low, int high) {
        // 1.定义哨兵
        int pivot = arr[low];
        while(low < high) {
            // 2.从右侧开始扫描,扫描到比哨兵小的值结束,记录high
            while(low < high && arr[high] >= pivot) {
                high--;
            }
            // 3. 将该值移动到左端,此时high位置空出
            arr[low] = arr[high];
            // 4.再从左侧开始扫描,扫描到比哨兵大的值结束,记录low
            while(low < high && arr[low] <= pivot) {
                low++;
            }
            // 5. 将该值移动到右端
            arr[high] = arr[low];
        }
        // 6.最后将哨兵插入
        arr[low] = pivot;
        return low;
    }
    public static void quick_Sort(int[] arr, int low, int high) {
        // 递归出口
        if(low < high) {
            int pivot = partition(arr, low, high);
            quick_Sort(arr, low, pivot-1);
            quick_Sort(arr, pivot+1, high);
        }
    }
  • 在工业中,快排还是可以优化的,因为我们平时选择的哨兵都很随意,选择第一个元素。当哨兵选择的值尽量靠中间时,可以使我们的快排效率达到最大。
  • 所以我们可以采取三点中值法进行选择哨兵
  • 这是快排partition部分的代码,我们采用三点中值法选择哨兵。每次选哨兵时,不选最大也不选最小,而是选择中间大的元素。
	public static int partition(int[] arr, int low, int high) {
        // 三点中值法
        // 中间下标
        int midIndex = low + ((high - low) >> 1);
        // 中间值下标
        int midValueIndex = -1;
        if(arr[low] <= arr[midIndex] && arr[low] >= arr[high]) {     // 最右侧值 <= 第一个值 <= 中间值
            midValueIndex = low;
        }else if(arr[high] <= arr[midIndex] && arr[high] >= arr[low]) {    //  最右侧值 <= 中间值
            midValueIndex = high;
        }else {
            midValueIndex = midIndex;
        }

        // 1.定义哨兵
        int pivot = arr[low];
        while(low < high) {
            // 2.从右侧开始扫描,扫描到比哨兵小的值结束,记录high
            while(low < high && arr[high] >= pivot) {
                high--;
            }
            // 3. 将该值移动到左端,此时high位置空出
            arr[low] = arr[high];
            // 4.再从左侧开始扫描,扫描到比哨兵大的值结束,记录low
            while(low < high && arr[low] <= pivot) {
                low++;
            }
            // 5. 将该值移动到右端
            arr[high] = arr[low];
        }
        // 6.最后将哨兵插入
        arr[low] = pivot;
        return low;
    }

2.2 归并排序

  • 归并排序可以称为:分治模式的完美诠释

    • 分解:将n个元素分成各个n/2个元素的子序列
    • 解决:将两个子序列递归地排序
    • 合并:合并两个已排序的子序列以得到排序结果
  • 我们是通过递归的方式再次将n/2分解成n/2,一直分解到最小,不能再分解为止。然后进行合并。

与快排不同点:

  • 归并的分解比较随意
  • 归并的重点是合并
  • 快排的重点是partition()函数,重点是划分

代码注意点:

  • 最大的坑点就是,我们虽然单独开了一个数组,新开的数组值并且也复制了和原数组一样
  • 但是每一次原数组的变化,我们的新开数组也要跟着变化,否则的话我们排序是失败的
     /**
     * 归并排序
     * @param arr
     * @param left
     * @param right
     */
    public static void mergeSort(int[] arr, int left, int right) {
        helper = Arrays.copyOf(arr, arr.length);
        // 4.递归出口
        if(left < right) {
            // 中间值
            int mid = left + ((right - left) >> 1);
            // 1.找重复、找变化:对中间值左半部分进行归并排序
            mergeSort(arr, left, mid);
            // 2.找重复、找变化:对中间值右半部分进行归并排序
            mergeSort(arr, mid+1, right);
            // 3.两个部分都排序后进行最重要的合并
            merge(arr,left,mid,right);
        }
    }

	/**
     * 归并排序的合并
     * @param arr
     * @param l
     * @param mid
     * @param r
     */
    public static void merge(int[] arr, int l, int mid, int r) {
        // 指向原数组的指针
        int current = l;
        // 辅助数组的左指针
        int left = l;
        // 辅助数组的中间右指针
        int right = mid + 1;
        while(left <= mid && right <= r) {
            if(helper[left] <= helper[right]) {
                // 左右两边进行判断,将小的入数组
                arr[current++] = helper[left++];
            }else {
                arr[current++] = helper[right++];
            }
        }
        // 需要判断指针是否越界
        while (left <= mid) {
            // 当指针没越界,证明我们还没有合并完,所以将剩余的元素全部放入数组中
            arr[current++] = helper[left++];
        }
        while (right <= r) {
            arr[current++] = helper[right++];
        }
    }

 	public static void main(String[] args) {
        int[] arr = {11,24,5,32,50,34,54,76};
        mergeSort(arr, 0, arr.length-1);
        System.out.println(Arrays.toString(arr));
    }

2.3 堆排序

用数组实现一颗二叉树,那么我们需要知道:

  • 左节点:2*i+1
  • 右结点:2*i+2
  • 父节点:(i-1)/2

大根堆的构造:

  • 大根堆就是根节点都大于其子节点的树
  • 从最后一个非终端结点开始,从后往前进行调整
  • 每次调整从上往下,将子节点比自己大的元素进行交换
  • 调整为大根堆

排序思想:

  • 构造完大根堆以后,整个数组最大值就是堆结构的顶端
  • 将顶端的数与末尾的数交换,此时末尾的数为最大值,剩余待排序数组个数为n-1
  • 将剩余的n-1个数,再次构造成大根堆,再将顶端的数与末尾的数交换
  • 如此反复,就能得到一个升序的数组
    // 1.大根堆——测试用例
    public static void main(String[] args) {
        int[] arr = {5,2,6,8,9,1,3,7};
        heapMaxSort(arr,arr.length-1);
        System.out.println(Arrays.toString(arr));
    }
    // 2.大根堆——排序调用
    public static void heapMaxSort(int[] arr, int len) {
        // 1. 先建立大根堆
        buildMaxHeap(arr, len);
        for (int i = len; i > 1; i--) {
            // 2. 将堆顶元素与最后一个元素互换,最后一个变成了最大值
            // 并且每一次交换都是交换的堆顶元素
            Utils.swap(arr, 1,i );
            // 3. 每一次调整都需要缩小范围
            heapAdjustMax(arr, 1, i-1);
        }
    }
    // 3.建立大根堆
    public static void buildMaxHeap(int[] arr, int len) {
        for (int i = len / 2; i > 0; i--) { // 从后往前调整所有非终端结点
            heapAdjustMax(arr, i, len);
        }
    }
    // 4.调整大根堆
    // 将以K为根的子树调整为大根堆
    public static void heapAdjustMax(int[] arr, int k, int len) {
        // 将arr[0]暂存子树的根节点
        arr[0] = arr[k];
        for (int i = 2 * k; i <= len; i *= 2) { // 从根节点向下对子节点进行比较
            if (i < len && arr[i] < arr[i + 1]) {
                // 取key较大的子节点的下标
                i++;
            }
            // 根节点和子节点中最大值进行比较,若根结点仍然是最大值,则无需调整。
            if (arr[0] >= arr[i]) {
                break;
            } else {
                // arr[i]比较大,所以将arr[i]调整到父节点上
                arr[k] = arr[i];
                k = i; // 修改k值,继续筛选
            }
        }
        // 因为k是变化的,所以被筛选结点的值放入最终位置
        arr[k] = arr[0];
    }

2.4 计数排序

  • 适用范围:序列关键字比较集中,已知边界,且边界较小
  • 用辅助数组对数组中出现的数字计数,元素转下标,下标转元素
  • 其实和哈希表差不多吧
  • 开辟一个数组中最大元素值的空间,然后扫描标记
  • 在顺序扫描新数组,将有标记的下标替代原数组即可
  • 是一种排序效率的突破,但是空间换时间,所以适用于小范围数据

2.5 桶排序

通过“分配”和“收集”过程来实现排序

思想:设计k个桶(编号0~k-1),然后将输入n个数分布到各个桶中去,对各个桶中的数进行排序,然后按照次序把各个桶中的元素列出即可。

  • 计数排序就很像我们的桶排序,只是计数排序的桶比较多、比较浪费。而桶排序的桶将很多数进行了分类,所以就节约了桶。
  • 桶排序和散列表的链式结构非常像
  • 适用于均匀分配的数据
  • 后续文章会实现代码

2.6 基数排序

  • 基于桶排序的一种排序算法

  • 假设一组数据 73 22 93 43 55 14 28 65 39 81

  • 我们按照个位数字放入桶中

  • 然后将数字拿出来 81 22 73 93 43 14 55 65 28 39

  • 再次进行桶排序,根据十位数进行分配

  • 最后我们在收集桶里的数据,14 22 28 39 43 55 65 73 81 93

  • 需要进行几轮进桶出桶,取决于最大数的位数

2.7 冒泡排序

  • 谁大谁上,每一轮都把最大的顶到天花板
  • 效率太低O(n^2)——掌握swap
    // 简单优化:加一个flag进行判断,如果一趟没有交换过,那么就是有序的,直接结束排序即可
    public static void bubbleSort(int[] arr) {
        // 俩俩比较,所以外层比较 n-1次
        for (int i = 0; i < arr.length-1; i++) {
            // 优化标记
            boolean flag = false;
            // 内层比较,取决于i,因为外层比较过的最大值,无需进行继续比较,所以在外层的基础上-i
            for (int j = 0; j < arr.length-i-1; j++) {
                if(arr[j] > arr[j+1]) {
                    // 如果前一个数 > 后一个数,就进行交换
                    Utils.swap(arr, j, j+1);
                    flag = true;
                }
            }
            if(flag) {
                break;
            }
        }
    }

2.8 选择排序

  • 效率较低,经常用它内部循环方式来找最大值和最小值
	public static void selectSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            // 最小值下标
            int minIndex = i;
            for (int j = i+1; j < arr.length; j++) {
                // 用它依次与数组后面的值进行比较
                if(arr[minIndex] > arr[j]) {
                    minIndex = j;
                }
            }
            // 将记录的最小值下标与原记录进行交换
            Utils.swap(arr,i,minIndex);
        }
    }

2.9 插入排序

  • 虽然平均效率低,但是在序列基本有序时,效率较高
  • 找到该数值,需要插入的位置,然后对数组进行移动,将该数插入进去即可。
  • Arrays这个工具类在1.7里做了较大的改动

三、排序的实战练习

3.1 调整数组顺序,奇数在左、偶数在右

输入一个整数数组,调整数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。要求时间复杂度为O()。

解法一:快排思想

  • 左右双指针同时扫描,遇到奇数与偶数停止互换。
     /**
     * 奇数在左、偶数在右
     */
    public static void johh(int[] arr) {
        // 左指针
        int left = 0;
        // 右指针
        int right = arr.length - 1;

        while(left < right) {
            // 如果左边扫描是奇数,则继续扫。扫到偶数停止,记录left
            while(arr[left] % 2 != 0) {
                left++;
            }
            // 如果右边扫描是偶数,则继续扫。扫到奇数停止,记录right
            while(arr[right] % 2 == 0) {
                right--;
            }
            if(left <= right) {
                Utils.swap(arr, left, right);
            }
        }
    }

解法二:归并思想

  • 再开一个数组,仍然是双指针
  • 左右扫描,扫到奇数就放前面,扫到偶数就放后面
  • 但是这种方法很明显多开了个O(n)的空间

3.2 第k个元素

以尽量高的效率求出一个乱序数组中按数值顺序的第K个元素值

算法思想:

  • 这道题的算法应用我们分区的思想

  • 当我们用快排的分区时,我们哨兵的位置 就是他实际排序的下标,哨兵左侧都是小的值,右侧都是大的值

  • 此时我们在进行比较哨兵的位置,是否是我们要找的K位置。

    • 如果 K小于哨兵的位置,我们就进行左侧寻找,对左侧进行划分

    • 如果 K大于哨兵的位置,我们就进行右侧寻找,对右侧进行划分

    • 直到我们找到K的位置为止

  • 容易出错的地方就是我们下标的确定

    /**
     * 第k个元素
     * @param arr 数组
     * @param left 左指针
     * @param right 右指针
     * @param indexK 找K个元素。第2个元素,在数组中实际上是arr[1]
     */
    public static int selectK(int[] arr, int left, int right, int indexK) {
        // 拿到分区的中间下标,此值的位置就是排序后的位置
        int index = 快排.partition(arr, left,right);
        // 哨兵实际的位置
        int realIndex = index - left + 1;
        if(realIndex == indexK) { // 如果说分区的实际位置==我们要找的元素位置
            return arr[index];
        }else if(indexK < realIndex) { // 如果我们找的元素位置 < 分区下标位置,则去左侧继续分区寻找
            return selectK(arr, left,index-1,indexK);
        }else {
            // indexK-realIndex 如果在右侧,则递归后范围变小了,所以要减去前面的realIndex
            return selectK(arr, index+1, right, indexK-realIndex);
        }
    }

 	public static int partition(int[] arr, int low, int high) {
        // 三点中值法
        // 中间下标
        int midIndex = low + ((high - low) >> 1);
        // 中间值下标
        int midValueIndex = -1;
        if(arr[low] <= arr[midIndex] && arr[low] >= arr[high]) {     // 最右侧值 <= 第一个值 <= 中间值
            midValueIndex = low;
        }else if(arr[high] <= arr[midIndex] && arr[high] >= arr[low]) {    //  最右侧值 <= 中间值
            midValueIndex = high;
        }else {
            midValueIndex = midIndex;
        }

        // 1.定义哨兵
        int pivot = arr[low];
        while(low < high) {
            // 2.从右侧开始扫描,扫描到比哨兵小的值结束,记录high
            while(low < high && arr[high] >= pivot) {
                high--;
            }
            // 3. 将该值移动到左端,此时high位置空出
            arr[low] = arr[high];
            // 4.再从左侧开始扫描,扫描到比哨兵大的值结束,记录low
            while(low < high && arr[low] <= pivot) {
                low++;
            }
            // 5. 将该值移动到右端
            arr[high] = arr[low];
        }
        // 6.最后将哨兵插入
        arr[low] = pivot;
        return low;
    }

3.3 寻找发帖水王

Tngo是微软亚洲研究院的一个试验项目。研究院的员工和实习生们都很喜欢在Tango上面交流灌水。传说,Tango有一大“水王”,他不但喜欢发贴,还会回复其他D发的每个帖子。坊间风闻该“水王”发帖数目超过了帖子总数的一半。如果你有一个当前论坛上所有帖子(包括回帖)的列表,其中帖子作者的D也在表中,你能快速找出这个传说中的Tango水王吗?

算法思想:消除法

  • 因为题中说了数量超过了数组的一半,就意味着即使两个不同的数字互相消除,那么最后剩下的一个数仍然是我们要找的数字。
  • 消除的数字出现次数改为0,所以他出现的次数最少为1次。
     /**
     * 不同的数,两两消除
     * @param arr
     */
    public static void searchW(int[] arr) {
        // 候选下标
        int calIndex = 0;
        // 出现次数
        int times = 1;
        for (int i = 1; i < arr.length-1; i++) {
            if(times == 0) {
                // 如果消除后,我们应该更新候选值
                calIndex = i;
                times = 1;
                continue;
            }
            // 如果两者相等,则次数加1
            if(arr[calIndex] == arr[i]) {
                times++;
            }else {
                times--;
            }
        }
        // 最后剩下的元素次数一定大于等于1,就是我们的候选值
        System.out.println(arr[calIndex]);
    }

3.4 最小可用ID

在非负数组(乱序)中找到最小的可分配的id(从1开始编号),数据量1000000

    /**
     * 解法一:快排 复杂度O(nLog(n))
     * @param arr
     */
    public static void searchMinId(int[] arr) {
        快排.quick_Sort(arr, 0, arr.length-1);
        int i = 0;
        while(true) {
            if(arr[i] == i+1) {
                i++;
                continue;
            }
            System.out.println(i+1);
            break;
        }
    }

解法二:O(n)

  • 开辟一个空间,依次比较将数组中的数标记到新数组中1。
  • 最后扫描一遍新数组,找到值为0的就是我们要找的值。
  • 但是如果数据量较大,那么我们开辟的空间也将非常大。
  public static void searchMinIdTwo(int[] arr) {
        int[] helper = new int[arr.length+1];
        for (int i = 0; i < arr.length; i++) {
            // 我们排好序后的数字肯定会小于他的长度,如果突然来个10000,那么前面不会最大值只为10
            // 如果最大值只为10,那么我们就可以忽略掉10000
            // 缺少的部分肯定在0——10之间
            if(arr[i] < arr.length+1)
                helper[arr[i]]++;
        }
        for (int i = 1; i < helper.length; i++) {
            if(helper[i] < 1) {
                System.out.println(i);
                break;
            }
        }
    }

解法三:利用快排思想的partition()

  • 大家自行研究吧~~~😶‍🌫️

3.5 合并有序数组

给定两个排序后的数组A和B,其中A的末端有足够的缓冲空间容纳B。编写一个方法,将B合并入A并排序

算法思想:

  • 与归并排序的思想很像,将两个数组合并成一个有序的数组
  • 同时使用三个指针进行操作

public class 合并有序数组 {
    public static void main(String[] args) {
        int[] arr1 = {1,3,6,7,10,12,17};
        int[] arr2 = new int[12];
        arr2[0] = 2;
        arr2[1] = 4;
        arr2[2] = 5;
        arr2[3] = 9;
        arr2[4] = 15;
        mergeArr(arr1, arr2);
        Utils.printf(arr2);
    }

    // 获取数组元素个数
    public static int getElementCount(int[] arr) {
        int count = 0;
        for (int i = 0; i < arr.length; i++) {
            if(arr[i] != 0) {
                count++;
            }
        }
        return count;
    }

    // 我们要将arr1的数,放入扩容的arr2中
    public static void mergeArr(int[] arr1, int[] arr2) {
        // 指向arr2数组末尾的指针,用来移动
        int current = arr2.length - 1;
        // 指向arr1数组末尾的指针
        int p1 = arr1.length-1;
        // 指向arr2最后一个元素的指针
        int p2 = getElementCount(arr2) - 1;
        // arr1数组元素全部放到arr2中为止
        while(p1 != -1) {
            while(arr1[p1] > arr2[p2]) {
                arr2[current--] = arr1[p1--];
            }
            while(p2 != -1 && arr1[p1] <= arr2[p2]) {
                arr2[current--] = arr2[p2--];
            }
            // 需要单独考虑如果arr2的元素个数比arr1少,那么p2很可能快速到达0
            while(p2 == -1 && p1 != -1) {
                arr2[current--] = arr1[p1--];
            }
        }
    }
}

3.6 逆序对个数

一个数列,如果左边的数大,右边的数小,则称这两个数位一个逆序对。求出一个数列中有多少个逆序对。

算法思想:

  • 通过归并排序将左右两侧都变成有序,我们根据右侧进行判断
  • 如果右侧比左侧数小,那么就是逆序,逆序的个数就是左侧剩余数的个数
  • 归并排序通过递归已经将元素进行就拆分,当我们无法继续拆分的时候,肯定执行当前的函数,所以就进行了合并,然后再从小问题合并成最后大的问题,最后就解决了。
  • 这也是非常经典的分治思想

public static void merge(int[] arr, int l, int mid, int r) {
        // 记录逆序数
        int count = 0;
        // 指向原数组的指针
        int current = l;
        // 辅助数组的左指针
        int left = l;
        // 辅助数组的中间右指针
        int right = mid + 1;
        while(left <= mid && right <= r) {
            if(helper[left] <= helper[right]) {
                arr[current++] = helper[left++];
            }else {
                arr[current++] = helper[right++];
                count += mid - left + 1;
            }
        }
        while (left <= mid) {
            arr[current++] = helper[left++];
        }
        while (right <= r) {
            arr[current++] = helper[right++];
        }
    }

public static void mergeSort(int[] arr, int left, int right) {
        helper = Arrays.copyOf(arr, arr.length);
        // 4.递归出口
        if(left < right) {
            // 中间值
            int mid = left + ((right - left) >> 1);
            // 1.找重复、找变化:对中间值左半部分进行归并排序
            mergeSort(arr, left, mid);
            // 2.找重复、找变化:对中间值右半部分进行归并排序
            mergeSort(arr, mid+1, right);
            // 3.两个部分都排序后进行最重要的合并
            merge(arr,left,mid,right);
        }
    }

3.7 排序数组中找和的因子

给定已排序数组arr和k,不重复打印arr中所有相加和为k的不降序二元组如
输入arr={-8,-4,-3,0,2,4,5,8,9,10},k=10
输出(0,10)(2,8)

  • 经典双指针扫描思想
  public static void selectSum(int[] arr, int k) {
        int left = 0;
        int right = arr.length - 1;
        while(left <= right) {
            // 如果左侧扫描值仍然小于k,则继续向右扫描。因为数组是递增的
            while (arr[left] + arr[right] < k) {
                left++;
            }
            // 如果右侧扫描值仍然大于k,则继续向左扫描。因为数组是递增的
            while(arr[left] + arr[right] > k) {
                right--;
            }
            // 最后判断我们两个指针位置的数是否等于k,如果等于k,则继续移动一个指针,让扫描继续下去
            if(arr[left] + arr[right] == k && left < right) {
                System.out.println("("+arr[left]+","+arr[right]+")");
                left++;
            }
        }
    }

3.8 前K个数

求海量数据(正整数)按逆序排列的前k个数(topK),因为数据量太大,不能全部存储在内存中,只能一个一个地从磁盘或者网络上读取数据,请设计一个高效的算法来解决这个问题
不限制用户输入数据个数,用户每输入一个数据就回车使得程序可立即获得这个数据,用户输入-1代表输入终止
然后用户输入K,代表要求得topK
请输出topK,从小到大,空格分割

解法一:

  • 在已有数据中维持一个最小值,当我们有新的数据要进入时,与最小值进行比较
    • 如果新的数据比我们的最小值大,那么就让他进入我们的数组中
    • 如果没有最小值大,则没有进入数组的资格
  • 然后逆序输出我们的数组即可
  • 但是我们这个并没有进行排序,所以时间复杂度是O(n)
public class 前k个数 {

    public static int[] arr;
    public static int size;
    public static int k;
    
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        k = scan.nextInt();
        arr = new int[k];
        int x = 0;
        while(x != -1) {
            x = scan.nextInt();
            sout(x);
        }
    }

    public static void sout(int x) {
        // 数组元素个数为达到k个,直接插入
        if(size < k) {
            arr[size++] = x;
        }else if(size == k) {
            // 达到k个后,进行判断
            int min = selectMin(arr);
            if(x > min) {
                arr[0] = x;
            }
            Utils.printf(arr);
        }
    }

    /**
     * 寻找数组中的最小值
     * @param arr
     * @return
     */
    public static int selectMin(int[] arr) {
        int min = 99999;
        int index = -1;
        for (int i = 0; i < arr.length; i++) {
            if(min > arr[i]) {
                min = arr[i];
                index = i;
            }
        }
        // 将我们的最小值换到第一个位置
        Utils.swap(arr,index,0);
        return min;
    }
}

解法二:

  • 使用小顶堆可以将复杂度优化到O(nLg(n))
  • 我们维持一个小顶堆,每一次都用根结点(最小值)进行比较
  • 当有新的值加入后,我们继续进行小顶堆调节,将最小值调节到根节点,反复如此

3.9 所有员工年龄排序

公司现在要对几万员工的年龄进行排序,因为公司员工的人数非常多,所以要求排序算法的效率要非常高,你能写出这样的程序吗

  • 输入:输入可能包含多个测试样例,对于每个测试案例
    • 输入的第一行为一个整数n(1<=n<=1000000):代表公司内员工的人数。
    • 输入的第二行包括个整数:代表公司内每个员工的年龄。其中,员工年龄
    • age的取值范围为(1<=age<=99)。
  • 输出:对应每个测试案例
    • 请输出排序后的n个员工的年龄,每个年龄后面有一个空格。
  • 采用计数排序解决问题,时间复杂度O(n)
public class 所有员工年龄排序 {
    public static int[] arr;
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        arr = new int[100];
        while(n != 0) {
            int age = scan.nextInt();
            arr[age]++;
            n--;
        }
        for (int i = 0; i < arr.length; i++) {
            while(arr[i] != 0) {
                System.out.println(i);
                arr[i]--;
            }
        }
    }
}

3.10 特殊排序

输入一个正整数数组,把数组里所有整数拼接起来排成一个数,打印出能拼接出的所有数字中最小的一个。
例如输入数组{3,32,321},则打印出这3个数字能排成的最小数字为:321323

  • 我们可以自定义排序的规则
	public static int f(Integer[] arr) {
        // 通过匿名内部类,自定义比较规则
        Arrays.sort(arr,new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                String s1 = o1 + "" + o2;
                String s2 = o2 + "" + o1;
                return s1.compareTo(s2);
            }
        });
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            sb.append(arr[i]);
        }
        return Integer.parseInt(sb.toString());
    }

3.11 判断数组的包含问题

输入两个字符串str1和str2,请判断str1中的所有字符是否都存在与str2中

  • 学会合理使用Java的API类库

解法一:利用api判断

	public static boolean isInclude(String s1, String s2) {
        for (int i = 0; i < s1.length(); i++) {
            // 取出s1串中的字符
            char c = s1.charAt(i);
            // 通过api判断是否s2字符串是否包含c
            if(s2.indexOf(c) == -1) {
                return false;
            }
        }
        return true;
    }

解法二:快排 + 二分查找

	public static boolean isIncludes(String s1, String s2) {
        // 将s2字符串转成字符数组
        char[] s2Arr = s2.toCharArray();
        // 变成数组的目的就是为了快排后二分查找
        Arrays.sort(s2Arr);
        for (int i = 0; i < s1.length(); i++) {
            char c = s1.charAt(i);
            if(Arrays.binarySearch(s2Arr, c) == -1) {
                return false;
            }
        }
        return true;
    }

四、结尾

  • 对于蓝桥杯排序知识内容就总结这么多,若想深入学习等待后续更新。
  • 我将会继续更新关于蓝桥杯方向的学习知识,感兴趣的小伙伴可以关注一下。
  • 文章写得比较走心,用了很长时间,绝对不是copy过来的!
  • 尊重每一位学习知识的人,同时也尊重每一位分享知识的人。
  • 😎你的点赞与关注,是我努力前行的无限动力。🤩
posted @ 2023-01-07 21:45  lx-Meteor  阅读(138)  评论(1编辑  收藏  举报