簡析資料結構
一、資料結構的定義
官方定義:
資料結構是指相互之間存在著一種或多種關係的資料元素的集合和該集合中資料元素之間的關係組成。
簡單理解:
資料結構就是設計資料以何種方式組織並存儲在計算機中。比如:列表、集合與字典等都是一種資料結構。
程式=資料結構+演算法
二、資料結構的分類
資料結構按照其邏輯結構可分為線性結構、樹結構、圖結構
線性結構:資料結構中的元素存在一對一的相互關係(列表)
樹結構:資料結構中的元素存在一對多的相互關係(堆)
圖結構:資料結構中的元素存在多對多的相互關係
-----python的列表
在其他程式語言中被稱為“陣列”,在資料結構中可以統稱為“順序表”,即在記憶體的儲存中是線性的。
列表和陣列的不同之處:
1.陣列要求儲存的資料型別是一樣的。python的列表沒有要求。
2.陣列的長度在宣告時就固定了,不可變的。python的列表長度可變。
python中的列表在資料結構學科中又被稱作是動態表,它在記憶體中的儲存方式大概是這樣的,當你建立一個列表時,系統會先分配一塊特定的記憶體,當列表中元素增滿時,系統會再從新開闢一塊之前容量2倍的新記憶體,然後將之前的資料copy到新開記憶體中,釋放原來的舊地址,反之也是。通過這種 動態擴張的形式讓 python的列表保證了長度可變。
陣列通過規定儲存的資料型別是相同的,同時是線性儲存的特點,通過 陣列的首地址和下標 的關係保證了查詢和儲存資料的高效。而python的列表通過 儲存元素記憶體指標的方式 既可以保證可以儲存不同資料型別的資料,又可以保證查詢和儲存的高效(python在記憶體儲存方面有一個小資料池的概念,它在python實現資料儲存上起到了很大的作用)。
----棧
棧(Stack)是一個數據集合,可以理解為只能在一端進行插入或刪除操作的列表。
棧的特點是:後進先出(last-in, first-out)棧有棧頂和棧底兩個基礎概念。
python語言實現棧的方式:
藉助python的列表及兩個基本的操作append和pop方法就可以實現棧。
class Stark(list): def __init__(self, *args, **kwargs): super(Stark, self).__init__(*args, **kwargs) def push(self, val): self.append(val) def _pop(self): return self.pop()
棧的實際應用(括號匹配問題)
括號匹配問題:
給一個字串,其中包含小括號、中括號、大括號,求該字串中的括號是否匹配。
例如:
()()[]{} 匹配
([{()}]) 匹配
[]( 不匹配
[(]) 不匹配
def brace_match(s): stack = [] match = {')':'(', ']':'[', '}':'{'} match2 = {'(':')', '[':']', '{':'}'} for ch in s: if ch in {'(', '[', '{'}: stack.append(ch) elif len(stack) == 0: print("缺少%s" % match[ch]) return False elif stack[-1] == match[ch]: stack.pop() else: print("括號不匹配") return False if len(stack) > 0: print("缺少%s" % (match2[stack[-1]])) return False return True
棧的應用——迷宮問題
給一個二維列表,表示迷宮(0表示通道,1表示圍牆)。給出演算法,求一條走出迷宮的路徑。

螢幕快照 2018-11-04 下午7.00.37.png
解決思路:
在一個迷宮節點(x,y)上,可以進行四個方向的探查:maze[x-1][y], maze[x+1][y], maze[x][y-1], maze[x][y+1]
思路:從一個節點開始,任意找下一個能走的點,當找不到能走的點時,退回上一個點尋找是否有其他方向的點。
方法:建立一個空棧,首先將入口位置進棧。當棧不空時迴圈:獲取棧頂元素,尋找下一個可走的相鄰方塊,如果找不到可走的相鄰方塊,說明當前位置是死衚衕,進行回溯(就是講當前位置出棧,看前面的點是否還有別的出路)
maze = [ [1,1,1,1,1,1,1,1,1,1], [1,0,0,1,0,0,0,1,0,1], [1,0,0,1,0,0,0,1,0,1], [1,0,0,0,0,1,1,0,0,1], [1,0,1,1,1,0,0,0,0,1], [1,0,0,0,1,0,0,0,0,1], [1,0,1,0,0,0,1,0,0,1], [1,0,1,1,1,0,1,1,0,1], [1,1,0,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1,1,1] ] dirs = [ lambda x,y:(x-1,y),#上 lambda x,y:(x,y+1),#右 lambda x,y:(x+1,y),#下 lambda x,y:(x,y-1),#左 ] def solve_maze(x1, y1, x2, y2): stack = [] stack.append((x1,y1)) maze[x1][y1] = 2 while len(stack) > 0:# 當棧不空迴圈 cur_node = stack[-1] if cur_node == (x2,y2): #到達終點 for p in stack: print(p) return True for dir in dirs:# 四個方向探查 next_node = dir(*cur_node) if maze[next_node[0]][next_node[1]] == 0:#找到一個能走的方向 stack.append(next_node) maze[next_node[0]][next_node[1]] = 2# 2表示已經走過的點 break else: #如果一個方向也找不到 stack.pop() else: print("無路可走") return False
----佇列
佇列(Queue)是一個數據集合,僅允許在列表的一端進行插入,另一端進行刪除。
進行插入的一端稱為 隊尾(rear) ,插入動作稱為進隊或入隊
進行刪除的一端稱為 隊頭(front) ,刪除動作稱為出隊
佇列的性質:先進先出(First-in, First-out)
雙向佇列:佇列的兩端都允許進行進隊和出隊操作。

螢幕快照 2018-11-04 下午5.38.49.png
佇列能否簡單用列表實現?為什麼?
初步設想:
列表+兩個下標指標
建立一個列表和兩個變數,front變數指向隊首,rear變數指向隊尾。初始時,front和rear都為0。
進隊操作:元素寫到li[rear]的位置,rear自增1。
出隊操作:返回li[front]的元素,front自減1。

螢幕快照 2018-11-04 下午5.41.31.png
佇列的實現原理——環形佇列

螢幕快照 2018-11-04 下午5.44.12.png
環形佇列:當隊尾指標front == Maxsize + 1時,再前進一個位置就自動到0。
實現方式:求餘數運算
隊首指標前進1:front = (front + 1) % MaxSize
隊尾指標前進1:rear = (rear + 1) % MaxSize
隊空條件:rear == front
隊滿條件:(rear + 1) % MaxSize == front
python內建的佇列模組
使用方法:from collections import deque
建立佇列:queue = deque(li)
進隊:append
出隊:popleft
雙向佇列隊首進隊:appendleft
雙向佇列隊尾進隊:pop
佇列的實際應用
linux命令中的head(檢視檔案前幾行)和tail(檢視檔案後幾行)
佇列解決迷宮問題
from collections import deque maze = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 1, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 1, 1, 0, 1], [1, 1, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ] dirs = [ lambda x, y: (x - 1, y),# 上 lambda x, y: (x, y + 1),# 右 lambda x, y: (x + 1, y),# 下 lambda x, y: (x, y - 1),# 左 ] def solve_maze2(x1, y1, x2, y2): queue = deque() path = []# 記錄出隊之後的節點 queue.append((x1, y1, -1)) maze[x1][y1] = 2 while len(queue) > 0: cur_node = queue.popleft() path.append(cur_node) if cur_node[0] == x2 and cur_node[1] == y2:# 到終點 real_path = [] x, y, i = path[-1] real_path.append((x, y)) while i >= 0: node = path[I] real_path.append(node[0:2]) i = node[2] real_path.reverse() for p in real_path: print(p) return True for dir in dirs:# 四個方向探查 next_node = dir(cur_node[0], cur_node[1]) if maze[next_node[0]][next_node[1]] == 0:# 可以走 queue.append((next_node[0], next_node[1], len(path) - 1)) maze[next_node[0]][next_node[1]] = 2# 標記為已經走過 else: print("無路可走") return False
----連結串列
連結串列中每一個元素都是一個物件,每個物件稱為一個節點,包含有資料域key和指向下一個節點的指標next。通過各個節點之間的相互連線,最終串聯成一個連結串列。
節點定義:

螢幕快照 2018-11-04 下午7.37.27.png
建立連結串列
頭插法 和 尾插法
建立連結串列.GIF
頭插法程式碼演示
class Node: def __init__(self, data): self.data = data self.next = None def create_linklist(li): head = Node(None)# 頭節點 for num in li: node = Node(num) node.next = head.next#先接後面的再接前面的,防止丟失資料 head.next = node return head def print_linklist(head): node = head.next while node: print(node.data) node = node.next
尾插法程式碼演示
class Node: def __init__(self, data): self.data = data self.next = None def create_linklist_tail(li): head = Node(None)# 空的頭節點 tail = head# 定義一個尾巴記錄一下 for num in li: node = Node(num) tail.next = node# 連結新資料 tail = node# 更新尾巴 return head def print_linklist(head): node = head.next while node: print(node.data) node = node.next
連結串列節點的插入和刪除:
插入:
p.next = curNode.next
curNode.next = p
刪除:
p = curNode.next
curNode.next = curNode.next.next
del p

連結串列的插入和刪除.GIF
雙向連結串列
雙鏈表中每個節點有兩個指標:一個指向後面節點、一個指向前面節點。
和連結串列的情況大體一致,當涉及到插入刪除是記得畫圖,直觀的保證資料的安全。
連結串列和順序表的對比:
連結串列在插入和刪除的操作上明顯快於順序表
連結串列的記憶體可以更靈活的分配
連結串列這種鏈式儲存的資料結構對樹和圖的結構有很大的啟發性
----雜湊表
雜湊表概念:
一個通過雜湊函式來計算資料儲存位置的資料結構,通常支援如下操作:
insert(key, value):插入鍵值對(key,value)-----鍵值就是字典,有鍵沒值就是集合
get(key):如果存在鍵為key的鍵值對則返回其value,否則返回空值
delete(key):刪除鍵為key的鍵值對
直接尋值表
當關鍵字的全域U比較小時,直接定址是一種簡單而有效的方法。

螢幕快照 2018-11-04 下午8.45.48.png
直接定址技術缺點:
當域U很大時,需要消耗大量記憶體,很不實際
如果域U很大而實際出現的key很少,則大量空間被浪費
無法處理關鍵字不是數字的情況
雜湊
直接定址表:key為k的元素放到k位置上
改進直接定址表:雜湊(Hashing)
構建大小為m的定址表T
key為k的元素放到h(k)位置上---- h為雜湊函式
h(k)是一個函式,其將域U對映到表T[0,1,...,m-1]
雜湊表
雜湊表(Hash Table,又稱為散列表),是一種線性表的儲存結構。雜湊表由一個直接定址表和一個雜湊函式組成。雜湊函式h(k)將元素關鍵字k作為自變數,返回元素的儲存下標。
簡單雜湊函式:
除法雜湊:h(k) = k mod m (取餘數)
乘法雜湊:h(k) = floor(m(kA mod 1)) 0<A<1
假設有一個長度為7的陣列,雜湊函式h(k)=k mod 7。元素集合{14,22,3,5}的儲存方式如下圖。

螢幕快照 2018-11-04 下午8.54.26.png
雜湊衝突
由於雜湊表的大小是有限的,而要儲存的值的總數量是無限的,因此對於任何雜湊函式,都會出現兩個不同元素對映到同一個位置上的情況,這種情況叫做雜湊衝突。
比如:h(k)=k mod 7, h(0)=h(7)=h(14)=...
解決雜湊衝突
開放定址法:
開放定址法:如果雜湊函式返回的位置已經有值,則可以向後探查新的位置來儲存這個值。
線性探查:如果位置i被佔用,則探查i+1, i+2,……
二次探查:如果位置i被佔用,則探查i+12,i-12,i+22,i-22,……
二度雜湊:有n個雜湊函式,當使用第1個雜湊函式h1發生衝突時,則嘗試使用h2,h3,……
拉鍊法
拉鍊法:雜湊表每個位置都連線一個連結串列(連結串列的插入和查詢的時間複雜度都是O(1)的),當衝突發生時,衝突的元素將被加到該位置連結串列的最後。

拉鍊法
雜湊表在Python中的應用
字典與集合都是通過雜湊表來實現的。
在Python中的字典:a = {'name': 'wangjifei', 'age': 27, 'gender': 'Man'}
使用雜湊表儲存字典,通過雜湊函式將字典的鍵對映為下標。
假設h(‘name’) = 3, h(‘age’) = 1, h(‘gender’) = 4,則雜湊表儲存為[None, 27, None, ’wangjifei’, ‘Man’]
-----二叉樹
二叉樹的鏈式儲存:
將二叉樹的節點定義為一個物件,節點之間通過類似連結串列的連結方式來連線。

螢幕快照 2018-11-04 下午9.12.40.png
二叉樹的遍歷:
前序遍歷(根--左--右):EACBDGF
中序遍歷(左--根--右):ABCDEGF
後序遍歷(左--右--根):BDCAFGE
層次遍歷:EAGCFBD

螢幕快照 2018-11-04 下午9.14.28.png
有一種題是:給前中後三種遍歷結果的其中兩種順序,畫出二叉樹來
解題思路: 前序定根,中序分左右
二叉樹的四種遍歷程式碼實現:
from collections import deque class BiTreeNode: # 構建二叉樹 def __init__(self, data): self.data = data self.lchild = None self.rchild = None a = BiTreeNode('A') b = BiTreeNode('B') c = BiTreeNode('C') d = BiTreeNode('D') e = BiTreeNode('E') f = BiTreeNode('F') g = BiTreeNode('G') e.lchild = a e.rchild = g a.rchild = c c.lchild = b c.rchild = d g.rchild = f root = e def pre_order(root): # 前序遍歷 if root: print(root.data, end='') pre_order(root.lchild) pre_order(root.rchild) def in_order(root): # 中序遍歷 if root: in_order(root.lchild) print(root.data, end='') in_order(root.rchild) def post_order(root): # 後序遍歷 if root: post_order(root.lchild) post_order(root.rchild) print(root.data, end='') def level_order(root): # 層次遍歷(佇列實現) queue = deque() queue.append(root) while len(queue) > 0: node = queue.popleft() print(node.data, end='') if node.lchild: queue.append(node.lchild) if node.rchild: queue.append(node.rchild)
樹還有 二叉搜尋樹 和 AVL樹 ,如果有想進一步瞭解的,自行網上查詢資料吧!!!