左神算法笔记(一)排序算法

本文深入探讨了各种排序算法,包括选择排序、冒泡排序、插入排序、归并排序、快速排序、堆排序、计数排序和基数排序。详细介绍了它们的时间复杂度、空间复杂度和稳定性,并提供了具体的代码实现。此外,还提到了异或运算、二分法、递归和堆结构在排序算法中的应用。重点分析了快速排序和堆排序在不同场景下的优势,以及如何根据数据特性选择合适的排序算法。

排序算法复习

点击跳转视频连接

了解时间复杂度

  • 常数操作:一个操作如何和样本数据量没有关系,每次都是固定时间内完成的操作,称为常数操作,eg:加减乘除…

    int a = arr[i]; // 常数操作

    int b = list.get(i); // 非常数操作

  • 时间复杂度:一个算法流程中常数操作数量的一个指标(在程序运行过程中一共进行了多少次常数操作)。在常数操作数量计算表达式中,不要低阶项、只要高阶项,并且忽略高阶项系数所剩下的部分 ,使用O表示(读作big O),例如 选择排序 常数操作数量表达式:a*n^2 + b*n + c,时间复杂度就是O(N^2)

    评价一个算法流程的好坏,首先看时间复杂度指标,当时间复杂度相同(但是常数操作可能并不相同,例如一个是乘法操作,一个是位运算),就需要分析不同数据样本下的实际运行时间来确定那个算法更好

  • 额外空间复杂度:算法运行所耗费的存储空间

排序

总结
// 基于比较的排序:选择、冒泡、插入、归并、快排、堆排
// 不基于比较的排序:计数排序、基数排序

// 稳定性:数组中值相同的元素,使用排序算法排序后,这些相同的元素相对次序位置不发生改变,该排序算法就具备稳定性
// 稳定排序:冒泡、插入、归并、一切桶排序思想下的排序
// 非稳定排序:选择、快排、堆排

// 时间复杂度位O(N^2):选择、冒泡、插入
// 时间复杂度位O(N*logN):归并、快排、堆排
// 时间复杂度位O(N):计数排序、基数排序
排序算法时间复杂度空间复杂度稳定性
选择排序O(N^2)O(1)×
冒泡排序O(N^2)O(1)
插入排序O(N^2)O(1)
归并排序O(N*logN)O(N)
快排O(N*logN)O(logN)×
堆排O(N*logN)O(1)×
计数排序O(N)
基数排序O(N)
  • 一般情况下选择一种排序算法的话,选择快速排序 (虽然快排和堆排时间复杂度是一样,但是快排的常数时间是最快的),只有空间有限制的时候才选择堆排序

  • 基于比较的排序,没有一种算法可以将时间复杂度做到O(N*logN)以下

  • 基于比较的排序,在时间复杂度为O(N*logN)情况下,空间复杂度在O(N)以下,没有一种算法可以做到稳定

  • 常见的坑(非常难,不要求掌握,会破坏):

    • 归并排序额外空间复杂度可以通过 “归并排序内部缓存法” 降为O(1),但是其稳定性就失去了

    • "原地归并排序"可以将额外空间复杂度变为O(1),但是时间复杂度会变成O(N^2)

    • 快速排序可以通过论文"01 stable sort"将稳定性变为稳定,但是于此同时,其额外空间复杂度会变成O(N)

    • 有一道题,是奇数的放在数组的左边,偶数放在数组的右边,还要求原来的相对次序不变,能实现,但是很难,可以参考论文"01 stable sort"!

      经典的快排划分做不到稳定性,但是它是01标准,和奇偶问题一个调整策略,经典快排做不到,所以我不知道这个题怎么解

  • 工程上对排序的排序

    • 充分利用O(N*logN)和O(N^2)排序的各自优势

      例如经典快排,在叫样本量的可以使用插入排序进行排序,大样本量在使用快排,利用了各自的优势

    • 稳定性的考虑

      基础类型的数据使用快排,非基础类型使用归并排序(保证稳定性)

一、选择排序

数组中有n个元素

在数组上0~n-1上选取一个最小值,放到0位置上

