算法学习(持续更新中)

news2024/11/24 20:26:40

学习视频:一周刷爆LeetCode,算法大神左神(左程云)耗时100天打造算法与数据结构基础到高级全家桶教程,直击BTAJ等一线大厂必问算法面试题真题详解(马士兵)_哔哩哔哩_bilibili

时间复杂度

一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。

时间复杂度为一个算法流程中,常数操作数量的一个指标。常用O(读作big O)来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中发生了多少常数操作,进而总结出常数操作数量的表达式。

在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度就位O(f(N))。

评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数项时间”.

常数操作:例如数组根据索引寻值,加、减、乘、除、比较

排序

选择排序

每次选择 i 到 n-1 范围内值最小的索引位置 t ,每一次内层循环结束之后交换 i 和 t 索引位置上的值,每一次外层循环 i 的值加 1

private static void selectSort(Integer[] nums) {
        for(int i=0;i<nums.length-1;i++){
            int t=i;
            for(int j=i+1;j<nums.length;j++){
                if(nums[t]>nums[j]){
                    t=j;
                }
            }
            swap(nums,i,t);
        }
    }

时间复杂度:O(n^2) ,空间复杂度:O(1)

冒泡排序

每次从 0 到 i 的范围中将相邻的值进行两两比较,将大的一个值移到后面,这样每一次内层循环结束后,i 位置的值就是0 到 i 范围内最大的值,每一次外层循环 i 的值减 1.

private static void BubbleSort(Integer[] nums) {
        int n=nums.length;
        for(int i=n-1;i>0;i--){
            for(int j=0;j<i;j++){
                if(nums[j]>nums[j+1]){
                    swap(nums, j, j+1);
                }
            }
        }
    }

时间复杂度:O(n^2) ,空间复杂度:O(1)

插入排序

i 从 0 开始,维护一个 0 到 i 范围的有序数组,每一次 i++ ,令 j 等于 i,j 所在位置的元素 和 j-1 所在位置的元素进行比较 因为前 j-1 个元素是有序的,所以 j 就往前进行比较,如果 j 所在位置 比 j-1 小,就进行交换,再往前进行比较,知道比前面的大为止。

private static void MySort(Integer[] nums) {
        if(nums==null || nums.length<2){
            return;
        }
        int l=nums.length;
        for(int i=1;i<l;i++){
            for(int j=i;j>0;j--){
                if(nums[j]<nums[j-1]){
                    swap(nums,j,j-1);
                }else{
                    break;
                }
            }
        }
    }

最坏情况下 时间复杂度为 O(n^2),空间复杂度为O(1)

归并排序

将一个数组分为左右两个子数组,先将这两个子数组分别排好序,再融合,使得父数组也有序。

public static void mergeSort(int[] arr,int l,int r){
        if(l==r) return;
        process(arr,l,r);
    }
​
    private static void 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);
    }
​
    private static void merge(int[] arr, int l, int m, int r) {
        int[] help=new int[r-l+1];
        int p1=l;
        int p2=m+1;
        int i=0;
        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 j = 0; j < help.length; j++) {
            arr[l+j]=help[j];
        }
    }
根据master公式可以得到:
T(N) = 2T(N/2)+O(N) --> 每一次将父函数分成两个数据量为父函数一半的子函数,mergn函数的时间复杂度为O(N)
所以:log(a,b)==d-->log(2,2)==1
所以归并排序的时间复杂度为:O(N*logN),空间复杂度:O(N)
归并排序的扩展
1)小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和

例如:[1,3,4,2,5]:左边比1小的数,没有,为0;3左边比3小的数,1个1;4左边比4小的数:1 * 1+1 * 3=4;2左边比2小的数:1个1;5左边比5小的数:1+3+4+2=10;所以小和为:0+1+4+1+10=16。

也可以看一个数右边有几个比他大的数:1右边有4个数比他大,1 * 4=4;3右边有两个数比他大,2 * 3=6;4右边有1个数比他大,1 * 4=4;2右边有1个数比他大,1 * 2=2;5右边没有比他大的数 ,0;所以小和为:4+6+4+2+0=16。

代码实现:

public static int smallSum(int[] arr,int l,int r){
        if(l==r) return 0;
        return process(arr,l,r);
    }
​
    private static int process(int[] arr, int l, int r) {
        if(l==r) return 0;
        int mid=l+((r-l)>>1);
        return process(arr,l,mid) + process(arr,mid+1,r) + merge(arr,l,mid,r);
    }
​
    private static int merge(int[] arr, int l, int m, int r) {
        int[] help=new int[r-l+1];
        int p1=l;
        int p2=m+1;
        int i=0;
        int res=0;
        while(p1<=m && p2<=r){
            //判断右边数组中有几个比左边数组当前p1下标下的数大,计算和
            res+=arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
            //在merge的时候,当右边数组p2下标对应的数跟左边数组p1下标对应的数相等的时候,先把右边数组的数加入到临时数组中,因为在去掉了右边与左边相等的数之后,才能得到右边比左边大的数的数量。
            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 j = 0; j < help.length; j++) {
            arr[l+j]=help[j];
        }
        return res;
    }

