第二课 递归,归并排序和快速排序

33 阅读5分钟

工作之余也学了第二课,又花了好长时间,突然觉得相比较来说,听懂实在是最简单最省时间的事情了,要消化,总结,运用才是最难的。我一般听课的时候会记在笔记本上,然后再抽时间总结在这里,这次的总结写完了好像会了,又把上次学的忘了个7788,还有人像我这样学的慢,忘的快的人吗哈哈哈哈

不过呢,我这个人性子容易急,心也不够细,多磨磨性子也是好的,学一秒就要有一秒的效果,绝对不能白学!好了言归正传,第二课学了2个排序,分别是归并排序和快速排序,这俩个排序都是基于递归开展的,而且复杂度都是O(N*logN),而之前学的冒泡,选择,插入排序的复杂度都是O(N^2),归并和快排的效率更高,速度更快。因为归并和快排的比较范围小,没有浪费,而其他三种是大范围比较,过于浪费

一 递归和master公式

1.递归

从一个小案例来理解递归 例如:使用递归求出一个数组的最大值 思路:找出这个数组的中间值,然后分别求出左边数组的最大值和右边数组的最大值,再进行最大值之间的比较

    let arr =[2,6,3,5,8]
    process(arr,0,arr.length-1);
    function process(arr,L,R){
        if(L===R) return arr[L]
        let mid = L+((R-L)>>1)
        // 这块不写成(L+R)/2,是为了防止L+R范围过大而溢出,
        //可以改为L+(R-L)/2,也就是L+(R-L)>>1,右移运算符速度更快
        let leftMax = process(arr,L,mid);
        let rightMax = process(arr,mid+1,R);
        return Math.max(leftMax,rightMax);
    }
    // 可以理解为一个二叉树的遍历,不断进行压栈,
    // 最外面的分支有了结果才能返回上层

2.master公式

是用来求解时间复杂度的,等规模子问题的递归可以直接套用

什么叫等规模的子问题,一个数组等分成2份去求最大值,等分成3份,4份,5份都可以,只要是等分的就行,上面案例就是等分成2份

公式:T(N)=a * T(N/b) + O(N^d);

a:子问题调用的次数,上述案例调用了2次,left,right

b:子问题的规模,上述案例中等分成2份子问题,规模就是2

O(N^d):除子问题外其他的复杂度,上述案例其他复杂度为O(1),只有简单的赋值语句,没有遍历,d=0

上述案例的公式为: T(N)=2 * T(N/2)+O(1)

master公式的时间复杂度结论

logb(a) < d => O(N^d);

logb(a) > d => O(N * logb(a));

logb(a) == d => O(N^d * logN);

二 归并排序

思路:将数组一分为二,使得左边有序,右边有序,最后左右进行比较再合并得出最终有序的数组

   let arr = [2,5,6,3,7]
   process(arr,0,arr.length-1);
   function process(arr,L,R){
       if(L===R) return;
       let mid = L+((R-L)>>1);
       process(arr,L,mid);
       process(arr,mid+1,R);
       merge(arr,L,mid,R)
   }
   function merge(arr,L,M,R){
       let arr1=new Array(R-L+1);
       let i=0,p1=L,p2=mid+1;
       // 如果p1,p2不越界,哪个小就把哪个放到新数组中,
       // 对应的指针向前一步,新数组的index+1
       while(p1<=mid && p2<=R){
           arr1[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
       }
       // 当不满足循环条件,或者循环结束说明p1,或者p2有一个先越界
       // 那么对面数组剩余的项,可以直接添加到新数组中
       while(p2<=R){
           arr[i++]=arr[p2++]
       }
       while(p1<=mid){
           arr[i++]=arr[p1++]
       }
       // 将arr1数组拷贝到arr数组中
       for(let i=0;i<arr.length;i++){
           arr[L+i]=arr1[i];
       }
   }
   
   //归并排序的时间复杂度
   T(N)=2*T(N/2)+O(N)
   log2(2)==1 ,即O(N*logN)

应用:求数组的小和

将数组中每一个数左边比自己小的数都加起来,可以从逆向思维看,也就是把每一个数右边比自己大的次数加起来,可以使用递归,将数组一分为二,算出左边数组的小和,和右边数组的小和,并进行合并,当左边的数(p1)小于右边的数(p2)时,大于左边数(p1)的次数为右边数(p2)到R的长度(R-P2+1),这样递归算出来的次数是不会重复的,因为每次左边的数都在和新的右边的数进行比较合并

三 快速排序(3.0)

思路:从数组中随机选取一个数n,并将n和数组中最后一个数交换,然后将数组划分为[<n,==n,>n],由外向内进行递归划分,先划分整个数组,再划分每个部分,最后一定有序

如何划分?(荷兰国旗问题)

设当前指针为i,设置左区和右区

  1. arr[i]<arr[n],左区右边第一个数和arr[i]交换,左区向前一步,i++
  2. arr[i]==arr[n],i++;
  3. arr[i]>arr[n],右区左边第一个数和arr[i]交换,右区向前一步,i不动;
    let arr = [2,5,4,5,6,1]
    quickSort(arr,0,arr.lenght-1);
    function quickSort(arr,L,R){
        if(L>=R) return;
        let n=Math.random()*(R-L+1);
        swap(arr,n,R); //交换函数
        let arr1 = partition(arr,L,R);
        // 返回的是==n的下标
        // 例如排序后的数组为[2,1,4,5,5,6],则返回的arr1=[3,4]
        quickSort(arr,L,arr1[0]-1);
        quickSort(arr,arr1[1]+1,R);
    }
    function partition(arr,L,R){
        let less=L-1,more=R,i=0;
        while(i<more){
            if(arr[i]<arr[R]){
                swap(arr,i++,++less)
            }else if(arr[i]>arr[R]){
                swap(arr,i,--R)
            }else{
                i++
            }
        }
        // 将n,R再换回去
        swap(arr,R,more);
        return [less+1,more];
    }