在数组上1~n-1上选取一个最小值,放到1位置上

  • Code

    public statis void selectionSort(int[] arr) {
        if(arr == null || arr.length < 2) {
            return;
        }
        for(int i == 0; i <= arr.length; i++) {
            int minIndex = i;
            for(int j == i+1; j< arr.length; j++) {
                if() {
                    minIndex = arr[j] < arr[minIndex] ? j : minIndex;
                }
                swap(arr, i, minIndex);
            }
        }
    }
    
    public static void swap(int arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
  • 时间复杂度:O(N^2)

  • 额外空间复杂度:O(1)

二、冒泡排序

数组中有n个元素

在数组上0~n-1上从左到右两两比较,谁大谁往右移动,一次下来最大数就排好序

在数组上0~n-2上从左到右两两比较,谁大谁往右移动,一次下来最大数就排好序

  • Code

    public static void bubbleSort() {
        if(arr == null || arr.length < 2) {
            return;
        }
        for(int i == arr.lrngth - 1; i > 0; i--){ // 0~n-1上
            for(int j == 0; j < i; j++){ // 从左到右两两比较,大的放右边
                if(arr[j] > arr[j+1]) {
                    swap(arr, j, j+1);
                }
            }
        }
    }
    // 使用位运算进行交换,不申请额外空间(前提是i和j不相等,就是说不能是同一个内存区域异或操作后会变成0)
    public static void swap(int arr, int i, int j){
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }
    
  • 时间复杂度:O(N^2)

  • 额外空间复杂度:O(1)

三、插入排序

插入排序操作类似于打扑克中的起牌操作(没起一张牌,插入到适合的位置,保证有序)

数组中有n个元素

做到0~0范围有序(小的放左大的放右,右侧比左侧值小,则交换)

做到0~1范围有序

0~n范围有序

  • Code

    public static void insertionSort(int[] arr) {
        if(arr == null || arr.length < 2) {
            return;
        }
        // 1~n上的元素依此插入到已排序数组中
        for(int i = 1; i < arr.length; i++) {
            for(int j = i - 1; j >= 0 && arr[j] > arr[j+1]; j--){
    			swap(arr, j, j+1);
            }
        }
    }
    // 使用位运算进行交换,不申请额外空间(前提是i和j不相等,就是说不能是同一个内存区域异或操作后会变成0)
    public static void swap(int arr, int i, int j){
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }
    
  • 时间复杂度:O(N^2)

    数据状况不同,插入排序的时间复杂度则不同,在默认有序的情况下时间复杂度是O(N)的,但是当数据反向有序的时候,常数操作数量表达式是一个等差数列求和,时间复杂度就是O(N^2)

  • 额外空间复杂度:O(1)

四、归并排序

数组中有n个元素

先让0n/2范围上有序,再让n/2+1n范围上有序,然后再让整体有序

同理,0~n/2也采用上述方法,从中间划分,先让左侧有序,再让右侧有序,最后让整体有序

最后整个范围就是有序的

  • Code

    public static void mergeSort(int[] arr) {
        if(arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length-1);
    }
    public static process(int[] arr, int L, int R) {
        if(L == R) {
            return;
        }
        int mid = L + (R-L)>>1;
        process(arr, L, mid);
        process(arr, mid+1, R);
        merge(arr, L, mid, R);
    }
    
    // 两个指针分别指向左侧、右侧的最左位置,两数比较取小值放入辅助空间,下标右移依此进行比较
    public static void merge(int[] arr, int L, int M, int R) {
        int[] help = new int(R-L+1);
        int i = 0;
        int p1 = L;
        int p2 = M+1;
        while(p1<=M && p2<=R) {
            help[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
        }
        while(p1 < M){
            help[i++] = arr[p1++];
        }
        while(p2 < R){
            help[i++] = arr[p2++];
        }
        for(int i = 0; i < help.length; i++) {
            arr[L+i] = help[i];
        }
    }
    
  • 时间复杂度:通过master公式求出系数待人公式求得为:O(N*logN)

    选择、冒泡、插入排序,大量的比较信息被丢弃,而归并排序利用了有序信息,减少了比较次数,所以时间复杂度便小了

  • 空间复杂度:O(N)

五、快排

根据荷兰国旗问题(给定一个数,将数组中的元素按照给定数值划分为小于、等于、大于三个区域)进行类似解决

  • 快排1.0:使用数组最右边做划分值进行划分,让左部分做到小于等于划分值的在左边,大于等于划分值的在右边,最后和大于区域的第一个数做交换(递归求解)

    时间复杂度O(N^2)

  • 快排2.0:使用数组最右边做划分值进行划分,让左部分做到小于划分值的在左边,等于划分值的在中间,大于等于划分值的在右边,最后和大于区域的第一个数做交换(递归求解)

    时间复杂度O(N^2),但是2.0版本本质上比快排1.0版本要快,因为2.0依此划分搞定一批等于划分值的数,而1.0版本一次划分只能搞定一个数,最差情况:[1,2,3,4,5,6,7]

  • 快排3.0:随机使用数组中的某一个数,和最后一个数做交换,并用此数做划分值进行划分,让左部分做到小于划分值的在左边,等于划分值的在中间,大于等于划分值的在右边,最后和大于区域的第一个数做交换(递归求解)

    时间复杂度O(N*logN)

  • Code

    public static void main(String[] args) {
        int[] arr = new int[]{1, 5, 3, 4, 2, 6};
        System.out.println(arr);
        quickSort(arr);
        for (int num : arr) {
            System.out.print(num);
        }
    }
    
    private static void quickSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length - 1);
    }
    
    private static void process(int[] arr, int L, int R) {
        if (L >= R) return;
        // 随机选取一个数
        int random = L + (int) (Math.random() * (R - L + 1));
        swap(arr, random, R);
    
        // 开始划分
        int[] p = partition(arr, L, R);
        process(arr, L, p[0] - 1);
        process(arr, p[1] + 1, R);
    }
    
    private static int[] partition(int[] arr, int L, int R) {
        int p1 = L - 1;
        int p2 = R;
        int i = L;
        while (i < p2) {
            if (arr[i] < arr[R]) {
                swap(arr, i++, ++p1);
            } else if (arr[i] == arr[R]) {
                i++;
            } else if (arr[i] > arr[R]) {
                swap(arr, i, --p2);
            }
        }
        swap(arr, p2, R);
        return new int[]{p1 + 1, p2};
    }
    
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
  • 时间复杂度:O(N*logN),因为是随机选取的一个数进行划分,所以好的情况和坏的情况都会存在,所以需要求所有情况的概率累加情况计算其长期期望值

  • 额外空间复杂度:O(logN),最坏情况下递归会开N层,所以最坏的额外空间复杂度为O(N )

