Java海量数据处理算法

文章目录

    • Hash法
    • Bit-map法
    • Bloom filter法
    • 数据库优化法
    • 倒排索引法
      • 正向索引
    • 外部排序法
    • trie树
      • 兄弟单词
      • 求解兄弟单词的经典例子
    • 双层桶法
    • MapReduce法
    • 经典实例
      • top K问题
        • BFPRT算法(TOP-K问题必会)
      • 重复问题
      • 排序问题

Hash法

在处理海量数据的过程中,使用hash法一般可以快速存取,统计某些数据,将大量数据进行分类,例如提取某日访问网站次数最多的IP地址等.

常用散列函数的构建方法如下:

  1. 直接寻址法
  • 取关键字或关键字的某个线性函数值为散列地址,即h(key)=key或h(key)=a*key+b.直接寻址法不会产生冲突,时间复杂度为o(1),空间复杂度为o(n),但由于它没有压缩映像,因此,当关键字集合很大时,使用这种hash函数是不可能实现地址编码的散列的(因为key集合必须有穷且不能超出物理存储大小).
  1. 取模法
  • 选择一个合适的正整数p,令h(key)=key mod p.p如果选择的是比较大的素数,则效果比较好,一般选取p为tablesize,即散列表的长度.
  1. 数学分析法
  • 根据关键字的个数n和r进制,通过统计在各个位上数符出现频率找出各个位上出现出现次数最接近n/r的数符作为不变的散列地址位.
  1. 折叠法
  • 将关键字分成位数为t的几个部分(最后一部分可能小于t),然后把各部分按位对齐进行相加,将所得的和舍弃进位,留下t位作为散列地址. 当关键字位数很多,而且关键字中每位上数字分布比较均匀时,采用折叠法比较合适.比如key:5669,(56+69)%100=25.
  1. 平方取中法
  • 将关键字进行平方运算,然后从结果的中间取出若干位(位数于散列地址的位数相同),将其作为散列地址,具体取几位,由散列表的表长决定.
  1. 除留余数法
  • 取关键字除以某个数p( p 小于等于 散列表的长度)的余数作为散列地址.它与取模法的区别在于p不一定等于tablesize(散列表长度),p一般选取质数或大于20的和数.
  1. 随机数法
  • 选择一个随机函数,然后用关键字key的随机函数值作为散列地址,即h(key)=random(key).

在hash表的构建过程中,hash冲突是不可避免地,解决冲突的主要途径是当一个关键字映射到散列表中的某个地址且该地址已有关键字时,为该关键字寻找新的存储地址.

常用于解决地址冲突的方法如下:

  1. 开放地址法
    基本思想是当发生地址冲突时,在散列表中再按照某种方法继续探测其他的存储地址,直到找到空闲的地址为止.
    在这里插入图片描述
  2. 链地址法
    若散列表空间为[0,m-1],则设置一个由m个指针组成的一维数组CH[m],然后在寻找关键字散列地址的过程中,所有散列地址为i的数据元素都插入到头指针为CH[i]的链表中.这种方法比较适合冲突严重的情况下使用.
    比如有8个元素[a,b,c,d,e,f,g,h],采用某种散列函数得到的地址为[0,2,4,1,0,8,7,2],采用链地址法后如图:
    Java海量数据处理算法_第1张图片
  3. 再散列法
    当发生冲突时,切换散列函数计算地址,直到计算出一个无冲突的地址.
  4. 建立公共溢出区
    假设散列函数的值域为[0,m-1],则设向量HashTable[0…m-1]为基本表,再新增存储空间向量OverTable[0…v]用于存储发生冲突的记录.

Bit-map法

位图法的基本原理是使用位数组来表示某些元素是否存在.例如从8位电话号码中查找重复号码.本法适用于海量数据的快速查找,判重,删除等.
比如集合为**{2,7,9,4,1,10},则生成一个10位的串(因为最大值为10),将集合中对应的位 置1,有1101001011**.排序自动完成(字符串下标有序)
位图法(Bit-map)排序的时间复杂度为o(n),但是它是以空间换时间,且排序前集合大小最好已知.

