文章目录
Hash法
在处理海量数据的过程中,使用hash法一般可以快速存取,统计某些数据,将大量数据进行分类,例如提取某日访问网站次数最多的IP地址等.
常用散列函数的构建方法如下:
- 直接寻址法
- 取关键字或关键字的某个线性函数值为散列地址,即h(key)=key或h(key)=a*key+b.直接寻址法不会产生冲突,时间复杂度为o(1),空间复杂度为o(n),但由于它没有压缩映像,因此,当关键字集合很大时,使用这种hash函数是不可能实现地址编码的散列的(因为key集合必须有穷且不能超出物理存储大小).
- 取模法
- 选择一个合适的正整数p,令h(key)=key mod p.p如果选择的是比较大的素数,则效果比较好,一般选取p为tablesize,即散列表的长度.
- 数学分析法
- 根据关键字的个数n和r进制,通过统计在各个位上数符出现频率找出各个位上出现出现次数最接近n/r的数符作为不变的散列地址位.
- 折叠法
- 将关键字分成位数为t的几个部分(最后一部分可能小于t),然后把各部分按位对齐进行相加,将所得的和舍弃进位,留下t位作为散列地址. 当关键字位数很多,而且关键字中每位上数字分布比较均匀时,采用折叠法比较合适.比如key:5669,(56+69)%100=25.
- 平方取中法
- 将关键字进行平方运算,然后从结果的中间取出若干位(位数于散列地址的位数相同),将其作为散列地址,具体取几位,由散列表的表长决定.
- 除留余数法
- 取关键字除以某个数p( p 小于等于 散列表的长度)的余数作为散列地址.它与取模法的区别在于p不一定等于tablesize(散列表长度),p一般选取质数或大于20的和数.
- 随机数法
- 选择一个随机函数,然后用关键字key的随机函数值作为散列地址,即h(key)=random(key).
在hash表的构建过程中,hash冲突是不可避免地,解决冲突的主要途径是当一个关键字映射到散列表中的某个地址且该地址已有关键字时,为该关键字寻找新的存储地址.
常用于解决地址冲突的方法如下:
- 开放地址法
基本思想是当发生地址冲突时,在散列表中再按照某种方法继续探测其他的存储地址,直到找到空闲的地址为止.
- 链地址法
若散列表空间为[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],采用链地址法后如图:
- 再散列法
当发生冲突时,切换散列函数计算地址,直到计算出一个无冲突的地址. - 建立公共溢出区
假设散列函数的值域为[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法
布隆过滤器常用于判断一个元素是否在集合中或者检查英语单词是否拼写正确.最经典的使用就是垃圾邮件地址匹配.
布隆过滤器以牺牲正确率为前提换取空间效率与时间效率的提高.当它判断某元素不属于这个集合时该元素一定不属于这个集合,当它判断某元素属于这个集合时,该元素不一定属于这个集合.
使用布隆过滤器的难点是如何根据输入元素个数n来确定数组m的大小以及Hash函数.
布隆过滤器不能删除元素.
CBF(Counting Bloom Filter)和SBF(Spectral Bloom Filter)是布隆过滤器的扩展,CBF将位数组中的每一位扩展为一个counter,从而支持元素的删除操作.SBF采用counter中的最小值来近似表示元素的出现频率.
数据库优化法
常见数据库优化方法如下:
- 优秀的数据库管理工具
比如MySQL和Oracle - 数据分区
例如,针对按年份存取的数据,可以按年进行分区,不同的数据库有不同的分区方式,不过处理机制却大体相同,例如sql server的数据库分区将不同的数据存于不同的文件组下,而不同的文件组存于不同的磁盘分区下. - 索引
索引一般可以加速数据的检索速度,加速表与表之间的连接,提高性能. - 缓存机制
缓存大小设置的好差也关系到数据处理的成败. - 加大虚存
当内存不足时,可以增加虚拟内存来解决. - 分批处理
- 使用临时表和中间表
- 优化查询语句
- 使用视图
将数据按一定规则分散到各个基本表中,查询或处理过程可以基于视图进行 - 使用存储过程
- 用排序来取代非顺序存取
- 使用采样数据进行数据挖掘
倒排索引法
倒排索引是目前搜索公司对搜索引擎最常用的存储方式,也是搜索引擎的核心内容.
倒排索引就是按照关键字建立索引.
倒排索引被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射.
有两种倒排索引形式:
- 第一种形式是一条记录的水平反向索引包含每个引用单词的文档的列表.
- 第二种形式是一个单词的水平反向索引又包含每一个单词在一个文档中的位置.
正向索引
正向索引用来存储每个文档的单词的列表.正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文件中的验证这样的查询.
正向索引中文档指向它所包含的那些单词,而反向索引则是单词指向了包含它的文档.
外部排序法
外排序法就是以文件的形式存储待排序对象,排序时再把它们一部分一部分的调入内存进行处理.
一般采用归并排序等方式实现外部排序,主要分成两个步骤:
第一步,生成若干初始归并段,把含有n个记录的文件按内存大小划分为若干长度为L的子文件,然后分别将子文件调入内存,采用有效的内部排序算法排序后返回外存.
第二步,进行多路归并,即对这些初始归并段进行多次归并使得有序的归并段逐渐扩大,最后生成一个有序的文件.
外排的缺陷是消耗大量的IO,效率不会太高.
trie树
字典树是一种用于快速字符串检索的多叉树结构,其原理是利用字符串的公共前缀来减少时空开销,即用空间换时间,从而达到提高程序效率的目的.
字典树常用于统计和排序大量的字符串.
字典树的优点是最大限度地减少无谓地字符串比较,查询效率比散列表高.
字典树的特征:
- 根结点不包含字符,除根结点外每一个结点都只包含一个字符.
- 从根结点到某一个结点,路径上经过的字符连接起来,为该结点对应的字符串.
- 每个结点的所有子结点包含的字符都不相同.
字典树适用于数据量大,重复多,但是数据种类小可以放入内存的情况.
使用例子如下:
兄弟单词
一个单词a,如果通过交换单词中字母顺序可以得到另一个单词b,称b是a的兄弟单词,比如army和mary互为兄弟单词.
已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个字符串是另一个字符串的前缀子串.一般可以采用如下三种方法:
- 迭代法,对于每一个单词,都要去查找其前面的单词是否包含它,看每个字符串是否为字符串集中某个字符串的前缀,由于需要不停的进行迭代比较,因此此时的时间复杂度为o(n²).
- Hash法.使用hash存储所有字符串的所有前缀子串.建立存有子串Hash的时间复杂度为o(n*len),查询复杂度为o(n).
- **字典树(trie树).**假设要查询的单词是abcd,则只需要去查找以a开头的单词是否存在abcd即可,在以a开头的单词中,找到以b作为第二个字母的单词即可,所以建立trie树的总复杂度为o(n*len).实际查询的复杂度为o(len).
求解兄弟单词的经典例子
public class 查找兄弟单词 {
//字典树结点
class TrieNode{
Vector<String> 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<wd.length()) {
//给字母对应的next下标对象初始化
if (next.next[swd[i]-'a'] == null) {
TrieNode nn=new TrieNode();
next.next[swd[i]-'a']=nn;
}
//进入下一层
next=next.next[swd[i]-'a'];
i++;
}
next.bwords.add(wd);
}
//查找该单词的兄弟单词
boolean SearchNode(TrieNode root,String wd) {
char swd[]=wd.toCharArray();
Arrays.sort(swd);
int i=0;
//查看单词的字母对应的next是否都有初始化
while(i<wd.length()) {
if (root.next[swd[i]-'a']!=null) {
root=root.next[swd[i]-'a'];
i++;
}else {
break;
}
}
if (i==wd.length()) {
for(int j=0;j<root.bwords.size();j++) {
System.out.print(root.bwords.get(j)+" ");
}
System.out.println();
return true;
}
return false;
}
public void findBrother() {
TrieNode root=new TrieNode();
InsertNode(root, "hehao");
InsertNode(root, "ehaoh");
InsertNode(root, "haohe");
InsertNode(root, "aoheh");
InsertNode(root, "facri");
InsertNode(root, "et");
SearchNode(root, "oheha");
}
public static void main(String[] args) {
new 查找兄弟单词().findBrother();
}
}
堆
堆是一种树形数据结构.常用于海量数据求前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个最大数的方