【数据结构与算法学习笔记】一、排序算法

这篇博客详细介绍了排序算法,包括异或运算在交换数中的应用、简单排序(选择、冒泡、插入)、递归行为分析、堆排序以及不基于比较的排序方法如计数和基数排序。重点讲解了快速排序的荷兰国旗问题和堆排序的过程。建议在需要排序时优先考虑快速排序,其次考虑堆排序,以达到高效和稳定性的平衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、排序算法

1. 异或运算^

二进制中相同为1,不同为0,可以理解为无进位相加

性质:

  • 0 ^ N = N 、N ^ N = 0
  • 交换律:a ^ b = b ^ a
  • 结合律:a ^ b ^ c = a ^ (b ^ c) 运算顺序不影响结果
交换两个数(swap)

注意:只能在保证每个数都不一样的情况下使用,否则会把数字变成0(任何数异或自己等于0)

if (arr[i] == arr[j]){return;} //保证两个数不一样
    public static void swap(int arr[], int i, int j){
        arr[i] = arr[i] ^ arr[j]; // a = 甲 ^ 乙
        arr[j] = arr[i] ^ arr[j]; //b = 甲 ^ 乙 ^ 乙 = 甲
        arr[i] = arr[i] ^ arr[j]; //a = 甲 ^ 乙 ^ 甲 = 乙
    }
异或例题:找出数组中出现奇数次的数

1、数组中有一个数出现了奇数次,其他出现了偶数次

public static int FindOddTimesNum1(int[] arr){
    int oddNum = 0;
    for (cur : arr){
        oddNum ^= cur; //全部异或,得到结果
    }
    return oddNum;
}

2、数组中有两个数出现了奇数次,其他出现了偶数次

public static int FindOddTimesNum2(int[] arr){
    int oddNum = 0;
    for(cur : arr){
        oddNum ^= cur;
    }
    //重复上一步,得到oddNum = a ^ b
    //因为两个数不一样,所以其中必有一位是1
    
    //提取出1的位置
    int pos = oddNum & (~oddNum + 1)
    //例 oddNum = 1001
    //~oddNum = 0110
    //~oddNum + 1 = 0111
    //oodNum & (~oddNum + 1) = 0001
    //这样提取出来就是最右边是1的一位还是1,其他位置上都为0
    
    int oddNum2 = 0;
    for(cur : arr){
        if((cur & pos) == 0){ //因为pos其他位都是0,&结果一定是0不用管,如果结果=0那么说明这个数在这个1位置上也是0,可以改写成 (cur & pos) == pos
            oddNum2 ^= cur;
        }
    System.out.println(oddNum2 + " " + (oddNum ^ oddNum2))
    }
}

2. 简单排序

  • 选择排序
public static void selectionSort(int[] arr){
    //排除空数组或过小数组
    if(arr == null || arr.length<2){return;} 
    for(int i = 0;i < arr.length - 1;i++){ //i ~ N-1
         int minIndex = i;
         for(int j = i + 1; j < arr.length; j++){ //i ~ N - 1上找最小值的下标
            minIndex = arr[j] < arr[minIndex] ? j : minIndex; 
            }
            swap(arr,i,minIndex); //交换
    }
}
  • 冒泡排序
public static void BubbleSort(int[] arr){
        //去掉不合适的数组
        if (arr == null || arr.length < 2){return;}

        for (int len = arr.length - 1; len > 0; len--){ //每次循环确定最后一个值,i的循环最大值前移一位
            for (int i = 0; i < len; i++){
                if (arr[i] > arr[i + 1]){ //前一个比后一个大,往上冒泡
                    swap(arr,i,i+1); //交换
                }
            }
        }
    }
  • 插入排序
    public static void InsertionSort(int[] arr){
        //去掉不合适的数组
        if (arr == null || arr.length < 2){return;}

        for(int i = 1; i < arr.length; i++){
            for(int j = i - 1; j >= 0; j--){
                if(arr[j] >  arr[j + 1]){
                    swap(arr,j,j+1);
                }
            }
        }
    }

3.递归行为

1、时间复杂度分析

master公式:
T(N) = a * T(N/b) + O(Nd)

logba < d O(Nd)

logba > d O(Nlog~b~a)

logba == d O(Nd * logN)

2、寻找最大值

    public static int process(int[] arr, int L, int R){
        if(L==R){return arr[L];}
        int mid = L + ((R-L) >> 1);
        //直接计算Mid可以是(L+R)/2,但在极端情况下R会过大导致没法计算,因此可以改写成L + (R-L)/2,然后>>1意为右移一位,等同于除2
        int leftMax = process(arr,L,mid);
        int rightMax = process(arr,mid + 1,R);
        return Math.max(leftMax,rightMax);
    }

