【数据结构】3.跳表和散列

这篇具有很好参考价值的文章主要介绍了【数据结构】3.跳表和散列。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.顺序链表字典

1.1字典抽象父类

#pragma once
using namespace std;

template<class K, class E>
class dictionary
{
public:
    virtual ~dictionary() {}
    // 返回字典是否为空
    virtual bool empty() const = 0;
    // 返回有多少键值对
    virtual int size() const = 0;
    // 根据K返回E
    virtual pair<const K, E>* find(const K&) const = 0;
    // 根据K删除键值对
    virtual void erase(const K&) = 0;
    // 插入一个键值对
    virtual void insert(const pair<const K, E>&) = 0;
};

1.2节点结构体

#pragma once
using namespace std;
template <class K, class E>
struct pairNode
{
    typedef pair<const K, E> pairType;
    pairType element;
    pairNode<K, E>* next;

    pairNode(const pairType& thePair) :element(thePair){}
    pairNode(const pairType& thePair, pairNode<K, E>* theNext):element(thePair)
    {
        next = theNext;
    }
};

1.3顺序链表字典实现

#pragma once
#include <iostream>
#include "dictionary.h"
#include "pairNode.h"

using namespace std;

template<class K, class E>
class sortedChain : public dictionary<K, E>
{
protected:
    pairNode<K, E>* firstNode; // 第一个结点
    int dSize;                 // 字典的大小

public:
    sortedChain() { firstNode = NULL; dSize = 0; }
    ~sortedChain();

    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>&);
    void output();
};

template<class K, class E>
sortedChain<K, E>::~sortedChain()
{
    while (firstNode != NULL)
    {
        pairNode<K, E>* nextNode = firstNode->next;
        delete firstNode;
        firstNode = nextNode;
    }
}

template<class K, class E>
pair<const K, E>* sortedChain<K, E>::find(const K& theKey) const
{// 返回匹配的指针, 不存在则返回NULL
    pairNode<K, E>* currentNode = firstNode;

    while (currentNode != NULL && currentNode->element.first != theKey)
        currentNode = currentNode->next;

    if (currentNode != NULL && currentNode->element.first == theKey)
        return &currentNode->element;

    return NULL;
}

template<class K, class E>
void sortedChain<K, E>::insert(const pair<const K, E>& thePair)
{// 在字典中插入一个键值对, 并且覆盖已经存在的键值对
    pairNode<K, E>* p = firstNode;
    pairNode<K, E>* tp = NULL;

    while (p != NULL && p->element.first < thePair.first)
    {// 终止后要插入的结点在tp后, p之前
        tp = p;
        p = p->next;
    }

    if (p != NULL && p->element.first == thePair.first)
    {// 如果有匹配的键值对, 替换旧值
        p->element.second = thePair.second;
        return;
    }

    // 如果没有匹配的键值对, 创建一个新的节点, 该节点的下一个结点为p
    pairNode<K, E>* newNode = new pairNode<K, E>(thePair, p);

    if (tp == NULL) firstNode = newNode;
    else tp->next = newNode;

    dSize++;
    return;
}

template<class K, class E>
void sortedChain<K, E>::erase(const K& theKey)
{// 如果存在, 删除关键字为theKey的键值对
    pairNode<K, E>* p = firstNode;
    pairNode<K, E>* tp = NULL;

    // 如果存在, tp为theKey前一个结点, p为theKey结点
    while (p != NULL && p->element.first < theKey)
    {
        tp = p;
        p = p->next;
    }

    if (p != NULL && p->element.first == theKey)
    {
        if (tp == NULL) firstNode = p->next;
        else tp->next = p->next;

        delete p;
        dSize--;
    }
}
template<class K, class E>
void sortedChain<K, E>::output()
{
    pairNode<K, E>* p = firstNode;
    while (p != NULL)
    {
        cout << p->element.first << " " << p->element.second << " ";
        p = p->next;
    }
    cout << endl;
}

2.跳表

跳表可以近似实现二分查找的效果

【数据结构】3.跳表和散列

以下面长度为7的链表举例,该跳表通过3条链表进行存储。假设要查找元素80:

  • 首先在第2级链表查找,因为80大于40,所以在第3个节点右侧查找
  • 然后在第1级链表查找,因为80大于75,所以在第5个节点右侧查找
  • 最后在第0级链表查找,找到元素80