流程图:

2)逆序对问题

在一个数组中,左边的数如果比右边的数大,则拆两个数构成一个逆序对,请打印所有逆序对。

代码:

//int[] arr={3,2,4,5,1};
private static int merge(int[] arr, int l, int m, int r) {
        int[] help=new int[r-l+1];
        int p1=l;
        int p2=m+1;
        int i=0;
        int res=0;
        while(p1<=m && p2<=r){
            //在 1)的代码上加入这一段判断逻辑,就可以打印出所有的逆序对
            if(arr[p1]>arr[p2]){
                int p11=p1;
                while(p11<=m){
                    System.out.println("["+arr[p11++]+" "+arr[p2]+"]");
                }

            }
            res+=arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
            //这里与 1)有差别,当左边和右边相等的时候,移动左边的
            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 j = 0; j < help.length; j++) {
            arr[l+j]=help[j];
        }
        return res;
    }

运行结果:

[3 2]
[5 1]
[2 1]
[3 1]
[4 1]

快速排序

思考问题

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

思路:定义一个小于等于区,初始下标为-1,i 从0开始遍历数组,可以分为两种情况 (1)arr[i] <= num,nums[i]和小于等于区的下一个数交换,小于区右扩,i++。(2)arr[i] > num,i++。

代码:

//arr={3,4,6,7,5,3,5,8},num=5
public static void func1(int[] arr,int num){
        int i=0;
        int left=-1;
        while(i<arr.length){
            if(arr[i]<=num){
                swap(arr,++left,i++);
            }else if(arr[i]>num){
                i++;
            }
        }
        System.out.println(Arrays.toString(arr));
    }

运行结果:

[0, 0, 5, 3, 5, 7, 6, 8]

2)荷兰国旗问题:给定一个数组arr,和一个数num,请把小于num的数放在数组的左边。等于num的数放在数组的中间,大于num的数放在数组的右边,要求额外空间复杂度为O(1),时间复杂度O(N)。

思路:定义一个小于区,初始下标为-1;定义一个大于区,初始下标为arr.length,i 从0开始遍历数组,有三种情况:(1)arr[i] < num ,nums[i]和小于区的下一个数交换,小于区右扩,i++。(2)arr[i] = num ,i++。(3)arr[i] > num ,交换 num[i] 和大于区前一个数,大于区左扩。

代码:

//arr={3,4,6,7,5,3,5,8},num=5
public static void func2(int[] arr,int num){
        int i=0;
        int left=-1;
        int right=arr.length;
        while (i<right){
            if(arr[i]<num){
                swap(arr,++left,i++);
            }else if(arr[i]==num){
                i++;
            }else{
                swap(arr,--right,i);
            }
        }
        System.out.println(Arrays.toString(arr));
    }

运行结果:

[0, 0, 3, 5, 5, 7, 8, 6]

快排1.0

使用思考1)的方式,每次确认一个数的位置

    public static void quickSort(int[] arr){
        if(arr==null || arr.length<2){
            return;
        }
        quickSort(arr,0,arr.length-1);
    }
​
    public static void quickSort(int[] arr,int l,int r){
        if(l<r){
            //r所在位置的元素作为基准点
            int i = partition(arr, l, r);
            quickSort(arr,l,i);
            quickSort(arr,i+1,r);
        }
    }
​
    public static int partition(int[] arr, int l, int r){
        int left=l-1;
        while(l<r){
            if(arr[l]<=arr[r]){
                swap(arr,++left,l++);
            }else if(arr[l]>arr[r]){
                l++;
            }
        }
        swap(arr,left+1,r);
        //left是小于区的边界
        return left;
    }

对于数组:arr={1,2,3,4,5,6,7,8,9},最坏时间复杂度为O(N^2)

快排2.0

使用 思考 2) 的方式,每次确认一种数的位置

代码:

    public static void quickSort(int[] arr){
        if(arr==null || arr.length<2){
            return;
        }
        quickSort(arr,0,arr.length-1);
    }
    //选择数组中最后一位为 基准点
    public static void quickSort(int[] arr,int l,int r){
        if(l<r){
            //p的大小为2,是与最后一位数相等的数的左右边界
            int[] p=partition(arr,l,r);
            //左边继续
            quickSort(arr,l,p[0]-1);
            //右边继续
            quickSort(arr,p[1]+1,r);
        }
    }
​
    private static int[] partition(int[] arr, int l, int r) {
        int[] p=new int[2];
        //小于区域边界
        int less=l-1;
        //大于区域边界
        int more=r;
        //当l达到大于边界时说明已经分类完成
        while(l<more){
            //l所在位置比基准点位置小,arr[l]和小于区的下一个数交换,小于区右扩,l++
            if(arr[l]<arr[r]){
                swap(arr,++less,l++);
            }else if(arr[l]==arr[r]){
                l++;
            }else{
                //arr[l]>arr[r],l位置上的值与大于区前一个位置上的值进行交换,大于区左扩
                swap(arr,--more,l);
            }
        }
        //基准点所在当前数组的最后一位r,和大于arr[r]上的第一个值的位置(也就是大于区的边界)进行交换。
        swap(arr,r,more);
        p[0]=less+1;//等于基准点的数的左边界
        p[1]=more;//等于基准点的数的有边界
        //此时:l~less 就是比基准点小的数,more+1~r就是比基准点大的数
        return p;
    }

