大数据算法简介

大数据算法简介

今天我们来开个新坑——大数据算法。

PS:这是我第一次用md写正式的博客文章,写作的工具从一开始的ipad手写笔 到ipad+键盘 终于过渡到正轨了。。。

目录

引言:

我们之前已经学习过了基础的数据结构与算法,可是这些算法在实际生产中的使用场景还是有限的,特别是当需要处理的数据量变大时,经典的算法往往受限于时间、空间,而束手无策。因此,从现在开始需要逐渐学习如何处理海量数据的算法。本文在经典算法的基础上介绍以下几种常见的大数据处理算法:

  • 布隆过滤器
  • Bit Map
  • 堆与堆排序

在具体介绍以上算法之前,我们先来思考几个常见的问题:

  • 问题1:现有 用户IP-访问网站 的海量日志数据,找出日志中访问目标网站(比如百度)次数最多的一个IP地址。(全球的IPv4地址共2^32=4G个,意味着算法必须最多支持这么大体量的数据!另外,可使用的内存空间不超过1G)

  • 问题2:搜索引擎通过日志文件记录了最近一段时间内客户检索的字符串组成一个巨大的字符串数组,现要找出这段时间内“热度”最高的10个查询字符串。(假设日志中共有1000w条记录,每个查询串的大小最大为255B,且程序要求占用的内存空间不超过1G)

  • 问题3:现有A,B两个文件,各存放50亿条URL,每条URL占用64字节,内存限制是4G,需要找出A,B文件共同的URL。如果是三个乃至n个文件呢?

  • 问题4:已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。

如果你只熟悉经典的数据结构与算法,那么以上几道题可能会觉得有些难办了。那么本文通过以上4种情景来抛砖引玉,开始对大数据算法的介绍。

一、 布隆过滤器

1.1 概念

布隆过滤器是一种类似哈希表的数据结构,本质上是一个二进制数组,从其行为来看,像哈希表那样支持快速查找和插入:可以向其中插入一条数据,可以判断此数据是否已存在。
在这里插入图片描述
​ 图1 布隆过滤器的结构

1.2 插入

现在我们需要向布隆过滤器中插入一条数据,比如字符串“baidu"。如果是一般的哈希表,需要做的事情是将字符串”baidu“输入哈希函数,通过哈希函数计算出对应的哈希值,也就是哈希表的key值,通过此key值将”baidu“映射到对应的位置。显然,当数据量变大时,采用这种方式会出现哈希冲突现象:有多个目标对象经哈希函数映射得到的哈希值是相同的;传统的哈希表会对哈希冲突采用一些应对方式,常用的有:开链法、线性勘察、二次勘察。。。但不论采用什么方法只是补救一下而已,还是不能从根本上解决哈希冲突。而且,采用各种补救方法可能会造成访问哈希表的时间效率大打折扣,当哈希冲突频繁发生时,已经体现不出哈希表的查找性能优势了。因此,大批量的数据明显不适合使用传统的哈希表,这就是我们引入布隆过滤器的原因。接下来我们看看布隆过滤器是怎么做的:

首先,需要使用多个哈希函数对字符串”baidu“进行映射,会得到多个哈希值,将对应哈希值位置的数组下标值置为1,过程如下图所示:
在这里插入图片描述
​ 图2 布隆过滤器的插入

1.3 查找

向布隆过滤器中查找一个目标字段,比如字符串”tencent“;同样先将其经过多个哈希函数映射得到多个哈希值,然后检查布隆过滤器中对应下标,如果所有哈希值的下标位置元素都为1,则认为目标字段存在于布隆过滤器中;反之,则不存在。如下图所示:
在这里插入图片描述
​ 图3 布隆过滤器的查找

1.4 布隆过滤器的性能分析

了解布隆过滤器的插入和查找后,考虑一下:布隆过滤器的查找一定是可靠的吗?显然不一定,如果也发生了类似哈希冲突的情况:不同的目标使用多个哈希函数计算出的多个映射值正好一模一样,会怎么样呢?此时就可能会出现误报,明明没有此元素,却因为冲突认为有此元素。