3、归并排序

    public static void MergeSort(int[] arr,int L, int R){
        if(L==R){return;}
        int mid = L + ((R-L)>>1);
        MergeSort(arr,L,mid);
        MergeSort(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 <= M){
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        while(p1 <= M){
            help[i++] = arr[p1++];
        }
        while(p2 <= M){
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++){
            arr[L + i] = help[i];
        }

    } 

1)求小和问题

求出每个数左边比这个数小的和,全部相加为整个数组的小和

    public static void MergeSort(int[] arr,int L, int R){
        if(L==R){return 0 ;}
        int mid = L + ((R-L)>>1);
        return MergeSort(arr,L,mid); //返回左边小和+右边小和+所有小和(即右组对左组)
        + MergeSort(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;
        int res = 0; //记录小和
        while(p1 <= M && p2 <= M){
        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 <= M){
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++){
            arr[L + i] = help[i];
        }
        return res; //返回小和

    } 

4、快速排序

1)荷兰国旗问题

一、给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。额外空间复杂度O(1),时间复杂度O(N)

思路:

变量left代表左边界,i指向当前位置

  1. [i] <= num,和left+1交换,left++,i++
  2. [i] > num, i++
    public static void Holland(int[] arr,int num){
        int left = 0;
        for (int i = 0;i < arr.length;i++){
            if (arr[i] <= num){
                swap(arr,i,left++);
            }
        }
    }

二、给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,大于num的数放在数组的右边,等于num的数放在数组中间。额外空间复杂度O(1),时间复杂度O(N)

思路:

变量left代表左边界,right代表右边界,i指向当前位置

  1. [i] < num,和left+1交换,left++,i++
  2. [i] == num,i++
  3. [i] > num,和right-1交换,right–,i不变
    public static void Holland2(int[] arr,int num){
        int left = -1;
        int right = arr.length;
        int i = 0;
        while (i < right){
            if(arr[i] < num){
                swap(arr,i++,++left);
            }else if(arr[i] > num){
                swap(arr,i,--right);
            }else {
                i++;
            }
        }
    }

2)快速排序quicksort

public static void quickSort(int[] arr, int L, int R){
    if(L < R){
        swap(arr,L + (int) (Math.random() * (R - L + 1)), R);
        int[] p = partition(arr, L, R);
        quickSort(arr, L, p[0] - 1); //左区
        quickSort(arr, p[1] + 1, R); //右区
    }
}

public static int[] partition(int[] arr, int L, int R){
    int less = L - 1;
    int more = R;
    while(L < more){
        if(arr[L] < arr[R]){
            swap(arr, ++less, L++);
        }else if(arr[L] > arr[R]){
            swap(arr, --more, L);
        }else{
            L++;
        }
    }
    swap(arr, more, R);
    return new int[] {less + 1, more};
}

4.堆排序

1)完全二叉树

从左往右依次填的树就是完全二叉树

父节点 (i - 1) / 2

左子 2 * i + 1

右子 2 * i+2

2)堆排序

每一个子树的头节点是最大值/最小值的称为大/小根堆

插入堆heapInsert,把下面的数往上移

public static void heapInsert(int[] arr, int index){
    //1、当比父节点大时就执行交换,小于等于父节点就跳出while
    //2、当index=0时,就变成了自己跟自己比,也会停止
    while(arr[index] > arr[(index - 1) / 2] ){
        swap(arr,index,(index - 1) / 2);
        index = (index - 1) / 2;
    }
}

堆化数组heapify,把上面的数往下移

public static void heapify(int[] arr, int index, int heapSize){ //heapSize用来记录堆的边界
    int left = index * 2 +1; //左子下标
    while(left < heapSize){ //先判断下方有无孩子,因为右一定大于左所以只判断左
    
    //两个孩子谁大把下标给largest
    //右儿子存在 && 右子比左子大 ? 右子 : 左子
    int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
    
    //父与子直接谁大把下标给largest
    largest = arr[largest] > arr[index] ? largest : index;
    
    if(largest == index){ //没比过就跳出了
        break;
    }
    swap(arr, largest, index); //子大那就交换
    index = largest;
    left = index * 2 + 1;
    }
}

堆排序heapSort

  1. 依次执行heapInsert,完成数组堆化
  2. 把heapSize位置的数跟0位置交换,heapSize–,此时最大值到最后位置且与heap断连
  3. 对0位置执行heapify,保持数组为大根堆
  4. 重复操作2-3,当heapSize==0时,排序完成
