算法导论 散列表 11.1答案以及乘法散列法

时间:2021-08-13 16:48:58

今天是开通博客后第一次做算法导论习题呢。。。

首先是算法导论散列表11.1的答案。
11.1-1 假设一动态集合S用一个长度为m的直接寻址表T来表示,请给出一个查找S中最大元素的过程,你所给的过程在最坏情况下的运行时间是多少?

第一题,很简单按照我的理解这题就是没有卫星数据,意味着key值就是数值大小也是下标,那我们就从下往上遍历整个表,直到找到一个非空的slot为止,这很好理解,下标越大值就越大,但是最糟糕情况下就需要O(n)的渐近时间,发生在空表或数据在表头处这种情况。

11.1-2 使用bit vector来做一个没有satellite data的直接寻址表,要求O(1)的字典操作时间。
看到这题想到剑指offer还是编程之美里面的一道题,大意就是需要在一段非常大的不重复整数中寻找一个数有没有,解决方案就是bit vector。

开始分析,首先,先决条件:不重复整数,应该还要正整数,这样的整数在所属集合中独一无二,而每个位在二进制中也是独一无二的,因为位置是不会重合的,所以每一个为对应一个整数,而整个集合使用一个大数组做为存储,假设我们一共拥有1024个整数,并且我们一个字节8各位,那么我们使用char[1024/8] 的数组,然后使用位运算判断每个位的0或1从而确认数据的存在,大致思路就是这样。

可以轻易的得到key/8就是数组的下标,而key%8就是在每个字节中的顺序,由于8为2的幂,我们可以使用位运算key>>3,key&(7)来得到所需要的值
代码如下

#include <iostream>
#include <cmath>
#include <vector>
#include <assert.h>
class BitVector
{
public:
    BitVector(int upperLimit=1024)
        : Bit(8),
        Vector(roundUp(upperLimit)/8),
        offset(3)
    {
        //std::cout<<Vector.capacity()<<std::endl
    }

    bool find(size_t key)
    {
        assert(key < 1024);
        return Vector[key >> offset] & (1 << (key & (Bit - 1)));
    }

    void insert(size_t key)
    {
        assert(key < 1024);
        Vector[key >> offset] |= (1 << (key & (Bit - 1)));
    }

    void remove(size_t key)
    {
        assert(key < 1024);
        Vector[key >> offset] &= ~(1 << (key & (Bit - 1)));
    }

private:

    // 每个字节位的大小
    const size_t Bit;

    // 偏移量,也就是lgBit
    const size_t offset;

    //我们的容器
    std::vector<char> Vector;

    // 作用是将数字num上调为bytes,这里也就是8的倍数,
    // 来源于stl源码解析中对空间适配器的解析一章
    static size_t roundUp(size_t num, size_t bytes= 8)
    {
        return ((num) + bytes - 1)&~(bytes- 1);
    }

};



int main()
{
    BitVector mBitVector;
    for (int i = 1; i <= 30;++i)
    {
        int num = rand() % 1024;
        mBitVector.insert(num);
        std::cout << num << std::endl;
    }

    for (int i = 0; i < 1024;++i)
    {
        if (mBitVector.find(i))
        std::cout << "find " << i << std::endl;
    }

    return 0;
}

在这段代码里我们使用了位向量建立了一个简单的直接寻址表,数据大小为0到1023这1024个数(不是1到1024),

11.1-3如何实现一个字典操作为常量而且拥有重复关键字的直接寻址表。

老实说,这一道题没什么好讲的,摆明了就是让我们预习一下后一节的内容,方法很简单的,就是在每一个寻址表寻的是一个链表而不是一个单纯的数据了,恩,应该还要是双向链表,恩,对,就是11.2的链接法hash,当然如果是11.4的opening address也可以。

11.1-4 星号题,题目不写了,想通了就很简单,既然大数组不能初始化,也就不能直接当寻址表,那么我们就把附加数组当寻址表就好了,题目又告诉你附加数组的大小就是大数组中关键字的多少,那么我们就把真正的地址放在附加数组里,然后大数组中存放附加数组的位置信息。

假设大数组为T,附加数组为S,保存地址信息,并且我们加一个变量为N=关键字数目,现在将S,N初始化为0,一旦我要insert一个数据,比如7好了,那么S[]=sth,然后T[7]=N,然后N++;然后我插入9,S[N]=sth,T[9]=N,N++,一样的流程,然后我们要find,就去查找S[T[key]]是否有效,然后是delete,把S[T[key]]设置为nil就好了,都是常量操作,所以,我们并不是直接使用T作为寻址表,T是一个间接寻址表,T和S,以及数据之间是一个单射关系,如同我们的函数一样,函数名为T,参数为key,操作为S,返回值为data。

———————–以上为11.1章节的答案,接下去是乘法散列法的一些总结

#include<iostream>
#include<string.h>;
#include <assert.h>
#include <time.h>
int a[11];

int hash(int key)
{
    // 乘法hash function对于数据较大的情况有非常好的效果
    // 但是如果数据较小,在100以内,那么冲突就会非常多
    // 甚至出现空槽现象

    float A = 0.6180339887f;
    int m = 1 << 10;
    float temp = static_cast<float>(static_cast<int>(A*key));
    int hashKey = floor((A*key - temp)*m);

    return hashKey;
}

void distKey(int hashkey)
{
    int n = hashkey * 11 / (1024);
    assert(n >= 0 && n <= 10);
    switch (n)
    {
    case 0:a[0]++; break;
    case 1:a[1]++; break;
    case 2:a[2]++; break;
    case 3:a[3]++; break;
    case 4:a[4]++; break;
    case 5:a[5]++; break;
    case 6:a[6]++; break;
    case 7:a[7]++; break;
    case 8:a[8]++; break;
    case 9:a[9]++; break;
    case 10:a[10]++; break;
    default:
        break;
    }
}

void show()
{
    for (int i = 0; i <= 10; ++i)
    {
        printf("hashKey = %d -- %d : %d\n", 1024 * i / 11, 1024 * (i + 1) / 11, a[i]);
    }
}
int main()
{
    srand((unsigned int)time(NULL));
    memset(a, 0, sizeof(a));
    int hashKey;
    int key;
    int mod = 0;

    for (int i = 0; i < 1000000; ++i)
    {

        while (mod == 0) mod = rand() % 100000;//mod may be zero
        key = rand() % 100000000;
        key %= mod;
        hashKey = hash(key);
        distKey(hashKey);
    }

    show();

    return 0;
}

虽说很多地方不如除法散列法,不过还是蛮有优点的,mit公开课上,教授举了一个很好的例子,车轮法,这个乘法散列就好像是一个幸运大转盘一样,你转一次转过了A*360度,比如我的key是500,那么我就把这个轮盘转500次,他会落在哪里?随机性很大,所以这是个不错的hash函数,感觉比除法要好,不过有一个致命的缺点,那就是数据不能太小,而且数据不能太一致(感觉是废话?),数据平均值越小越容易导致每个slot分布不均衡,我的代码在mod小于30,也就是均值低于30的时候基本是每次都有空槽出现,我也不知道怎么回事,应该是和rand函数与时间seed的关系有关,不过均值大于100的话基本是均衡分布,至于A的选择呢,,,虽然不知道为什么老K选他,不过我也觉的黄金比例是个不错的数字。