六、堆排序

首先将数组进行heapInsert操作,将被数组变成一个大根堆

然后从数组中取出最大的那个数,与大根堆最后一个数做交换,出去最大的那个数,剩余元素进行heapify操作,是其剩余部分也转化成大根堆

依此执行上述步骤,直到取出所有元素时停止(heapSize等于零)

  • Code

    private static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        // 首先将数组变成大根堆
        // 方法一,O(N*logN)
        for (int i = 0; i < arr.length; i++) { // O(N)
            heapInsert(arr, i); // O(logN)
        }
        // 方法二,自底向上,逐步让数组变成大根堆O(N)
        // for (int i = arr.length - 1; i >= 0; i--) {
        //     heapify(arr, i, arr.length);
        // }
    
        int heapSize = arr.length;
        while (heapSize > 0) { // O(N)
            swap(arr, 0, --heapSize); // O(1)
            heapify(arr, 0, heapSize); // O(logN)
        }
    }
    
    // heapInsert
    private static void heapInsert(int[] arr, int index) {
        // 当前节点和父节点进行比较,如果大于父节点,则进行交换,当前节点来到父节点,重复操作,直到于根节点比较完成才结束
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }
    
    // heapify
    private static void heapify(int[] arr, int index, int heapSize) {
        // 判断是否有孩子,如果有判断此节点是否小于孩子节点,小于侧进行交换,当前节点来到较大的孩子节点位置,重复操作,直到此节点没有孩子节点或者比孩子节点数值大结束
        int left = index * 2 + 1;
        while (left < heapSize) {
            int maxIndex = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            if (arr[index] >= arr[maxIndex]) {
                break;
            } else {
                swap(arr, index, maxIndex);
                index = maxIndex;
                left = index * 2 + 1;
            }
        }
    }
    
  • 时间复杂度:O(N*logN),不管是heapInsert操作还是heapify操作,保持堆结构不变最大需要调整二叉树的深度次,堆的深度LogN,从头到尾要依此获取大根堆的最大值,剩余部分heapify,因此时间复杂度数学上求长期期望得到为O(N*logN)

