如何用递归处理一个数组中的数据成为一个树结构_数据结构与算法学习笔记:哈希表(散列表)...

红黑树为代表的各种二叉平衡搜索树可以实现插入和查找的渐进

时间效率,但是要想应用二叉平衡搜索树这一结构进行数据存储,需要要求存储的数据可以
比较大小,但是日常的开发需求中所存储的数据有许多种情况是不可以比较大小的,比如同时存储着性别,姓名和年龄的结构体。此时需要引入哈希表这一数据结构,哈希表不要求所存储的数据可以比较大小,并且可以实现插入,查找,访问等操作具有
的平均时间效率。

哈希表(散列表):

7be9744e431aa84066b229c834e6daa8.png
table那里数据一栏实际上存储的时key-value对,不仅仅只是数据

如上图,哈希表一般由若干个哈希函数和一个内部数组构成。

哈希函数:实现将输入的键值key(一个key对应一个需要存储的value),通过一定的算法转换为数组的索引值。

内部数组(桶数组):也成为buckets或buckets array,数组中的每一个元素称为bucket(桶),数组的大小一般设计为

由图可知哈希表实现平均的

插入,查找,访问时间效率的关键就是在其内部数组,哈希表将每个存入的关键字分配一个对应的数组索引,并将该关键字-value对这一结构存储在数组中。因此访问哈希表就相当于随机访问数组,因此可以实现高效率的插入和查找以及访问操作。但是代价是数组的空间往往不是被完全应用,造成了空间复杂度较高,所以哈希表是一种用
空间换取时间的数据结构。

哈希冲突:

cd1c0c3dfd5830b921be866526adc646.png

2c3c634840305f7fbbc936b261ba174f.png

哈希冲突指的是不同的关键字通过哈希函数得到的索引有可能是一样的,这时就出现了不同关键字,但是索引值相同的情况。此时需要另外操作用来解决哈希冲突,我看的网课(以java实现的)只介绍了用在数据处用单链表或者红黑树(可以通过某些特定操作实现虽然key不能比较大小但是仍可以用红黑树存储)来存储重复的索引值对应的数据。节点较少时使用单链表而不是双向链表的原因主要是每次插入操作发现得到的索引非空时,一般是从该索引值处的链表第一个结点单向遍历进行查找是否已经存在要插入的key,如果已经存在,那么直接覆盖key对应的value,如果不存在,那么插入新的key-value对。

哈希函数:

哈希函数中所要做的主要有以下两步:

1.将传入的key转换为哈希码,哈希码一定是整数。

当传入的key就是整型int,可以直接将key作为哈希码

当传入的key是浮点型float,将浮点型在计算机内存储的二进制码对应的整型为哈希码

当传入的key是long(64位),将long与进行向右32位的无符号右移动的long取异或,最后强制转换为32位int哈希码。(我不清楚C++ 有没有这个限制,我所听的网课用的java限制哈希码只能是32位整型,所以这里直接照搬java处理方法,后边我用C++实现的时候也对应这个思路写一份代码)如下图。

8f19bebd5c78bdc6edbd518e2a3b3e21.png