public static void heapSort(int[] arr){
    if(arr == null || arr.length < 2){
        return;
    }
    
    for(int i = 0; i < arr.length; i++){ //O(N)
        //把每个数依次执行插入堆,完成数组堆化
        heapInsert(arr,i); // O(logN)
    }
    int heapSize = arr.length;
    swap(arr,0,--heapSize);
    while(heapSize > 0){ //时间复杂度O(N)
        heapify(arr, 0, heapSize); //O(logN)
        swap(arr, 0 , --heapSize); //O(1)
    }
    

只让数组变成大根堆的方法:

倒过来,从后往前使用heapify

for(int i = arr.length - 1; i >= 0; i--){
    heapify(arr, i, arr.length);
}

小根堆(优先级队列)

  • 有缺点:中间的数弹出很低效,有需要中间数操作的手写堆,只需要头尾操作的可以用黑盒
PriorityQueue<Integer> heap = new PriorityQueue<>();
while(!heap.isEmpty()){
    System.out.println(heap.poll());//每次弹出的就是最大值
}

比较器概念

  • 等同于C++重载比较运算符
public static class IdAscendingComparator implements Comparator<Student>{
    //返回负数的时候,第一个参数排前面
    //返回正数,第二个数排前面
    //返回0,无所谓
    @Override
    pubic int compare(Student 01, Student 02){
        return 01.id - o2.id;
    }
}

Arrays.sor(students,new IdAscendingComparator())

利用比较器,实现黑盒大根堆排序

public static class Acomp implements Comparator<Integer>{
    @opverride
    public int compare(Integer arg0, Integer arg1){
        return arg1 - arg0;
        }
    }
    
    PriorityQueue<Integer> heap = new PriorityQueue<>(new Acomp());
    while(!heap.isEmpty()){
    System.out.println(heap.poll());//每次弹出的就是最小值
    }
    

5.不基于比较的排序

  • 必须根据数据状况定制,没那么好用

计数排序

统计词频

基数排序

  • 排的数据必须有进制
  1. 把所有数字的位数统一到跟最多位的数一样,如[2,100] -> [002,100]
  2. 根据末位数字堆到对应数字的桶里
  3. 先进先出,把数字倒出来
  4. 根据倒数第二位数字堆到对应数字桶里
  5. 先进先出,把数字倒出来
  6. 重复操作,把所有位排完数组就有序了
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 L, int R, int digit){
    final int radix = 10; //以10为基底,即所有数都是10进制
    int i = 0, j = 0;
    //有多少个数准备多少个辅助空间
    int[] bucket = new int[R - L + 1];
    for(int d = 1; d <= digit; d++){ //有多少位就进出多少次
        //10个空间
        //count[0]当前位(d位)是0的数字有多少个
        //count[1]当前位(d位)是(0和1)的数字有多少个
        //count[2]当前位(d位)是(0、1和2)的数字有多少个
        //count[i]当前位(d位)是(o~i)的数字有多少个
        //相当于先每个位置词频统计,再在每一位上把把前面的从词频累加
        //于是开始从后往前遍历数组,count里的数是多少,就说明在这个数前面有几个数,count里的这个值就是数应该在桶里的位置
        
        int[] count = new int[radix]; //count[0..9]
        //词频统计
        for(i = L; i <= R; i++){
            j = getDigit(arr[i], d);
            count[j]++;
        }
        //累加
        for(i = 1; i < radix; i++){
            count[i] = count[i] + count[i - 1];
        }
        //进桶,根据对应位置count得出该在的位置,进桶后该位置count--
        for(i = R; i >= L; i--){
            j = getDigit(arr[i], d);
            bucket[count[j] - 1]  = arr[i];
            count[j]--;
        }
        for(i = L, j = 0; i <= R; i++, j++){
            arr[i] = bucket[j];
        }
    }
}
//提取出指定位置的数字
public static int getDigit(int x, int d){
    return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
排序算法时间复杂度空间复杂度稳定性
选择O(n2)o(1)×
冒泡O(N2)O(1)
插入O(N2)O(1)
归并O(N*logN)O(N)
快排(随机p)O(N*lognN)O(logN)×
O(N*logN)O(1)×

总结:能用快排用快排,空间不够用堆排,需要稳定性用归并

基于比较的排序:

  1. 时间复杂度小于O(N*logN)不存在
  2. 在时间复杂度在O(N*logN)的情况下,空间复杂度在O(N)以下且保持稳定性不存在
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值