資料結構與演算法之線性結構
什麼是資料結構
資料結構是指相互之間存在著一種或多種關係的資料元素的集合和該集合中資料元素之間的關係的組成。
-
資料結構就是設計資料以何種方式儲存在計算機中,列表、字典等都算是資料結構。
-
程式=資料結構+演算法,資料結構屬於靜態的部分,演算法的呼叫為動態部分
資料結構的分類
根據邏輯結構劃分:
- 線性結構:資料結構中的元素一對一的關係,一前驅,一後繼。
- 樹結構:資料結構中元素一對多的關係,一前驅,多後繼。
-
圖結構:資料結構中元素存在多對多的關係,多前驅,多後繼,我也不會。
- 判斷一個圖形能不能一筆畫完,就判斷它的奇數度節點數目是否為0或2.這種能一筆畫完的就是尤拉圖,奇數度節點為四個,就是兩筆畫完。
線性結構
列表
列表和陣列
python中的列表和其他語言中的陣列很相似,區別為:
- 陣列是定長的。
- 陣列的資料型別也必須一致。
- 對列表或陣列來說,它們的下標操作是最快的。
列表解決的變長問題的方式
- 假設一開始在記憶體中分配了四個元素儲存的空間,那麼前四個元素的append操作不會出現問題。
- 當第五次append操作時,會先在記憶體中分配一個能夠儲存八個元素的空間,也就是翻倍。
- 然後進行復制,把以前的四個元素依次放到相應的位置上。
- 若再次超出長度,則繼續執行上述操作。
- 也就是使用了動態表的原理
append操作會不會使速度變慢?
- 根據攤還分析,沒有變長時的append和變長時的append均攤,最後的複雜度時O(3).
- append越往後,變長時的出現頻率就會越小
- 浪費了一部分空間,最壞情況應該是浪費了長度除二減一的空間。
列表解決多資料型別問題的方式
- 對於純整數的陣列,它的每一個元素佔4個位元組,那麼就事先計算好記憶體分配的大小,計算方法為:- 第一個元素的地址+元素個數 乘 4
- python的列表裡存的不是值,而是指向這個值的記憶體地址。
- 地址的大小是一樣的,32位裡地址是4個位元組,64位裡地址是8個位元組。
- 這種方法的缺點是記憶體開銷翻倍,這也是python被人詬病的地方。
棧
相關知識點
總是能聽到一個詞 堆疊 ,堆(heap)和棧(stack)是兩個東西,傳統的程式語言中把記憶體分為兩個地方,堆空間和棧空間,堆儲存的是一些動態生成的物件,與資料結構中的堆是不同的,棧空間由系統呼叫,存放函式的引數值,區域性變數的值。
應該是早年間翻譯的問題,一般聽到堆疊指的就是棧。
- 棧是一個數據集合,可以理解為只能在一端進行插入和刪除操作的列表。
-
棧的特點:後進先出(last-in,first-out)
- 棧頂:操作永遠在棧頂。
- 棧底:最後一個元素。
-
棧的基本操作:
- 進棧(壓棧):push
- 出棧:pop
- 取棧頂: gettop
-
關於出棧順序的問題:
- 對於某個元素,如果進展順序在它前面的元素出棧時在它後面,那麼前面的元素順序是相反的。
- 不知道說的明不明白
- 卡特蘭數,n個數的出棧順序,就是卡特蘭數的第n項。
棧的應用--括號匹配問題
- 給定一個字串,問其中字串是否匹配。
- 括號本身滿足棧的性質
-
匹配失敗的情況:
- 括號不匹配
- 匹配完畢棧沒空
- 棧空了又進元素
def brace_match(s): stack = [] d ={'(':')','[':']','{':'}'} for ch in s: if ch in {'(','[','{'}: stack.append(ch) elif len(stack): print('多了%s' %ch) return False elif d[stack[-1]] == ch: stack.pop() else: print('%s不匹配'%ch) if len(stack)==0: return True else: print("未匹配") return False
佇列
相關知識點:
佇列是一個數據集合,僅允許在列表的一端插入,另一端刪除。
- 進行插入的時隊尾,進行刪除操作的是隊首,插入和刪除操作也被稱為進隊(push)和出隊(pop)。
- 佇列的性質:先進先出(first-in,first-out)
- 雙向佇列:兩邊都能進行插入刪除操作的佇列。
佇列的陣列實現:
- 簡單的pop(0)操作複雜度過高,不採用。
-
由於陣列定長,不能繼續新增資料,如果是列表,出隊的操作就會出現空位,所以想辦法讓陣列變成一個圓環。
- 設定兩個指標,隊首指標front,隊尾指標rear。
- 由於,佇列滿的時候和佇列空的時候rear和front都在一個位置,那麼就無法判斷了。於是設定成佇列滿的時候減去一做為隊滿的標誌。
-
這種佇列就叫做環形佇列。
- 當隊尾指標front=最大長度+1時,再前進一個位置就自動到0.
-
實現方式:求餘數運算
- 隊首指標前進1:front=(front+1)%maxsize
- 隊尾指標前進1:rear=(rear+1)%maxsize
- 隊空條件:rear=front
- 隊滿條件:(rear+1)%maxsize=front
通過兩個棧做一個佇列的方法
- 1號棧進棧 模擬進隊操作。
- 2號站出棧,如果2號棧空,把1號站依次出棧並進2號棧,模擬出隊操作。
- 通過攤還分析,時間複雜度還是O(1)。
python關於佇列的模組
import queue#涉及執行緒安全用queue from collections import deque#常用解題的用deque q = deque()#是一種雙向佇列,popleft出隊 #模擬linux命令 head和tail,假如是tail 5 deque(open('a.text','r',encooding='utf8'),5) #建立一個定長的佇列,當佇列滿了之後,就會刪除第一行,繼續新增
連結串列
相關知識點:
連結串列就是非順序表,與佇列和棧對應。
-
連結串列中每一個元素都是一個物件,每個物件稱為一個節點,包含有資料域key和指向下一個節點的next,通過各個節點之間的相互連線,最終串聯成一個連結串列。
- 在機械硬碟中,檔案就是以連結串列的形式儲存的。
- 以FAT32為例,檔案的單位是檔案塊(block),一個檔案塊的大小是4k,一個檔案的內容是由連結串列的方式連線檔案塊組成的。
- 連結串列的第一個節點被稱為頭節點,資料可以是空的,也可以有值。
-
頭節點為空也是為了表示空連結串列,也叫做帶空節點的連結串列,頭節點也可以記錄連結串列的長度
節點定義
class Node(object): def __init__(self,item): self.item=item self.next=None #eg a=Node(1) b=Node(2) c=Node(3) a.next=b b.next=c#連結串列的最後一個節點的next就為None
連結串列類的實現
class LinkList: def __init___(self,li,method='tail'): self.head = None self.tail = None if method == 'head': self.create_linklist_head(li) if method == 'tail' self.create_linklist_tail(li) else: rais ValueError('unsupport') #頭插法 def create_linklist_head(self,li): self.head = Node(0) for v in li: n = Node(v) n.next = l.next#當插入下一個元素時,應該與下一個節點連線後再跟頭節點連線 self.head.next = n self.head.data += 1 #尾插法 def create_linlist_tail(self,li): self.head = Node(0) self.tail = self.head for v in li: p = Node(v) self.tail.next = p self.tail = p self.head.data += 1 #連結串列的遍歷輸出 def traverse_linlist(self): p = self.head.next while p: yield p.data p = p.next
插入刪除總結
- 插入
#p表示待插入節點,curNode表示當前節點 p.next = curNode.next#不能當前連線直接斷開 curNode,next = p
- 刪除
p = curNode.next curNode.next = p.next del p#不寫也一樣,引用計數,python的記憶體回收機制
雙鏈表
雙鏈表中每個節點有兩個指標:一個指向後面節點、一個指向前面節點。
節點定義:
class Node(object): def __init__(self, item=None): self.item = item self.next = None self.prior = None
雙鏈表的插入和刪除
- 插入
p.next = curNode.next curNode.next.prior = p p.prior = curNode curNode.next = p
- 刪除
p = curNode.next curNode.next = p.next p.next.prior = curNode del p
連結串列的複雜度分析
連結串列與列表相比
- 按元素值查詢:列表可以使用二分法是O(logn),連結串列是O(n)
- 按下標查詢:O(1),O(n)
- 再某元素後插入:O(n),O(1)
-
刪除莫元素:O(n),O(1)
總的來說連結串列再插入和刪除某元素的操作時明顯快於順序表,而且通過雙鏈表可以更容易實現棧和佇列。
雜湊表
直接定址表
雜湊表就是直接定址表的改進。當關鍵字的全域U比較小時,直接定址是一種簡單有效的方法。
- 全域的意思就是它的取值範圍。
-
也就是直接把關鍵字為key的value放在key的位置上
直接定址的缺點: - 當域U很大時,需要消耗大量記憶體。
- 如果U很大,但關鍵字很少,浪費大量空間。
-
若關鍵字不是數字則無法處理。
直接定址表的改進: - 構建大小為m的定址表T
- key為k的元素放到h(k)上
- h(k)是一個函式,其將域U對映到表T(0,1,..,m-1)
雜湊表
雜湊表是一個通過雜湊函式計算資料儲存位置的線性表的儲存結構,又叫做散列表。
- 雜湊表由一個直接定址表和一個雜湊函式組成。
- 雜湊函式h(k)將元素關鍵字k作為自變數,返回元素的儲存下標。
-
雜湊表的基本操作:
- insert(key,value):插入鍵值對。
- get(key):如果存在鍵為key的鍵值對則返回其value。
- delete(key):刪除鍵為key的鍵值對。
簡單雜湊函式
h(k)= k mod m h(k) = floor(m(KA mod 1)) 0<A<1
雜湊衝突
由於雜湊表的大小是有限的,而要儲存資訊的數量是無限的,因此,對於任何雜湊函式,都會出現兩個元素對映到同一個位置的情況,這種情況就叫做雜湊衝突。
解決雜湊衝突的方法:
開放定址法:如果雜湊函式返回的位置已經有值,則可以向後探查新的位置來儲存這個值。
-
線性探查:如果位置p被佔用,則探查
p+1,p+2....
。 -
二次探查:如果位置p被佔用,則探查
p+1**2,p-1**2,p+2**2
。 - 二度雜湊:有n個雜湊函式,當使用第一個雜湊函式h1發生衝突時,則使用h2。
-
雜湊表的快速查詢可以以空間換時間,需要保證元素個數除以陣列容積小於0.5,這個比值就是裝載率。
拉鍊法:雜湊表的每個位置都連線一個連結串列,當衝突發生時,衝突的元素被加到該位置連結串列的最後。 - 拉鍊表需要保證每一個連結串列的長度都不要太長。
- 拉鍊法的裝載率是可以大於一的。
- 插入、查詢等操作的時間複雜度是O(1)的。
雜湊在python中的應用
- 字典和集合都是通過雜湊表來實現的
- 集合可以看作沒有value的字典,因為集合也有不重複的性質。
- 通過雜湊函式把字典的鍵對映為函式:
dic = {'name':'cui'} #可以認為是h('name')=1,則雜湊表為[None,'cui']