欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

算法思想 - 分区算法

最编程 2024-05-21 14:50:50
...

其实相比于分治算法,我更愿意称之为分治“思想”。因为这种思想的应用非常广泛,不仅是在计算机中,在生活中到处都是分治思想的影子。当一项工程庞大到一个个体无法完成,我们就需要和别人进行合作,这种合作其实就是分治思想。当然,具体到计算机领域,分治算法就有了更加严格的约束。

分治算法

分治算法(divide and conquer)的核心思想就是四个字,分而治之。也就是将问题划分成多个规模较小,并且结构与元问题相似的子问题,依次(常常使用递归)地解决这些子问题,然后再合并其结果,就可以得到原问题的解。

分治算法的基础步骤

由于分治算法常常使用递归来实现,所以推广开来,在使用递归实现分治算法的过程中,每一层递归都应该有这样三个操作:

  • 分解:将原问题分解成一系列子问题
  • 解决:当问题分解到一定程度,可以很容易地解决这个问题
  • 合并:将子问题的结果合并,最终形成原问题的解

分治算法要满足的条件

一个算法是直接运行在计算机上的,所以一个算法首先要能够在计算机上实现,因此,我们会对分治算法提出一些要求:

  • 原问题和分解后的小问题具有相同的模式。如果你的问题非常复杂,分解后需要解决的问题很多,那你应该考虑将这个问题定义为一项“工程”,而不要尝试使用一个算法就解决它
  • 元问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法和动态规划的最明显的区别。
  • 具有分解终止条件。当问题足够小时,可以直接求解,这也对应了递归中的终止条件。
  • 可以将子问题的结果合并成原问题的结果,且合并不能太复杂

分治算法应用举例

分治算法的原理非常简单,但是想要灵活应用却很不容易。接下来,让我们通过一个例子,深入了解分治算法的思想。

在之前的排序算法中,我们讲过有序度和逆序度的概念,它等于一个数组中逆序对的个数:
逆序对.jpg

请问,如何求一组数的逆序度呢?

最先想到的就是对每个数组逐一和后面的数字进行比对,将逆序度相加,这是非常暴力的解决方式,我们可以使用分治算法来试试:

我们想求数组 A 的逆序对的个数,首先可以将它分解成前后两半 A1 和 A2 ,分别计算他们的逆序对个数 K1 和 K2,然后计算 A1 与 A2 之间的逆序对个数 K3。则数组A 的逆序对个数就是 K1+K2+K3。

这个过程是不是让你想到了归并排序?没错,我们可以通过改造归并排序,得到一个数组的逆序度。

归并排序中有一个非常关键的操作,就是合并两个数组,在这个合并的过程中,我们可以计算两个数组的逆序对个数了。每次合并操作,我们都可以计算一部分逆序度,当合并完成时,就可以获得这个数组的逆序数了:


求逆序对-使用归并.jpg

具体代码如下:

private int num = 0; // 全局变量或者成员变量

public int count(int[] a, int n) {
  num = 0;
  mergeSortCounting(a, 0, n-1);
  return num;
}

private void mergeSortCounting(int[] a, int p, int r) {
  if (p >= r) return;
  int q = (p+r)/2;
  mergeSortCounting(a, p, q);
  mergeSortCounting(a, q+1, r);
  merge(a, p, q, r);
}

private void merge(int[] a, int p, int q, int r) {
  int i = p, j = q+1, k = 0;
  int[] tmp = new int[r-p+1];
  while (i<=q && j<=r) {
    if (a[i] <= a[j]) {
      tmp[k++] = a[i++];
    } else {
      num += (q-i+1); // 统计p-q之间,比a[j]大的元素个数
      tmp[k++] = a[j++];
    }
  }
  while (i <= q) { // 处理剩下的
    tmp[k++] = a[i++];
  }
  while (j <= r) { // 处理剩下的
    tmp[k++] = a[j++];
  }
  for (i = 0; i <= r-p; ++i) { // 从tmp拷贝回a
    a[p+i] = tmp[i];
  }
}

如果你是python使用者,你可以参考我的代码(我直接使用了之前在归并排序中给出的代码,所以都是之前的注释)

import random

l = [40, 55, 94, 82, 60, 20, 42, 70, 93, 58]
a = [20, 40, 42, 55, 58, 60, 70, 82, 93, 94]

num = 0  # 设置一个全局变量

def merge_sort(L):
    '''
    启动归并排序
    :param L: 待排序数组
    :return: 无返回
    '''
    _merge_sort(L, 0, len(L) - 1)


def _merge_sort(L, left, right):
    '''
    在这个函数中实现递归
    :param L:
    :param left: 归并区间的起始指针(角标)
    :param right: 结束指针(角标)
    :return: 无返回
    '''
    if left < right:
        mid = (left + right) // 2
        _merge_sort(L, left, mid)
        _merge_sort(L, mid + 1, right)
        merge(L, left, mid, right)
        # 请注意调用顺序,我们先分割排序,然后合并


def merge(L, left, mid, right):
    '''
    合并两个数组,这里需要使用一个临时数组存储合并的数据
    '''
    global num  # 声明num为全局变量
    temp = []
    i = left  # i为左边数组的起始位置
    j = mid + 1  # j为右边数组的起始位置
    while i <= mid and j <= right:  # 两边数组进行比较
        if L[i] < L[j]:
            temp.append(L[i])
            i += 1
        else:
            num += mid - i + 1  # 计算逆序度
            temp.append(L[j])
            j += 1

    # 确保两个指针都走到最后
    while i <= mid:
        temp.append(L[i])
        i += 1

    while j <= right:
        temp.append(L[j])
        j += 1

    L[left:right + 1] = temp  # 将临时变量放入数组中


# _sort = merge_sort(l)
# print(_sort)
# print(l)

if __name__ == "__main__":
    data = list(range(5))
    random.shuffle(data)
    print(data)
    merge_sort(data)
    print(data)
    print(num)

总结

分治算法的思想在于:拆->解决->合并。这是一个非常重要的算法思想,实际上,之前我们讲到的很多内容都用到了分治的思想,在这里,就不过多介绍相关例子了,你可以回顾之前的内容,相信你会有更多收获。
以这个思想为基础,人们建立了计算机中的分布式集群处理系统。两者的差距似乎很大,但是它们的联系却又如此简单。最后,附上专栏老师的一段话,希望你可以走的更远:

其实,创新并非离我们很远,创新的源泉来自对事物本质的认识。无数优秀架构设计的思想来源都是基础的数据结构和算法,这就是算法的魅力所在。


以上就是分治算法的全部内容

注:本文章的主要内容来自我对极客时间app的《数据结构与算法之美》专栏的总结,我使用了大量的原文、代码和截图,如果想要了解具体内容,可以前往极客时间