好在,由于我们可以指定多个不同的哈希函数,使发生冲突的概率尽可能降低,起码比哈希表低很多。有人统计过 布隆过滤器的误报率p哈希函数个数k布隆过滤器长度m插入元素个数n之间的关系:
在这里插入图片描述
​ 图4 布隆过滤器的误报率

由上图可见,在插入元素个数一定的情况下:布隆过滤器长度越长、哈希函数个数越多 误报率越低。

由以上分析,由于其完全使用位来表示元素,且误报率较低,所以在不要求100%准确率的情况下,非常适合使用布隆过滤器来进行目标元素查找任务。进一步,在推荐在线系统中,还经常用布隆过滤器来完成去重工作:现在要推荐一个内容,需要到下发历史里查找该内容是否存在,存在则干掉。用这种数据结构在后端推荐系统中去重,可以最大程度节省空间和时间。唯一的不足是会认为某个没有下发过的内容是下发过的,而被去重服务干掉。

二、Bit Map

2.1 概念

bit-map(位图),用若干个位来记录一个整数(序号)的状态。状态是啥呢?最简单的状态应该就是 是否存在 了。比如可以用0表示此数不存在,1此数表示存在。这样就可以判断一些整数是否在bitmap中出现过了。类似布隆过滤器,位图的最大优势在于其占用空间很小,可以用几个bit来表示原本4字节或者更多空间的整数。

2.2 插入

按理说,只要指定下标很容易向位图中插入一个数字,只需将对应下标位置为1即可。而具体怎么操作取决于你是用什么数据结构建立位图的:

  • 建立方式1:直接建立的bitmap
int n=32*10000;//要表示的元素个数
bool* bitmap=new bool[n];//直接以位为单位建立bitmap
bitmap[200]=1;//插入元素200

这种情况,可以直接用下标来操作每一位的状态。

  • 建立方式2:用int为单位建立的bitmap
int* bitmap=new int[10000];//用一个四字节的int类型为单位建立bitmap,这意味着int数组的一个4字节int元素可以表示32个整数的状态
bitmap[200/32]=bitmap[200/32] | (1<<(200%32));//这时应该使用位运算将200插入bitmap

这种情况,应该使用位运算来操作int中的每一位。”200/32"定位到元素200应该插入到第几个int中;“200%32”表示应该插入到当前int元素的第几位上;然后将原int值和1<<(200%32)取或即可完成操作。(将1左移200%32位)

2.3 移除

移除和插入操作类似:

  • 建立方式1:直接建立的bitmap
int n=32*10000;//要表示的元素个数
bool* bitmap=new bool[n];//直接以位为单位建立bitmap
bitmap[200]=0;//移除元素200
  • 建立方式2:用int为单位建立的bitmap
int* bitmap=new int[10000];//用一个四字节的int类型为单位建立bitmap,这意味着int数组的一个4字节int元素可以表示32个整数的状态
bitmap[200/32]=bitmap[200/32] & ~(1<<(200%32));//与插入的位运算方式正好相反

2.4 bit map应用

结合bit map的特点,可以总结出以下几类应用场景:

  • 排序

    对于没有重复的数字的数据,可以先将这些数字存入位图,然后遍历位图,输出内容为1的下标值,即可完成对数据的排序。时间、空间复杂度均为O(n)。

    可见使用位图排序本身具有很大的速度优势,尤其是对大批量数据的情况。可是一个致命的问题是输入的数据不能有任何重复。。。

  • 查重

    如果需要查找一组数据中出现重复的数据是什么,那么首先想到的应该是位图:

    首先对每个数据的状态进行一个编码:

    00:之前没出现;01:出现过一次;11出现第二次及以上。(每2位表示一个元素的状态)

    那么,以2bit为单位遍历输入数据:初始时,所有元素状态均为00;第一次插入一个元素后,该元素的状态从00转化成01;而当第二次插入此元素时,状态从01转化成11,并标记此元素为一个重复元素;第三次及以上插入此元素时,状态一直保持为11。

  • 查找

    和布隆过滤器类似,位图也具备快速查找的优势。而由于布隆过滤器是经过多重哈希映射的,所以使用位图存储相同大小的数据时,无疑比使用布隆过滤器占用更多的空间,但位图可以确保一定不会出现哈希冲突,所以在要求查找的误报率尽可能低时,位图是一种选择。