Bloom filter法

布隆过滤器常用于判断一个元素是否在集合中或者检查英语单词是否拼写正确.最经典的使用就是垃圾邮件地址匹配.
布隆过滤器以牺牲正确率为前提换取空间效率与时间效率的提高.当它判断某元素不属于这个集合时该元素一定不属于这个集合,当它判断某元素属于这个集合时,该元素不一定属于这个集合.
Java海量数据处理算法_第2张图片
Java海量数据处理算法_第3张图片
使用布隆过滤器的难点是如何根据输入元素个数n来确定数组m的大小以及Hash函数.
布隆过滤器不能删除元素.
CBF(Counting Bloom Filter)和SBF(Spectral Bloom Filter)是布隆过滤器的扩展,CBF将位数组中的每一位扩展为一个counter,从而支持元素的删除操作.SBF采用counter中的最小值来近似表示元素的出现频率.

数据库优化法

常见数据库优化方法如下:

  1. 优秀的数据库管理工具
    比如MySQL和Oracle
  2. 数据分区
    例如,针对按年份存取的数据,可以按年进行分区,不同的数据库有不同的分区方式,不过处理机制却大体相同,例如sql server的数据库分区将不同的数据存于不同的文件组下,而不同的文件组存于不同的磁盘分区下.
  3. 索引
    索引一般可以加速数据的检索速度,加速表与表之间的连接,提高性能.
  4. 缓存机制
    缓存大小设置的好差也关系到数据处理的成败.
  5. 加大虚存
    当内存不足时,可以增加虚拟内存来解决.
  6. 分批处理
  7. 使用临时表和中间表
  8. 优化查询语句
  9. 使用视图
    将数据按一定规则分散到各个基本表中,查询或处理过程可以基于视图进行
  10. 使用存储过程
  11. 用排序来取代非顺序存取
  12. 使用采样数据进行数据挖掘

倒排索引法

倒排索引是目前搜索公司对搜索引擎最常用的存储方式,也是搜索引擎的核心内容.
倒排索引就是按照关键字建立索引.
倒排索引被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射.
有两种倒排索引形式:

  • 第一种形式是一条记录的水平反向索引包含每个引用单词的文档的列表.
  • 第二种形式是一个单词的水平反向索引又包含每一个单词在一个文档中的位置.
    Java海量数据处理算法_第4张图片

正向索引

正向索引用来存储每个文档的单词的列表.正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文件中的验证这样的查询.
正向索引中文档指向它所包含的那些单词,而反向索引则是单词指向了包含它的文档.

外部排序法

外排序法就是以文件的形式存储待排序对象,排序时再把它们一部分一部分的调入内存进行处理.
一般采用归并排序等方式实现外部排序,主要分成两个步骤:
第一步,生成若干初始归并段,把含有n个记录的文件按内存大小划分为若干长度为L的子文件,然后分别将子文件调入内存,采用有效的内部排序算法排序后返回外存.
第二步,进行多路归并,即对这些初始归并段进行多次归并使得有序的归并段逐渐扩大,最后生成一个有序的文件.
外排的缺陷是消耗大量的IO,效率不会太高.

trie树

字典树是一种用于快速字符串检索的多叉树结构,其原理是利用字符串的公共前缀来减少时空开销,即用空间换时间,从而达到提高程序效率的目的.
字典树常用于统计和排序大量的字符串.
字典树的优点是最大限度地减少无谓地字符串比较,查询效率比散列表高.
字典树的特征:

  1. 根结点不包含字符,除根结点外每一个结点都只包含一个字符.
  2. 从根结点到某一个结点,路径上经过的字符连接起来,为该结点对应的字符串.
  3. 每个结点的所有子结点包含的字符都不相同.
    字典树适用于数据量大,重复多,但是数据种类小可以放入内存的情况.
    使用例子如下:
    Java海量数据处理算法_第5张图片

兄弟单词

一个单词a,如果通过交换单词中字母顺序可以得到另一个单词b,称b是a的兄弟单词,比如army和mary互为兄弟单词.

