查找算法

1. 基本概念

查找结构通常有四种操作:查询某个特定元素是否在表中,检索满足条件的某个特定元素的各种属性,在查找表中插入某一数据元素,从查找表中删除某个元素

  • 只涉及前两种操作的称为静态查找,包括顺序查找,二分(折半)查找,散列查找等,涉及到后面两种操作的称为动态查找,包括二叉排序树查找,散列查找等
  • 平均查找长度:所有查找过程中关键码比较次数的平均值,衡量查找算法效率的主要指标

2. 折半查找(二分查找)

  • 二分查找仅适用于事先已经排序过的线性表顺序存储结构(需要方便定位查找区域,存储结构具有随机存储的特点)
  • 二分查找的时间复杂度为O(log2N),查找成功或者查找不成功,最坏情况下需要log2N + 1次检索

3. 键树

键树称为数字查找树,是度大于等于2的书,书的每个结点包含的是组成关键字的符号,如果关键字是数值,则结点包含一个数位,如果关键字是单词,则结点包含一个字母字符,如下图所示。

键树.png

3.1 双链树(树的孩子-兄弟链来表示键树)

每个Node有三个域:

  • symbol域:存储关键字的一个字符
  • son域:存储指向第一个子树的根针
  • brother域:指向右兄弟指针
    查找过程是,从根结点出发,顺着son查找,如果相等,继续下一个son。否则沿着brother查找。直到到了空指针为止。此时若仍未完成key的匹配,查找不成功。

双链表.png

3.2 字典树

字典树,又叫做Trie树,单词查找树,或者前缀树,是一种用于快速检索的多叉树结构,树的每个结点包含d个指针域,d是关键字符的基,比如英文字母的字典树是26叉树,数字字典树是10叉树

字典树1.png

字典树2.jpg

  • Trie树基本性质:

    1. 根结点不包含字符,除了根结点之外每个结点包含一个字符
    2. 从根结点到某一个叶子节点,路径上经过的字符连接起来,为一个字符串
    3. 每个结点所包含的子结点包含的字符串不同
  • 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!=''
		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树的应用:
    1. 给定一个单词a,如果通过交换字幕的顺序可以得到另外的单词b,那么称a和b是是兄弟单词,现在要求给一个字典,用户输入一个单词,可以根据字典找到该单词的兄弟单词,要求时间和空间效率尽可能高。
      答:解法一:hash_map 和链表,定义一个ID,使得兄弟单词有相同的id,不是兄弟单词有不同的id,这个id可以是将单词从小到大排序后作为其ID,也可以是将单词各个字母对应一个质数,将质数相乘当做hash id。创建一个hash_map,它的key为单词的id,value为兄弟单词链表的起始地址。所有的兄弟单词存放在一个链表中。当需要找到该兄弟单词时,只需要计算单词id,然后到map中找到对应的链表即可。
      解法二:利用Trie树,单词插入Trie树前,先按照字母排序,将排序后的字母放入Trie树,在树的结点中增加一个vector,用于记录所有的兄弟单词

    2. 数据文件A:1000万条关键词,数据文件B:关键词与ID的对应表,100万条左右,现在将A中关键词替换为ID,可用内存为1GB,硬盘不限
      答:使用文件B生成Trie树,然后用Trie树实现关键词对ID 的快速查找,Trie_node结点中包含ID信息,主要是实际应用中关键词之间可能有很多前缀相同现象,所以空间耗费不会很高

  • 参考:
    1. 海量数据处理之Tire树(字典树)
    2. 字典树(Trie树)实现与应用

4.后缀树和后缀数组(suffix tree)

5. 哈希表

  • 哈希表的设计目的:空间换取时间,基于快速存取的角度设计,根据关键字直接访问的数据结构,通过某种规则将关键字映射到数组某个位置,这个映射规则称为哈希函数/散列函数
  • 哈希冲突:不同的关键字通过哈希函数计算得到了相同的数组下标,在设计hash函数应该尽量避免这样的冲突,同时还要设计处理好可能产生的冲突。
  • 哈希函数:如果两个hash值不同,那么对应的这两个hash值的原始输入是不相同的,但是两个hash值相同,原始输入的两个key值不一定是相同的。
    1. 常用hash函数:直接定址法,数字分析法,平方取中法,除留余数法,折叠法
    2. 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的编号输出

原文地址:https://www.cnblogs.com/zoe-mine/p/7593675.html