分治算法

分冶算法的基本思想是将原问题分解为几个规模较小的但类似原问题的子问题,递归地求解这些了问题,然后再合并这些子问题的解来建立原问题的解

分冶算法在每层递归时都有三个步骤:

  • 分解,原问题分为若干子问题,这些子问题都是原问题的规模较小的实例
  • 解决,递归解决这些子问题,如果子问题的规模足够小,则直接求解
  • 合并, 合并子问题的解成原问题的解

分治算法中需要使用递归,在使用递归时,一定要确定问题边界,即问题规模较小的条件。常见的归并排序以及求解数组中连续子数组和的最大值,都可以使用分治法解决

归并排序

归并排序的基本思想是,将数组分成两个子数组,使用递归对两个子数组进行排序,合并两个已正确排序的子数组。

注意,递归的边界条件即是,子数组中只有一个元素。假定数组中只有两个元素,分解成两个子数组,每个子数组中只有一个元素,子数组中的元素此时当然就是已经“排序正确”的,合并子数组,则整个数组已正确排序。

数组合并是整个过程中最复杂的地方。可以想象手边有两堆扑克牌,每堆扑克牌都是从小到大排序完毕,比较每堆扑克牌最上边的牌的大小,取最小的放到手上,直到有一堆扑克牌已经被取完,此时再将剩下的那一堆扑克牌全部放到手上,则手中的所有牌都是按从小到大顺序排列好。

为了界定每堆扑克牌是否已经取完,本文中向每堆扑克牌最后放入一张哨兵牌。以便于节省大量的判断,牌堆是否已经取完。

  //p是数组中合并的起始index,p是中间位置,r是末位
  public static void merge(int[] array, int p, int q, int r){
    int n1 = q - p + 1;
    int n2 = r - (q + 1) + 1;
    int[] left = new int[n1 + 1];
    int[] right = new int[n2 + 1];
    int i = 0;
    int j = 0;
    for (i = 0; i < n1; i++) {
        left[i] = array[p + i];
    }
    left[n1] = Integer.MAX_VALUE;
    for (j = 0; j < n2; j++) {
        right[j] = array[q + 1 + j];
    }
    right[n2] = Integer.MAX_VALUE;
    i = 0;
    j = 0;
    //哨兵牌为正整数最大值,所以子数组中不可能有大于它的数,假设left子数组已经被取完,只剩下哨兵牌
    //right子数组剩下的元素则必然全都小于哨兵牌,则可全取right子数组,而不用判定子数组是否已经取完
    for (int k = p; k <= r; k++) {
        if (left[i] < right[j]) {
            array[k] = left[i];
            i ++;
        }else {
            array[k] = right[j];
            j++;
        }
    }
}

最复杂的合并工作已经完成,则分解子问题和求解子问题则很简单了。

  public static void sort(int[] array, int p, int r){
    if (p < r) {
        int q = (r + p)/2;
        sort(array, p, q);
        sort(array, q + 1, r);
        merge(array, p, q, r);
    }
}

连续子数组和最大值

如果在一个数组中,找出一个连续子数组和的最大值。此问题必须是在有负数的数组中才有意义,如果数组中全为正数,那么此问题的解即为数组所有元素之和

此问题也可以用分治算法解决,将数组分解为两个子数组,那么问题的解必然为以下三个之一:

  • 左子数组中的连续子数组和最大值
  • 右子数组中的连续子数组和最大值
  • 包含跨越分隔左右子数组的中间值的连续子数组的和的最大值。

分治算法有三步,分解子问题、解决子问题、全并子问题。本问题中,合并子问题非常简单,如果以上三个值已经求出来,通过比较大小即可得知,时间复杂度为1,分解子问题和解决子问题中只有一步较为复杂,即是上文中的第三步,求 包含中间值的连续子数组和的最大值,不过将此问题单独提出来也是比较简单的,因为此连续子数组必然包含中间值。

根据中间值将数组分成两半,分别求取左右两边的连续子数组和的最大值,再相加即可。时间复杂度为n。

  private static int[] getMaxSumSubArrayCorssMidel(int[] array, int low, int middle, int high){
    int left_sum = Integer.MIN_VALUE;
    int sum = 0;
    int maxLeftIndex = 0;
    for (int i = middle; i >= low; i--) {
        sum = sum + array[i];
        if (sum > left_sum) {
            left_sum = sum;
            maxLeftIndex = i;
        }
    }
    sum = 0;
    int right_sum = Integer.MIN_VALUE;
    int maxRightIndex = 0;
    for (int i = middle + 1; i <= high; i++) {
        sum = sum + array[i];
        if (sum > right_sum) {
            right_sum = sum;
            maxRightIndex = i;
        }
    }
    return new int[]{maxLeftIndex, maxRightIndex, left_sum + right_sum};
}

递归求解子问题的边界点是什么呢?数组只有一个元素,则连续子数组和的最大值则为数组唯一元素。确定了边界,则剩余代码非常容易写了

  private static int[] getMaxSumSubArray(int[] array, int low, int high){
    if (low == high) {
        return new int[]{low, high ,array[low]};
    }else {
        int middle = (low + high)/2;
        int[] left = getMaxSumSubArray(array, low, middle);
        int[] right = getMaxSumSubArray(array, middle + 1, high);
        int[] corss = getMaxSumSubArrayCorssMidel(array, low, middle, high);
        if (left[2] >= right[2] && left[2] >= corss[2]) {
            return left;
        }else if (right[2] >= left[2] && right[2] >= corss[2]) {
            return right;
        }else{
            return corss;
        }
    }
}

求取数组中元素的最大值

求取数组中元素的最大值也可以使用分治法解决,通常人们都使用二分法查找,后续将讨论分治算法的时间复杂度,可计算得出,二分法和分治算法的时间复杂度是一样的。不过递归本身效率不高,执行一次函数需要入栈出栈多次,分治算法的最终效率肯定是不及二分查找的。不过此文中只为展示分治算法的使用。

同理,将数组分成两个子数组,分别求取两个子数组中的最大值,再将两个子数组的最大值比较,取其大者,则可求得此问题的解。

本例中合并子问题非常简单,只需要比较子问题的解大小即可,取其大者就ok了,时间复杂度为1

  private static int findMax(int[] array, int p, int q){
    if (p == q) {
        return array[q];
    }else {
        int middle = (p + q)/2;
        int left = findMax(array, p, middle);
        int right = findMax(array, middle + 1, q);
        return (left > right) ? left : right;
    }
}

未完待续,关于分治算法的时间复杂度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容