七、计数排序

是一类不基于比较的排序

创建一个包含所有情况的一个数组

遍历给定的整个数组,在创建数组中找到其对应的位置,并且对其进行计数

等待数组遍历结束,将创建的数组从左到右遍历(不为空的进行输出),得到排序结果

  • 时间复杂度:O(N)

  • 额外空间复杂度:O(K)

八、基数排序

给定一个待排序的数组

首先找出数组中最大的那个数,并判断该数一共有几位,将不足此位数的其他元素,前面补0

准备十个桶,依此代表0~9

然后从将数组中的元素依此遍历,按照个位数字依此进桶,让后按顺序出桶(同一个桶里,先进的先出)

重复上述操作,依此比较十位、百位…直到最高位

最后出桶结束,数组就已经排好序了

  • Code

    // 思路
    // 1.获取此数组中的最大数,并求出其位数(进桶出桶次数)
    // 2.遍历最大位数次数(代表进行了位数次入桶和出桶)
    // 3.使用特殊方式进行进桶和出桶操作
    
    // 基数排序
    public static void radixSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        radixSort(arr, 0, arr.length - 1, maxBits(arr));
    }
    
    // 求一个数组(十进制数)中最大数的位数
    public static int maxBits(int[] arr) {
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max, arr[i]);
        }
        int res = 0;
        while (max != 0) {
            res++;
            max /= 10;
        }
        return res;
    }
    
    // 基数排序核心方法
    public static void radixSort(int[] arr, int begin, int end, int digit) {
        final int radix = 10;
        int i = 0, j = 0;
        int[] count = new int[radix]; // 此数组用于记录各元素小于等于指定位上的数出现的次数,例如:当前是所处位数为十位,count[5]代表十位上小于等于6的元素一共出现了几次
        int[] bucket = new int[end - begin + 1]; // 辅助数组
    
        // 完成最大位数次入桶和出桶操作
        for (int d = 1; d <= digit; d++) {
            // 初始化count数组,计数都设为0
            for (i = 0; i < radix; i++) {
                count[i] = 0;
            }
            // (1)统计指定位上为i的元素出现的次数
            for (i = begin; i <= end; i++) {
                j = getDigit(arr[i], d);
                count[j]++;
            }
            // (2)优化count数组,表示为指定位上小于等于i的元素出现的次数
            for (i = 1; i < radix; i++) {
                count[i] = count[i] + count[i - 1];
            }
            // (3)数组各元素完成一次进桶
            for (i = end; i >= begin; i--) {
                j = getDigit(arr[i], d);
                bucket[count[j] - 1] = arr[i];
                count[j]--;
            }
            // (4)数组各元素完成一次进桶
            for (i = begin, j = 0; i <= end; i++, j++) {
                arr[i] = bucket[j];
            }
        }
    }
    
    // 获取指定位上的数
    public static int getDigit(int x, int d) {
        return ((x / ((int) Math.pow(10, d - 1))) % 10);
    }
    
  • 时间复杂度:O(N)

  • 额外空间复杂度:O(K)

其他