【数据结构】3.跳表和散列

 注意这里红色和绿色框起来的部分,在计算机中以数组的形式存储,也就是headerNode结构体里包含自身元素的值(头节点没有值),以及一个指针数组,数组0号位置存储的是元素20的指针,1号位置存储的是元素24的指针,2号位置存储的是元素40的指针

 每次插入元素的时候,可以通过概率计算它被保存在第几级链表,他保存在第1级链表的概率应该为1/2,第2级链表的概率为1/4,之后为1/8以此类推,假设他保存在第2级,则应该为第0,1,2级的对应位置都插入一个节点,具体代码实现如下

2.1跳表节点结构体

#pragma once

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];
    }
};

2.2跳表实现

#pragma once
#include <iostream>
#include <string>
#include "dictionary.h"
#include "skipNode.h"

using namespace std;

template<class K, class E>
class skipList : public dictionary<K, E>
{
protected:
    float cutOff;                           // 用来确定层数
    int level() const;                      // 用随机数计算插入节点在第几级链表
    int levels;                             // 当前最大的非空链表
    int dSize;                              // 键值对个数
    int maxLevel;                           // 允许的最大链表层数
    K tailKey;                              // 最大关键字
    skipNode<K, E>* search(const K&) const; // 根据Key查找节点
    skipNode<K, E>* headerNode;             // 头节点指针(每一级的头节点)
    skipNode<K, E>* tailNode;               // 尾节点指针(每一级的尾节点)
    skipNode<K, E>** last;                  // last[i] 表示第 i 层的最后一个结点

public:
    skipList(K, 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>&);
    void output() const;

};

template<class K, class E>
skipList<K, E>::skipList(K largeKey, int maxPairs, float prob)
{// 构造函数, 关键字小于largeKey, 且个数最多为maxPairs, 概率因子(0, 1)
    cutOff = prob * RAND_MAX;
    maxLevel = (int)ceil(logf((float)maxPairs) / logf(1 / prob)) - 1;
    levels = 0;  // 初始化层数
    dSize = 0;
    tailKey = largeKey;

    // 创建头结点尾结点和数组last
    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;
}

template<class K, class E>
skipList<K, E>::~skipList()
{// 删除所有节点以及last数组
    skipNode<K, E>* nextNode;

    // 根据第0级删除所有节点
    while (headerNode != tailNode)
    {
        nextNode = headerNode->next[0];
        delete headerNode;
        headerNode = nextNode;
    }
    delete tailNode;

    delete[] last;
}

template<class K, class E>
pair<const K, E>* skipList<K, E>::find(const K& theKey) const
{// 返回匹配的 键值对指针 或 NULL
    if (theKey >= tailKey)
        return NULL;

    // 位置 beforeNode 是关键字为 theKey 的节点之前最右边的位置
    skipNode<K, E>* beforeNode = headerNode;
    for (int i = levels; i >= 0; i--)                       // 自上而下: 从上级链表向下查找
        while (beforeNode->next[i]->element.first < theKey) // 自左而右: 第 i 级的指针链表一直向右移动
            beforeNode = beforeNode->next[i];

    if (beforeNode->next[0]->element.first == theKey)
        return &beforeNode->next[0]->element;

    return NULL;
}

template<class K, class E>
int skipList<K, E>::level() const
{// 假设prob是1/2, 这里就是它是第几级链表的概率, 级越高概率越低, 数据量规模足够大时每层都是下面一层的1/2
    int lev = 0;
    while (rand() <= cutOff)    // 每次小于它的概率都是1/2
        lev++;
    cout << "它位于第" << lev << "级链表" << endl;
    return (lev <= maxLevel) ? lev : maxLevel;
}

template<class K, class E>
skipNode<K, E>* skipList<K, E>::search(const K& theKey) const
{// 搜索关键字 theKey, 把每一级链表中要查看的最后一个节点存储在 last 中
 // 返回 theKey 对应的键值对(如果有的话,在insert中进行了判断)
    skipNode<K, E>* beforeNode = headerNode;
    for (int i = levels; i >= 0; i--)
    {
        while (beforeNode->next[i]->element.first < theKey)
            beforeNode = beforeNode->next[i];
        last[i] = beforeNode;  // 把每一层theKey的前一个结点都存储到last数组里
    }
    return beforeNode->next[0];
}