已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个字符串是另一个字符串的前缀子串.一般可以采用如下三种方法:

  1. 迭代法,对于每一个单词,都要去查找其前面的单词是否包含它,看每个字符串是否为字符串集中某个字符串的前缀,由于需要不停的进行迭代比较,因此此时的时间复杂度为o(n²).
  2. Hash法.使用hash存储所有字符串的所有前缀子串.建立存有子串Hash的时间复杂度为o(n*len),查询复杂度为o(n).
  3. **字典树(trie树).**假设要查询的单词是abcd,则只需要去查找以a开头的单词是否存在abcd即可,在以a开头的单词中,找到以b作为第二个字母的单词即可,所以建立trie树的总复杂度为o(n*len).实际查询的复杂度为o(len).

求解兄弟单词的经典例子

public class 查找兄弟单词 {
//字典树结点
	class TrieNode{
	Vector bwords=new Vector<>();
	//对应26个字母
	TrieNode next[]=new TrieNode[26];
	public TrieNode() {
		// TODO Auto-generated constructor stub
		for (int i = 0; i < 26; i++) {
			next[i]=null;
		}
	}
};
//比较字符大小
int CmpChar(char c1,char c2) {
	return (c1-c2);
}
//给字典树添加字符串,
void InsertNode(TrieNode root,String wd) {
	if (wd.length()==0) {
		return;
	}
	if (root==null) {
		root=new TrieNode();
	}
	int i=0;
	//将字符串转字符数组
	char swd[]=wd.toCharArray();
	//升序排序(自然排序)字母,如果是兄弟单词,则自然排序后字符数组相同
	Arrays.sort(swd);
	TrieNode next=root;
	while(i

堆是一种树形数据结构.常用于海量数据求前N大(小顶堆)或者前N小(大顶堆).

双层桶法

桶排序一般适用于寻找第K大的数,寻找中位数,寻找不重复或重复的数字.
桶排序示例:

public class 桶排序 {

	class Node{
	int key;
	Node next;
};

//升序排序,有十个桶,排序0~99的数
void IncSort(int[] keys,int bucketsize) {
	int size=keys.length;
	Node[] bucket_table=new Node[bucketsize];
	for (int i = 0; i < bucketsize; i++) {
		bucket_table[i]=new Node();
		bucket_table[i].key=0;
		bucket_table[i].next=null;
	}
	for (int j = 0; j < size; j++) {
		Node node=new Node();
		node.key=keys[j];
		node.next=null;
		int index = keys[j]/10;
		Node p =bucket_table[index];
		if (p.key==0) {
			bucket_table[index].next=node;
			(bucket_table[index].key)++;
		}else {
			while(p.next!=null&&p.next.key<=node.key) {
				p=p.next;
			}
				node.next=p.next;
				p.next=node;
				(bucket_table[index].key)++;
			
		}
		
	}
	for (int b = 0; b < bucketsize; b++) {
		for(Node k=bucket_table[b].next;k!=null;k=k.next) {
			System.out.print(k.key+" ");
		}
	}
}
public static void main(String[] args) {
	int[] array= {49,37,39,36,38,65,97,76,13,27,49};
	new 桶排序().IncSort(array, 10);
}
}

MapReduce法

基于Hadoop可以非常轻松和方便完成处理海量数据的分布式并行程序.

经典实例

top K问题

在大规模数据处理中,经常会遇到在海量数据中找出出现频率最高的前K个数,或者从海量数据中找出最大的前K个数,这就是top K问题.

通用方案:分治+Trie树/hash+小顶堆

例题:有1亿个浮点数,如何找出其中最大的10000个?

解法1:将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法时间复杂度为o(nlgn),例如快排.在32位机器上,float型占4Byte,1亿个浮点数就要占400MB,不论内存能不能一次性装下400MB的数据,这个通过内部排序找出前10000个最大数的方式无疑是最慢的.

解法2: 局部淘汰法,用一个容器保存前10000个数,然后将剩余的所有数字一一与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内的这10000个数就是最大的10000个数.否则就将10000个数中最小的与比较数替换.时间复杂度为o(m²+n),m为容器大小,n为未进容器的剩余数.

解法3:分治法,将1亿个数据分成100份,每份100万个数据,找出每份数据中最大的10000个,最后在剩下的100X10000个数据中找出最大的10000个.

具体实现如下:用快速排序将数据分为两堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次再分出两堆,直到大堆个数N小于10000,就在小的那堆快排找出第10000-N大的数字,递归,直到找出第10000大的数.需要执行找出前10000个数101次.

解法4:最小堆法,先读入前10000个数来创建大小为10000的小顶堆,建堆的时间 复杂度为o(nlgn)(n为数组大小),然后遍历后续数字,并与堆顶(最小数字)进行比较,若比堆顶数字大,则替换并重新调整堆,时间复杂度为(mXnlgn),m为调整最小堆次数.

解法5:hash法,如果这1亿个数里面有很多重复的数,先通过hash法,把这1亿个数字去掉重复,然后通过分治法或最小堆法找出最大的10000个数.

BFPRT算法(TOP-K问题必会)

BFPRT算法又叫中位数的中位数算法,如果被问到海量数据的TOP-K问题,你能说出这个算法估计会很加分。
该算法的最坏时间复杂度为O(n),最差空间复杂度为O(logN).
算法思路:
(1):将n个元素划分为[n/5]个组,每个组5个元素,若有剩余则舍去;
(2):使用排序方法找到[n/5]个组中每一组的中位数;
(3):对于(2)中找到的所有中位数,递归(1)(2)查找中位数的中位数,作为Partition划分过程的主元。
(4):进行Partition划分,即一次快排。
(5):判断主元的位置与K的大小,有选择的对左边或右边递归。

为什么要花费这么多步骤去寻找划分数呢?假设数组长度为n,那么整个数组一共有n/5个中位数,在中位数组找出中位数组的中位数(划分数),那么就会有3*(n/10)个数比划分数小-如图红色区域所示:
Java海量数据处理算法_第6张图片
我们一次就可以至少刷掉3*(n/10)、最多7*(n/10)的数量级的数据进行递归排序,节省了很多时间!

public class BFPRT算法 {

	//打印结果
	public static void printArray(int[] arr) {
		for (int i = 0; i != arr.length; i++) {
			System.out.println(arr[i]+" ");
		}
		System.out.println();
	}
	//得到前K个最小数
	public static int[] getMinKNumsByBFPRT(int[] arr,int k) {
		
		if (k<1||k>arr.length) {
			return arr;
		}
		//找出比k小的前k个数,返回第k个数的值
		int minKth=getMinKthByBFPRT(arr,k);
		//res前k个结果集
		int[] res=new int[k];
		int index=0;
		for (int i = 0; i != arr.length; i++) {
			if (arr[i]<minKth) {
				res[index++]=arr[i];
			}
		}
		for (; index != res.length; index++) {
			res[index]=minKth;
		}
		return res;
	}
	//找出比k小的前k个数
	public static int getMinKthByBFPRT(int[] arr,int K) {
		int[] copyArr=copyArray(arr);
		return select(copyArr,0,copyArr.length-1,K-1);
	}
	//复制数组
	public static int[] copyArray(int[] arr) {
		int[] res=new int[arr.length];
		for (int i = 0; i != res.length; i++) {
			res[i]=arr[i];
		}
		return res;
	}
	//用划分值与k相比,依次递归排序
	public static int select(int[] arr,int begin,int end,int i) {
		//begin数组的开始 end数组的结尾  i表示要求的第k个数
		if (begin == end) { 
			return arr[begin];
		}
		//找出划分值(中位数组中的中位数)
		int pivot = medianOfMedians(arr, begin, end);
		int[] pivotRange = partition(arr, begin, end, pivot);
		//小于放左边,=放中间,大于放右边
		if (i >= pivotRange[0] && i <= pivotRange[1]) {
			return arr[i];
		} else if (i < pivotRange[0]) {
			return select(arr, begin, pivotRange[0] - 1, i);
		} else {
			return select(arr, pivotRange[1] + 1, end, i);
		}
	}
	//找出中位数中的中位数
	public static int medianOfMedians(int[] arr, int begin, int end) {
		int num = end - begin + 1;      
		  //分组:每组5个数,不满5个单独占一组
		int offset = num % 5 == 0 ? 0 : 1;     
		//mArr:中位数组成的数组
		int[] mArr = new int[num / 5 + offset];  
		//计算分开后各数组的开始位置beginI 结束位置endI
		for (int i = 0; i < mArr.length; i++) {  
			int beginI = begin + i * 5;
			int endI = beginI + 4;
			//对于最后一组(不满5个数),结束位置要选择end
			mArr[i] = getMedian(arr, beginI, Math.min(end, endI));
		}
		return select(mArr, 0, mArr.length - 1, mArr.length / 2);
	}
	//划分过程,类似快排
	public static int[] partition(int[] arr, int begin, int end, int pivotValue) {
		int small = begin - 1;
		int cur = begin;
		int big = end + 1;
		while (cur != big) {
			if (arr[cur] < pivotValue) {
				swap(arr, ++small, cur++);
			} else if (arr[cur] > pivotValue) {
				swap(arr, cur, --big);
			} else {
				cur++;
			}
		}
		int[] range = new int[2];
		//比划分值小的范围
		range[0] = small + 1;
		 //比划分值大的范围
		range[1] = big - 1; 
		
		return range;
	}
	//计算中位数
			public static int getMedian(int[] arr, int begin, int end) {
				insertionSort(arr, begin, end);//将数组中的5个数排序
				int sum = end + begin;
				int mid = (sum / 2) + (sum % 2);
				return arr[mid];
			}
	//数组中5个数排序(插入排序)
			public static void insertionSort(int[] arr, int begin, int end) {
				for (int i = begin + 1; i != end + 1; i++) {
					for (int j = i; j != begin; j--) {
						if (arr[j - 1] > arr[j]) {
							swap(arr, j - 1, j);
						} else {
							break;
						}
					}
				}
			}
			//交换元素顺序
			public static void swap(int[] arr, int index1, int index2) {
				int tmp = arr[index1];
				arr[index1] = arr[index2];
				arr[index2] = tmp;
			}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] arr = { 6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9 };
		printArray(getMinKNumsByBFPRT(arr, 3));
	}

}

重复问题

针对重复问题一般使用Bit-map(位图法)来解决.

经典例题:已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数.

8位整数可以表示的最大十进制数是99999999,因此用位图法表示则存储8位整数需要99999999bit约等于99Mbit=99/8=12.375MB.

public class 位图法获取不同号码 {

