数据结构与算法笔记:基础篇 - 分治算法:谈一谈大规模计算框架MapReduce中的分治思想

本文主要是介绍数据结构与算法笔记:基础篇 - 分治算法:谈一谈大规模计算框架MapReduce中的分治思想,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

概述

MapReduce 是 Google 大数据处理的三姐马车之一,另外两个事 GFS 和 Bigtable。它在倒排索引、PageRank 计算、网页分析等搜索引擎相关的技术中都有大量的应用。

尽管开发一个 MapReduce 看起来很高深。实际上,万变不离其宗,它的本质就是本章要学的这种算法思想,分支算法。


如何理解分支算法?

为什么说 MapReduce 的本质就是分治算法呢?先来看看什么事分治算法?

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

这个定义看起来有点类似递归地定义。关于分治和递归,我们在 排序(下) 的时候讲过,分治算法是一种处理问题的思想,递归是一种编程技巧。实际上,分治算法一般都比较适合用递归来实现。分治算法的递归实现中,每一次递归都会涉及三个操作:

  • 分解:将原问题分解为一系列子问题;
  • 解决:递归地求解各个子问题,若子问题足够小,则直接求解;
  • 合并:将子问题的结果合并成原问题。

分治算法能解决的问题,一般要满足以下几个条件:

  • 原问题与分解成的小问题具有相同的模式;
  • 原问题分解的子问题可以独立解决,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别,等讲到动态规划,会详细对比这两种算法;
  • 具有分解终止条件,也就是说,当问题足够小时,可以直接求解;
  • 可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减少算法总体复杂度的效果了。

分治算法应用举例分析

理解分治算法的原理并不难,但是要想灵活应用并不容易。所以,接下来,我会带你用分治算法解决我们在讲排序的时候设计的一个问题,加深你对分治算法的理解。

还记得我们在排序算法里讲到的数据的有序度、逆序度的概念吗?我当时讲到,我们用有序度表示一组数据的有序程度,用逆序度表示一组数据的无序程度。

假设我们有 n 哥数据,我们期望数据从小到大排列,那完全有序的数据的有序度就是 n(n-1)/2,逆序度等于 0;相反,倒序排列的数据的有序度就是 0,逆序度就是 n(n-1)/2。除了这两种极端情况外,我们通过计算有序对或逆序对的个数,来表述数据的有序度或逆序度。

在这里插入图片描述

现在的问题是,如何编码求出一组数据的有序对个数或者逆序对个数呢? 因为有序对个数和逆序对个数的求解方式是类似的,所以你可以之思考逆序对个数的求解方法。

最笨的方法是,拿每个数组跟它后面的数字比较,看看有几个比它小。我们把比它小的数字的个数记作 k,通过这样的方式,把每个数字都考察一遍之后,然后对每个数字对应的 k 值求和,最后得到的总和就是逆序对个数。不过,这样擦做的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。有没有更加高效的处理方法呢?

我们用分治法来试试。我们套用分治法的思想来求数组 A 的逆序对个数。我们可以将数组分成前后两半 A1 和 A2,分别计算 A1 和 A2 之间的逆序对个数 K1 和 K2,然后再计算 A1 和 A2 之间逆序对个数 K3。那数组 A 的逆序对个数就等于 K1+K2+K3。

我们前面讲过,使用分治算法其中一个要求是,子问题合并的代价不能太大,否则就起不到了降低时间复杂度的效果了。那回到这个问题,如何快速计算出两个子问题 A1 与 A2 之间的逆序对个数呢?

这里就要借助归并排序算法了。你可以先试着想想,如何借助归并排序算法来解决呢?

归并排序中有一个非常关键的操作,就是将两个有序的小数组,合并成一个有序的数组。实际上,在这个合并的过程中,我们就可以计算这连个小数组的逆序对个数了。每次合并操作,我们都计算逆序对个数,把这些计算出来的逆序对个数求和,就是这个数组的逆序对个数了。

在这里插入图片描述