template<class K, class E>
void skipList<K, E>::insert(const pair<const K, E>& thePair)
{// 把 thePair 插入字典, 或覆盖已有关键字的键值对
    if (thePair.first >= tailKey)
    {
        cout << "关键字过大" << endl;
        return;
    }

    // 查看关键字是否已经存在
    skipNode<K, E>* theNode = search(thePair.first);
    if (theNode->element.first == thePair.first)
    {   // 已经存在则更新键值对的值
        theNode->element.second = thePair.second;
        return;
    }

    // 不存在则插入新的键值对
    int theLevel = level(); // 新节点存在于几级链表
    // 如果这个链表级别太高了, 让他只变为当前链表等级+1
    if (theLevel > levels)
    {
        theLevel = ++levels;
        last[theLevel] = headerNode;
    }

    skipNode<K, E>* newNode = new skipNode<K, E>(thePair, theLevel + 1);
    for (int i = 0; i <= theLevel; i++)
    {// 在每一级插入该节点
        newNode->next[i] = last[i]->next[i];
        last[i]->next[i] = newNode;
    }

    dSize++;
    return;
}

template<class K, class E>
void skipList<K, E>::erase(const K& theKey)
{// 删除关键字为theKey的数对
    if (theKey >= tailKey)
        return;

    // 判断是否有匹配的键值对
    skipNode<K, E>* theNode = search(theKey);
    if (theNode->element.first != theKey)
        return;

    // 从跳表删除键值对
    for (int i = 0; i <= levels && last[i]->next[i] == theNode; i++)
        last[i]->next[i] = theNode->next[i];
 
    // 更新调表级数
    while (levels > 0 && headerNode->next[levels] == tailNode)
        levels--;

    delete theNode;
    dSize--;
}

template<class K, class E>
void skipList<K, E>::output() const 
{
    skipNode<K, E>* p;
    for (int i = levels; i >= 0; i--)
    {
        p = headerNode;
        while (p->next[i]->element.first < tailKey)
        {
            p = p->next[i];
            cout << p->element.second << ", ";
        }
        cout << endl;
    }
}

3.散列

3.1散列表描述

字典的另一种表示方法是散列(hashing),它用一个散列函数(哈希函数)把字典的键值对映射到一个散列表中,键值对为p,关键字为k,则p = f(k)

3.2散列函数和散列表

  1. 桶和起始桶:散列表的每一个位置叫一个(bucket),f(k) 起始桶(home bucket),桶的数量等于散列表的长度。散列函数可以将多个关键字映射到同一个桶,所以桶可以容纳多个键值对。散列函数中最常用的是除法散列函数,其中 k 是关键字,D 是散列表的长度  f(k) = k % D
  2. 冲突和溢出:假设两个关键字对应的起始桶相同,就是发生了冲突(collision),但一个桶可以存放多个键值对,所以只要桶足够大,就没什么问题。但如果没有空间存储一个新键值对时,就发生了溢出(overflow)
  3. 找一个好的散列函数:如果映射到散列表中任何一个桶的关键字数量大致相等,发生冲突和溢出的平均数就小,均匀散列函数(uniform hash function)就是这样的函数
size_t operator()(const string theKey)
{
    unsigned long hashValue = 0;
    int length = (int) theKey.length();
    for(int i=0; i<length; i++)
        hashValue = 5 * hashValue + theKey.at(i);
    return size_t(hashValue);
}

3.3 线性探查

我们有一个函数 p=f(k)p = k % 11 ,首先插入 80 % 11 = 3,然后再想插入 58 % 11 = 3 时发现已经满了,最简单的办法是找到下一个可以存放该数据的桶,这种方法叫线性探查(Linear Probing)

【数据结构】3.跳表和散列

我们把58存储在4号桶;然后插入24,2号桶为空所以插入2号桶;接下来插入35,用线性探查插入到5号桶;最后插入98时由于10号桶已有数据,所以插入到0号桶。这种情况下散列可以看成是循环链表

【数据结构】3.跳表和散列

由此可以确定搜索方法,首先计算起始搜索桶f(k)

  1. 如果关键字k的桶已找到, 则找到了键值对
  2. 到达一个空桶, 说明未找到
  3. 回到起始桶f(k), 说明未找到

3.4 散列的数组实现

实现一个hash类,将key转换成非负的整数