如果我们选择的数组为 arr={1,2,3,4,5,6,7,8,9},那么最坏的时间复杂度为O(N^2),

快排3.0

就是在快排2.0的基础上添加了一个随机交换的操作,保证不能人为的列出最坏执行情况

代码:

    //int[] arr={3,4,6,7,5,3,5,8};
    public static void quickSort(int[] arr){
        if(arr==null || arr.length<2){
            return;
        }
        quickSort(arr,0,arr.length-1);
    }
    //选择数组中最后一位为 基准点
    public static void quickSort(int[] arr,int l,int r){
        if(l<r){
            //随机选择一个位置的值与最后一位交换
            swap(arr,r,l+(int)(Math.random()*(r-l+1)));
            //p的大小为2,是与最后一位数相等的数的左右边界
            int[] p=partition(arr,l,r);
            //左边继续
            quickSort(arr,l,p[0]-1);
            //右边继续
            quickSort(arr,p[1]+1,r);
        }
    }
​
    private static int[] partition(int[] arr, int l, int r) {
        int[] p=new int[2];
        //小于区域边界
        int less=l-1;
        //大于区域边界
        int more=r;
        //当l达到大于边界时说明已经分类完成
        while(l<more){
            //l所在位置比基准点位置小,arr[l]和小于区的下一个数交换,小于区右扩,l++
            if(arr[l]<arr[r]){
                swap(arr,++less,l++);
            }else if(arr[l]==arr[r]){
                l++;
            }else{
                //arr[l]>arr[r],l位置上的值与大于区前一个位置上的值进行交换,大于区左扩
                swap(arr,--more,l);
            }
        }
        //基准点所在当前数组的最后一位r,和大于arr[r]上的第一个值的位置(也就是大于区的边界)进行交换。
        swap(arr,r,more);
        p[0]=less+1;//等于基准点的数的左边界
        p[1]=more;//等于基准点的数的有边界
        //此时:l~less 就是比基准点小的数,more+1~r就是比基准点大的数
        return p;
    }

运行结果:

[3, 3, 4, 5, 5, 6, 7, 8]

时间复杂度:O(N*log(N))

堆排序

代码:

    /**
        int[] arr={4,2,3,1,7};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    **/
​
    /**
     * 堆排序
     * @param arr 待排序的数组
     * 时间复杂度:O(N*log(N))
     * 空间复杂度:O(1)
     */
    public static void heapSort(int[] arr){
        if(arr==null || arr.length<2){
            return;
        }
        //先调用heapInsert将数组变为大根堆的结构
        for (int i = 0; i < arr.length; i++) { //O(N)
            heapInsert(arr,i);//O(log(N))
        }
​
        //如果仅仅是将数组变成大根堆结构的话,这段代码要快一些
//        for(int i=arr.length-1;i>=0;i--){
//            heapify(arr,i,arr.length);
//        }
        /**
         * 然后每一次将堆的最大值(所在位置为0)与最后一个元素互换,堆的长度减1,然后使用heapify调整堆结构,
         * 这样,数组就从后往前依次有序起来了
         **/
        int heapSize=arr.length;
        swap(arr,0, --heapSize);
        while (heapSize>0){ //O(N)
            heapify(arr,0,heapSize);//O(log(N))
            swap(arr,0,--heapSize);//O(1)
        }
    }
​
​
    /**
     * index上的值满足大顶堆的条件从下往上移动
     * @param arr 数组
     * @param index 要移动的元素
     * 时间复杂度取决于树的高度:O(logN)
     */
    public static void heapInsert(int[] arr,int index){
        //index和index的父节点进行比较,如果arr[index]>arr[(index-1)/2],说明它比父节点的值要大,就交换这父子元素的位置。否则,while循环结束
        //当index=0的时候,(index-1)/2 =0,此时的条件不满足,while循环就结束
        while(arr[index]>arr[(index-1)/2]){
            swap(arr,index,(index-1)/2);
            index=(index-1)/2;
        }
    }
​
    /**
     * index 上的值满足大顶堆的条件从上往下移动
     * @param arr 数组
     * @param index 要移动的元素
     * @param heapSize 堆的大小
     * 时间复杂度取决于树的高度:O(logN)
     */
    public static void heapify(int[] arr,int index,int heapSize){
        //当前节点的左孩子节点
        int left=2*index+1;
        //当左孩子节点存在的时候
        while (left<heapSize){
            //选择左孩子和右孩子中大的一个
            int largest=(left+1) < heapSize && arr[left+1]>arr[left]?(left+1):left;
            //选择父节点和孩子节点之间大的一个
            largest=arr[largest]>arr[index]?largest:index;
            //如果父节点是大的一个,说明已经满足大顶堆的条件,break
            if(largest==index){
                break;
            }
            //交换父节点和孩子节点的值
            swap(arr,index,largest);
            //移动到交换后的孩子节点
            index=largest;
            //当前节点的左孩子节点
            left=index*2+1;
        }
    }