尽管画了张图来解释,但个人觉得,对于工程师来说,看代码更好理解一些,所以我们把这个过程翻译成了代码,你可以结合着图和文字描述一起看下。

    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[i++];}}while (i <= q) { // 处理剩下的tmp[k++] = a[i++];}while (j <= r) { // 处理剩下的tmp[k++] = a[j++];}for (i = 0; i <= r-p; i++) { // 从tmp拷贝回aa[p+i] = tmp[i];}}

很多同学经常会说,某某算法思想如此巧妙,我是怎么也想不到的。实际上,确实是的。有些算法确实非常巧妙,并不是每个人短时间都能想到的。比如这个问题,并不是每个人都能想到可以借助归并排序算法来解决。不夸张地说,如果之前没接触过,觉得部分人都想不到。但是,如果我告诉你可以借助归并排序算法来解决,那你就应该想到如何改造归并排序,来求解这个问题了,只要你能做到这一点,我觉得就很棒了。

关于分治算法,还有两道比较经典的问题:

  • 二维平面上有 n 个点,如何快速计算出两个距离最近的点对?
  • 有两个 nn 的矩阵 A,B,如何快速求解两个矩阵的成绩 C=AB?

分治思想在海量数据处理中的应用

分治算法思想的应用是非常广发的,并不仅限于指导编程和算法设计。它还经常用在海量数据处理的场景中。我们前面讲的数据结构和算法,大部分都是基于内存存储和单机处理。但是,如果要处理的数据量非常大,没法一次性放到内存中,这个时候,这些数据结构和算法就无法工作了。

比如,给 10GB 订单文件按照金额排序这样一个需求,看似是一个很简单的排序问题,但是因为数据量大,有 10GB,而我们机器的内存可能只有 2、3GB 这样子,无法一次性加载到内存,也就无法通过单纯地使用快排、归并排序等基础算法来解决了。

要解决这种数据量大到内存装不下的问题,我们就可以利用分治的思想。我们可以将海量的数据集合根据某种方法,划分为几个小的数据集合,每个小的数据集合单独加载到内存来解决,然后再将小数据集合合并成大数据集合。实际上,利用这种分治的处理思路,不仅仅能克服内存的限制,还能利用多线程或者多机处理,加快处理的速度。

比如刚刚的例子,给 10GB 的订单排序,我们可以先扫描一遍订单,根据订单的金额,将 10GB 的文件划分为几个金额区间。比如订单金额 1 到 100 元的放到一个小文件,101 到 200 的放到另一个文件,以此类推。这样每个小文件都可以单独加载到内存排序,最后将这些有序的小文件合并,就是最终有序的 10GB 订单数据了。

如果订单存储在类似 GFS 这样的分布式系统上,当 10GB 订单被划分成多个小文件的时候,每个文件可以并行加载到多态机器上处理,最后再将结果合并在一起,这样并行处理的速度也加快了很多。不过,这里有一点要注意,就是数据的存储与计算所在的机器是同一个或在网络中靠的很近(比如一个局域网内,数据存取速度很快),否则就会因为数据访问的速度,导致整个处理过程不但不会变快,反而有可能变慢。

你可能还有印象,这个就是我们在讲线性排序的时候举的例子。实际上,在前面已经学习的课程中,还讲了很多利用分治算法来解决的问题。

谈一谈大规模计算框架MapReduce中的分治思想

刚刚举的订单的例子,数据有 10GB 大小,可能给你的感受还不强烈。那如果我们要处理的数据时 1T、10T、100T 这样的数据,那一台机器处理的效率肯定是非常低的。而对于谷歌搜索引擎来说,网页爬取、清洗、分析、分词、计算权重、倒排索引等等各个环节中,都会面对如此海量的数据(比如网页)。所以利用集群并行处理显然是大势所趋。

一台机器过于低效,那我们就把人去拆分到多态机器上来处理。如果拆分之后的小人物之间互不干扰,独立计算,最后再将结果合并。这不就是分治思想吗?

实际上,MapReduce 框架只是一个任务调度器,底层依赖 GFS 来存储数据,依赖 Borg 管理机器。它从 GFS 中拿数据,交给 Brog 中的机器执行,并且时刻监控机器执行的进度,一旦机器出现宕机、进度卡壳等,就重新从 Brog 中调度一台机器执行。

尽管 MapReduce 的模型非常简单,但是在 Google 内部应用非常广泛。它除了可以用来处理这种数据与数据之间存在关系的任务,比如 MapReduce 的经典例子,统计文件中单词出现的频率。此外,它还可以用来处理数据与数据之间没有关系的任务,比如对网页分析、分词等,每个网页可独立的分析、分词,而这两个网页之间没有关系。网页几十亿、上百亿,如果单机处理,效率地下,我们就可以利用 MapReduce 提供可靠、高性能、高容错的并行计算框架,并行地处理这几十亿、上百亿的网页。

小结

本章讲了一种应用非常广泛的算法思想,分治算法。

分治算法用四个字概况就是 “分而治之”,将原问题划分成几个规模较小而结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解。这个思想非常简单、好理解。

本章讲解了两种分治算法的经典的应用场景,一个是用来指导编码,降低问题求解的时间复杂度,另一个是解决海量数据处理问题。比如 MapReduce 本质上就是利用了分治思想。

我们也时常感叹 Google 的创新能力如此之强,总是在引领技术的发展。实际上,创新并非离我们很远,创新的源泉来自对事物本质的认识。无数优秀架构设计的思想来源都是基础的数据结构和算法,这本身就是算法的一个魅力所在。

这篇关于数据结构与算法笔记:基础篇 - 分治算法:谈一谈大规模计算框架MapReduce中的分治思想的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1075210

相关文章

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

Python基础文件操作方法超详细讲解(详解版)

《Python基础文件操作方法超详细讲解(详解版)》文件就是操作系统为用户或应用程序提供的一个读写硬盘的虚拟单位,文件的核心操作就是读和写,:本文主要介绍Python基础文件操作方法超详细讲解的相... 目录一、文件操作1. 文件打开与关闭1.1 打开文件1.2 关闭文件2. 访问模式及说明二、文件读写1.

C#数据结构之字符串(string)详解

《C#数据结构之字符串(string)详解》:本文主要介绍C#数据结构之字符串(string),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录转义字符序列字符串的创建字符串的声明null字符串与空字符串重复单字符字符串的构造字符串的属性和常用方法属性常用方法总结摘

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Python Dash框架在数据可视化仪表板中的应用与实践记录

《PythonDash框架在数据可视化仪表板中的应用与实践记录》Python的PlotlyDash库提供了一种简便且强大的方式来构建和展示互动式数据仪表板,本篇文章将深入探讨如何使用Dash设计一... 目录python Dash框架在数据可视化仪表板中的应用与实践1. 什么是Plotly Dash?1.1

C#基础之委托详解(Delegate)

《C#基础之委托详解(Delegate)》:本文主要介绍C#基础之委托(Delegate),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 委托定义2. 委托实例化3. 多播委托(Multicast Delegates)4. 委托的用途事件处理回调函数LINQ

基于Flask框架添加多个AI模型的API并进行交互

《基于Flask框架添加多个AI模型的API并进行交互》:本文主要介绍如何基于Flask框架开发AI模型API管理系统,允许用户添加、删除不同AI模型的API密钥,感兴趣的可以了解下... 目录1. 概述2. 后端代码说明2.1 依赖库导入2.2 应用初始化2.3 API 存储字典2.4 路由函数2.5 应

Python GUI框架中的PyQt详解

《PythonGUI框架中的PyQt详解》PyQt是Python语言中最强大且广泛应用的GUI框架之一,基于Qt库的Python绑定实现,本文将深入解析PyQt的核心模块,并通过代码示例展示其应用场... 目录一、PyQt核心模块概览二、核心模块详解与示例1. QtCore - 核心基础模块2. QtWid

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

最新Spring Security实战教程之Spring Security安全框架指南

《最新SpringSecurity实战教程之SpringSecurity安全框架指南》SpringSecurity是Spring生态系统中的核心组件,提供认证、授权和防护机制,以保护应用免受各种安... 目录前言什么是Spring Security?同类框架对比Spring Security典型应用场景传统