当传入的key是字符串,则按照下图所述算法进行处理(之后我打算用C++实现一遍,我不清楚C++是否有这个性质,所以我到时在代码中手动实现

156cfbb71fe95c90d7d333dd127110ce.png

当传入的key是自己定义的结构,则需要自己设计一个产生哈希码的算法,设计原则如下

4d24fc309820982e5be2fd36e0a52a44.png
其他情况的哈希码的求法也遵循这两个条件

求得的哈希值尽量不要一样,这一就可以尽量避免哈希冲突,所以哈希表的插入,查找,访问等操作的平均时间效率就会高。两个对象如果求得的哈希码一样,这不代表两个对象就是一样的,两个对象是否一样取决于equal函数的返回结果,也就是取决于设计设个equal函数的人判断两个对象一样的标准。

2.将哈希码与内部数组大小作位运算,得到数组索引值(得到的数组索引值一定是小于等于数组大小-1)。

这一步就只有这一行代码,这一段代码相当于hashcode % (table.size() - 1),但因为是位运算所以效率高很多。

return hashcode & (table.size() - 1);//hashcode为第一步得到的哈希值

对于这段代码的理解:

因为数组的大小被设定为了

,所以对数组大小减去一所得到的整数的二进制形式为

da41d1443ffa24b59e70924dd413b822.png

将其与任意一个数进行与运算,所得到的数都是小于等于

。比如对于
因为相与后所得的结果最大的二进制形式也就是1111。所以将相与后的结果作为索引一定不会造成数组越界(就算是负数,相与后符号位也变成了0,也就是变成正数)。

由以上讨论,我们可以得知我们不用担心获得哈希码时哈希码太大导致int越界的问题,因为我们只是想得到一个整数,而不是要求整数具体要等于多少。

用哈希表实现简易的Map:(部分内存管理细节省略,key类型唯一,value类型唯一,不完整,仅仅体现思路)

公共接口:

//假设K是自定义类型(内置类型我还不知道怎么像java一样直接用成员函数返回哈希值)
//要求K中具有有一个bool成员iscompare,如果为true则可以比较大小,如果为false则不可以比较大小
//如果K中iscompare为true,则必须提供一个成员函数compare来比较两个对象的大小
//k1.compare(k2) 返回1则k1>k2,返回-1则k1<k2,返回0则k1大小等于k2(不一定是同一对象,只是大小相同)
//要求K的实现中具有equal函数来规定什么情况下认为两个K对象可以当作是同一个
template <typename K, typename V>
struct Node//HashMap所要存储的key-value结构,将要存储在红黑树结构中
{          
 
    int hash = 0;//哈希值,默认取0
    bool color = 1;//红黑树新节点默认为Red
    Node<K, V>* lc = nullptr;
    Node<K, V>* rc = nullptr;
    Node<K, V>* parent = nullptr;
    K key;
    V value;
    Node(K key, V value, Node<K, V>* _parent):key(key), value(value)
    {
        hash = key == nullptr? 0:key.hashcode();//如果非空则调用自定义类型的成员函数返回哈希值,如果为空则置0
        parent = _parent;   
    }


    //以下都是红黑树结点的各种操作函数
    bool hasTwoChildren() 
    {
        return this->lc && this->rc
    }
    bool isLeftChildren()
    {
        return this->parent && this == this->parent->lc;
    }
    bool isRightChildren()
    {
        return this->parent && this == this->parent->rc;
    }
    Node<K, V>* sibling()//返回兄弟节点
    {
        if(isLeftChildren())
            return this->parent->rc;
        if(isRightChildren())
            return this->parent->lc;
        return nullptr;
    }
};
class HashMap
{
public:
    HashMap(){};//默认构造函数
    void clean();
    void put(const K& key, const V& value);
    V* get(const K& key);
    bool remove(const K& key);
    bool containValue(const V& value);
    bool resize();//扩容操作
    int index(const K& key)
    {
        return (key.hashcode()) & (table.size() - 1);
    }
private:
    int size = 16;//size一般取2的n次方,size为数组容量长度
    int used_size = 0;//数组包含的Node数量
    vector<Node*> table (size);//数组默认长度取16
    void moveNode(Node<K, V>* p);//移动节点函数,用来辅助扩容操作
    Node<K, V>* search(Node<K, V>* p, const K& key)//辅助查找函数
    {                                               //在p为根节点的红黑树中递归查找是否存在key
        if(!p)
            return nullptr;
        if(key.equal(p->key))
            return p;
        search(p->lc, key)
        search(p->rc, key)
            return nullptr;
    }

    //假设以下包含了红黑树的各种操作函数,比如插入后修复函数,删除后修复函数等等

clean()实现:

void HashMap::clean()
{
    if(!used_size)//数组包含的非空元素数量为0
        return;
    for(int i = 0; i < size; i++)
    {
        table[i] = nullptr;
        {释放i对应数组位置的红黑树}
    }
    used_size = 0;
}

put(K key, V value)实现:

//向HshMap中插入由传入参数指定的key-value对
//红黑树的向左向右由
//如果已经存在相同的key,则直接覆盖value
//否则就是插入新的key-value对
template <typename K, typename V>
bool HashMap::put(const K& key, const V& value)
{
    int index = index(key)
    bool search = false;//遍历搜索标志,如果已经递归遍历过所有红黑树结点则为true
    Node<K, V>* root = table[index];
    if(!root)//如果索引处为nullptr,则直接插入即可
    {
        root = new Node<K, V>(key, value, nullptr);
        table[index] = root;
        used_size++;
        after_insert(root);//调用插入后修复红黑树性质函数
    }

    //root如果不为空,先检查是否已经存在相同的key
    int h1 = key.hashcode();
    int cmp = 0;//红黑树搜索方向标志,cmp = -1向左,cmp = 1向右,cmp = 0说明就是相等,应该覆盖
    Node<K, V>* p = root;//辅助指针,如果不存在重复key那么p指向应该插入的位置的父亲
    while(root)
    {
        p = root;
        int h2 = root->key.hashcode();//获取当前节点key的哈希值
        if(h1 > h2)
            cmp = 1;
        if(h1 < h2)
            cmp = -1;
        //如果发现哈希值相同(哈希值相同还不足以说明就是要找的key),首先尝试能否通过K本身就是可比较的函数继续决定向左向右搜索红黑树
        if(h1 == h2)
        {
            if(key.iscompare && key.compare(root->key != 0)//K是可比较大小的类型
            {
                if(key.compare(root->key) == 1)//key > p->key
                    cmp = 1;
                if(key.compare(root->key == -1))//key < p->key
                    cmp = -1;
            
            }
            else//如果K是不可以比较大小的类型或者大小相同,还得进一步判断是否是同一key
                if(search)//如果已经遍历过了还能来到这里,那么就说明肯定已经不存在了,直接默认向右即可
                    cmp = 1;
                else
                {
                    //因为无法比较大小,此时只能递归遍历去查找是否已经有key存在了
                    Node<K, V>* result = search(root, key);
                    if(result)//如果找到,说明key存在
                    {
                        cmp = 0;
                        root = result;
                    }
                    search = 1;//已经搜索过了,如果能来到这里说明key不存在,交给下一轮循环进行了
        
                }
        }
        if(cmp == 1)
            root = root -> rc;
        if(cmp == -1)
            root = root -> lc;
        if(cmp == 0)//是同一个key,直接覆盖value即可
        {  
            root->value = value;
            return false;//插入失败,直接覆盖了原来key的value
        }

    } 
    //如果能来到这里,说明确实不存在了,可以根据cmp指定的位置插入新的key-value对了
    Node<K, V>* temp = new Node<K, V>(key, value, p);
    if(cmp == 1)
    {
        p->rc = temp;
        after_insert(p->rc)//调用插入后修复红黑树性质函数
    }
    if(cmp == -1)
    {
        p->lc = temp;
        after_insert(p->lc)//调用插入后修复红黑树性质函数
    }
    used_size++;
   return true;//插入成功
    

    
    
}

get(K key)的实现:

//get根据key找到对应的Value并返回指向V指针,如果不存在key则返回空指针
template <typename K, typename V>
V* HashMap::get(const K& key)
{
    int index = index(key);//获得索引
    Node<K, V>*  root = table[index];
    if(!root)//如果root是空
        return nullptr;
    int h1 = key.hashcode();//获取待查找key的哈希值
    while(root)
    {
        int h2 = root->key.hashcode();//获取当前节点key的哈希值
        if(h1 > h2)
            cmp = 1;
        if(h1 < h2)
            cmp = -1;
        //如果发现哈希值相同(哈希值相同还不足以说明就是要找的key),首先尝试能否通过K本身就是可比较的函数继续决定向左向右搜索红黑树
        if(h1 == h2)
        {
            if(key.iscompare && key.compare(root->key != 0)//K是可比较大小的类型
            {
                if(key.compare(root->key) == 1)//key > p->key
                    cmp = 1;
                if(key.compare(root->key == -1))//key < p->key
                    cmp = -1;
            
            }
            else//如果K是不可以比较大小的类型或者大小相同,还得进一步判断是否是同一key
                if(search)//如果已经遍历过了还能来到这里,那么就说明肯定已经不存在了,直接默认向右插入即可
                    cmp = 1;
                else
                {
                    //因为无法比较大小,此时只能递归遍历去查找是否已经有key存在了
                    Node<K, V>* result = search(root, key);
                    if(result)//如果找到,说明key存在
                    {
                        cmp = 0;
                        root = result;
                    }
                    search = 1;//已经搜索过了,如果能来到这里说明key不存在,交给下一轮循环进行了
        
                }
        }
        if(cmp == 1)
            root = root -> rc;
        if(cmp == -1)
            root = root -> lc;
        if(cmp == 0)//是同一个key,直接退出循环
            break;
    }
    
    if(root)
        return &root->value;/退出循环后要么找到了,此时root不是空,要么没找到,此时root是空
    else
        return nullptr;
}

remove(const K& key):的实现

//删除Map中存在的由key指定的key-value对
//删除成功返回true,反之返回false
template <typename K, typename V>
bool remove(const K& key)
{
    int index = index(key);//获得索引
    Node<K, V>*  root = table[index];
    if(!root)//如果root是空,则删除失败
        return false;
    while(root)
    {
        int h2 = root->key.hashcode();//获取当前节点key的哈希值
        if(h1 > h2)
            cmp = 1;
        if(h1 < h2)
            cmp = -1;
        //如果发现哈希值相同(哈希值相同还不足以说明就是要找的key),首先尝试能否通过K本身就是可比较的函数继续决定向左向右搜索红黑树
        if(h1 == h2)
        {
            if(key.iscompare && key.compare(root->key != 0)//K是可比较大小的类型
            {
                if(key.compare(root->key) == 1)//key > p->key
                    cmp = 1;
                if(key.compare(root->key == -1))//key < p->key
                    cmp = -1;
            
            }
            else//如果K是不可以比较大小的类型或者大小相同,还得进一步判断是否是同一key
                if(search)//如果已经遍历过了还能来到这里,那么就说明肯定已经不存在了,直接默认向右插入即可
                    cmp = 1;
                else
                {
                    //因为无法比较大小,此时只能递归遍历去查找是否已经有key存在了
                    Node<K, V>* result = search(root, key);
                    if(result)//如果找到,说明key存在
                    {
                        cmp = 0;
                        root = result;
                    }
                    search = 1;//已经搜索过了,如果能来到这里说明key不存在,交给下一轮循环进行了
        
                }
        }
        if(cmp == 1)
            root = root -> rc;
        if(cmp == -1)
            root = root -> lc;
        if(cmp == 0)//找到key,直接退出循环
            break;
    }
    if(!root)//如果不存在key,则删除失败
        return false;
    else
    {
        {删除root指向的红黑树结点的函数}
        {红黑树的删除后的处理函数}
        return true;
    }
}

containsValue(const V& value)的实现:

//检测哈希表中是否存在value这个值
//对于value哈希表没有太好的解决办法,只能是对table中每个位置的红黑树依次进行层序遍历
//这个操作时间效率最低,为O(n)
template <typename K, typename V>
bool HashMap::containsValue(const V& value)
{
    queue<Node<K, V>*> temp = {};//辅助队列
    for(int i = 0; i < size; i++)
    {
        temp.push(table[i]);
        while(!temp.isempty())
        {
            Node<K, V>* p = temp.front();
            temp.pop();
            if(p)
            {
                if(value == p->value)
                    return true;
                temp.push(p->lc);
                temp.push(p->rc);
            }
        }
    }
    return false;
}

moveNode()以及resize()操作的实现:

//当装填因子: used_size/size 的值大于0.75,就说明此时哈希表中哈希冲突现象比较严重
//此时扩容操作对哈希表的结点进行重新排布,扩容一般是将size扩大为为原来两倍
//由于数组索引index是由 哈希码&(size-1) 获得,所以扩容后的索引要么和原来一样还是index
//要么就扩大为index + 扩容前的size,所以可以缓解哈希冲突
//整体思路是生成一个扩容后的数组,对旧数组进行层序遍历一个个得将结点重新按照哈希表的插入规则插入新的数组中
//虽然扩容以及移动操作需要O(n)时间效率,但是从平均的意义上看,可以有效改善哈希表的时间效率
template <typename K, typename V>
bool HashMap::resize()
{
    if(static_cast<double>(used_size/size) < 0.75)
        return false;//装填因子不够大,不需要扩容
    vector<Node<K, V>*>* old_table = table;
    int old_size = size;
    size = 2*size;
    table = new vector<Node<K, V>*>(size);//扩容后的新数组

    queue<Node<K, V>*> temp = {};//辅助队列
    for(int i = 0; i < old_size; i++)
    {
        temp.push(old[i]);
        while(!temp.isempty())
        {
            Node<K, V>* p = temp.front();
            temp.pop();
            if(p)
            {
                moveNode(p);
                temp.push(p->lc);
                temp.push(p->rc);
            }
        }
    }
    
}

template <typename K, typename V>
void HashMap::moveNode(Node<K, V>* x)
{
    //将x所指的结点视作要插入数组的新节点,所以一切都要重置
    x->lc = nullptr;
    x->rc = nullptr;
    x->parent = nullptr;
    x->color = 1;//红黑树新插入结点默认为红
    int index = index(key)
    Node<K, V>* root = table[index];
    if(!root)//如果索引处为nullptr,则直接插入即可
    {
        root = x;
        table[index] = root;
        used_size++;
        after_insert(root);//调用插入后修复红黑树性质函数
    }

    //root如果不为空,先检查是否已经存在相同的key
    int h1 = key.hashcode();
    int cmp = 0;//红黑树搜索方向标志,cmp = -1向左,cmp = 1向右.
    Node<K, V>* p = root;//辅助指针,如果不存在重复key那么p指向应该插入的位置的父亲
    while(root)
    {
        p = root;
        int h2 = root->key.hashcode();//获取当前节点key的哈希值
        if(h1 > h2)
            cmp = 1;
        if(h1 < h2)
            cmp = -1;
        //如果发现哈希值相同(哈希值相同还不足以说明就是要找的key),首先尝试能否通过K本身就是可比较的函数继续决定向左向右搜索红黑树
        if(h1 == h2)
        {
            if(key.iscompare && key.compare(root->key != 0)//K是可比较大小的类型
            {
                if(key.compare(root->key) == 1)//key > p->key
                    cmp = 1;
                if(key.compare(root->key == -1))//key < p->key
                    cmp = -1;
            
            }
            else//如果K是不可以比较大小的类型或者大小相同,直接默认向右,因为这里是移动操作,不可能有相同的key
                cmp = 1;
        }
        if(cmp == 1)
            root = root -> rc;
        if(cmp == -1)
            root = root -> lc;

    } 
    //到达了可以插入新键值对的地方
    if(cmp == 1)
    {
        p->rc = x;
        after_insert(p->rc)//调用插入后修复红黑树性质函数
    }
    if(cmp == -1)
    {
        p->lc = x;
        after_insert(p->lc)//调用插入后修复红黑树性质函数
    }
    

    
    
    
    
    
 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值