查找算法

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!='\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树的应用:
    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的编号输出

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 原文出处:http://www.cnblogs.com/maybe2030/p/4715035.html引文出处:...
    明教de教主阅读 9,117评论 0 7
  • 目录 [1. 顺序查找][2. 二分查找][3. 插值查找][4. 斐波那契查找][5. 树表查找][6. 分块查...
    jiangmo阅读 17,501评论 4 6
  • 本文的整理基于:http://blog.csdn.net/qq_23217629/article/details/...
    阿阿阿阿毛阅读 1,588评论 0 3
  • 教你如何迅速秒杀掉:99%的海量数据处理面试题 本文经过大量细致的优化后,收录于我的新书《编程之法》第六章中,新书...
    Helen_Cat阅读 7,403评论 1 39
  • 不开心 疲惫的时候: 可以应付着所有人, 却唯独骗不了自己的心; 能够承担着很多压力, 却不得不承受着压抑。 有时...
    一个没有归宿感的人阅读 275评论 0 0