首页 > 其他分享 >【数据结构】3.跳表和散列

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

时间:2023-10-03 13:11:32浏览次数:44  
标签:const int next theKey 跳表 键值 return 数据结构 散列

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.跳表

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

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

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

 注意这里红色和绿色框起来的部分,在计算机中以数组的形式存储,也就是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)

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

由此可以确定搜索方法,首先计算起始搜索桶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 散列的链式实现

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

 

 

#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();
    }
};

 

标签:const,int,next,theKey,跳表,键值,return,数据结构,散列
From: https://www.cnblogs.com/stux/p/17740271.html

相关文章

  • [数据结构和算法] 堆/优先队列的实现
    预备知识:完全二叉树可以用数组表示:从下标0开始存储数据:左子节点=2*父节点+1,右子节点=2*父节点+2;从下标1开始存储数据:左子结点=2*父节点,右子节点=2*父节点+1;堆:大根堆:父节点的值大于等于左右子节点的值;小根堆:父节点的值小于等于左右子节点的值;......
  • 【数据结构】2.栈和队列
    1.栈1.1栈的抽象父类#pragmaoncetemplate<classT>classStack{public://析构函数virtual~Stack(){}//栈是否为空virtualboolempty()const=0;//栈的大小virtualintsize()const=0;//栈顶元素virtualT&top()=0......
  • 基础数据结构:数组实现的单链表(静态链表)、双链表
    1、单链表(静态链表)以AcWing.826为例,题目要求如下:实现一个单链表,链表初始为空,支持三种操作:向链表头插入一个数;删除第k个插入的数后面的数;在第k个插入的数后插入一个数。现在要对该链表进行M次操作,进行完所有操作后,从头到尾输出整个链表。注意:题目中第k个插入的数并不是指当......
  • 【数据结构】串
    串串的顺序实现简单的模式匹配算法KMP算法KMP算法的进一步优化串的顺序实现初始化#defineMaxSize50typedefcharElemType;//顺序存储表示typedefstruct{ElemTypedata[MaxSize];intlength;}SString;/***初始化串*/voidInitString(SString*string)......
  • 【数据结构】线性表
    线性表顺序表链式存储单链表双链表知识目录顺序表概念:用一组地址连续的存储单元依次存储线性表的数据元素,这种存储结构的线性表称为顺序表。特点:逻辑上相邻的数据元素,物理次序也是相邻的。只要确定好了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存......
  • 【数据结构】栈、队列和数组
    栈、队列和数组栈队列数组数组的顺序表示和实现顺序表中查找和修改数组元素矩阵的压缩存储特殊矩阵稀疏矩阵栈初始化#defineMaxSize50//栈中元素的最大个数typedefcharElemType;//数据结构typedefstruct{inttop;//栈顶指针ElemTypedata[MaxSize];//存放栈中......
  • 深入理解算法与数据结构
    ......
  • Redis数据结构
    本文大部分知识整理自网上,在正文结束后都会附上参考地址。如果想要深入或者详细学习可以通过文末链接跳转学习。前言本文主要介绍关于Redis的五种基本数据结构的底层实现原理,然后来分析我们常用的使用场景。先简单回顾一下知识点。Redis是一个开源(BSD许可)的,内存中的数据结......
  • C++常见算法&数据结构模版
    各种常见算法&数据结构模板1.最长不下降子序列(LIS)1.1\(O(n^2)\)做法点击查看代码for(inti=1;i<=n;i++){ cin>>a[i]; dp[i]=1; } for(inti=1;i<=n;i++){ for(intj=1;j<i;j++){ if(a[i]>a[j]){ dp[i]=max(dp[i],dp[j]+......
  • Go每日一库之155:go-spew(输出 Go 数据结构)
    对于应用的调试,我们经常会使用fmt.Println来输出关键变量的数据。或者使用log库,将数据以log的形式输出。对于基础数据类型,上面两种方法都可以比较方便地满足需求。对于一些结构体类型数据,通常我们可以先将其序列化后再输出。如果结构体中包含不可序列化的字段,比如func类型......