​
    public static void swap(int[] arr,int i,int j){
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }

运行结果:

[1, 2, 3, 4, 7]

桶排序(基数排序)

代码:

  /**
    *    int[] arr={1,21,32,14,57,83,64,100,101};
    *    radixSort(arr);
    *    System.out.println(Arrays.toString(arr));
    **/
    public static void radixSort(int[] arr){
        if(arr==null || arr.length<2){
            return;
        }
        //maxbits(arr) 获取数组中最大数的长度
        radixSort(arr,0,arr.length-1,maxbits(arr));
    }
​
    private static void radixSort(int[] arr, int L, int R, int digit) {
        //10进制
        final int radix=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~2)的数字有多少个
             * count[3] 当前(d位)是(0~3)的数字有多少个
             * count[i] 当前(d位)是(0~i)的数字有多少个
             **/
            int[] count=new int[radix]; //count[0....9]
            for(i=L;i<=R;i++){
                //j=获取当前arr[i]的第d位(个、十、百、千.....)数的值
                j=getDigit(arr[i],d);
                //对应第j位上的数加1。(0~9)
                count[j]++;
            }
            for(i=1;i<radix;i++){
                //count[i]=数组中的数在第d位上小于等于i的有多少
                count[i]+=count[i-1];
            }
            //从数组的右边开始遍历
            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];
            }
        }
    }
​
    private static int getDigit(int i, int d) {
        int res=0;
        while(d--!=0){
            res=i%10;
            i/=10;
        }
        return res;
    }
​
    private 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;
    }

运行结果:

[1, 14, 21, 32, 57, 64, 83, 100, 101]

时间复杂度:O(N),空间复杂度:O(1)

异或运算

异或运算 也叫做无进位运算,当两个操作数相同时,异或运算的结果为 0。当两个操作数不同时,异或运算的结果为 1。

1011 ^ 1001  = 0010  , 0 ^ N = N  ,  N ^ N = 0
​
    1 0 1 1
    1 0 0 1
 =  0 0 1 0
    
    
    //异或操作满足交换律和结合率:
    a^b=b^a
    a^bc=a^(b^c)

当交换两个数的值时,可以使用异或操作,这样就不会产生额外的空间

    @Test
    public void swap(){
        int a=1;
        int b=2;
​
        a=a^b;
        b=a^b;
        a=a^b;
        
        System.out.println("a="+a+"\n"+"b="+b);
    }
​
​
​
 输出:
        a=2
        b=1

原因:

a=1,b=2
第一次异或: a=a^b,b=b   ---> a=1^2,b=1
第二次异或: a=a^b,b=a^b^b=a  ---> a=1^2,b=1^2^2-->因为2^2=0,所以b=1^0=1
第三次异或: a=a^b^a,b=a  ---> a=1^2^1(原本是a^b^b,因为后面一个b在第二步异或的时候变成了a的值,所以这里是1)-->a=1^1^2=0^2=2
​
所以a和b就完成了交换。

但是:使用异或操作完成的交换功能在程序中只能作用于地址位置不同的两个对象,如果是同一个地址位置来进行异或,会把值抹为0

例:存在一个int类型的数组

(1)其中有一种数出现了奇数次,其余都出现了偶数次,求这个奇数次的数 (使用时间复杂度为 (O(n),空间复杂度为O(1))。

使用一个整数0,对数组中的每一个数进行异或运算,因为异或运算不在乎数的顺序,所以就是 0 ^ 所有的偶数次的数(为0)^ 奇数次的数(剩一个) ---> 结果就是 0 ^ 一个奇数次的数 = 奇数次的数。

//int[] nums=new int[]{0,0,1,3,1,2,3};
public static void printOddTimesNum1(int[] nums){
        int eor=0;
        for (int num : nums) {
            eor=eor^num;
        }
        System.out.println(eor);
    }
​
/**输出为:
            2
**/

(2)有两种数出现了奇数次,其余都出现了偶数次,求这个偶数次的数

//int[] nums=new int[]{0,1,3,1,2,3};
public static void printOddTimesNum2(int[] nums){
        int eor=0;
         /**
            eor = 奇数1 ^ 奇数2,
            因为奇数1和奇数2不相同,所以eor一定不等于0,这表示eor的32位二进制中至少有一位为1.
            那么在为1的这个二进制位置上,奇数1和奇数2的值一定不同(一定是其中一个为1,另一个为0),所以我们就可以先求出这个位              置,然后再去与数组中的值进行异或,得到其中一个奇数
        **/
        for (int num : nums) {
            eor=eor^num;
        }
        /**
            这里就是来求出最右边为1的位置
            例如:
            eor = 10100 
            ~eor = 01011  
            ~eor+1 = 01100
            eor & ~eor+1 :  10100
                            01100
                     --->   00100
                     所以最右边为1的位置就是00100(4)
        **/
        int rightIndex=eor & ~eor+1;
        int onlyOne=0;
        for (int num : nums) {
            if((num & rightIndex)==1){
                //如果右边第4为为1才进行异或运算,onlyOne最后得到的就是奇数1或者奇数2的值
                onlyOne = onlyOne^num;
            }
        }
        System.out.println("num1:"+onlyOne+"\n"+"num2:"+(eor ^ onlyOne));
    }
​
//输出: num1:0
//      num2:2

二分查找

1)在一个有序数组中,找某个数是否存在