#pragma once
#include <iostream>
#include <string>

using namespace std;

// 把 Key 转换成非负整数
template<class K> class myhash;
template<>
class myhash<string>
{
public:
    size_t operator()(const string theKey) const
    {
        unsigned long hashValue = 0;
        int length = (int)theKey.length();
        for (int i = 0; i < length; i++)
            hashValue = 5 * hashValue + theKey.at(i);

        return size_t(hashValue);
    }
};

template<>
class myhash<int>
{
public:
    size_t operator()(const int theKey) const
    {
        return size_t(theKey);
    }
};

template<>
class myhash<char>
{
public:
    size_t operator()(const char theKey) const
    {
        int ret = (int)theKey;
        return size_t(ret);
    }
};

数组散列的实现

#pragma once
#include <iostream>
#include "hash.h"

using namespace std;

template<class K, class E>
class hashTable
{
protected:
    int search(const K&) const;
    pair<const K, E>** table;  // 哈希表
    myhash<K> hash;            // 将K转换为非负整数
    int dSize;                 // 字典的大小
    int divisor;               // 节点个数

public:
    hashTable(int theDivisor = 11);
    ~hashTable() { delete[] table; }

    bool empty() const { return dSize == 0; }
    int size() const { return dSize; }
    pair<const K, E>* find(const K&) const;
    void insert(const pair<const K, E>&);
    void output() const;


};

template<class K, class E>
hashTable<K, E>::hashTable(int theDivisor)
{
    divisor = theDivisor;
    dSize = 0;

    // 分配数组的空间
    table = new pair<const K, E>*[divisor];
    for (int i = 0; i < divisor; i++)
        table[i] = NULL;
}

template<class K, class E>
int hashTable<K, E>::search(const K& theKey) const
{// 查找关键字为 theKey 的键值对, 如果存在匹配的键值对则返回他的位置, 如果不存在则返回可以插入的位置

    int i = (int)hash(theKey) % divisor;  // 起始桶
    int j = i;    // j 从起始桶开始搜索
    do
    {
        if (table[j] == NULL || table[j]->first == theKey)
            return j;
        j = (j + 1) % divisor;  // j 移动到下一个桶
    } while (j != i);           // 回到起始桶位置

    return j;  // 表满
}

template<class K, class E>
pair<const K, E>* hashTable<K, E>::find(const K& theKey) const
{// 返回匹配的指针或NULL

    int b = search(theKey);

    // 空或者和Key不相等都说明没有匹配的键值对
    if (table[b] == NULL || table[b]->first != theKey)
        return NULL;

    return table[b];  // 匹配到键值对
}

template<class K, class E>
void hashTable<K, E>::insert(const pair<const K, E>& thePair)
{// 插入thePair, 若存在相同的则更新值, 表满打印提示

    int b = search(thePair.first);

    if (table[b] == NULL)
    {
        // 没有匹配的键值对则插入
        table[b] = new pair<const K, E>(thePair);
        dSize++;
    }
    else
    {
        if (table[b]->first == thePair.first)
        {// 匹配的键值对相等则更新值
            table[b]->second = thePair.second;
        }
        else // 不相等说明表已经满
            cout << "散列表已满" << endl;
    }
}

template<class K, class E>
void hashTable<K, E>::output() const
{
    for (int i = 0; i < divisor; i++)
    {
        if (table[i] == NULL)
            cout << "NULL" << endl;
        else
            cout << table[i]->first << " "
            << table[i]->second << endl;
    }
}

3.5 散列的链式实现

每个桶可以无限增加记录,所以链表实现解决了溢出问题

【数据结构】3.跳表和散列

 

 

#pragma once
#include <iostream>
#include "hash.h"           // 将K转换为非负整数
#include "dictionary.h"     // 字典
#include "sortedChain.hpp"  // 顺序链表字典

using namespace std;

template<class K, class E>
class hashChains : public dictionary<K, E>
{
protected:
    sortedChain<K, E>* table;  // 散列表
    myhash<K> hash;            // 将K转换为非负整数
    int dSize;                 // 元素个数
    int divisor;               // 节点个数

public:
    hashChains(int theDivisor = 11)
    {
        divisor = theDivisor;
        dSize = 0;

        // 初始化一个顺序链表字典的数组作为散列表
        table = new sortedChain<K, E>[divisor];
    }

    ~hashChains() { delete[] table; }

