在处理海量数据的过程中,使用hash法一般可以快速存取,统计某些数据,将大量数据进行分类,例如提取某日访问网站次数最多的IP地址等.
常用散列函数的构建方法如下:
在hash表的构建过程中,hash冲突是不可避免地,解决冲突的主要途径是当一个关键字映射到散列表中的某个地址且该地址已有关键字时,为该关键字寻找新的存储地址.
常用于解决地址冲突的方法如下:
位图法的基本原理是使用位数组来表示某些元素是否存在.例如从8位电话号码中查找重复号码.本法适用于海量数据的快速查找,判重,删除等.
比如集合为**{2,7,9,4,1,10},则生成一个10位的串(因为最大值为10),将集合中对应的位 置1,有1101001011**.排序自动完成(字符串下标有序)
位图法(Bit-map)排序的时间复杂度为o(n),但是它是以空间换时间,且排序前集合大小最好已知.
布隆过滤器常用于判断一个元素是否在集合中或者检查英语单词是否拼写正确.最经典的使用就是垃圾邮件地址匹配.
布隆过滤器以牺牲正确率为前提换取空间效率与时间效率的提高.当它判断某元素不属于这个集合时该元素一定不属于这个集合,当它判断某元素属于这个集合时,该元素不一定属于这个集合.
使用布隆过滤器的难点是如何根据输入元素个数n来确定数组m的大小以及Hash函数.
布隆过滤器不能删除元素.
CBF(Counting Bloom Filter)和SBF(Spectral Bloom Filter)是布隆过滤器的扩展,CBF将位数组中的每一位扩展为一个counter,从而支持元素的删除操作.SBF采用counter中的最小值来近似表示元素的出现频率.
常见数据库优化方法如下:
倒排索引是目前搜索公司对搜索引擎最常用的存储方式,也是搜索引擎的核心内容.
倒排索引就是按照关键字建立索引.
倒排索引被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射.
有两种倒排索引形式:
正向索引用来存储每个文档的单词的列表.正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文件中的验证这样的查询.
正向索引中文档指向它所包含的那些单词,而反向索引则是单词指向了包含它的文档.
外排序法就是以文件的形式存储待排序对象,排序时再把它们一部分一部分的调入内存进行处理.
一般采用归并排序等方式实现外部排序,主要分成两个步骤:
第一步,生成若干初始归并段,把含有n个记录的文件按内存大小划分为若干长度为L的子文件,然后分别将子文件调入内存,采用有效的内部排序算法排序后返回外存.
第二步,进行多路归并,即对这些初始归并段进行多次归并使得有序的归并段逐渐扩大,最后生成一个有序的文件.
外排的缺陷是消耗大量的IO,效率不会太高.
字典树是一种用于快速字符串检索的多叉树结构,其原理是利用字符串的公共前缀来减少时空开销,即用空间换时间,从而达到提高程序效率的目的.
字典树常用于统计和排序大量的字符串.
字典树的优点是最大限度地减少无谓地字符串比较,查询效率比散列表高.
字典树的特征:
一个单词a,如果通过交换单词中字母顺序可以得到另一个单词b,称b是a的兄弟单词,比如army和mary互为兄弟单词.
已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个字符串是另一个字符串的前缀子串.一般可以采用如下三种方法:
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);
}
}
基于Hadoop可以非常轻松和方便完成处理海量数据的分布式并行程序.
在大规模数据处理中,经常会遇到在海量数据中找出出现频率最高的前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问题,你能说出这个算法估计会很加分。
该算法的最坏时间复杂度为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)个数比划分数小-如图红色区域所示:
我们一次就可以至少刷掉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.