//int res=binarysearch(nums,target,0,nums.length-1);
private static int binarysearch(Integer[] nums, Integer target,Integer left,Integer right) {
        if(left>right) return -1;
        int l=left;
        int r=right;
        int mid=(l+r)>>>1;
        if(nums[mid]>target){
            return binarysearch(nums,target,left,mid-1);
        }else if(nums[mid]<target){
            return binarysearch(nums,target,mid+1,right);
        }
​
        return mid;
    }

2)在一个有序数组中,找 >= 某个数最左侧的位置

/**
        Integer[] nums2={1,1,1,1,1,2,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5};
        System.out.println(binarysearch2(nums2, 3, 0, nums2.length - 1));
**/
private static int binarysearch2(Integer[] nums, Integer target,Integer left,Integer right) {
        if(left>right) return -1;
        int l=left;
        int r=right;
        int t=0;
        while(l<=r){
            int mid=(l+r)>>>1;
            if(nums[mid]>=target){
                /**
                 * 找到了一个 >= target的数,将位置记录下来,
                 * 不过也可能在这个数的左边还存在 >= target 的数,
                 * 所以就更新右边界,去左边继续寻找
                 */
                t=mid;
                r=mid-1;
            }else{
                //这里就是 nums[mid]的值比target要小,更新左边界,继续去数组右边去找
                l=mid+1;
            }
        }
​
        return t;
    }
​
//输出:10

3)局部最小值问题 (数组无序,相邻的两个数不相等 局部最小定义:arr[0] < arr[1] 数组0位置的值就是局部最小,arr[arr.length-1] < arr[arr.length-2] 数组最后一个位置的值就是局部最小,对于数组其他位置上的数 ,满足 arr[i-1] > arr[i] < arr[i+1] i位置上的数就是局部最小)

/**
    Integer[] nums3={1,0,1,3,6,5,1,2,3};
    System.out.println(binarysearch3(nums3));
**/
private static int binarysearch3(Integer[] nums) {
        int l=0;
        int r=nums.length-1;
        if(nums[l]<nums[l+1]) return l;
        if(nums[r]<nums[r-1]) return r;
        //如果执行到了这里,表示数组的两个边界都不是局部最小值,那么在这数组之中,一定存在一个局部最小值
        while(l<=r){
            int mid=(l+r)>>>1;
            if(nums[mid]<nums[mid-1] && nums[mid]<nums[mid]+1){
                return mid;
            }
            if(nums[mid]>nums[mid-1]){
                r=mid-1;
            } else if (nums[mid]>nums[mid+1]) {
                l=mid+1;
            }
        }
        return -1;
    }
​
//输出: 1

对数器

有一个你想要测的方法 a,

实现复杂度不好但是容易实现的方法 b ,

实现一个随机样本产生器,

把方法 a 和方法 b 跑相同的随机样本,看看得到的结果是否一样。

如果有一个随机样本是的对比结果不一致,打印样本进行人工干预,改对方法 a 或 方法 b

当样本数量很多时对比测试是否依然正确,如果依然正确,就可以确定方法 a 已经正确

这里以插入排序作为方法a,java中本身提供的sort方法作为方法b

public static void main(String[] args) {
        int testTime=500000;
        int maxSize=100;
        int maxValue=100;
        boolean succeed=true;
        for(int i=0;i<testTime;i++){
            int[] arr1=generateRandomArray(maxSize,maxValue);
            int[] arr2=copyArray(arr1);//对arr1数组深拷贝
            MySort(arr1);//插入排序 a方法
            comparator(arr2);//b方法
            if(!isEqual(arr1,arr2)){//如果排序后的两个方法中的值存在差异
                succeed=false;
                break;
            }
        }
        System.out.println(succeed ? "Nice":"Fucking fucked");
​
        int[] array = generateRandomArray(maxSize, maxValue);
        System.out.println(Arrays.toString(array));
        MySort(array);
        System.out.println(Arrays.toString(array));
​
    }
//随机生成一个长度位maxSize,最大值为maxValue的int类型的数组
    public static int[] generateRandomArray(int maxSize,int maxValue){
        /**
         * Math.random() -> [0,1) 所有的小数,等概率返回一个 因为在计算机中,数的位数是有限制的,所以可以确定所有的小数,在数学上就不行。
         * Math.random()*N -> [0,N) 所有的小数,等概率返回一个
         * (int)(Math.random()*N) -> [0,N-1] 所有的整数,等概率返回一个
         */
​
        int[] arr=new int[(int)((maxSize+1)*Math.random())];//长度随机
        for(int i=0;i<arr.length;i++){
            arr[i]=(int) ((maxValue+1)*Math.random())-(int)(maxValue*Math.random());
        }
        return arr;
    }
