O'one数据结构的详细指南

81 阅读6分钟

在这篇文章中,我们将设计一个数据结构,以恒定时间O(1)返回字符串出现的最大和最小次数。这将使用哈希姆和双链表,利用各自的优势来解决这个问题。

hashlink

目录:

  1. 问题陈述
  2. 天真方法(使用二维双链表)
  3. 优化的方法(使用哈希图和双链表)
  4. 时间和空间复杂度分析

要尝试类似的数据结构设计问题,请到这个列表
这与Leetcode问题432相似。所有O'one数据结构。

问题陈述

该问题要求设计一个数据结构来存储字符串和它们的计数。它应该能够在恒定时间内执行以下操作。

递增键数,inc(key)-- 插入一个计数为1的新键或增加一个现有键的计数。
Decrement key count, dec(key)-- 如果键的计数为1,我们就删除它,否则我们就递减它的计数。如果该键不存在,该函数不做任何事情。在此操作之前,保证键存在于数据结构中。
Get max key getMax(key)
-- 返回具有最大计数的键之一,否则如果没有键存在,则返回一个空字符串。
Get min key, getMin(key)
-- 返回具有最小计数的键之一,否则返回一个空字符串,表示没有键存在。

简单的方法(使用二维双链表)

我们的想法是使用一个有序的二维双链表,每一行对应一个计数值,同一行的所有键都有相同的计数值。为了便于理解,我把行和节点互换使用。

假设我们对这个数据结构进行以下操作:

  1. 将 "a "和 "b "增加4倍。
  2. 将 "c "和 "d "增加3倍
  3. 增加 "e "2次
  4. 增加 "f "和 "g "1次

下面是我们在上面的操作之后的额外操作的结果:

allOne.drawio-1-

当一个键被递增时,如果最后一行的计数=current_row.count+1,则将键从当前行移到最后一行,否则在当前行之前插入新行,其计数为current_row.count+1。
当一个键被递减时,如果最后一行的计数=current_row.count-1,则将键从当前行移到最后一行,否则在当前行之后插入行,其计数为current_row_count-1。

通过这种方法,获得最大值和最小值的时间是恒定的O(1),我们只需在恒定的O(1)时间内返回第一行的第一个键为最大计数,最后一行的第一个键为最小计数,只要这些行是按降序排列。

这种方法的问题是,当我们增加键或减少键时,我们必须做更多的工作来维持这个降序,这使得时间复杂度为线性O(n)。

这可以通过采用一种以其高效的访问时间而闻名的数据结构来改善。一个哈希图。你可以在本帖末尾的链接中了解更多关于它是如何做到这一点的。
同时,我们将在下面讨论这种方法。

优化的方法(使用哈希图和双链表)

在这种方法中,我们的想法是使用一个双链表,这将导致O(1)操作的插入和删除,头部将有最大的计数,尾部的计数最少。因此,头部将在恒定的时间内返回最大键,尾部返回最小键。我们使用hashmap来引用列表节点以获得有效的访问时间,这也是我们在之前的天真方法中遇到的问题。

递增

条件1:键存在于列表中

  1. 检查key是否在第一行。
  2. 如果上述条件成立,在当前行之前创建并插入一个新行,其计数值为currentRow.count + 1,其数据为key(string)。
  3. 否则从当前行中删除key(string),并将其放在当前行之前的行中,其计数为currentRow.count+1。

条件2:键不存在于列表中

  1. 检查列表是否为空。
  2. 如果上述条件成立,创建并插入一个新的节点,其计数为1,并将key(string)作为其数据放在列表的最后。
  3. 否则,存在一个节点,所以将key(string)放在列表最后的现有节点中。

递减

条件1:键存在于列表中

  1. 检查带有key的节点的值是否为1,如果是,将其从列表中删除。
  2. 检查key是否在第一个节点上。
  3. 如果上述条件成立,在当前行之后创建并插入一个新行,其计数值为currentRow.count - 1,其值为key(string)。
  4. 否则从当前节点移除key(string)并将其放在列表的最后一行。

条件2:键不存在于列表中
键不存在于列表中,所以不做任何事情:

getMax()

  1. 检查列表是否为空,不返回。
  2. 否则返回第一个节点的第一个键。

getMin()

  1. 检查列表是否为空,并不返回。
  2. 否则返回最后一个节点的第一个键。

代码

class AllOne{
    public:
        //create the structure of a row
        struct row{
            list<string> strings;
            int val;
            row(const string &s, int x) : strings({s}), val(x){}
        };
        
        //initialize hashmap and doubly linked list
        unordered_map<string, pair<list<row>::iterator, list<string>::iterator>> strMap;
        list<row> strList;
        
        //increment procedure
        void inc(string key){
            //the key does not exist in the map
            if(strMap.find(key) == strMap.end()){
                if(strList.empty() || strList.back().val != 1){
                    auto newRow = strList.emplace(strList.end(), key, 1);
                    strMap[key] = make_pair(newRow, newRow->strings.begin());
                }else{
                    auto newRow = --strList.end();
                    newRow->strings.push_front(key);
                    strMap[key] = make_pair(newRow, newRow->strings.begin());
                }
            }else{
                //key exist in the map
                auto row = strMap[key].first;
                auto col = strMap[key].second;
                auto lastRow = row;
                --lastRow;
                if(lastRow == strList.end() || lastRow->val != row->val + 1){
                    auto newRow = strList.emplace(row, key, row->val + 1);
                    strMap[key] = make_pair(newRow, newRow->strings.begin());
                }else{
                    auto newRow = lastRow;
                    newRow->strings.push_front(key);
                    strMap[key] = make_pair(newRow, newRow->strings.begin());
                }
                row->strings.erase(col);
                if(row->strings.empty())
                    strList.erase(row);
            }
        }

        void dec(string key){
            //key does not exist in the map
            if(strMap.find(key) == strMap.end())
                return;
            else{
                //key exists in the map
                auto row = strMap[key].first;
                auto col = strMap[key].second;
                if(row->val == 1){
                    row->strings.erase(col);
                    if(row->strings.empty())
                        strList.erase(row);
                    strMap.erase(key);
                    return;
                }
                auto nextRow = row;
                ++nextRow;
                if(nextRow == strList.end() || nextRow->val != row->val - 1){
                    auto newRow = strList.emplace(nextRow, key, row->val - 1);
                    strMap[key] = make_pair(newRow, newRow->strings.begin());
                }else{
                    auto newRow = nextRow;
                    newRow->strings.push_front(key);
                    strMap[key] = make_pair(newRow, newRow->strings.begin());
                }
                row->strings.erase(col);
                if(row->strings.empty())
                    strList.erase(row);
            }
        }
        //get first key in first row in the map
        string getMax(){
            if(strList.empty())
                return "";
            return strList.front().strings.front();
        }
        
        //get the first key in the last row in the map
        string getMin(){
            if(strList.empty())
                return "";
            return strList.back().strings.front();
        }
};

时间和空间复杂度分析

通过使用哈希姆,我们跟踪一个键的位置,访问时间是O(1),使用一个链接列表来存储节点,增量和减量也是O(1)。总的来说,这些操作的运行时间是恒定的O(1)

另一方面,空间复杂度将是线性的O(n),n是我们需要存储的键的数量。

问题

你能想到这种数据结构在现实世界中的应用吗?

参考文献