HashTable、函数对象 学习笔记

1 目标

结合一道简单的题目Leetcode-两数之和,学习HashTable、和函数对象

2 题意

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

示例:

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

3 思路

author's blog == http://www.cnblogs.com/toulanboy/

3.1 思路出发点

这是昨天的打卡题(2020年10月3日),虽然之前做过,但是知道有更好解法,故昨晚学习了一下。

谈下暴力法:只需双重循环,两两尝试匹配。复杂度是0(n^2)

但,如果能拥有常数级别时间复杂度的查找find和插入insert的数据结构,那么结合该数据结构,我们可以使用以下逻辑来实现O(n)的解题。

具体逻辑:从前往后遍历nums数组。对于nums[i],用O(1)查找该数据结构,查看之前是否出现过他的匹配数字。

  • 若有,找到答案,退出。
  • 若没有,则把当前数字用O(1)放入到数据结构,然后继续nums[i+1]。

总体复杂度:O(n)。

而hashtable就是能满足我们需求的数据机构!

3.2 HashTable

概述:通过数组+链表的形式,结合hash算法,查找和插入的时间复杂度为常数级。

3.2.1 HashTable 结构

(1)表面认识

注释的内容会在后面解析,刚开始学习,我们先看大体,再看细节。

组成架构:该数据结构包含多个桶bucket[1],然后每个桶里面可以放很多数值[2]。

插入逻辑:对于一个新来的数值key[3],通过一个简单的运算[4],确定该数值key应该放那个桶,然后把它丢进去[5]即可。

查找逻辑:对于需要被查找的数值key,参考插入逻辑(先通过一个运算确定它在哪个桶),再去这个桶里面逐一遍历出来。

(2)稍微深入的学习

[1] 多个桶bucket:这是通过顺序数组来实现的。

[2] 每个桶里面可以放很多数值:实际上,每个桶存储的都是一个指针,该指针指向一条链表。

[3] key:放入的数值,不限定类型。在C++层面,如果是单一类型,那么可以对应标准库的unodered_set。如果是键值对(结构体),那么可以对应标准库的unodered_map。

[4] 简单的运算:这个是hash运算。给定指定数据,hash运算会将其转换为一串数字

[5] 把它丢进去:这个是hash冲突的处理方法,如果多个数据都hash到同一个桶,那么我们将这个视为hash冲突。而这里处理冲突的方法,就是使用一条链表,将所有hash到这个桶的数据都串起来,然后只需把链表头指针放到桶里面就行,这个处理方案的方法被称为链地址法

(3)结构总结

hashtable的结构利用hash运算,将数值映射到某个桶。如果出现冲突,那么就使用链表处理冲突。由于hash运算不需要复杂的运算,所以使得他的查找效率和插入效率非常高。

(4)其他

Q:后期数据太多,链表太长影响效率?

A:可以设定阈值,当达到阈值时,则进行重哈希rehash(),将当前数组数据迁移到更大的数组。

(5)上面内容主要从以下博文学习得到,建议感兴趣的同学可以细看下面的文章。

3.2.2 HashTable 对应的标准库

C++新标准中有2个STL容器是用hashtable作为底层实现的:

(1)unodered_set,能够存储单类型的容器。例如建立一个字符串类型的hashtable。

(2)unodered_map,能够存储键值对的容器。例如建立一个 <姓名,年龄>的hashtable。

3.2.3 unodered_set使用示例

关于标准库的使用,如果是int,string,float这些基本类型,那么STL自带的hash函数能够处理,那么建立时只需传递数据类型。如:

unodered_set<int> age_set;//建立1个int类型的hashtable
unodered_map<string, age> person_map;//建立1个<string, age>类型的hashtable

若是其他类型,则还需要传递hash函数以及比较函数。而这2个函数一般通过函数对象的形式的传递。

下面代码使用了函数对象。若暂时不知道的,可以先看下一小节。

/*
unordered_set的样例代码。
*/
# include<iostream>
# include<unordered_set>
using namespace std;

//定义1个类
class Point{
public:
    int x;
    int y;
    Point(int x, int y){
        this->x = x;
        this->y = y;
    }
};
//定义Point的hash类
//由于其重载了(),故其实例化后的对象,类似于函数指针。
class PointHash{
    public:
    size_t operator()(const Point& p)const{
        //这里调用STL的hash为我们计算中间值
        return hash<int>()(p.x) + hash<int>()(p.y);
    }
};
//定义Point的equal类
//由于其重载了(),故其实例化后的对象,类似于函数指针。
class PointEqual{
    public:
    bool operator()(const Point& a, const Point& b)const{
        return a.x == b.x;
    }
};

int main(){

    unordered_set<Point, PointHash, PointEqual> my_set;
    my_set.insert(Point(11, 22));

    for(auto it = my_set.begin(); it != my_set.end(); ++it){
        cout << it->x << ","<< it->y << endl;
    }
    /*
    输出:11, 22
    */

    auto result = my_set.find(Point(11, 22));
    if(result != my_set.end()){
        cout << result->x << ","<< result->y << endl;
    }
    /*
    输出:11, 22
    author's blog == http://www.cnblogs.com/toulanboy/
    */
    return 0;
}

3.3.4 参考文章

该部分参考文章如下,作者写得太好了,感谢。

3.3 函数对象

3.3.1 基本概念

函数对象,也被称为伪函数,在STL容器中经常被使用。

本质是一个类,该类重载了(),其实例化的对象可以实现函数调用的效果。

举个例子:

class My_Lovely_Add{
    public:
    //重载()
    int operator()(int a, int b){
        return a+b;
    } 
};

int main(){

    My_Lovely_Add f;
    cout << f(11, 22) << endl;
    //输出:33
    return 0;
}

上述My_Lovely_Add类由于重载了(),故其实例化的对象可以实现函数调用的效果。

3.3.2 与函数指针的异同

他们两者都能实现具体函数的传递,从目前的学习来看,函数对象具备以下优点:

  • 可以使用inline。
  • 可以通过类成员记录调用情况。

3.3.3 参考文章

4 代码

然后,就可以使用hashtable的STL之一 unodered_map来解题了!

class Solution {
public:
    //学习了官方题解:https://leetcode-cn.com/problems/two-sum/solution/liang-shu-zhi-he-by-leetcode-solution/
    vector<int> twoSum(vector<int>& nums, int target) {
        //创建unordered_map,利用其O(1)的插入和查找进行快匹配
        unordered_map<int, int> u_map;
        //创建unordered_map的迭代器
        unordered_map<int, int>::iterator it;

        for(int i=0; i<nums.size(); ++i){
            //看看前面是否出现有匹配的数字
            it = u_map.find(target-nums[i]);
            //有则输出
            if(it != u_map.end())
                return {it->second, i};
            //否则,把当前数字放进Map,继续往下
            u_map.insert(pair(nums[i], i));
        }
        return {};
    }
};

写到最后:

(1)整理这个简短的内容,不知不觉已经过去2小时,午饭时间都过了。。can~

(2)这个只是简单的概述,没有特别具体深入,但希望对你有帮助~

posted @ 2020-10-04 13:11  偷懒人  阅读(869)  评论(0编辑  收藏  举报