​
​
//深拷贝
private static int[] copyArray(int[] arr1) {
        if(arr1==null) return null;
        int[] arr=new int[arr1.length];
        for (int i = 0; i < arr1.length; i++) {
            arr[i]=arr1[i];
        }
        return arr;
    }
​
//插入排序
private static void MySort(int[] nums) {
        if(nums==null || nums.length<2){
            return;
        }
        int l=nums.length;
        for(int i=1;i<l;i++){
            for(int j=i;j>0;j--){
                if(nums[j]<nums[j-1]){
                    swap(nums,j,j-1);
                }else{
                    break;
                }
            }
        }
    }
​
//交换
private static void swap(int[] nums, int i, int j) {
        int tmp= nums[i];
        nums[i]= nums[j];
        nums[j]=tmp;
    }
​
//java提供的方法
private static void comparator(int[] arr2) {
        Arrays.sort(arr2);
    }
​
//比较两个数组中的内容是否完全一致
public static boolean isEqual(int[] arr1,int[] arr2){
        for (int i = 0; i < arr1.length; i++) {
            if(arr1[i]!=arr2[i]) return false;
        }
        return true;
    }
​
​

## 比较器

返回负数的时候,认为第一个参数排在前面   -----> 升序

返回正数的时候,认为第二个参数排在前面   -----> 降序

返回0的时候,谁排在前面无所谓

PriorityQueue<Integer> heap = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1-o2;
            }
        });

递归

递归求数组中的最大值

剖析递归行为何递归行为时间复杂度的估算

用递归方法找一个数组中的最大值,系统上到底是怎么做的

master公式的使用

T(N) = a*T(N/b) + O(N^d)

1)log(b,a) > d --> 时间复杂度为:O(N^log(b,a))

2)log(b,a) = d --> 时间复杂度为:O(N^d * logN)

3)log(b,a) < d --> 时间复杂度为:O(N^d)

递归求数组中的最大值

public static int findMax(int[] arr,int left,int right){
        if(left==right){
            return arr[left];
        }
        int mid=left+((right-left)>>1);
        int leftMax=findMax(arr,left,mid);
        int rightMax=findMax(arr,mid+1,right);
        return Math.max(leftMax,rightMax);
    }

执行流程

随着每一次findMax函数的调用,都会在java虚拟机栈中创建一个栈帧,等待每一个栈帧中的内容执行完毕后返回结果给上一层调用它的栈帧,这样一层层的调用,到最后,最开始的那一个栈帧对应的函数就可以得到最终的结果。

根据master公式,我们可以得出下面这个数据

T(N) = 2T(N/2)+O(1)
 a=2,b=2,d=0
 a=2因为一个函数中包含了两次子函数的调用
 b=2因为调用一次子函数的数据是父函数的一半
 d=0因为除了调用子函数之外的代码的时间复杂度是常数项
 所以 log(a,b)>d
 所以T(N)=T(N^log(1,1))=T(N),跟直接循环得到数组中的最大值的时间复杂度结果一样

堆结构

堆结构就是用数组实现的完全二叉树结构,完全二叉树是一种特殊的二叉树,其中除了最后一层外,每一层的节点都被完全填满,并且最后一层的节点都靠左对齐。

完全二叉树中如果每颗子树的最大值都在顶部就是大根堆

完全二叉树中如果每颗子树的最小值都在顶部就是小根堆

堆结构的heapInsert与heapify操作

/**
     * index上的值满足大根堆的条件从下往上移动
     * @param arr 数组
     * @param index 要移动的元素
     * 时间复杂度取决于树的高度:O(logN)
     */
    public static void heapInsert(int[] arr,int index){
        //index和index的父节点进行比较,如果arr[index]>arr[(index-1)/2],说明它比父节点的值要大,就交换这父子元素的位置。否则,while循环结束
        //当index=0的时候,(index-1)/2 =0,此时的条件不满足,while循环就结束
        while(arr[index]>arr[(index-1)/2]){
            swap(arr,index,(index-1)/2);
            index=(index-1)/2;
        }
    }
​
    /**
     * index上的值满足大根堆的条件从上往下移动
     * @param arr 数组
     * @param index 要移动的元素
     * @param heapSize 堆的大小
     * 时间复杂度取决于树的高度:O(logN)
     */
    public static void heapify(int[] arr,int index,int heapSize){
        //当前节点的左孩子节点
        int left=2*index+1;
        //当左孩子节点存在的时候
        while (left<heapSize){
            //选择左孩子和右孩子中大的一个
            int largest=(left+1) < heapSize && arr[left+1]>arr[left]?(left+1):left;
            //选择父节点和孩子节点之间大的一个
            largest=arr[largest]>arr[index]?largest:index;
            //如果父节点是大的一个,说明已经满足大顶堆的条件,break
            if(largest==index){
                break;
            }
            //交换父节点和孩子节点的值
            swap(arr,index,largest);
            //移动到交换后的孩子节点
            index=largest;
            //当前节点的左孩子节点
            left=index*2+1;
        }
    }