	int ARRNUM=100;
	//8位数的号码最少也是1开头
	int mmin=10000000;
	int mmax=99999999;
	int N=(mmax-mmin+1);
	int BITS_PRE_WORD=32;
	//一个int 有 4byte
	int WORD_OFFSET(int b) {
		return b/BITS_PRE_WORD;
	}
	//获取余下的位(bit),为补零做准备
	int BIT_OFFSET(int b) {
		return b %BITS_PRE_WORD;
		}
	
	void SetBit(int[] words,int n) {
		n-=mmin;
		//找到数对应的int并给这个int设置bit
		words[WORD_OFFSET(n)] |= (1<

排序问题

在海量数据面前,一个整数占用4字节(4byte),如果一个文件有9亿条不重复的9位整数,一次性读取数据需要占用9亿X4字节 约等于 3.6GB内存.

对这个文件数据进行排序.

解法1:数据库排序法.将文本文件导入数据库中,让数据库进行索引排序操作后提取数据到文件中.该方法虽然操作简单但是运算速度慢且对数据库设备要求较高.

解法2:分治法.通过hash法将9亿条数据分为20段,每段大约5000万条,即占用200MB内存,分别对20段数据进行快速排序,再进行19(10+5+2+1+1)次归并排序.该方法虽然缩小了每次使用的内存大小,但是编码复杂,速度也慢.

解法3:位图法.声明一个包含9位整数的bit数组一共需要120MB(10亿bit/8)内存,比如读取到341245909这个数就现在内存中找到341245909这个bit然后置1.

你可能感兴趣的:(算法总结)