Python數(shù)據(jù)結(jié)構(gòu)與算法之鏈表,無序鏈表詳解
鏈表是一系列元素的集合,這些元素的內(nèi)存是散亂的。
無序鏈表則是一系列邏輯無序元素的集合,只是通過鏈表數(shù)據(jù)結(jié)構(gòu)連接起來。雖然這些元素整體來看是散亂的,但其中每一個元素都有一個相對于其他元素的位置信息。所以鏈表需要維持元素之間的相對位置,但是也不需要特意在一段內(nèi)存空間中存儲這些位置信息。
以下圖中的元素集合為例,這些元素的位置看上去都是隨機的。如果可以為每一個元素附加一份信息,即下一個元素的位置,那么這些元素的相對位置就能通過自身指向下一個元素的鏈接來表示。
附加信息后則可表示為:
需要注意的是,使用鏈表時我們必須指明鏈表中第一個元素的位置。一旦知道第一個元素的位置,就能根據(jù)其中的鏈接信息訪問第二個元素,接著訪問第三個元素,依此類推。指向鏈表第一個元素的引用被稱作頭 。最后一個元素需要知道自己沒有下一個元素。
認真觀察鏈表,我們將每一個元素視為一個節(jié)點,鏈表則是通過他們之間的相對位置關系把這些節(jié)點串起來的。每一個節(jié)點至少包含了兩個基本屬性,首先就是節(jié)點的數(shù)據(jù)變量,其次就是節(jié)點對下一個節(jié)點位置的指向關系。
我們首先來構(gòu)造節(jié)點。
節(jié)點(Node)是構(gòu)建鏈表的基本數(shù)據(jù)結(jié)構(gòu)。每一個節(jié)點對象都必須持有至少兩份信息。首先,節(jié)點必須包含一個數(shù)據(jù)元素,我們稱之為節(jié)點的數(shù)據(jù)變量 。其次,節(jié)點必須保存指向下一個節(jié)點的引用。下面的代碼展示了 Node 類的Python實現(xiàn)。在構(gòu)建節(jié)點時,需要為其提供初始值。Node 類也需要包含訪問和修改數(shù)據(jù)的方法,以及指向下一 個元素的引用。
class Node: def __init__(self, initdata): self.data = initdata # 數(shù)據(jù)元素 self.next = None # 指向下一節(jié)點的引用 def getData(self): return self.data def getNext(self): return self.next def setData(self, newdata): self.data = newdata def setNext(self, newnext): self.next = newnext
需要注意的是,None 在 Node 類以及之后的鏈表中起到了重要的作用。節(jié)點指向 None 的引用代表著后面沒有新節(jié)點。所以,在 Node 的構(gòu)造方法中我們將 next 的初始值設為 None 。
節(jié)點(Node)的類構(gòu)建完畢后,接下來我們開始構(gòu)建整個鏈表(LinkList)的類。
如前所述,鏈表是基于節(jié)點集合來構(gòu)建的,每一個節(jié)點都通過顯式的引用指向下一個節(jié)點。只要知道第一個節(jié)點的位置(第一個節(jié)點包含第一個元素),其后的每一個元素都能通過對下一個節(jié)點的引用找到。因此,LinkList 類必須包含指向第一個節(jié)點的引用。下列代碼展示了 LinkList 類的構(gòu)造方法。
class LinkList: def __init__(self): self.head = None
下圖展示了鏈表無節(jié)點和有節(jié)點的兩種情況:
我們已經(jīng)有了鏈表頭的創(chuàng)建方法,我們規(guī)定鏈表頭的初始指向為None,當鏈表中有內(nèi)容(節(jié)點)時,鏈表頭則指向節(jié)點。
那么我們還需要一個方法來判斷鏈表頭的指向。
我們使用 isEmpty 方法檢查鏈表的頭部是否為指向None的引用。布爾表達式 self.head == None 當且僅當鏈表中沒有節(jié)點時才為真。由于新的鏈表是空的,因此構(gòu)造方法必須和檢查是否為空的方法保持一致。這體現(xiàn)了使用 None 表示鏈表末尾的好處。
class LinkList: def isEmpty(self): return self.head == None
接下來我們構(gòu)建鏈表節(jié)點的添加方法。
為了將節(jié)點添加到鏈表中,我們需要實現(xiàn) add 方法。但在實現(xiàn)之前,需要解決一個重要問題:新節(jié)點要被放在鏈表的哪個位置?由于鏈表是無序元素的集合,因此新元素相對于已有元素的位置并不重要。新的元素可以在任意位置。因此,將新元素放在最簡便的位置是最合理的選擇。 由于鏈表只提供一個入口(頭部),因此其他所有節(jié)點都只能通過第一個節(jié)點以及 next 鏈接來訪問。這意味著添加新節(jié)點最簡便的位置就是頭部,或者說是鏈表的起點位置。我們把新節(jié)點作為鏈表的第一個節(jié)點,并且把已有的節(jié)點鏈接到該元素的后面。
下列代碼展示了 add 方法的實現(xiàn):
class LinkList: def add(self, item): temp = Node(item) temp.setNext(self.head) self.head = temp
代碼第 3 行創(chuàng)建一個新節(jié)點,并且將 item 作為其數(shù)據(jù)?,F(xiàn)在需要將新節(jié)點與已有的鏈表結(jié)構(gòu)鏈接起來。這一過程需要兩步,如下圖所示。圖中第 1 步(代碼第 4 行),將新節(jié)點的 next 引用指向當前鏈表中的第一個節(jié)點。 這樣一來,原來的鏈表就和新節(jié)點正確地鏈接在了一起。圖中第 2 步,修改鏈表頭的指向, 使其指向新創(chuàng)建的節(jié)點。代碼第 5 行的賦值語句,完成了這一操作。
上述兩步的順序非常重要。如果顛倒代碼第 4 行和第 5 行的順序,會發(fā)生什么呢?如果先修改鏈表頭的指向,由于頭節(jié)點是唯一指向鏈表節(jié)點的外部引用,因此所有的已有節(jié)點都將丟失并且無法訪問。
接下來我們要實現(xiàn)的方法還有 length 、search 以及 remove ,這些方法都基于遍歷鏈表。遍歷是指系統(tǒng)地訪問每一個節(jié)點,具體做法是額外用一個外部引用從鏈表的頭節(jié)點開始訪問。隨著訪問每一個節(jié)點,我們將這個外部引用通過“遍歷”下一個引用來指向下一個節(jié)點。
實現(xiàn)length方法(計算鏈表中節(jié)點的個數(shù)/鏈表長度)
class LinkList: def length(self): current = self.head count = 0 while current != None: count = count + 1 current = current.getNext() return count
為了實現(xiàn) length 方法,需要遍歷鏈表并且記錄訪問過多少個節(jié)點。上述代碼展示了計算鏈表中節(jié)點個數(shù)的Python代碼。current 就是外部引用,它在第 3 行中被初始化為鏈表頭的引用。此時如果鏈表中沒有節(jié)點,current 將指向 None,如果鏈表中有節(jié)點,current 此時就是指向鏈表中的第一個結(jié)點。
在計算開始時,由于沒有訪問到任何節(jié)點,因此 count 被初始化為 0 。第 5~7 行實現(xiàn)遍歷過程。只要 current 指向的不是鏈表的結(jié)尾(None),就將它指向下一個節(jié)點(第 7 行)。每當current 指向一個新節(jié)點時,將count加 1。最終,循環(huán)完成后返回 count 。
實現(xiàn)search方法(搜索鏈表中的某個節(jié)點)
在鏈表中搜索一個值同樣也會用到遍歷。每當訪問一個節(jié)點時,檢查該節(jié)點中的元素是否與要搜索的元素相同。在搜索時,可能并不需要完整遍歷鏈表就能找到該元素。事實上,如果遍歷到鏈表的末尾,就意味著要找的元素不在鏈表中。如果在遍歷過程中找到所需的元素,就沒有必要繼續(xù)遍歷了。
class LinkList: def search(self, item): current = self.head found = False while current != None and not found: if current.getData() == item: found = True else: current = current.getNext() return found
上述代碼展示了 search 方法的實現(xiàn)。與在 length 方法中相似,遍歷從列表的頭部開始(第3行)。我們使用布爾型變量 found 來標記是否找到所需的元素。由于一開始時并未找到該元素,因此第 4 行將 found 初始化為False 。
第 5 行的循環(huán)既考慮了是否到達鏈表末尾,也考慮了是否已經(jīng)找到目標元素。只要還有未訪問的節(jié)點并且還沒有找到目標元素,就繼續(xù)檢查下一個節(jié)點。第 6 行檢查當前節(jié)點中的元素是否為目標元素。如果是,就將 found 設為True。
實現(xiàn)remove方法(移除鏈表中的某個節(jié)點)
remove 方法在邏輯上需要分兩步。
第 1 步,遍歷列表并查找要移除的元素(節(jié)點)。一旦找到該元素(假設元素在鏈表中),就必須將其移除。第 1 步與 search 非常相似。從一個指向鏈表頭節(jié)點的外部引用開始,遍歷整個鏈表,直到遇到需要移除的元素。假設目標元素已經(jīng)在鏈表中,因此我們預測循環(huán)會在 current 抵達 None 之前結(jié)束。這意味著可以在判斷條件中使用布爾型變量 found 。
第 2 步,移除元素(節(jié)點)。當 found 被設為 True 時,current 將指向需要移除的節(jié)點。該如何移除它呢?一種做法是將節(jié)點包含的值替換成表示其已被移除的標識值(比如 None 或者 Null,完全可以自定義)。這種做法的問題是,節(jié)點的數(shù)量和元素的數(shù)量不再匹配。更好的做法是移除整個節(jié)點。
為了將包含元素的整個節(jié)點移除,需要將其前面的節(jié)點中的 next 引用指向 current 之后的節(jié)點。然而,并沒有反向遍歷鏈表的方法。由于current 已經(jīng)指向了需要修改的節(jié)點之后的節(jié)點,此時做修改為時已晚。
這一困境的解決方法就是在遍歷鏈表時使用兩個外部引用。current 與之前一樣,標記在鏈表中的當前位置。新的引用 previous 總是指向 current 上一次訪問的節(jié)點。這樣一來,當current 指向需要被移除的節(jié)點時,previous 就剛好指向真正需要修改的節(jié)點。
class LinkList: def remove(self, item): current = self.head previous = None found = False while not found: if current.getData() == item: found = True else: previous = current current = current.getNext() if current == None: return found if previous == None: self.head = current.getnext() else: previous.setNext(current.getNext())
上面的代碼展示了完整的 remove 方法。第 3~4 行對兩個引用進行初始賦值。注意,current 與其他遍歷例子一樣,從鏈表的頭節(jié)點開始。由于頭節(jié)點之前沒有別的節(jié)點,因此 previous 的初始值是None。
第 7~8 行檢查當前節(jié)點中的元素是否為要移除的元素。如果是,就設 found 為 True 。 如果不是,則將 previous 和 current 往下一個節(jié)點的方向各移動一次。這兩條語句的順序十分重要。必須先將 previous 移動到current 的位置,然后再移動 current。這一過程經(jīng)常被稱 為“蠕動”,因為 previous 必須在 current 移動之前指向其當前位置。
下圖展示了這一過程:
一旦搜索過程結(jié)束,就需要執(zhí)行移除操作。如移除圖中數(shù)據(jù)值為 17 的節(jié)點,修改過程如下:
有一種特殊情況需要注意,如果被移除的節(jié)點正好是鏈表的第一個節(jié)點,那么 current 會指向鏈表中的第一個節(jié)點,previous 的值則是None 。在這種情況下,需要修改鏈表的頭節(jié)點,而不是 previous 指向的節(jié)點,如圖所示:
代碼第 13 行檢查是否遇到上述特殊情況。如果 previous 沒有移動,當 found 被設為True 時,它的值仍然是None 。在這種情況下 (第13行),鏈表的頭節(jié)點指向被修改成指向當前頭節(jié)點的下一個節(jié)點,從而達到移除頭節(jié)點的效果。但是,如果 previous 的值不是None ,則說明需要移除的節(jié)點在鏈表結(jié)構(gòu)中的某個位置。在這種情況下,previous 指向了 next 引用需要被修改的節(jié)點。第 16 行使用 previous 的 setNext 方法來完成移除操作。注意,在兩種情況中,修改后的引用都指向current.getNext() 。
代碼匯總
# 構(gòu)造節(jié)點 class Node: def __init__(self, initdata): self.data = initdata # 數(shù)據(jù)元素 self.next = None # 指向下一節(jié)點的引用(python變量賦值的本質(zhì)是引用) def getData(self): # 獲取節(jié)點的數(shù)據(jù)元素值 return self.data def getNext(self): # 獲取下一節(jié)點的引用 若有節(jié)點則直接視作為下一節(jié)點 return self.next def setData(self, newdata): # 給節(jié)點的數(shù)據(jù)元素設置值 self.data = newdata def setNext(self, newnext): # 給節(jié)點設置對下一節(jié)點的引用 self.next = newnext # 構(gòu)造鏈表 class LinkList: def __init__(self): # 設置鏈表的頭部指向 無節(jié)點時為None self.head = None def isEmpty(self): # 判斷鏈表是否為空(是否有節(jié)點) return self.head == None def add(self, item): # 給鏈表增加新節(jié)點(從鏈表頭位置增加) temp = Node(item) # 創(chuàng)建節(jié)點并對新節(jié)點數(shù)據(jù)元素賦值 temp.setNext(self.head) # 新節(jié)點指向當前鏈表中的第一個節(jié)點 self.head = temp # 鏈表頭指向新節(jié)點 def length(self): # 計算鏈表長度(節(jié)點個數(shù)) current = self.head # 引入current作為指向標識 指向第一個節(jié)點 或者None count = 0 # 引入count作為計數(shù)變量 while current != None: # 當指向標識指向的不是鏈表末尾的None時 count = count + 1 # 節(jié)點計數(shù)加一 current = current.getNext() # 指向標識向后移動 return count def search(self, item): # 搜索鏈表中節(jié)點 current = self.head # 引入current作為指向標識 指向第一個節(jié)點 found = False # 引入found作為尋找狀態(tài)標識 while current != None and not found: # 當current沒有指向None 并且 未找到節(jié)點 if current.getData() == item: # 如果current當前指向的節(jié)點是尋找的節(jié)點 found = True # 找到 else: # 否則 current = current.getNext() # 指向標識向后移動 return found def remove(self, item): # 從鏈表中移除節(jié)點 current = self.head # 引入current指向標識指向第一個節(jié)點 previous = None # 引入previous指向標識指向current的上一個節(jié)點 所以當current指向第一個節(jié)點時 previous初始化指向的是None found = False # 引入尋找狀態(tài)標識 尋找需要移除的節(jié)點 while not found: # 遍歷節(jié)點 以找到為循環(huán)結(jié)束條件 if current.getData() == item: # 如果當前節(jié)點是目標節(jié)點 found = True # 找到 else: # 當前節(jié)點不是目標節(jié)點 previous = current # previous指向當前節(jié)點 current = current.getNext() # current指向移動到下一個節(jié)點 if current == None: # 如果到最后都沒找到目標節(jié)點 return found # 搜索過程結(jié)束 if previous == None: # 如果previous目前指向的是None 代表current指向的是鏈表的第一個節(jié)點 也就是說刪除的目標節(jié)點就是第一個節(jié)點 self.head = current.getnext() # 鏈表頭直接指向None表示刪除第一個節(jié)點 else: # 其他情況下 previous.setNext(current.getNext()) # 當前節(jié)點的上一個節(jié)點指向當前節(jié)點的下一個節(jié)點
無注釋版:
class Node: def __init__(self, initdata): self.data = initdata self.next = None def getData(self): return self.data def getNext(self): return self.next def setData(self, newdata): self.data = newdata def setNext(self, newnext): self.next = newnext class LinkList: def __init__(self): self.head = None def isEmpty(self): return self.head == None def add(self, item): temp = Node(item) temp.setNext(self.head) self.head = temp def length(self): current = self.head count = 0 while current != None: count = count + 1 current = current.getNext() return count def search(self, item): current = self.head found = False while current != None and not found: if current.getData() == item: found = True else: current = current.getNext() return found def remove(self, item): current = self.head previous = None found = False while not found: if current.getData() == item: found = True else: previous = current current = current.getNext() if current == None: return found if previous == None: self.head = current.getnext() else: previous.setNext(current.getNext())
代碼運行結(jié)果如下圖:
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注腳本之家的更多內(nèi)容!
相關文章
python中的selenium安裝的步驟(瀏覽器自動化測試框架)
這篇文章主要介紹了python中的selenium安裝的步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03解決TensorFlow GPU版出現(xiàn)OOM錯誤的問題
今天小編就為大家分享一篇解決TensorFlow GPU版出現(xiàn)OOM錯誤的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-02-02用python + openpyxl處理excel2007文檔思路以及心得
最近要幫做RA的老姐寫個合并excel工作表的腳本……源數(shù)據(jù)是4000+個excel 工作表,分布在9個xlsm文件里,文件內(nèi)容是中英文混雜的一些數(shù)據(jù),需要從每張表中提取需要的部分,分門別類合并到多個大的表里。2014-07-07