堆结构的增大和减少:

堆结构的增大:将增加的值放到数组的最后一位,然后调用heapInsert函数

堆结构的减少:将要删除的元素所在位置记录为index,并且和最后一位互换,然后数组长度减1,调用Heapify函数

所以也可以说堆结构就是优先级队列结构。

在Java中封装了一个优先级队列PriorityQueue

PriorityQueue<Integer> heap = new PriorityQueue<>();

PriorityQueue默认的就是使用小根堆实现的,也可以使用比较器自己定义。

如果在我们的需求中仅仅只是对堆中的数据进行add和poll的话,就可以使用PriorityQueue,这样的效率也比较高。

比如:

已知一个几乎有序的数组(几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离不超过k,并且k相对于数组的长度来说比较小),请选择一个合适的排序算法针对这个数据进行排序。

代码:

/**
*       int[] arr2={2,3,1,4,6,5};
*       heapSortByK(arr2,2);
*       System.out.println(Arrays.toString(arr2));
**/
public static void heapSortByK(int[] arr,int k){
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        int index=0;
        /**
         * 先将数组中前k的元素加入到堆中,这前k的元素中,一定包含数组0位置上正确的数字,就是数字中的最小值
         */
        for(;index<=Math.min(k,arr.length);index++){
            heap.add(arr[index]);
        }
        int i=0;
        /**
         * 每一次将数组中k位置后面的元素加入堆中,并弹出堆顶的位置,
         * 弹出的元素就是数字i位置上应该放的元素。
         */
        for(;index<arr.length;index++){
            heap.add(arr[index]);
            arr[i++]=heap.poll();
        }
        while (!heap.isEmpty()){
            arr[i++]=heap.poll();
        }
    }

运行结果:

[1, 2, 3, 4, 5, 6]

不过如果我们的业务需要修改堆中的结构,比如删除或修改一个堆中的数据,那么使用PriorityQueue就不是一个最好的选择,因为PriorityQueue它不能确定我们所修改的位置,只能一个一个的遍历堆中的数据,判断这个数据是否需要进行heapify,这样PriorityQueue的效率就不高。如果我们有这样的需求,就需要我们自己手写堆,我们手写的堆在修改堆结构时可以知道是那里发生了修改,直接在修改的位置进行heapify即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1531574.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024年云仓酒庄佛山发布会:赋能

原标题&#xff1a;2024年云仓酒庄佛山发布会圆满落幕&#xff0c;朱囿臻总赋能引领行业新篇章 近日&#xff0c;备受瞩目的云仓酒庄佛山发布会圆满落幕。此次发布会汇聚了业内精英、经销商代表以及媒体人士&#xff0c;共同见证了云仓酒庄在佛山市场的启航。在此&#xff0c;…

实现防抖函数并支持第一次立刻执行(vue3 + ts环境演示)

1、先看一效果&#xff1a; 2、实现思路&#xff1a; 使用定时器setTimeout和闭包实现常规防抖功能&#xff1b;增加immediate字段控制第一次是否执行一次函数&#xff08;true or false&#xff09;&#xff1b;增加一个flag标识&#xff0c;在第一次执行时&#xff0c;将标…

YZ系列工具之YZ08:窗体加载图片后进行放大查看

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套一部VBA手册&#xff0c;教程分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的…

【安全类书籍-2】Web渗透测试:使用Kali Linux

目录 内容简介 作用 下载地址 内容简介 书籍的主要内容是指导读者如何运用Kali Linux这一专业的渗透测试平台对Web应用程序进行全面的安全测试。作者们从攻击者的视角出发,详细阐述了渗透测试的基本概念和技术,以及如何配置Kali Linux以适应渗透测试需求。书中不仅教授读者…

软件工程-第9章 软件工程项目管理概述

9.1 软件工程管理活动 9.2 软件规模、成本和进度估算 9.3 能力成熟度模型CMM 9.4 ISO 9000系列标准简介 9.5 CMM与ISO 9000系列标准的比较 9.6 本章小结

Docker与containerd:容器技术的双璧

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Docker幻想曲&#xff1a;从零开始&#xff0c;征服容器宇宙》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、前言 1、Docker和containerd的背景…

【C++】堆区空间的申请和释放--- 2024.3.19

目录 C和C的区别&#xff08;申请堆区空间&#xff09;C中的new和delete结束语 C和C的区别&#xff08;申请堆区空间&#xff09; 在c语言中&#xff0c;在遇到需要申请一块堆区空间时&#xff0c;我们往往会使用malloc申请&#xff0c;使用free进行释放&#xff0c;但是为什么…

深入浅出前端本地储存

