顺序映射/有序映射sorted map和跳表, since 2022-05-12

(2022.05.12 Thur)
前面介绍的映射允许用户在查询过程中通过精确检索找到key k的关联值。本节我们考虑另一种情况,即映射表中对应的key都是按顺序排列的。比如金融交易系统中的数据按timestamp排序,其中的timestamp作为映射的key。

有序映射sorted map中的key按升/降序排列,其属性和内置方法实现的功能与一般映射相同。

本文介绍有序映射的两种实现方式,分别基于数组和基于链表。

基于数组的有序映射

顾名思义,基于数组的有序映射其实现方式为数组array。这种方式实现的有序映射称为sorted search table。如下面的一个数组所示,为方便演示,数组中的元素只用元素的key k,代替该元素的(k, v) pair。该数组的index范围[0, 5],对应的元素的key为从小到大排序。

0 1 2 3 4 5
2 4 5 8 13 21

Python实现

该实现代码中,查找某key是否在映射中如果不存在则返回比该key大的最小元素。

class SortedSearchMap(MapBase):
    def _find_index(self, k, low, high):
        if high < low:
            return high + 1
        else:
            mid = (low + high) // 2
            if k == self._table[mid]._key:
                return mid
            elif k < self._table[mid]._key:
                return self._find_index(k, low, mid-1)
            else:
                return self._find_index(k, mid+1, high)
    def __init__(self):
        self._table = []
    def __len__(self):
        return len(self._table)
    def __getitem__(self, k):
        j = self._find_index(k, 0, len(self._table)- 1)
        if j == len(self._table) or self._table[j]._key != k:
            raise KeyError('Key Error: '+ repr(k))
        return self._table[j]._value
    def __setitem__(self, k, v):
        j = self._find_index(k, 0, len(self._table) − 1)
        if j < len(self. table) and self. table[j]._key == k:
            self._table[j]._value = v # reassign value
        else:
            self._table.insert(j, self._Item(k,v))
    def __delitem__(self, k):
        """Remove item associated with key k (raise KeyError if not found)."""
        j = self._find_index(k, 0, len(self. table) − 1)
        if j == len(self._table) or self._table[j]._key != k:
            raise KeyError('Key Error: '+ repr(k))
        self._table.pop(j) # delete item
    def __iter__(self):
        """Generate keys of the map ordered from minimum to maximum."""
        for item in self._table:
            yield item._key
    def __reversed__(self):
        """Generate keys of the map ordered from maximum to minimum."""
        for item in reversed(self._table):
            yield item._key
    def find_min(self):
        """Return (key,value) pair with minimum key (or None if empty)."""
        if len(self._table) > 0:
            return (self._table[0]._key, self._table[0]._value)
        else:
            return None
    def find_max(self):
        """Return (key,value) pair with maximum key (or None if empty)."""
        if len(self._table) > 0:
            return (self._table[−1]._key, self._table[−1]._value)
        else:
            return None

    def find_ge(self, k):
        '''Return (key,value) pair with least key greater than or equal to k.'''
        j = self._find_index(k, 0, len(self._table) − 1) # j s key >= k
        if j < len(self._table):
            return (self._table[j]._key, self._table[j]._value)
        else:
            return None
    def find_lt(self, k):
        '''Return (key,value) pair with greatest key strictly less than k.'''
        j = self._find_index(k, 0, len(self._table) − 1) # j s key >= k
        if j > 0:
            return (self._table[j−1]._key, self._table[j−1]._value) # Note use of j-1
        else:
            return None
    def find_gt(self, k):
        '''Return (key,value) pair with least key strictly greater than k.'''
        j = self._find_index(k, 0, len(self._table) − 1) # j s key >= k
        if j < len(self._table) and self._table[j]._key == k:
            j += 1 # advanced past match
        if j < len(self._table):
            return (self._table[j]._key, self._table[j]._value)
        else:
            return None
    def find_range(self, start, stop):
        '''Iterate all (key,value) pairs such that start <= key < stop.
            If start is None, iteration begins with minimum key of map.
            If stop is None, iteration continues through the maximum key of map.
        '''
        if start is None:
            j = 0
        else:
            j = self._find_index(start, 0, len(self._table)−1) # find first result
        while j < len(self._table) and (stop is None or self._table[j]._key < stop):
            yield (self._table[j]._key, self._table[j]._value)
            j += 1

复杂度分析

查找某key是否存在于这个映射中,可用二元搜索binary search。此时查询是否存在的复杂度为O(logn),这一点优于采用数组实现的无序映射的查询复杂度O(n)。对映射中的key赋值,如果是新加值,最坏情况是在index=0复杂度为O(n),而修改值则复杂度为O(logn)。删除key时,最坏情况是n-1个元素整体向前平移,则复杂度为O(n)

基于链表的有序映射

对链表做查询的复杂度为O(n),而修改的复杂度为O(1)。基于链表的有序映射,其实现方式被称为跳表skip list。跳表为查询和更新提供了一种折中方案,代价是更多的存储空间。

映射M的跳表包含了一些列list \{S_0, S_1, ..., S_h\}。其中的每个S_i保存了映射M的元素子集,且其中的key保持升序,并在头尾加入两个哨兵键sentinel keys,定义为- \infty+\infty是小于和大于所有可能键的值且可以插入到映射M中。此外list满足下面条件:

  • S_0包含映射M中的所有元素,包括哨兵key
  • 对于i = 1,..., h-1,list S_i包括了从S_{i-1}中随机生成的子集
  • S_h仅包含哨兵key- \infty+\infty

这里的h定义为跳表的高度

下面介绍如何在跳表中查询特定key。一个sorted map S_0,其中元素为10个,先需要找到key为55的元素关联的数值。首先从最高的一层S_5开始从-\infty找到该元素下一层S_4中的对应元素。向右寻找遇到元素17,17小于50所以接着找下一层S_3中的17右边的下一个元素,即25。25小于50,于是25找到下一层S_2中的25右边的下一个元素31,以此类推。直到找到key为50的元素。

Screen Shot 2022-05-12 at 1.50.28 PM.png

这种查询方式从开始查询到检索到key 50的元素,经过了6次查询(不含对哨兵的检索),即复杂度为O(log n)。如果修改元素,首先要查询到元素的位置,再进行修改,复杂度和查询相同。添加元素,除了找到适当的位置并加入元素,还要建立起关于该元素的塔tower,复杂度O(logn)

可以看到跳表的存在避免了链表表示map过程中的复杂度为O(n)的全文检索。代价是修改元素的代价从O(1)变为O(logn),和更多的存储空间。考虑到每一层的平均元素个数是前一层的50%,除S_0外,其他所有层的累计元素个数为n-1,故存储空间的占用近乎提高一倍。

Reference

1 Goodrich and etc., Data Structures and Algorithms in Python

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容