    bool empty() const { return dSize == 0; }
    int size() const { return dSize; }

    pair<const K, E>* find(const K& theKey) const
    {// 只需要确定初始桶的位置然后调用顺序链表字典的查找即可
        return table[hash(theKey) % divisor].find(theKey);
    }

    void insert(const pair<const K, E>& thePair)
    {
        int homeBucket = (int)hash(thePair.first) % divisor;    // 初始桶位置
        int homeSize = table[homeBucket].size();                // 当前该桶大小
        table[homeBucket].insert(thePair);                      // 插入thePair
        if (table[homeBucket].size() > homeSize)                // 如果桶大小发生改变
            dSize++;                                            // 字典元素个数++
    }

    void erase(const K& theKey)
    {
        table[hash(theKey) % divisor].erase(theKey);
    }

    void output() const
    {
        for (int i = 0; i < divisor; i++)
            if (table[i].size() == 0)
                cout << "NULL" << endl;
            else
                table[i].output();
    }
};

 文章来源地址https://www.toymoban.com/news/detail-711722.html

到了这里,关于【数据结构】3.跳表和散列的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构2:顺序表和链表

    目录 1.线性表 2.顺序表 2.1概念及结构 2.2接口实现 2.3数据相关面试题 2.4顺序表的问题及思考 3.链表 3.1链表的概念及结构 3.2链表的分类 3.3链表的实现 3.4链表面试题 3.5双向链表的实现 4.顺序表和链表的区别 线性表(linear list)是具有相同特征的数据元素的有限序列。线性表是

    2023年04月17日
    浏览(50)
  • 数据结构顺序表和链表(超详细)

    线性表 ( linear list ) 是 n 个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构, 常见的线性表:顺序表、链表、栈、队列、字符串 ... 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在

    2024年02月13日
    浏览(55)
  • 数据结构:2_顺序表和链表

    线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构, 常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的 ,线性表在物理上

    2024年01月18日
    浏览(55)
  • 【手撕数据结构】(三)顺序表和链表

    🎗️线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串… 🎗️线性表在逻辑上是线性结构,也就说是一条连续的直线。但是在物理结构上并不一定是连续的,线性表在物理上存储

    2024年02月05日
    浏览(54)
  • 【数据结构初阶】顺序表和链表(1)

    线性表(linear list) 是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上

    2024年02月08日
    浏览(242)
  • 数据结构奇妙旅程之顺序表和链表

    ꒰˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如需转载还请通知˶⍤⃝˶ 个人主页:xiaoxieʕ̯

    2024年02月05日
    浏览(66)
  • 数据结构修炼第二篇:顺序表和链表

    第一章 时间复杂度和空间复杂度 第二章 顺序表,列表 第三章 栈和队列 第四章 二叉树 第五章 排序 作者:🎈乐言🎈 简介:🎈大一学生,目前在致力于c/c++/python,高数的学习,有问题尽管问我,关注后私聊! 持续更新专栏:《c进阶》,《数据结构修炼》 🚀 (优质好文持

    2024年02月02日
    浏览(118)
  • 【数据结构篇C++实现】- 线性表 - 顺序表和链表

    友情链接:C/C++系列系统学习目录 故事导入: 幼儿园放学时,一个班级的小朋友,一个跟着一个排队出校,有一个打头,有一个收尾,当中的小朋友每一个都知道他前面和后面的一个是谁,就如同一根线把他们串联了起来。如此具有像线一样的性质的表就叫线性表 线性表(

    2024年02月07日
    浏览(57)
  • 初阶数据结构之---顺序表和链表(C语言)

    线性表: 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构。线性表在逻辑上是线性结构,也就是说是连续的一条直线。但在物理上并不一定是连续的。线性表在物理上存储时,通常以 数组 和 链式结构 的形式存储。

    2024年02月22日
    浏览(67)
  • <数据结构>顺序表和链表的比较|缓存命中率

    💭前言:通过之前对顺序表和链表的实现,我们可以发现在增删查改某些操作上两者的效率前言有所差异,本篇文章将总结二者的异同。 顺序表的实现 http://t.csdn.cn/Lxyg2 单链表的实现 http://t.csdn.cn/rHgjG 双链表的实现 http://t.csdn.cn/j3amO 📚顺序表通过数组来实现的,所以在物理

    2024年02月05日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包