引言 2021 年&#xff0c;如果你的前端应用&#xff0c;需要在浏览器上保存数据&#xff0c;有三个主流方案&#xff1a; CookieWeb Storage (LocalStorage)IndexedDB 这些方案就是如今应用最广、浏览器兼容性最高的三种前端储存方案 今天这篇文章就聊一聊这三种方案的历史…

漏洞扫描服务为什么这么重要?

漏洞扫描服务作为网络安全领域的重要一环&#xff0c;发挥着不可或缺的作用。漏洞扫描服务不仅能够实时防护网络免受各类恶意攻击&#xff0c;还能为网络管理人员提供定期的安全自我检测与评估&#xff0c;确保网络的稳定与安全运行。那么什么是漏洞扫描呢&#xff1f; 漏洞是在…

蓝桥杯之动态规划冲刺

文章目录 动态规划01背包小练一下01背包网格图上的DP完全背包 最长公共字符串最长递增子序列 动态规划 动态规划&#xff1a;确定好状态方程&#xff0c;我们常常是确定前 当状态来到 i 时&#xff0c;前 i 个物体的状态是怎么样的&#xff0c;我们并不是从一个点去考虑&#x…

【1】THIS IS NOT PROLIFIC PL2303. PLEASE CPMTACT YOUR SUPPLIER

0x01 问题描述 连接COM口连接不上&#xff0c;出现THIS IS NOT PROLIFIC PL2303. PLEASE CPMTACT YOUR SUPPLIER.如下图 0x02 问题解决 1、分析后&#xff0c;因为是windows 11 系统&#xff0c;就装一下驱动。右键单击--》属性 2、更新驱动程序--》浏览我的电脑以查找驱动程序…

企业应如何建立有效的内控制度?

内部控制制度&#xff08;以下简称“内控制度”&#xff09;作为企业生产经营活动自我调节、自我约束的内在机制&#xff0c;在企业管理系统中具有举足轻重的作用。内部控制制度的建立、健全及实施情况的好坏&#xff0c;是企业生产经营成败的关键。因此&#xff0c;企业应该建…

粒子群算法 - 目标函数最优解计算

粒子群算法概念 粒子群算法 (particle swarm optimization&#xff0c;PSO) 由 Kennedy 和 Eberhart 在 1995 年提出&#xff0c;该算法模拟鸟群觅食的方法进行寻找最优解。基本思想&#xff1a;人们发现&#xff0c;鸟群觅食的方向由两个因素决定。第一个是自己当初飞过离食物…

软件工程-第三版王立福-第1章 绪论

本书结合IEEE最新发布的软件工程体系SWEBOK&#xff0c;和IEEE/ACM软件工程学科小组公布的软件工程教育知识体系SEEK&#xff0c;北大本科生指定教材。注重基础知识的系统性&#xff0c;选材的先进性及知识的应用。2009年出版 软件开发本质的认识&#xff0c;两大技术问题&…

【Linux】Linux基本开发工具(yum) (vi/vim)的使用

本文章内容&#xff1a; 学习yum工具&#xff0c;进行软件安装掌握vim编辑器使用 Linux 软件包管理器 yum 什么是软件包 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序.但是这样太麻烦了, 于是有些人把一些常用的软件提前编译好, 做成…

一文带你详解天池工业数据集

人工智能是国家战略性新兴产业&#xff0c;制造业是国民经济的主体&#xff0c;随着人口红利的消失&#xff0c;加强设备自动化改造&#xff0c;提高生产自动化程度&#xff0c;减小劳动强度&#xff0c;改善作业环境&#xff0c;已经成为制造业的普遍共识。天池大赛开放出一批…

MySQL中的基本SQL语句

MySQL中的基本SQL语句 查看操作 1. 查看有哪些数据库 show databases; 2.切换数据库 use 数据库名;比如切换至 mysql数据库 use mysql;3.查看数据库中的表 show tables;4.查看表中数据 select 要查询的东西 from 表名 [ where 条件 ];select * from 表名…

大众EA111发动机

大众EA111发动机_什么是大众EA111发动机_太平洋汽车百科 大众EA111发动机_什么是大众EA111发动机_太平洋汽车百科 大众的EA111系列发动机是大众公司小排量发动机的主力&#xff0c;有1.2L、1.4L、1.6L三种排量。大众的EA111系列发动机融合了缸内直喷、涡轮增压等先进技术&…

数据库报Statement cancelled due to timeout错误处理方法

如上图&#xff0c;如果数据库报错&#xff0c;可考虑是事务导致锁表问题。 解决方法&#xff1a; 1.查看下在锁的事务 SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; 2&#xff0c;kill第一步查出的线程ID

EasyExcel模板填充list时按第一行格式合并单元格(含分页线设置)

前言&#xff1a; 在使用easyExcel填充list时&#xff0c;第一行存在合并单元格的情况下&#xff0c;后面使用forceNewRow()填充的行却没有合并样式。 模板&#xff1a; 填充后&#xff1a; 自定义拦截器&#xff1a; 根据官方文档的提示&#xff0c;我们需要自定义拦截器来…