【左程云 数据结构与算法笔记】P4 认识O(NlogN)的排序

150 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情下面是我整理的跟着b站左程云的数据结构与算法学习笔记,欢迎大家一起学习。

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

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

T(N)母问题的规模为N个数据

a子问题在等量的情况下被调用的次数

T(N/b)子问题的规模都是N/b的规模

O(N^d)除去调用子过程外,剩下过程的时间复杂度 满足子过程等规模的递归,都可以用master公式直接求解时间复杂度 我们先看一个得到最大值的递归算法代码

public static int getMax(int[] arr){  
    return process(arr, 0, arr.length-1);  
}  
public static int process(int[] arrr,int L,int R){  
    //判断是否只有一个数  
    if (L==R){  
        return arrr[L];  
    }  
    int mid=L+((R-L)>>1);//中点 右移一位比除2快  
    int leftMax=process(arrr, L, mid);  
    int rightMax=process(arrr, mid+1, R);  
    return Math.max(leftMax, rightMax);  
  
}  
  
public static void main(String[] args) {  
     int[] arr={4, 6, 2, 10};  
    int max = getMax(arr);  
    System.out.println(max);  
}

上面递归满足a=2 b=2 d=0

T(N)=2*T(N/2)+O(1)

假设取每一线段上的2/3求最大值 显然会有很大部分出现重复计算 此时T(N)=2T(N/ 3/2)+O(1) 假设把数据划分为总长的1/3,也满足master公式。 此时 T(N)=3*T(N/3)+O(1), 但若划分成左边1/3,右边2/3 此时两边的子问题的问题规模不一样,不能使用master公式。

一系列符合子问题规模等规模的行为的递归可以使用master公式 T(N)=a*T(N/b)+O(N^d) 求解时间复杂度 若log (b,a) < d, 时间复杂度为O(N^d) 若log (b,a) > d, 时间复杂度为O(N^log (b,a)) 若log (b,a) = d, 时间复杂度为O(N^d * logN) 三个参数 a b d知道就可以直接算得时间复杂度

归并排序

  • 简单递归,左边排好序,右边排好序,使其整体有序
  • 让其整体有序的过程用了排外序的方法
  • 利用master公式求解时间复杂度 实现思路,准备一片临时区域,先把左右都排好序,实现整体有序时,先比较左右两边,谁先小先拷贝,并指向下一位,当有一边到底时,另一边剩下的直接拷贝,再拷贝回原数组即实现归并排序 代码实现:
public static void mergeSort(int[] arr){  
    if (arr==null||arr.length<2){  
        return;  
    }  
    process(arr, 0, arr.length-1);  
}  
public 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);  
  
}  
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<=M){  
        help[i++]=arr[p2++];  
    }  
    for (int j = 0; j < help.length ; j++) {  
        arr[L+j]=help[j];  
    }  
}  
  
public static void main(String[] args) {  
    int[] arr={4, 6, 2, 10};  
    mergeSort(arr);  
      
}

T(N)=2T(N/2)+O(N) a=2,b=2,d=1 log(b,a)=d 则归并排序的时间复杂度为O(N * logN),额外空间复杂度O(N) 思考:为什么归并排序的时间复杂度为O(N * logN),而选择排序的时间复杂度为O(N^2) 比较行为没有浪费,变成了整体有序的部分跟下一个更大范围的部分merge成下一个整体有序的部分,比较行为的信息不断往下传递

小和问题

提升:如何通过归并算法的思想计算小和

小和(比数组中某个数大的个数乘以本身再相加) 如果是普通的相比,时间复杂度达到了O(N^2) 如何使用参考归并排序的思路降低时间复杂度到O(N*logN)

思考

为什么排序不能去掉:可以直接通过下标计算比该数大的个数,即左边第一个数1 右边第一个数为3按排序排好 此时右边的数都比1大,直接通过下标得出 与简单的归并排序相比,当左右两边相同时,先拷贝右边的,当到右边比左边大时,才可以知道左边数的小和 相当于计算时以左边为主,拷贝以右边为主 小和等于左边合并得出的小和加上右边合并得出的小和加上合并时产生的小和

  • 不会遗漏:在右侧依次比该数大
  • 不会多算:合并之后成为一个组,一个组内部不会产生小和 实现代码:
