1. 基本概念
查找结构通常有四种操作:查询某个特定元素是否在表中,检索满足条件的某个特定元素的各种属性,在查找表中插入某一数据元素,从查找表中删除某个元素
- 只涉及前两种操作的称为静态查找,包括顺序查找,二分(折半)查找,散列查找等,涉及到后面两种操作的称为动态查找,包括二叉排序树查找,散列查找等
- 平均查找长度:所有查找过程中关键码比较次数的平均值,衡量查找算法效率的主要指标
2. 折半查找(二分查找)
- 二分查找仅适用于事先已经排序过的线性表顺序存储结构(需要方便定位查找区域,存储结构具有随机存储的特点)
- 二分查找的时间复杂度为O(log2N),查找成功或者查找不成功,最坏情况下需要log2N + 1次检索
3. 键树
键树称为数字查找树,是度大于等于2的树,树的每个结点包含的是组成关键字的符号,如果关键字是数值,则结点包含一个数位,如果关键字是单词,则结点包含一个字母字符,如下图所示。
3.1 双链树(树的孩子-兄弟链来表示键树)
每个Node有三个域:
- symbol域:存储关键字的一个字符
- son域:存储指向第一个子树的根针
- brother域:指向右兄弟指针
查找过程是,从根结点出发,顺着son查找,如果相等,继续下一个son。否则沿着brother查找。直到到了空指针为止。此时若仍未完成key的匹配,查找不成功。
3.2 字典树
字典树,又叫做Trie树,单词查找树,或者前缀树,是一种用于快速检索的多叉树结构,树的每个结点包含d个指针域,d是关键字符的基,比如英文字母的字典树是26叉树,数字字典树是10叉树。
-
Trie树基本性质:
- 根结点不包含字符,除了根结点之外每个结点包含一个字符
- 从根结点到某一个叶子节点,路径上经过的字符连接起来,为一个字符串
- 每个结点所包含的子结点包含的字符串不同
Trie树通过字符串的公共前缀来降低开销,它的优点是最大限度减少无谓的字符串比较,其典型应用是用于统计和排序大量字符串。
Trie的缺点是:如果存在大量字符串,而这些字符串基本没有公共前缀,那么Trie树将非常消耗内存。
Trie树的实现:
#include<iostream>
#include<cstdlib>
using namespace std;
const int branchNum = 26;
struct Trie_node{
bool isStr; // 记录此处是否构成一个串
Trie_node * next[branchNum]; //指向各个子树的指针
// 初始化
Trie_node() :isStr(false){ memset(next, NULL, sizeof(next)); }
};
class Trie{
private:
Trie_node *root;
public:
Trie(){ root = new Trie_node(); }
void insert(const char * str);
bool search(char * str);
void deleteTrie(Trie_node * root);
Trie_node * getTrie(){ return root; }
};
void Trie::insert(const char * str){
Trie_node * location = root;
while (*str){
// 如果不存在则建立结点
if (location->next[*str - 'a'] == NULL){
Trie_node *temp = new Trie_node();
location->next[*str - 'a'] = temp;
}
location = location->next[*str - 'a'];
// 每插入一步,相当于新串路过,指针移动
str++;
}
location->isStr = true;//标记一个串
// Trie *temp = (Trie *) malloc(sizeof(Trie));
// for(int i =0;i<26,i++)
// temp->next[i] = NULL:
}
bool Trie::search(char * str){
Trie_node * location = root;
while (*str && location){ // *str!='\0'
location = location->next[*str - 'a'];
str++;
}
return (location != NULL && location->isStr);
}
void Trie::deleteTrie(Trie_node *root){
for (int i = 0; i < branchNum; i++){
if (root->next[i] != NULL)
deleteTrie(root->next[i]);
}
delete(root);
}
int main(){
char *str = "abcdefg";
Trie trie;
trie.insert(str);
if (trie.search(str)) cout << "true";
system("pause");
return 0;
}
- Trie树的应用:
给定一个单词a,如果通过交换字母的顺序可以得到另外的单词b,那么称a和b是是兄弟单词,现在要求给一个字典,用户输入一个单词,可以根据字典找到该单词的兄弟单词,要求时间和空间效率尽可能高。
答:解法一:hash_map 和链表,定义一个ID,使得兄弟单词有相同的id,不是兄弟单词有不同的id,这个id可以是将单词从小到大排序后作为其ID,也可以是将单词各个字母对应一个质数,将质数相乘当做hash id。创建一个hash_map,它的key为单词的id,value为兄弟单词链表的起始地址。所有的兄弟单词存放在一个链表中。当需要找到该兄弟单词时,只需要计算单词id,然后到map中找到对应的链表即可。
解法二:利用Trie树,单词插入Trie树前,先按照字母排序,将排序后的字母放入Trie树,在树的结点中增加一个vector,用于记录所有的兄弟单词数据文件A:1000万条关键词,数据文件B:关键词与ID的对应表,100万条左右,现在将A中关键词替换为ID,可用内存为1GB,硬盘不限
答:使用文件B生成Trie树,然后用Trie树实现关键词对ID 的快速查找,Trie_node结点中包含ID信息,主要是实际应用中关键词之间可能有很多前缀相同现象,所以空间耗费不会很高。
- 参考:
4.后缀树和后缀数组(suffix tree)
5. 哈希表
- 哈希表的设计目的:空间换取时间,基于快速存取的角度设计,根据关键字直接访问的数据结构,通过某种规则将关键字映射到数组某个位置,这个映射规则称为哈希函数/散列函数。
- 哈希冲突:不同的关键字通过哈希函数计算得到了相同的数组下标,在设计hash函数应该尽量避免这样的冲突,同时还要设计处理好可能产生的冲突。
- 哈希函数:如果两个hash值不同,那么对应的这两个hash值的原始输入是不相同的,但是两个hash值相同,原始输入的两个key值不一定是相同的。
- 常用hash函数:直接定址法,数字分析法,平方取中法,除留余数法,折叠法
- MD4、MD5(更安全)、SHA-1;(用于文件检验,数字签名,鉴权协议)
- 处理冲突的方法:链地址法(同义词存储在同一个线性链表中),开放定址法,再散列法(发生冲突时利用另一个哈希函数重新计算),建立一个公共溢出区(填入溢出表)
6. 一致性哈希
- 集群问题(待续)
7. 海量数据处理
7.1 hash映射-分治处理
对大文件处理时,若文件过大,无法一次性读入内存,将hash映射将文件元素映射到不同小文件中,在依次处理各个小文件,最后合并处理结果。
例子:a、b文件,各存放50亿个url,请找出a、b共同的URL?
答:遍历a,hash(url)%1024,将a分别存放在1024个文件中,对b进行同样操作,处理后,**所有可能相同的url都在对应的小文件中,如a0对应b0,然后分别对小文件进行遍历搜索处理等即可
7.2 Top K问题
- 常见问题:最大的k个数或者最小的k个数
- 如果数据能够一次性读入内存,快排的一次排序,时间复杂度为O(n)
- 如果海量数据,我们通常使用堆,(最大k个数为小根堆,最小K个数使用大根堆)
【编程之美】读书笔记:寻找最大的K个数
《编程之美》——寻找最大的K个数
// 快排 我们基于数组的第K个数字来调整时,最小的k个数
void getleastNumber(int *input, int n, int *output, int k){
if (input == NULL || output == NULL || k>n || n <= 0 || k <= 0)
return;
int start = 0;
int end = n - 1;
int index = Partition(input, start, end);
while (index != k - 1){
if (index > k - 1){
end = index - 1;
// 一趟快排
index = Partition(input, start, end);
}
else{
start = index + 1;
index = Partition(input, start, end);
}
}
for (int i = 0; i < k; ++i)
output[i] = input[i];
}
int Partition(int *a, int low, int high){
int pivot = a[low];
while (low < high){
while (low < high && a[high] >= pivot) --high;
a[low] = a[high];
while (low < high && a[low] <= pivot) ++low;
a[high] = a[low];
}
a[low] = pivot;
return low;
}
// 基于multiset的实现
void getLeastNumbers(const vector<int> &data, multiset<int, int> & leastNumbers, int k){
leastNumbers.clear();
if (k < 1 || data.size() < k)
return;
vector<int>::const iterator = data.begin();
for (; iter != data.end(); ++iter){
if (leastNumbers.size() < k)
leastNumbers.insert(*iter);
else{
mutiset<int, int>::iterator setiter = leastNumbers.begin();
if (*iter < *(leastNumbers.begin())){
leastNumbers.erase(setiter);
leastNumbers.insert(iter);
}
}
}
}
7.3 Bit-map
使用位数组来表示某些元素是否存在,采用bit作为单位存储数据
时间复杂度为O(n),以空间换时间,根据具体情况需要n位的串
例1: 40亿个不重复的unsigned int的值,没排过序,再给一个数,如何判断这个数是否在40个亿数中?
答:unsigned int 最多2^32个数,需要申请512M的内存,一个bit位代表一个unsigned int的值,读入40亿个数,设置对应bit位,读入数,查询相应的位例2:4,7,2,5,3排序
答:申请一个8位byte位,读入第一个值4,则将byte第5位置1,然后依次置位,最后遍历bit区域,将该位是1的编号输出