归并排序及分治算法

分治算法:

  1. 分解:将大规模的复杂问题拆分成可自然而然解决或已解决的小规模问题,小规模问题与大规模问题形式相同
  2. 解决:递归解决小规模问题的
  3. 合并:将各子问题的解合并得到原问题的解。

归并排序:

  1. 将问题一分为2,使用递归进行排序
  2. 将左右两边的排好序的数组合并为一个数组。
代码实现:
private static void mergeSort(int[] array,int start,int end){
    if(array == null) return;
    int len = end - start + 1;
    if(len < 2) return;
    
    int mid = (start + end)/2;  // 分解时间:常数
    mergeSort(array,start,mid); // 解决时间 
    mergeSort(array,mid+1,end);  
   	mergeArray(array,start,end); // 合并时间 O(n)
}

private static void mergeArray(int[] array,int start,int end){
    if(array == null || start < 0 || end < 0 || start>end) return;
    int[] newArray = new int[end -start +1];
    int mid = (start+end)/2;
    int left = start;
    int right = mid+1;
    int k = 0;
    while(left<=mid || right <= end){
        if(array[left]<array[right]){
            newArray[k++] = array[left++];
        }eles{
            newArray[k++] = array[right++];
        }
    }
    
    while(left<=mid){
        newArray[k++]=array[left++];
    }
    
    while(right<=end){
        newArray[k++]= array[right++];
    }
    
    // 复制回原来的数组
    for(int i = 0;i<array.length;i++){
        array[start+i] = newArray[i];
    }
}
时间复杂度分析:

总时间=分解时间+解决时间+合并时间。

分解时间是一个常数;解决时间使用递归树推,见下图;合并时间一次循环,根据主定理时间复杂度为O(n);

用递归树的方法解递归式T(n)=2T(n/2)+o(n):假设解决最后的子问题用时为常数c,则对于n个待排序记录来说整个问题的规模为cn。,递归分解为两部分,就是C(n/2),C(n/2)再分解为1/2,

[外链图片转存失败(img-dqiTDVrz-1562234936169)(./归并时间分析.png)]

总解决时间=Cn+Cn+····+Cn =(logn+1)*Cn; = cnlogn+cn ;根据主定理,只保留低阶项,去除高阶项的常数后即是时间复杂度。时间复杂度为: nlogn.

练习1:股票一次买卖问题

股票有涨有跌,选一个区间,计算哪天买入及卖出最赚钱。

问题分析:

问题的本质就是求每日价差间最大值,即a[j]-a[i] 的最大值,其中j>i;

穷举法:

遍历数组的差额获取最大值。

 static class BestStock {
        int profit;// 收益
        int minTime;// 最低股价日
        int maxTime; // 最高股价日
 }   
private static BestStock bestProfit(int[] array){
    int profit = 0;
    BestStock b = new BestStock();
    int i,j = 0;
    for(i=0;i<array.length;i++){
        for(j=i;j<array.length;j++){
            if(array[j]-array[i]> profit){
                profit = array[j]-array[i];
                  b.minTime = i;
                  b.maxTime = j;
            }
        }
    }
     b.profit= profit;
     return b;
}

穷举法的时间复杂度是O(n^2).

分治法:

将数组分为两半a[0]a[i],a[i+1]a[j],最佳的买卖时机可能是3种情况:

  1. 最佳的买卖时机 在0~i之间,最大收益P1
  2. 最佳的买卖时机 在i+1~j之间,最大收益P2
  3. 最佳的买卖时机 跨越中间数mid ,即0>买入日期>mid>卖出日期,最大收益P

对于第一第二种情况可以处理方式相同,对于第三种情况最大收益就是在右边区域最高股价max与左边区域最低股价min的差,即P 就是 p1,p2,a[max]-a[min]的最大值。

private static BestStock bestProfit(int[] array,int min,int max){
    BestStock b1,b2;
    BestStock b = new BestStock();
    if(min == max){
        b.profit = 0;
        b.minTime = min;
        b.maxTime = max;
        return b;
    }
    int mid = (max+min)/2;
    b1 = bestProfit(array,min ,mid);
    b2 = bestProfit(array,mid+1,max);
    b.minTime = array[b1.minTime]<array[b2.minTime]?b1.minTime:b2.minTime;
    b.maxTime = array[b1.maxTime]>array[b2.maxTime]?b2.maxTime:b2.maxTime;
    // 比较获取跨中间数的最大收益
    b.profit = max(b1.profit,b2.profit,array[b.maxTime]-array[b.minTime]);
    return   b;
}

private static int max(int a,int b,int c){
    if(a>=b && a>= c){
        return a;
    }
    
    if(b >= a && b >= c){
        return b;
    }
    
    if(c >= a && c >= b){
        return c;
    }
    return -1;
}

时间复杂度:O(n).

练习2:求最大子数组的和及起始结束位置。

其实股票一次买卖问题的本质就是在差额数组里求出一组和最大的子数组集合。

思路:

对于数组A,如果已知最大子数组是A[0,j],如果每增加一个元素 A[j+1],则最大子数组要么依然是A[0,j],要么是包含A[j+1]元素的子数组。

  1. 假设最大子数组就是a[0];
  2. 每增加一个元素,如果和小于0则舍弃前面的元素,重新开始组合,如果大于0,最大子数组包含最后的元素
  3. 如果子数组的和大于最大的和则重新赋值。
private static int[] maxSubArray(int[] array){
    int sum = array[0];
    int maxSum = array[0];
    int[] result = new int[3]; // 结果数组包含 开始位置&结束位置&子数组和
    result[0] = 0;
    for(int i = 0;i>array.length;i++){
        if(sum<0){
            result [0] = i;
            sum = array[i];
        }else{
            sum+=array[i];
        }
        if(sum>maxSum){
            maxSum = sum;
            result[1] = i;
            result[2] = maxSum;
        }
    }
    return result;
}

时间复杂度:O(n).

练习3:当一个数组中有超过 n/2 个元素是一样的,则称此元素为主元素,找出一个数组中的主元素。

分治法:
  1. 将问题拆分解决,当拆分到只有一个元素时必定是主元素
  2. 对于两段相连的子数组,分别获取左右的主元素,
  3. 如果左右一致则必定是主元素
  4. 比较左右主元素的计数大小,大的是主元素。

时间复杂度与归并排序类似:nlogn

  private static int findMainEleByDevice(int[] array,int start,int end){
        if (array == null) return -1;
        // 只有一个元素
        if (start == end){
            return array[start];
        }
        int mid = (end + start)/2;
        int leftMainEle = findMainEleByDevice(array, start, mid);
        int rightMainEle = findMainEleByDevice(array, mid + 1, end);
        // 左右主元素相等
        if (leftMainEle == rightMainEle){
            return leftMainEle;
        }
        return  getMainEle(array,leftMainEle,rightMainEle,start,end);
    }

    private static int getMainEle(int[] array, int leftMainEle, int rightMainEle, int start, int end) {
        // 数组的半数
        int half = (end - start +1)/2;
        int countLeft = 0;
        int countRight = 0;
        for (int i = start; i <= end; i++) {
            if (leftMainEle == array[i]){
                countLeft++;
            }
            if (rightMainEle == array[i]){
                countRight++;
            }
        }

        if (countLeft > half){
            return leftMainEle;
        }

        if (countRight > half){
            return rightMainEle;
        }
        return -1;
    }

时间复杂度为:nlogn.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章