一、异或运算
  • 异或运算(无进位相加:10110 + 00111 = 10001)

    • 性质:0^N = N;N^N = 0;且满足结合律(与顺序无关)

    • 交换a、b两数的值(前提不是一个数)

      a = 17;
      b = 13;
      a = a^b;
      b = a^b;
      a = a^b;
      
  • 例题

    // 1.已知一个数组中有一个数出现了奇数次其他所有数都出现了偶数次,求出现了奇数次的那个数
    // 思路:
    // 准备一个数 int eor = 0
    // 数组从左到右一次和eor做异或运算
    // 异或结束最后那个值就是要求的出现了奇数次的那个数
    public static void printOddTimesNum1(int[] arr) {
        int eor = 0;
        for(int cur : arr){
            eor ^= cur;
      }
        System.out.println(eor);
    }
    
    // 2.已知一个数组中有两个个数出现了奇数次其他所有数都出现了偶数次,求出现了奇数次的那两个数各是什么值
    // 思路:
    // 准备一个数 int eor = 0,假设数组中出现了奇数次的那两个数分别是a、b
    // 数组从左到右一次和eor做异或运算
    // 异或结束最后那个值则为a异或b的值
    // 然后求出a异或b用二进制表示最后一位为1的数,例如:a^b=10110,求出的值为00010=2
    // 用上述求出的值再次与数组中与此数与运算为0的元素依此做异或运算(或者与运算大于0的元素,主要是用于区分a和b)
    // 上步操作得出的值就为a和b其中的一个值
    // 再将a异或b的值与求出的其中的一个值异或就得到另一个数的值
    public static void printOddTimesNum1(int[] arr) {
        int eor = 0, onlyOne = 0;
        for(int cur : arr){
            eor ^= cur;
        }
        // 此时eor = a^b,且必有一个位置上为1(证明此位置上两个数不同)
        int rightOne = eor & (~eor + 1); // 获取a^b二进制值最右侧为1所表示的数
        for(int cur : arr){
            if(rightOne & cur == 0){
                onlyOne ^= cur;
            }
        }
        int otherOne = eor ^ onlyOne; 
        
        System.out.println(onlyOne+", "+otherOne);
    }
    
二、二分法
  • 时间复杂度:因为每次排除掉剩余的一半,所以时间复杂度为O(log(2,N)),也写做O(logN)

  • 例题:

    // 1.在一个有序数组中,找某个数是否存在
    
    // 2.在一个有序数组中,找大于等于某个数的最左侧数的位置(或者小于等于某个个数的最右侧数的位置)
    // [1,2,2,2,3,3,3,4,4,4,] 求大于等于3最左侧的数的下标位置,可得出为4
    
    // 3.无序数组arr,其中任意两个相邻的数不相等,求指定范围内的局部最小值(0位置比1位置数小,最后一个比倒数第二个数小,中间某个数比左侧和右侧都小,则这三个值都为局部最小)
    // 思路:
    // 先判断指定范围[i,j]的两个边界值是否式局部最小,是则返回,不是则(i,j)上一定比存在局部最小
    // 利用二分法,直接定位到[i,j]上最中间的那个数,判断是否为局部最小,是则返回,不是则比中间值大的那一侧而一定存在局部最小
    // 重复上述步骤,即可得到一个局部最小
    
三、递归