三、堆与堆排序

我在之前的笔记中也涉猎过堆的使用,但都写得很浅,这次来系统的整理一番!

3.1 概念

是一种具有“完全二叉树”(按照层序输出不会缺失中间节点)性质的数据结构,而且相较于完全二叉树具有如下的特殊性质:每个节点的值都大于等于(小于等于)其左右孩子的值。其中每个节点值大于等于左右孩子的堆称为大顶堆;每个节点值小于等于左右孩子的堆称为小顶堆
在这里插入图片描述

​ 图5 大顶堆与小顶堆

堆排序是一种利用堆进行排序的方法,在之间排序方法总结的文章中有介绍过。

3.2 堆排序算法介绍

堆排序本质上是一种选择排序,即每次选出最大(小)、次大(小)、第三大(小)。。。的元素。一般来说,升序排序使用大顶堆,降序排序使用小顶堆;下面给出堆排序的一般步骤:

  1. 将无序数组构造成一个堆(大顶或小顶)
  2. 将堆顶元素与未排序的最后一个元素交换 ([0,j])
  3. 重新构建堆,范围[0,j]
  4. 将j-1,跳回步骤2,直到j为0,数组全部有序。

下面是一段C++写的堆排序代码:

#pragma once
//堆排序
//平均时间复杂度:O(nlogn)
//20w整型数据表现:93ms
//2w整型数据表现:10ms
//堆排还是比归并和快排慢一点的,不过不需要递归,消耗内存肯定较小
#include"Sort.h"
class HeapSot : public Sort {
private:
	//建立一个大根堆(从倒数第二层节点开始向上整理节点)
	void buildHeap(int* _array,size_t _size) {
		for (int i = _size / 2 -1; i >=0 ; i--) {
			fixHeapNode(_array, i, _size);
		}
	}
	//维护一个大根堆节点,保证从此节点到指定的长度内的数组为大根堆(从node节点向下检查)
	void fixHeapNode(int* _array, int node,size_t _size) {
		int temp=_array[node];
		for (int swapNode = node * 2 + 1; swapNode < _size; swapNode = swapNode * 2 + 1) {
			if (swapNode + 1 < _size && _array[swapNode + 1] > _array[swapNode]) swapNode++;
			if (_array[swapNode] >= temp)
			{
				_array[node] = _array[swapNode];
				node = swapNode;
			}
			else break;
		}
		_array[node] = temp;
	}
public:
	void sort(int* _array, size_t _size)override {
		buildHeap(_array, _size);//先建好一个大根堆
		for (int i = _size-1; i > 0; i--) {
			//每次循环把堆顶和堆尾交换,由于是大根堆,交换后堆尾的元素一定是最大的!
			swap(_array[0], _array[i]);
			//不要动堆尾元素了,重新维护堆顶~(堆尾前一个元素)的堆,目的是找出第二大的元素,以便下次循环放到后面相应位置
			fixHeapNode(_array, 0, i);
		}
	}
};

3.3 优先级队列——堆算法在大数据处理的应用

优先级队列维护一个固定大小的堆,其最主要的作用在于对一组海量数组寻找其前n大(小)的元素。其方法和堆排序类似,只是维护的堆大小并不是整个数组长度,而是n。

一般来说,大顶堆求前n小,小顶堆求前n大。比如求前n小,我们比较当前元素与大顶堆里的最大元素,如果它小于最大元素,则应该替换那个最大元素,再调整堆结构,如此反复。。。最后得到的n个元素就是最小的n个。这样可以扫描一遍即可得到所有的前n元素,效率很高。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值