<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          或許是一本可以徹底改變你刷 LeetCode 效率的題解書

          共 11566字,需瀏覽 24分鐘

           ·

          2021-09-14 12:31

          經(jīng)過了半年時間打磨,投入諸多人力,這本 LeetCode 題解書終于要和大家見面了。

          背景

          自 LeetCode 題解 (現(xiàn)在已經(jīng)接近 45k star 了)項目被大家開始關(guān)注,就有不少出版社開始聯(lián)系我寫書。剛開始后的時候,我并沒有這個打算,覺得寫這個相對于博客形式的題解要耗費時間,且并不一定效果比博客形式的效果好。后來當(dāng)我向大家提及“出版社找我寫書”這件事情的時候,很多人表示“想要買書,于是我就開始打算寫這樣一本書。但是一個完全沒有寫書經(jīng)驗的人,獨立完成一本書工作量還是蠻大的,因此我打算尋求其他志同道合人士的幫助。

          團(tuán)隊介紹

          團(tuán)隊成員大都來自 985, 211 學(xué)校計算機(jī)系,大家經(jīng)常參加算法競賽,也堅持參加 LeetCode 周賽。在這個過程中,我們積累了很多經(jīng)驗,希望將這些經(jīng)驗分享給大家,以減少大家在刷題過程中的阻礙,讓大家更有效率的刷題。本書尤其適合那些剛剛開始刷題的人,如果你剛開始刷題,或者刷了很多題面對新題還是無法很好的解決,那么這本書肯定很適合你。最后歡迎大家加入我們的讀者群和作者進(jìn)行交流。

          讀者群會在新書出版之后的第一時間開放。

          • 作者 - lucifer
          • 作者 - BY 
          • 作者 - fanlu
          • 作者 - hlxing
          • 作者 - lazybing

          樣張

          這里給大家開放部分章節(jié)內(nèi)容給大家,讓大家嘗嘗鮮。

          我們開放了第八章第五小節(jié)給大家看,以下是具體內(nèi)容:

          8.5 1206. 設(shè)計跳表

          題目描述

          不使用任何庫函數(shù),設(shè)計一個跳表。

          跳表是在 


           時間內(nèi)完成增加、刪除、搜索操作的數(shù)據(jù)結(jié)構(gòu)。跳表相比于樹堆與紅黑樹,其功能與性能相當(dāng),并且跳表的代碼長度相較下更短,其設(shè)計思想與鏈表相似。

          跳表中有很多層,每一層是一個短的鏈表。在第一層的作用下,增加、刪除和搜索操作的時間復(fù)雜度不超過 

          。跳表的每一個操作的平均時間復(fù)雜度是 ,空間復(fù)雜度是 

          在本題中,你的設(shè)計應(yīng)該要包含這些函數(shù):

          • bool search(int target) : 返回 target 是否存在于跳表中。
          • void add(int num):  插入一個元素到跳表。
          • bool erase(int num): 在跳表中刪除一個值,如果  num  不存在,直接返回 false. 如果存在多個  num ,刪除其中任意一個即可。

          注意,跳表中可能存在多個相同的值,你的代碼需要處理這種情況。

          樣例:

          Skiplist skiplist = new Skiplist();

          skiplist.add(1);
          skiplist.add(2);
          skiplist.add(3);
          skiplist.search(0); // 返回 false
          skiplist.add(4);
          skiplist.search(1); // 返回 true
          skiplist.erase(0); // 返回 false,0 不在跳表中
          skiplist.erase(1); // 返回 true
          skiplist.search(1); // 返回 false,1 已被擦除

          約束條件:0 <= num, target <= 20000最多調(diào)用  50000  次  search, add, 以及  erase 操作。

          思路

          首先,使用跳表會將數(shù)據(jù)存儲成有序的。在數(shù)據(jù)結(jié)構(gòu)當(dāng)中,我們通常有兩種基本的線性結(jié)構(gòu),結(jié)合有序數(shù)據(jù),表達(dá)如下:

          • 有序鏈表,我們有三種基本操作:
            • 查找指定的數(shù)據(jù):時間復(fù)雜度為 

          •  為數(shù)據(jù)位于鏈表的位置。
          • 插入指定的數(shù)據(jù):時間復(fù)雜度為 
          •  為數(shù)據(jù)位于鏈表的位置。因為插入數(shù)據(jù)之前,需要先查找到可以插入的位置。
          • 刪除指定的數(shù)據(jù):時間復(fù)雜度為 
            •  為數(shù)據(jù)位于鏈表的位置。因為刪除數(shù)據(jù)之前,需要先查找到可以插入的位置。
          • 有序數(shù)組:
            • 查找指定的數(shù)據(jù):如果使用二分查找,時間復(fù)雜度為 
          •  為數(shù)據(jù)的個數(shù)。
          • 插入指定的數(shù)據(jù):時間復(fù)雜度為 
          • , 因為數(shù)組是順序存儲,插入新的數(shù)據(jù)時,我們需要向后移動指定位置后面的數(shù)據(jù),這里 

          •  為數(shù)據(jù)的個數(shù)。
          • 刪除指定的數(shù)據(jù):時間復(fù)雜度為 
          • , 因為數(shù)組是順序存儲,刪除數(shù)據(jù)時,我們需要向前移動指定位置后面的數(shù)據(jù),這里 

              •  為數(shù)據(jù)的個數(shù)。

            而神奇的跳表能夠在 

             時間內(nèi)完成增加、刪除、搜索操作。下面我們分別分析增加、刪除和搜索這 3 個三個基本操作。

            跳表的查找

            現(xiàn)在我們通過一個簡單的例子來描述跳表是如何實現(xiàn)的。假設(shè)我們有一個有序鏈表如下圖:原始方法中,查找的時間復(fù)雜度為 

          • 。那么如何來提高鏈表的查詢效率呢?如下圖所示,我們可以從原始鏈表中每兩個元素抽出來一個元素,加上一級索引,并且一級索引指向原始鏈表:

          • 如果我們想要查找 9 ,在原始鏈表中查找路徑是 

          • 1->3->4->7->9
          • , 而在添加了一級索引的查找路徑是 

          • 1->4->9
          • ,很明顯,查找效率提升了。按照這樣的思路,我們在第 1 級索引上再加第 2 級索引,再加第 3 級索引,以此類推,這樣在數(shù)據(jù)量非常大的時候,使得我們查找數(shù)據(jù)的時間復(fù)雜度為 

          • 。這就是跳表的思想,也就是我們通常所說的“空間換時間”。

            跳表的插入

            跳表插入數(shù)據(jù)看起來很簡單,我們需要保持?jǐn)?shù)據(jù)有序,因此,第一步我們需要像查找元素一樣,找到新元素應(yīng)該插入的位置,然后再插入。

            但是這樣會存在一個問題,如果我們一直往原始鏈表中插入數(shù)據(jù),但是不更新索引,那么會導(dǎo)致兩個索引結(jié)點之間的數(shù)據(jù)非常多,在極端情況下,跳表會退化成單鏈表,從而導(dǎo)致查找效率由 

          •  退化為 

          • 。因此,我們需要在插入數(shù)據(jù)的同時,增加相應(yīng)的索引或者重建索引。

            1. 方案 1:每次插入數(shù)據(jù)后,將跳表的索引全部刪除后重建,我們知道索引的結(jié)點個數(shù)為 
          • (在空間復(fù)雜度分析時會有明確的數(shù)學(xué)推導(dǎo)),那么每次重建索引,重建的時間復(fù)雜度至少是 

          •  級別,很明顯不可取。
          • 方案 2:通過隨機(jī)性來維護(hù)索引。假設(shè)跳表的每一層的提升概率為 
          •  ,最理想的情況就是每兩個元素提升一個元素做索引。而通常意義上,只要元素的數(shù)量足夠多,且抽取足夠隨機(jī)的話,我們得到的索引將會是比較均勻的。盡管不是每兩個抽取一個,但是對于查找效率來講,影響并不很大。我們要知道,設(shè)計良好的數(shù)據(jù)結(jié)構(gòu)往往都是用來應(yīng)對大數(shù)據(jù)量的場景的。因此,我們這樣維護(hù)索引:

          • 隨機(jī)抽取  個元素作為 1 級索引,隨機(jī)抽取 
            1.  作為 2 級索引,以此類推,一直到最頂層索引

            那么具體代碼該如何實現(xiàn),才能夠讓跳表在每次插入新元素時,盡量讓該元素有 

          •  的概率建立一級索引、

          •  的概率建立二級索引、

          •  的概率建立三級索引,以此類推。因此,我們需要一個概率算法。

            在通常的跳表實現(xiàn)當(dāng)中,我們會設(shè)計一個 randomLevel() 方法,該方法會隨機(jī)生成 1~MAX_LEVEL 之間的數(shù) (MAX_LEVEL 表示索引的最高層數(shù))

            • randomLevel() 方法返回 1 表示當(dāng)前插入的元素不需要建立索引,只需要存儲數(shù)據(jù)到原始鏈表即可(概率 1/2)
            • randomLevel() 方法返回 2 表示當(dāng)前插入的元素需要建立一級索引(概率 1/4)
            • randomLevel() 方法返回 3 表示當(dāng)前插入的元素需要建立二級索引(概率 1/8)
            • randomLevel() 方法返回 4 表示當(dāng)前插入的元素需要建立三級索引(概率 1/16)
            • ......

            可能有的同學(xué)會有疑問,我們需要一級索引中元素的個數(shù)時原始鏈表的一半,但是我們 randomLevel() 方法返回 2(建立一級索引)的概率是 

          • , 這樣是不是有問題呢?實際上,只要

          • randomLevel()
          • 方法返回的數(shù)大于 1,我們都會建立一級索引,而返回值為 1 的概率是 

          • 。所以,建立一級索引的概率其實是

          • 。同上,當(dāng) 

          • randomLevel()
          •  方法返回值 

          • >2
          •  時,我們會建立二級或二級以上的索引,都會在二級索引中添加元素。而在二級索引中添加元素的概率是 

          • 。以此類推,我們推導(dǎo)出 randomLevel() 符合我們的設(shè)計要求。

            下面我們通過仿照 redis zset.c 的 randomLevel 的代碼:

            ##
            # 1. SKIPLIST_P 為提升的概率,本案例中我們設(shè)置為 1/2, 如果我們想要節(jié)省空間利用效率,可以適當(dāng)?shù)慕档驮撝担瑥亩鴾p少索引元素個數(shù)。在 redis 中 SKIPLIST_P 被設(shè)定為 0.25。
            # 2. redis 中通過使用位運算來提升浮點數(shù)比較的效率,在本案例中被簡化
            def randomLevel():
                level = 1
                while random() < SKIPLIST_P and level < MAX_LEVEL:
                    level += 1
                return level

            跳表的刪除

            跳表的刪除相對來講稍微簡單一些。我們在刪除數(shù)據(jù)的同時,需要刪除對應(yīng)的索引結(jié)點。

            代碼

            from typing import Optional
            import random

            class ListNode:
                def __init__(self, data: Optional[int] = None):
                    self._data = data # 鏈表結(jié)點的數(shù)據(jù)域,可以為空(目的是方便創(chuàng)建頭節(jié)點)
                    self._forwards = [] # 存儲各個索引層級中該結(jié)點的后驅(qū)索引結(jié)點

            class Skiplist:

                _MAX_LEVEL = 16 # 允許的最大索引高度,該值根據(jù)實際需求設(shè)置

                def __init__(self):
                    self._level_count = 1 # 初始化當(dāng)前層級為 1
                    self._head = ListNode()
                    self._head._forwards = [None] * self._MAX_LEVEL

                def search(self, target: int) -> bool:
                    p = self._head
                    for i in range(self._level_count - 1-1-1): # 從最高索引層級不斷搜索,如果當(dāng)前層級沒有,則下沉到低一級的層級
                        while p._forwards[i] and p._forwards[i]._data < target:
                            p = p._forwards[i]

                    if p._forwards[0and p._forwards[0]._data == target:
                        return True

                    return False

                def add(self, num: int) -> None:
                    level = self._random_level() # 隨機(jī)生成索引層級
                    if self._level_count < level: # 如果當(dāng)前層級小于 level, 則更新當(dāng)前最高層級
                        self._level_count = level
                    new_node = ListNode(num) # 生成新結(jié)點
                    new_node._forwards = [None] * level
                    update = [self._head] * self._level_count # 用來保存各個索引層級插入的位置,也就是新結(jié)點的前驅(qū)結(jié)點

                    p = self._head
                    for i in range(self._level_count - 1-1-1): # 整段代碼獲取新插入結(jié)點在各個索引層級的前驅(qū)節(jié)點,需要注意的是這里是使用的當(dāng)前最高層級來循環(huán)。
                        while p._forwards[i] and p._forwards[i]._data < num:
                            p = p._forwards[i]

                        update[i] = p

                    for i in range(level): # 更新需要更新的各個索引層級
                        new_node._forwards[i] = update[i]._forwards[i]
                        update[i]._forwards[i] = new_node

                def erase(self, num: int) -> bool:
                    update = [None] * self._level_count
                    p = self._head
                    for i in range(self._level_count - 1-1-1):
                        while p._forwards[i] and p._forwards[i]._data < num:
                            p = p._forwards[i]
                        update[i] = p

                    if p._forwards[0and p._forwards[0]._data == num:
                        for i in range(self._level_count - 1-1-1):
                            if update[i]._forwards[i] and update[i]._forwards[i]._data == num:
                                update[i]._forwards[i] = update[i]._forwards[i]._forwards[i]
                        return True

                    while self._level_count > 1 and not self._head._forwards[self._level_count]:
                        self._level_count -= 1

                    return False

                def _random_level(self, p: float = 0.5) -> int:
                    level = 1
                    while random.random() < p and level < self._MAX_LEVEL:
                        level += 1
                    return level

            復(fù)雜度分析

            空間復(fù)雜度

            跳表通過建立索引提高查找的效率,是典型的“空間換時間”的思想,那么空間復(fù)雜度到底是多少呢?我們假設(shè)原始鏈表有 

          •  個元素,一級索引有 

          • ,二級索引有 

          • ,k 級索引有 

          •  個元素,而最高級索引一般有 

          •  個元素。所以,索引結(jié)點的總和是 

          •  ,因此可以得出空間復(fù)雜度是 

          •  是原始鏈表的長度。

            上面的假設(shè)前提是每兩個結(jié)點抽出一個結(jié)點到上層索引。那么如果我們每三個結(jié)點抽出一個結(jié)點到上層索引,那么索引總和就是 , 額外空間減少了一半。因此我們可以通過減少索引的數(shù)量來減少空間復(fù)雜度,但是相應(yīng)的會帶來查找效率一定的下降。而具體這個閾值該如何選擇,則要看具體的應(yīng)用場景。

            另外需要注意的是,在實際的應(yīng)用當(dāng)中,索引結(jié)點往往不需要存儲完整的對象,只需要存儲對象的 key 和對應(yīng)的指針即可。因此當(dāng)對象比索引結(jié)點占用空間大很多時,索引結(jié)點所占的額外空間(相對原始數(shù)據(jù)來講)又可以忽略不計了。

            時間復(fù)雜度

            查找的時間復(fù)雜度

            來看看時間復(fù)雜度 

             是如何推導(dǎo)出來的,首先我們看下圖:

            如上圖所示,此處我們假設(shè)每兩個結(jié)點會抽出一個結(jié)點來作為上一級索引的結(jié)點。也就是說,原始鏈表有 

          •  個元素,一級索引有 

          • ,二級索引有 

          • ,k 級索引有 

          •  個元素,而最高級索引一般有 

          •  個元素。也就是說:最高級索引 

          •  滿足 

          • , 由此公式可以得出 

          •  , 加上原始數(shù)據(jù)這一層, 跳表的總高度為 

          • 。那么,我們在查找過程中每一層索引最多遍歷幾個元素呢?從圖中我們可以看出來每一層最多需要遍歷 3 個結(jié)點。因此,由公式 

          • 時間復(fù)雜度 = 索引高度*每層索引遍歷元素個數(shù)
          • , 可以得出跳表中查找一個元素的時間復(fù)雜度為 

          • ,省略常數(shù)即為 

          • 插入的時間復(fù)雜度

            跳表的插入分為兩部分操作:

            1. 尋找到對應(yīng)的位置,時間復(fù)雜度為 
          •  為鏈表長度。
          • 插入數(shù)據(jù)。我們在前面已經(jīng)推導(dǎo)出跳表索引的高度為 
          • 。因此,我們將數(shù)據(jù)插入到各層索引中的最壞時間復(fù)雜度為 

            綜上所述,插入操作的時間復(fù)雜度為

            刪除的時間復(fù)雜度

            跳表的刪除操作和查找類似,只是需要在查找后刪除對應(yīng)的元素。查找操作的時間復(fù)雜度是 

          • 。那么后面刪除部分代碼的時間復(fù)雜度是多少呢?我們知道在跳表中,每一層索引都是一個有序的單鏈表,而刪除單個元素的復(fù)雜度為 

          • , 索引層數(shù)為 

          • ,因此刪除部分代碼的時間復(fù)雜度為

          • 。那么刪除操作的總時間復(fù)雜度為- 。我們忽略常數(shù)部分,刪除元素的時間復(fù)雜度為 

          • 擴(kuò)展

            在工業(yè)上,使用跳表的場景很多,下面做些簡單的介紹,有興趣的可以深入了解:

            1. redis 當(dāng)中 zset 使用了跳表
            2. HBase MemStore 當(dāng)中使用了跳表
            3. LevelDB 和 RocksDB 都是 LSM Tree 結(jié)構(gòu)的數(shù)據(jù)庫,內(nèi)部的 MemTable 當(dāng)中都使用了跳表

            配套網(wǎng)站

            等新書發(fā)布之后,我們會在官網(wǎng)開辟一個區(qū)域,大家可以直接訪問查看本書配套的配套代碼,包括 JavaScript,Java,Python 和 C++。也歡迎大家留言給我們自己想要支持的語言,我們會鄭重考慮大家的意見。

            效果大概是這樣的:

            官網(wǎng)地址:https://leetcode-solution.cn/book

            限時優(yōu)惠


            五折限時優(yōu)惠,時間到九月中旬



          瀏覽 65
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人草人人玩 | 欧洲成人免费视频 | 午夜免费福利视频一区二区三区 | 狠狠干超碰 | 亚洲AV无码成人精品涩涩麻豆 |