排序中,归并排序,堆排序都是通过递归来实现的

  • 例1:求一个数组中的指定范围内的最大值

    p(i,n) = max(p(i,mid), p(mid+1,n))

  • 通过master可以估算递归行为的时间复杂度

    • 前提:必须满足公式 T(N) = a*(TN/b)+O(N^d)

    • 例如求最大值递归就可以表示为:T(N) = 2*(TN/2)+O(1)

    • master公式

      log(b,a) > d 复杂度为O(N^log(b,a))
      
      log(b,a) = d 复杂度为O(N^d*logN)
      
      log(b,a) < d 复杂度为O(N^d)
      
    • 代入上式得出求最大值递归时间复杂度为O(N)

  • 归并排序

    • 整体就是一个简单递归,左侧排好序,右侧排好序,再让整体有序
    • 让其整体有序的过程用了外排序的方法(拷贝到额外的数组中去)
    • 利用master公式来求解时间复杂度
    • 归并排序的实质
    • 时间复杂度:O(N*logN),额外空间复杂度:O(N)
  • 小和问题

    // 小和:在一个数组上,每一个数左边比当前数小的数累加其看来,叫这个数组的小和
    // 思路:
    // 转换思路,数组操作到右依此求出右侧大于当前数值的个数,之后相乘累加即可
    // 左侧排序求小和的数值
    // 右侧排序求小和的数值
    // 合并时,合并操作产生的小和数值(注意!!!:合并比较时如果相等一定先拷贝右侧的数)
    // 三部分加起来就为数组整体的小和
    
    // 讲归并排序进行改写
    public static int smallSum(int[] arr) {
        if(arr == null || arr.length < 2) {
            return 0;
        }
        return process(arr, 0, arr.length-1);
    }
    public static int process(int[] arr, int L, int R) {
        if(L == R) {
            return;
        }
        int mid = L + (R-L)>>1;
        return process(arr, L, mid) + process(arr, mid+1, R) + merge(arr, L, mid, R);
    }
    
    // 两个指针分别指向左侧、右侧的最左位置,两数比较取小值放入辅助空间,下标右移依此进行比较
    public static int merge(int[] arr, int L, int M, int R) {
        int[] help = new int(R-L+1);
        int i = 0;
        int p1 = L;
        int p2 = M+1;
        int res = 0;
        while(p1<=M && p2<=R) {
            res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
            help[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
        }
        while(p1 < M){
            help[i++] = arr[p1++];
        }
        while(p2 < R){
            help[i++] = arr[p2++];
        }
        for(int i = 0; i < help.length; i++) {
            arr[L+i] = help[i];
        }
        result res;
    }
    
  • 荷兰国旗问题

  // 1.给定一个数组arr,和一个数num,请把小于等于num的数放到数组的左侧,大于num的数放到数组的右边,要求额外空间复杂度为O(1),时间复杂度为O(N)
  // 解决思路:
  // 1)arr[i] <= num arr[i]和小于等于区域的下一个数做交换,小于等于向右扩,i++
  // 2)arr[i] > num,i++
  
  // 2.给定一个数组arr,和一个数num,请把小于num的数放到数组的左侧,等于num的数放到数组的中间,大于num的数放到数组的右边,要求额外空间复杂度为O(1),时间复杂度为O(N)
  // 解决思路:
  // 1)arr[i] < num arr[i]和小于区域的下一个数做交换,小于向右扩,i++
  // 2)arr[i] = num,i++
  // 2)arr[i] > num,arr[i]和大于区域的前一个数做交换,大于区域左扩,i不变
四、堆结构

在逻辑概念上,堆其实是一个完全二叉树结构(要不是满二叉树,要不就是从左到右一次变满的一棵树)

一个数组,可以将从0开始连续的一段用二叉树进行表示,i位置上的左孩子:2*i+1,i位置的右孩子:2*i+2,i位置的父节点:(i-1)/2

堆是一种特殊的完全二叉树,可以分为大根堆和小根堆

  • 大根堆:对于一颗完全二叉树,每一棵子树的最大值就是头节点对应的那个值
  • 小根堆:对于一颗完全二叉树,每一棵子树的最小值就是头节点对应的那个值

数组可以依此加进二叉树中,并进行调整变成大根堆、小根堆

调整堆结构包含两个操作:heapInsert操作、heapify操作

  • heapInsert过程:每加入一个数,heapSize++,并和此数位置对应的父节点上的树进行比较,大于父节点,进行交换,重复操作,直到找到根节点,时间复杂度O(LogN)

    // heapInsert
    private static void heapInsert(int[] arr, int index) {
        // 当前节点和父节点进行比较,如果大于父节点,则进行交换,当前节点来到父节点,重复操作,直到于根节点比较完成才结束
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }
    
  • heapify过程:指定位置进行heapify,判断此节点是否有孩子节点,如果有判断是否孩子节点比此节点对应的数值大,如果大于,则进行交换,当前节点来到较大的孩子节点位置,重复以上操作,直到当前节点没有孩子节点或大于等于孩子节点时结束,时间复杂度O(LogN)

    // heapify
    private static void heapify(int[] arr, int index, int heapSize) {
        // 当前节点和父节点进行比较,如果大于父节点,则进行交换,当前节点来到父节点,重复操作,直到于根节点比较完成才结束
        int left = index * 2 + 1;
        while (left < heapSize) {
            // 获取左孩子和右孩子中数值较大孩子节点的位置
            int maxIndex = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            // 当前节点大于等于孩子节点时停止
            if (arr[index] >= arr[maxIndex]) {
                break;
            } else {
                swap(arr, index, maxIndex);
                index = maxIndex;
                left = index * 2 + 1;
            }
        }
    }
    
  • 优先级队列就是堆

    // 待添加到优先级队列的实体类
    static class Student {
        String name;
        int age;
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
    // 定义学生对象的比较器
    static Comparator<Student> cmp = new Comparator<Student>() {
        public int compare(Student stu1, Student stu2) {
            // 学生年龄升序排列
            return stu1.age - stu2.age;
        }
    }; 
    
    public static void main(String[] args) {
        PriorityQueue<Student> queue = new PriorityQueue<>(cmp);
        Student zhangSan = new Student("zhanzhangSangsan", 65);
        Student liSi = new Student("liSi", 18);
        Student wangWu = new Student("wangWu", 25);
        queue.add(zhangSan);
        queue.add(liSi);
        queue.add(wangWu);
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
    }
    
    // 结果: 
    // Student{name='liSi', age=18}
    // Student{name='wangWu', age=25}
    // Student{name='zhanzhangSangsan', age=65}
    // null
    
  • 堆排序扩展题目

    // 已知一个几乎有序的数组,就是说如果数组拍好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序
    private static void sortedArrDistanceLessK(int[] arr, int k) {
        // 优先级队列默认小根堆
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        int index = 0;
        for (; index <= Math.min(k, arr.length - 1); index++) {
            heap.add(arr[index]);
        }
        int i = 0;
        for (; index < arr.length; i++, index++) {
            heap.add(arr[index]);
            arr[i] = heap.poll();
        }
        while (!heap.isEmpty()) {
            arr[i] = heap.poll();
        }
    }
    
### 关于神算的学习资料 以下是关于神算些学习资料和笔记整理: #### 链表相关知识点 反转单向链表的核心在于调整节点之间的指向关系,通过遍历整个链表并逐步改变 `next` 的方向完成操作[^1]。 ```python def reverse_linked_list(head): prev = None current = head while current is not None: next_node = current.next # 记录下个节点 current.next = prev # 当前节点指向前节点 prev = current # 前移prev到当前节点 current = next_node # 移动current至下节点 return prev # 返回新的头结点 ``` 对于双向链表的反转,则需要同时处理 `pre` 和 `next` 指针的关系[^1]。 判断个链表是否为回文结构可以通过快慢指针找到中间节点,并将后半部分链表反转后再逐比较前后两部分节点值是否相同[^1]。 #### 排序算法分析 快速排序种分治的应用实例,在递归过程中会不断选取基准值并将数组划分为小于等于基准值的部分与大于基准值的部分[^3]。其时间复杂度通常为 O(N log N),但在最坏情况下可能退化为 O()[^4]。 堆排序利用了最大/最小堆这数据结构特性,每次都将根节点取出作为最终序列的部分,随后重新构建剩余元素构成的新堆直至全部排列完毕[^3]。 #### 贪心策略应用案例 在解决某些特定问题时可以运用贪心思想来简化求解过程。例如八皇后问题中可通过位运算技巧加速状态转移判定效率[^5]: ```python col_lim, pie_lim, na_lim = 0b00000000, 0b00000000, 0b00000000 limit = (1 << 8) - 1 def place_queen(row, col_lim, pie_lim, na_lim): if row == 8: return 1 pos = limit & (~(col_lim | pie_lim | na_lim)) res = 0 while pos != 0: p = pos & (-pos) pos -= p res += place_queen( row + 1, col_lim | p, (pie_lim | p) << 1, (na_lim | p) >> 1 ) return res ``` 以上代码片段展示了如何基于位掩码技术高效枚举合放置方案数量[^5]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值