public static int smallNum(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 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 res=0;  
public static int merge(int[] arr,int L,int m,int r){  
    int[] help=new int[r-L+1];  
    int i=0,p1=L,p2=m+1;  
    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<=m){  
        help[i++]=arr[p2++];  
    }  
    for (int j = 0; j < help.length ; j++) {  
        arr[L+j]=help[j];  
    }  
    return res;  
}  
public static void main(String[] args) {  
    int[] arr={1,3,4,2,5};  
    System.out.println(smallNum(arr));  
  
}

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

  • 当数组[i]<=num,[i]和<=区的下一个数互换,<=区右扩,i++
  • 当[i]>num,i++
  • 当i达到数组长度,循环结束,相当于一直把<=区域推着往下面走

问题升级,荷兰国旗问题

把给定数组分成小于,等于,大于三个区域,要求额外空间复杂度O(1),时间复杂度O(N) 解题思路

  • [i]<num,[i]和<区下一个互换,<区右扩
  • [i]=num,i++
  • [i]>num,[i]和>区前一个互换,>区左扩,i不变
  • 当大于区域与i撞上时,循环结束 实质是,小于区域右扩,大于区域左扩,小于区域推着等于区域去撞上大于区域 实现代码
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};  
}  
//交换数  
public static void swap(int[] arr,int i,int j){  
    int tmp = arr[i];  
    arr[i] = arr[j];  
    arr[j] = tmp;  
}

快速排序1.0

解题思路: 先按小于大于划分范围,在一个范围内用最后一个数做划分,会把这个数放到小于等于该数区域的最后一个位置,这样子他的位置就已经确定好,接着让小于和大于区域按照同样的方法递归,由于每次都能排好一个数,最终也能排好序。

快速排序2.0

解题思路: 基于荷兰国旗问题,先排好小于等于大于的范围,进而在小于和大于区域递归,由于如果最末尾值等于中间值,能一次性搞定=区域范围内的数,效率更快。即先按小于等于大于区域划分好,取最后一个数,将其放在小于等于区域数的右边,这样他的位置已经确定好。 思考: 这样子的算法遇到按1~9的顺序排好调用算法时,会出现最坏的情况,即划分值没有左边或者右边的情况,时间复杂度为O(N^2) 最好情况,最后一个数为中点,调用master公式,时间复杂度为T(N)=2T(N/2)+O(N),此时时间复杂度最小为O(N*logN)

快排3.0(快排随机)

随机选择一个数与最后一个数交换作为划分值,此时好情况与坏情况的出现成为概率问题,即时间复杂度看随机选择的数打在数组长度的位置,

  • 如果打在一半,时间复杂度为T(N)=2T(N/2)+O(N)
  • 如果打在1/3的位置,按照master公式计算得T(N)=T(N/3)+T(2/3N)+O(N) 所有可能出现的情况为1/N,只占1/N的权重 将所有情况加在一起求概率累加,求整体大的期望为O(NlogN)

完整代码

public static void main(String[] args) {  
    int[] arrs={4, 6, 2, 10};  
    quickSort(arrs);  
    for (int arr:arrs) {  
        System.out.println(arr);  
    }  
}  
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,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);// >区  
    }  
}  
//相当于荷兰国旗问题  
//默认以arr[r]做划分,arr[r]->p   <p =p >p  
//返回划分区域(左边界,右边界)返回一个长度为2的数组res  
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};  
}  
//交换数  
public static void swap(int[] arr,int i,int j){  
    int tmp = arr[i];  
    arr[i] = arr[j];  
    arr[j] = tmp;  
}

快排的空间复杂度 O(logN) 最差情况O(N)

总结

排序算法的稳定性及其汇总 同样值的个体之间,如果不因为排序而改变相对次序,这个排序有稳定性,反之没有 不具备稳定性的排序: 选择排序,快速排序,堆排序 具备稳定性的排序 冒泡排序,插入排序,归并排序,一切桶排序思想下的排序

复习一下:

  • 冒泡排序:不断判断后面值是否比前面大 =时不交换
  • 插入排序:不断扩大有序的范围 =时不交换
  • 归并:两个数列之间的merge,相等先拷贝左边
  • 快速排序:partition需要交换会破坏稳定性
  • 堆排序;在自己的二叉树上与上一个父比较,可以很容易破坏稳定性 ;实际可用于:挑选物美价廉的物品