数据结构:蛮有趣的跳表和散列

蛮有趣的跳表和散列

以前没有注意,以为跳表散列是个很简单的玩意,然而…然后恶向胆边生,翻开课本,细细研读,不得不说,这玩意还有点意思。

好吧,现在的我想骂人,骂他个日月无光,天地失色…

脾气发的差不多了,先来写个小开头,锵锵锵锵~~~~~
在我们的查找算法中,n个元素在有序素组上查找所需要的时间是 Θ \Theta (logn),在有序链表上查找所需要的时间是O(n)。对于程序来讲,复杂度不容乐观,所以我们就要引入跳表散列

字典

要说跳表散列,那么字典就是一个不得不说的故事了。
字典 是由一些形如(k,v) 的数对组成的集合,其中k是关键字,v是与关键字对应的值。依照惯例,我们来思考一下字典的结构以及一些操作。

抽象数据类型:Dictionary
{
      实例
            关键字各不相同的一组数据对
      操作
            empty();字典为空时返回true
            size();返回字典数对个数
            find(k);查找关键字为k的数对
            insert(p);插入数对
            earse(k);删除关键字为k的数对
}

这是我们初步构想的数据结构,不妨来实现一下:

template <class K,class E>
class dictionary
{
public:
    virtual ~dictionary(){}
    virtual bool empty() const = 0;//=0说明为纯虚函数(只能被继承,不能实例化) const说明不能修改数据成员
    virtual int size() const = 0;
    virtual pair<const K,E>* find(const K&) const = 0;
    virtual void erase(const K&) = 0;
    virtual void insert(const pair<const K,E>&) =0;
};

字典可以用线性表来进行描述,即为数组描述和链式描述,这里由于比较容易,就按下不表,说不定以后又不会了就来更新了…

跳表

我们为了提高有序链表的查找性能,我们会考虑到时间复杂度为O(n),但是隔壁的有序数组的折半查找却是 Θ \Theta (logn),这就是基本差了半个量级(关于复杂度的渐进记法描述可点击数据结构:渐进记法详解),我们就想到可不可以让链表也可以进行类似于折半查找的方法呢?
在这里插入图片描述
既然要进行折半查找,按照链式结构的特点,无外乎就是加指针,为了达到折半的效果,我们可以在链表最中间的元素加入指针。
在这里插入图片描述
这样我们就可以直接找到值为40的节点。我们进一步折半:
在这里插入图片描述
现在我们可以看到指针共分为三级,我们将其由下到上分别命名为零级链表,一级链表,二级链表,我们查找关键字为30的节点,共需三步。
到这里我们到了看看数据结构长啥样的时候了,先来看看我们定义的跳表节点结构以及节点的构造函数

template <class K,class E>
struct skipNode
{
    typedef pair<const K,E> pairType;

    pairType element;
    skipNode<K,E> **next;//指针数组

    skipNode(const pairType& thePair,int size){
        element = thePair;
        next = new skipNode<K,E>* [size];
    }
};

为了区分级数,我们定义了指针的数组next,用数组的索引数来表示级数。

到这里,故事貌似一直很和谐,但我们仔细看就发现了麻烦。

我们除了需要对链表进行查找操作外,还需要进行插入和删除的操作,但是对于级数已经确定下来的链表来讲,插入和删除无疑会破坏这种平衡。

不着急,我们慢慢看(因为我学的时候也一片茫然…)
从上面的链表可以看出,当有n个数对时,0级链表包含了所有的数对,但是1级链表每两个数对取一个,也就是说1级链表的节点个数n/2,2级链表则是每四个数对取一个,共有n/22 个节点。我们不难推理出i级链表的节点数为n/2i,一个数属于i-1级链表却不属于i级链表,这就涉及到了一个概率的问题。
还是以上边的链表为例,总共分为三级,0级链表节点同时属于1级链表的概率是50%,而1级链表的节点同时属于2级链表的概率也为50%。

说了这么多,我们来看看插入是咋回事。我们插入新的节点后希望保持我们的跳表的结构,那我们就会想到重头再来拍一次,但是这样是不可能的,会大大加大插入算法的复杂度,那么我们拼了老命搞出来的查找 Θ \Theta (logn)就毫无必要的。

那我们如何确定新插入节点的技术呢?首先我们可以确定新插入的节点一定属于0级链表(貌似说了句废话)。我们不求完全保持原来的结构,我们只是要在插入时尽量逼近这种结构,还是以上边的链表为例,我们想插入一个节点为76,容易知道要插入到75和80之间,将零级链表插好后,我们要看一下是否属于1级链表,概率为50%(没错,就是随机的…)

插入说完了,就来看看删除,删除我们是没有办法控制结构,除非重排一次,就单纯的删去这个节点就好了。

事已至此,我么也该来看看这的链表的数据结构了:

template <class K,class E>
class skipList:public dictionary<K,E>
{
public:
    skipList(K largeKey, int maxPairs = 10000, float prob = 0.5);
    ~skipList();
    bool empty() const {return dSize == 0;}
    int size() const {return dSize;}
    pair<const K, E>* find(const K&) const;
    void erase(const K&);
    void insert(const pair<const K, E>&);
    int level() const;//返回一个表示链级数的随机数
    void output(ostream& out) const;
protected:
    float cutOff;
    int levels;
    int dSize;
    int maxLevel;
    K tailKey;//最大关键字
    skipNode<K,E>* headerNode;
    skipNode<K,E>* tailNode;
    skipNode<K,E>** last;//last[i]表示i层的最大节点
};

接下来是构造函数,在构造函数中,我们需要计算最大级数,最大级数与概率有关,为log1-pN - 1。

template <class K,class E>
skipList<K,E>::skipList(K largeKey, int maxPairs, float prob)
{
    cutOff = prob*RAND_MAX;
    maxLevel = (int)ceil(logf((float)maxPairs)/logf(1/prob))-1;
    levels = 0;
    dSize = 0;
    tailKey = largeKey;

    pair<K,E> tailPair;
    tailPair.first = tailKey;
    headerNode = new skipNode<K,E> (tailPair,maxLevel+1);
    tailNode = new skipNode<K,E> (tailPair,0);
    last = new skipNode<K,E> *[maxLevel+1];
    for(int i = 0;i<=maxLevel;i++)
        headerNode->next[i] = tailNode;
}

上边有一个RAND_MAX,它是随机数的最大值,值为32767,prob*RAND_MAX即为层数。

未完待续…

原创文章 20 获赞 144 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_43714332/article/details/102827598
今日推荐