基礎資料結構 例:棧、佇列、連結串列、資料、字典、樹、等
目錄
資料結構:
棧
佇列
連結串列
3.1 單向連結串列
3.2 雙向連結串列
3.3 單向連結串列反轉
陣列
字典實現原理
5.1 雜湊表
5.2 雜湊函式
樹
6.1 二叉樹、滿二叉樹、完全二叉樹
6.2 hash樹
6.3 B-tree/B+tree
棧 stack
棧(stack)又名堆疊,它是一種運算受限的線性表。限定僅在表尾進行插入和刪除操作的線性表。這一端被稱為棧頂,把另一端稱為棧底。向一個棧插入新元素又稱作 進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。
棧作為一種資料結構,是一種只能在一端進行插入和刪除操作的特殊線性表。它按照先進後出的原則儲存資料,先進入的資料被壓入棧底,最後的資料在棧頂,需要讀資料的時候從棧頂開始彈出資料(最後一個數據被第一個讀出來)。棧具有記憶作用,對棧的插入與刪除操作中,不需要改變棧底指標。
棧是允許在同一端進行插入和刪除操作的特殊線性表。允許進行插入和刪除操作的一端稱為棧頂(top),另一端為棧底(bottom);棧底固定,而棧頂浮動;棧中元素個數為零時稱為空棧。插入一般稱為進棧(PUSH),刪除則稱為退棧(POP)。棧也稱為先進後出表。
棧可以用來在函式呼叫的時候儲存斷點,做遞迴時要用到棧!
棧的特點:
後進先出,最後插入的元素最先出來。
Python實現
# 棧的順序表實現 class Stack(object): def __init__(self): self.items = [] def isEmpty(self): return self.items == [] def push(self, item): return self.items.append(item) def pop(self): return self.items.pop() def top(self): return self.items[len(self.items)-1] def size(self): return len(self.items) if __name__ == '__main__': stack = Stack() stack.push("Hello") stack.push("World") stack.push("!") print(stack.size()) print(stack.top()) print(stack.pop()) print(stack.pop()) print(stack.pop()) # 結果 3 ! ! World Hello
# 棧的連結表實現 class SingleNode(object): def __init__(self, item): self.item = item self.next = None class Stack(object): def __init__(self): self._head = None def isEmpty(self): return self._head == None def push(self, item): node = SingleNode(item) node.next = self._head self._head = node def pop(self): cur = self._head self._head = cur.next return cur.item def top(self): return self._head.item def size(self): cur = self._head count = 0 while cur != None: count += 1 cur = cur.next return count if __name__ == '__main__': stack = Stack() stack.push("Hello") stack.push("World") stack.push("!") print(stack.size()) print(stack.top()) print(stack.pop()) print(stack.pop()) print(stack.pop()) # 結果 3 ! ! World Hello
佇列
佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,佇列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。佇列中沒有元素時,稱為空佇列。
佇列的資料元素又稱為佇列元素。在佇列中插入一個佇列元素稱為入隊,從佇列中刪除一個佇列元素稱為出隊。因為佇列只允許在一端插入,在另一端刪除,所以只有最早進入佇列的元素才能最先從佇列中刪除,故佇列又稱為先進先出(FIFO—first in first out)線性表.
佇列的特點:
先進者先出,最先插入的元素最先出來。
我們可以想象成,排隊買票,先來的先買,後來的只能在末尾,不允許插隊。
佇列的兩個基本操作:入隊 將一個數據放到佇列尾部;出隊 從佇列的頭部取出一個元素。佇列也是一種操作受限的線性表資料結構 它具有先進先出的特性,支援隊尾插入元素,在隊頭刪除元素。
佇列的概念很好理解,佇列的應用也非常廣泛如:迴圈佇列、阻塞佇列、併發佇列、優先順序佇列等。下面將會介紹。
順序佇列:
順序佇列就是我們常說的 “FIFO”(先進先出)佇列,它主要包括的方法有:取第一個元素(first方法)、進入佇列(enqueue方法)、出佇列(dequeue方法)、清空佇列(clear方法)、判斷佇列是否為空(empty方法)、獲取佇列長度(length方法),具體的實現看下面的原始碼。同樣,這裡在取佇列的第一個元素、以及出佇列的時候,也需要判斷下佇列是否為空。
class Queue: """ 順序佇列實現 """ def __init__(self): """ 初始化一個空佇列,因為佇列是私有的 """ self.__queue = [] def first(self): """ 獲取佇列的第一個元素 :return:如果佇列為空,則返回None,否則返回第一個元素 """ return None if self.isEmpty() else self.__queue[0] def enqueue(self, obj): """ 將元素加入佇列 :param obj:要加入佇列的物件 """ self.__queue.append(obj) def dequeue(self): """ 從佇列中刪除第一個元素 :return:如果佇列為空,則返回None,否則返回dequeued元素 """ return None if self.isEmpty() else self.__queue.pop(0) def clear(self): """ 清除整個佇列 """ self.__queue.clear() def isEmpty(self): """ 判斷佇列是否為空 返回:bool值 """ return self.length() == 0 def length(self): """ 獲取佇列長度 並返回 """ return len(self.__queue)
優先佇列:
優先佇列簡單說就是一個有序佇列,排序的規則就是自定義的優先順序大小。在下面的程式碼實現中,主要使用的是數值大小進行比較排序,數值越小則優先順序越高,理論上應該把優先順序高的放在佇列首位。值得注意的是,筆者這裡用list來實現的時候恰好順序是反的,即list中元素是從大到小的順序,這樣做的好處是取佇列的第一個元素、以及出佇列這兩個操作的時間複雜度為O(1),僅僅入佇列操作的時間複雜度為O(n)。如果是按照從小到大的順序,那麼將會產生兩個時間複雜度為O(n),一個時間複雜度為O(1)。
class PriorQueue: """ 優先佇列實現 """ def __init__(self, objs=[]): """ 初始化優先佇列,預設佇列為空 :引數objs:物件列表初始化 """ self.__prior_queue = list(objs) # 排序從最大值到最小值,最小值具有最高的優先順序 # 使得“dequeue”的效率為O(1) self.__prior_queue.sort(reverse=True) def first(self): """ 獲取優先佇列的最高優先順序元素O(1) :return:如果優先佇列為空,則返回None,否則返回優先順序最高的元素 """ return None if self.isEmpty() else self.__prior_queue[-1] def enqueue(self, obj): """ 將一個元素加入優先佇列,O(n) :param obj:要加入佇列的物件 """ index = self.length() while index > 0: if self.__prior_queue[index - 1] < obj: index -= 1 else: break self.__prior_queue.insert(index, obj) def dequeue(self): """ 從優先佇列中取出最高優先順序的元素,O(1) :return:如果優先佇列為空,則返回None,否則返回dequeued元素 """ return None if self.isEmpty() else self.__prior_queue.pop() def clear(self): """ 清除整個優先佇列 """ self.__prior_queue.clear() def isEmpty(self): """ 判斷優先佇列是否為空 返回:bool值 """ return self.length() == 0 def length(self): """ 獲取優先佇列的長度 返回: """ return len(self.__prior_queue)
迴圈佇列:
迴圈佇列,就是將普通的佇列首尾連線起來, 形成一個環狀,並分別設定首尾指標,用來指明佇列的頭和尾。每當我們插入一個元素,尾指標就向後移動一位,當然,在這裡我們佇列的最大長度是提前定義好的,當我們彈出一個元素,頭指標就向後移動一位。
這樣,列表中就不存在刪除操作,只有修改操作,從而避免了刪除前面節點造成的代價大的問題。
class Loopqueue: ''' 迴圈佇列實現 ''' def __init__(self, length): self.head = 0 self.tail = 0 self.maxSize = length self.cnt = 0 self.__list = [None]*length def __len__(self): ''' 定義長度 ''' return self.cnt def __str__(self): ''' 定義返回值 型別 ''' s = '' for i in range(self.cnt): index = (i + self.head) % self.maxSize s += str(self.__list[index])+' ' return s def isEmpty(self): ''' 判斷是否為空 ''' return self.cnt == 0 def isFull(self): ''' 判斷是否裝滿 ''' return self.cnt == self.maxSize def push(self, data): ''' 新增元素 ''' if self.isFull(): return False if self.isEmpty(): self.__list[0] = data self.head = 0 self.tail = 0 self.cnt = 1 return True self.tail = (self.tail+1)%self.maxSize self.cnt += 1 self.__list[self.tail] = data return True def pop(self): ''' 彈出元素 ''' if self.isEmpty(): return False data = self.__list[self.head] self.head = (self.head+1)%self.maxSize self.cnt -= 1 return data def clear(self): ''' 清空佇列 ''' self.head = 0 self.tail = 0 self.cnt = 0 return True
阻塞佇列
阻塞佇列是一個在佇列基礎上又支援了兩個附加操作的佇列。
阻塞(併發)佇列與普通佇列的區別在於,當佇列是空的時,從佇列中獲取元素的操作將會被阻塞,或者當佇列是滿時,往佇列裡新增元素的操作會被阻塞。試圖從空的阻塞佇列中獲取元素的執行緒將會被阻塞,直到其他的執行緒往空的佇列插入新的元素。同樣,試圖往已滿的阻塞佇列中新增新元素的執行緒同樣也會被阻塞,直到其他的執行緒使佇列重新變得空閒起來,如從佇列中移除一個或者多個元素,或者完全清空佇列.
2個附加操作:
支援阻塞的插入方法:佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿。
支援阻塞的移除方法:佇列空時,獲取元素的執行緒會等待佇列變為非空。
怎麼實現阻塞佇列?當然是靠鎖,但是應該怎麼鎖?一把鎖能在not_empty,not_full,all_tasks_done三個條件之間共享。好比說,現在有執行緒A執行緒B,他們準備向佇列put任務,佇列的最大長度是5,執行緒A在put時,還沒完事,執行緒B就開始put,佇列就塞不下了,所以當執行緒A搶佔到put權時應該加把鎖,不讓執行緒B對佇列操作。鬼畜區經常看到的計數君,線上程中也同樣重要,每次put完unfinished要加一,get完unfinished要減一。
import threading #引入執行緒 上鎖 import time from collections import deque # 匯入佇列 class BlockQueue: def __init__(self, maxsize=0): ''' 一把鎖三個條件(self.mutex,(self.not_full, self.not_empty, self.all_task_done)), 最大長度與計數君(self.maxsize,self.unfinished) ''' self.mutex = threading.Lock() # 執行緒上鎖 self.maxsize = maxsize self.not_full = threading.Condition(self.mutex) self.not_empty = threading.Condition(self.mutex) self.all_task_done = threading.Condition(self.mutex) self.unfinished = 0 def task_done(self): ''' 每一次put完都會呼叫一次task_done,而且呼叫的次數不能比佇列的元素多。計數君對應的方法, unfinished<0時的意思是呼叫task_done的次數比列表的元素多,這種情況就會丟擲異常。 ''' with self.all_task_done: unfinish = self.unfinished - 1 if unfinish <= 0: if unfinish < 0: raise ValueError("The number of calls to task_done() is greater than the number of queue elements") self.all_task_done.notify_all() self.unfinished = unfinish def join(self): ''' 阻塞方法,是一個十分重要的方法,但它的實現也不難,只要沒有完成任務就一直wait(), 就是當計數君unfinished > 0 時就一直wait()知道unfinished=0跳出迴圈。 ''' with self.all_task_done: while self.unfinished: self.all_task_done.wait() def put(self, item, block=True, timeout=None): ''' block=True一直阻塞直到有一個空閒的插槽可用,n秒內阻塞,如果在那個時間沒有空閒的插槽,則會引發完全的異常。 Block=False如果一個空閒的槽立即可用,則在佇列上放置一個條目,否則就會引發完全的異常(在這種情況下,“超時”將被忽略)。 有空位,新增到佇列沒結束的任務+1,他最後要喚醒not_empty.notify(),因為一開始是要在沒滿的情況下加鎖,滿了就等待not_full.wait, 當put完以後就有東西了,每當一個item被新增到佇列時,通知not_empty等待獲取的執行緒會被通知。 ''' with self.not_full: if self.maxsize > 0: if not block: if self._size() >= self.maxsize: raise Exception("The BlockQueue is full") elif timeout is not None: self.not_full.wait() elif timeout < 0: raise Exception("The timeout period cannot be negative") else: endtime = time.time() + timeout while self._size() >= self.maxsize: remaining = endtime - time.time() if remaining <= 0.0: raise Exception("The BlockQueue is full") self.not_full.wait(remaining) self.queue.append(item) self.unfinished += 1 self.not_empty.notify() def get(self, block=True, timeout=None): ''' 如果可選的args“block”為True,並且timeout是None,則在必要時阻塞,直到有一個專案可用為止。 如果“超時”是一個非負數,它會阻塞n秒,如果在那個時間內沒有可get()的項,則會丟擲空異常。 否則'block'是False,如果立即可用,否則就會丟擲空異常,在這種情況下會忽略超時。 同理要喚醒not_full.notify ''' with self.not_empty: if not block: if self._size() <= 0: raise Exception("The queue is empty and you can't call get ()") elif timeout is None: while not self._size(): self.not_empty.wait() elif timeout < 0: raise ValueError("The timeout cannot be an integer") else: endtime = time.time() + timeout remaining = endtime - time.time() if remaining <= 0.0: raise Exception("The BlockQueue is empty") self.not_empty.wait(remaining) item = self._get() self.not_full.notify() return item
阻塞佇列 摘選地址
併發佇列:
在併發佇列上JDK提供了兩套實現,
一個是以ConcurrentLinkedQueue為代表的高效能佇列非阻塞,
一個是以BlockingQueue介面為代表的阻塞佇列,無論哪種都繼承自Queue。
鏈式佇列:
鏈式佇列在建立一個佇列時,隊頭指標front和隊尾指標rear指向結點的data域和next域均為空。
class QueueNode(): def __init__(self): self.data = None self.next = None class LinkQueue(): def __init__(self): tQueueNode = QueueNode() self.front = tQueueNode self.rear = tQueueNode '''判斷是否為空''' def IsEmptyQueue(self): if self.front == self.rear: iQueue = True else: iQueue = False return iQueue '''進佇列''' def EnQueue(self,da): tQueueNode = QueueNode() tQueueNode.data = da self.rear.next = tQueueNode self.rear = tQueueNode print("當前進隊的元素為:",da) '''出佇列''' def DeQueue(self): if self.IsEmptyQueue(): print("佇列為空") return else: tQueueNode = self.front.next self.front.next = tQueueNode.next if self.rear == tQueueNode: self.rear = self.front return tQueueNode.data def GetHead(self): if self.IsEmptyQueue(): print("佇列為空") return else: return self.front.next.data def CreateQueueByInput(self): data = input("請輸入元素(回車鍵確定,#結束)") while data != "#": self.EnQueue(data) data = input("請輸入元素(回車鍵確定,#結束)") '''遍歷順序佇列內的所有元素''' def QueueTraverse(self): if self.IsEmptyQueue(): print("佇列為空") return else: while self.front != self.rear: result = self.DeQueue() print(result,end = ' ') lq = LinkQueue() lq.CreateQueueByInput() print("佇列裡元素為:") lq.QueueTraverse() # 結果 請輸入元素(回車鍵確定,#結束)5 當前進隊的元素為:5 請輸入元素(回車鍵確定,#結束)8 當前進隊的元素為:8 請輸入元素(回車鍵確定,#結束)9 當前進隊的元素為:9 請輸入元素(回車鍵確定,#結束)# 佇列的元素為: 5 8 9
連結串列
連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列由一系列結點(連結串列中每一個元素稱為結點)組成,結點可以在執行時動態生成。每個結點包括兩個部分:一個是儲存資料元素的資料域,另一個是儲存下一個結點地址的指標域。 相比於線性表順序結構,操作複雜。由於不必須按順序儲存,連結串列在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查詢一個節點或者訪問特定編號的節點則需要O(n)的時間,而線性表和順序表相應的時間複雜度分別是O(logn)和O(1)。
連結串列是一種物理儲存結構上非連續,非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。
連結串列的結構是多式多樣的,當時通常用的也就是兩種:
無頭單向非迴圈列表:結構簡單,一般不會單獨用來存放資料。實際中更多是作為其他資料結構的子結構,比如說雜湊桶等等。
帶頭雙向迴圈連結串列:結構最複雜,一般單獨儲存資料。實際中經常使用的連結串列資料結構,都是帶頭雙向迴圈連結串列。這個結構雖然複雜,但是使用程式碼實現後會發現這個結構會帶來很多優勢,實現反而簡單了。
連結串列的優點
插入和刪除的效率高,只需要改變指標的指向就可以進行插入和刪除。
記憶體利用率高,不會浪費記憶體,可以使用記憶體中細小的不連續的空間,只有在需要的時候才去建立空間。大小不固定,拓展很靈活。
連結串列的缺點
查詢的效率低,因為連結串列是從第一個節點向後遍歷查詢。
AAA連結串列原文連結
單向連結串列
單向連結串列(單鏈表)是連結串列的一種,其特點是連結串列的連結方向是單向的,對連結串列的訪問要通過順序讀取從頭部開始;連結串列是使用指標進行構造的列表;又稱為結點列表,因為連結串列是由一個個結點組裝起來的;其中每個結點都有指標成員變數指向列表中的下一個結點;列表是由結點構成,head指標指向第一個成為表頭結點,而終止於最後一個指向NULL的指標。
單鏈表的每一個節點中只有指向下一個結點的指標,不能進行回溯,適用於節點的增加和刪除。
class Node(object): """節點""" def __init__(self, elem): self.elem = elem self.next = None # 初始設定下一節點為空 ''' 上面定義了一個節點的類,當然也可以直接使用python的一些結構。比如通過元組(elem, None) ''' # 下面建立單鏈表,並實現其應有的功能 class SingleLinkList(object): """單鏈表""" def __init__(self, node=None): # 使用一個預設引數,在傳入頭結點時則接收,在沒有傳入時,就預設頭結點為空 self.__head = node def is_empty(self): '''連結串列是否為空''' return self.__head == None def length(self): '''連結串列長度''' # cur遊標,用來移動遍歷節點 cur = self.__head # count記錄數量 count = 0 while cur != None: count += 1 cur = cur.next return count def travel(self): '''遍歷整個列表''' cur = self.__head while cur != None: print(cur.elem, end=' ') cur = cur.next print("\n") def add(self, item): '''連結串列頭部新增元素''' node = Node(item) node.next = self.__head self.__head = node def append(self, item): '''連結串列尾部新增元素''' node = Node(item) # 由於特殊情況當連結串列為空時沒有next,所以在前面要做個判斷 if self.is_empty(): self.__head = node else: cur = self.__head while cur.next != None: cur = cur.next cur.next = node def insert(self, pos, item): '''指定位置新增元素''' if pos <= 0: # 如果pos位置在0或者以前,那麼都當做頭插法來做 self.add(item) elif pos > self.length() - 1: # 如果pos位置比原連結串列長,那麼都當做尾插法來做 self.append(item) else: per = self.__head count = 0 while count < pos - 1: count += 1 per = per.next # 當迴圈退出後,pre指向pos-1位置 node = Node(item) node.next = per.next per.next = node def remove(self, item): '''刪除節點''' cur = self.__head pre = None while cur != None: if cur.elem == item: # 先判斷該節點是否是頭結點 if cur == self.__head: self.__head = cur.next else: pre.next = cur.next break else: pre = cur cur = cur.next def search(self, item): '''查詢節點是否存在''' cur = self.__head while not cur: if cur.elem == item: return True else: cur = cur.next return False if __name__ == "__main__": # node = Node(100) # 先建立一個節點傳進去 ll = SingleLinkList() print(ll.is_empty()) print(ll.length()) ll.append(3) ll.add(999) ll.insert(-3, 110) ll.insert(99, 111) print(ll.is_empty()) print(ll.length()) ll.travel() ll.remove(111) ll.travel()
雙向連結串列
雙向連結串列也叫雙鏈表,是連結串列的一種,它的每個資料結點中都有兩個指標,分別指向直接後繼和直接前驅。所以,從雙向連結串列中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點。一般我們都構造雙向迴圈連結串列。
雙鏈表的每一個節點給中既有指向下一個結點的指標,也有指向上一個結點的指標,可以快速的找到當前節點的前一個節點,適用於需要雙向查詢節點值的情況。
和單鏈表類似,只不過是增加了一個指向前面一個元素的指標而已。
class Node(object): def __init__(self,val,p=0): self.data = val self.next = p self.prev = p class LinkList(object): def __init__(self): self.head = 0 def __getitem__(self, key): if self.is_empty(): print 'linklist is empty.' return elif key <0 or key > self.getlength(): print 'the given key is error' return else: return self.getitem(key) def __setitem__(self, key, value): if self.is_empty(): print 'linklist is empty.' return elif key <0 or key > self.getlength(): print 'the given key is error' return else: self.delete(key) return self.insert(key) def initlist(self,data): self.head = Node(data[0]) p = self.head for i in data[1:]: node = Node(i) p.next = node node.prev = p p = p.next def getlength(self): p = self.head length = 0 while p!=0: length+=1 p = p.next return length def is_empty(self): if self.getlength() ==0: return True else: return False def clear(self): self.head = 0 def append(self,item): q = Node(item) if self.head ==0: self.head = q else: p = self.head while p.next!=0: p = p.next p.next = q q.prev = p def getitem(self,index): if self.is_empty(): print 'Linklist is empty.' return j = 0 p = self.head while p.next!=0 and j <index: p = p.next j+=1 if j ==index: return p.data else: print 'target is not exist!' def insert(self,index,item): if self.is_empty() or index<0 or index >self.getlength(): print 'Linklist is empty.' return if index ==0: q = Node(item,self.head) self.head = q p = self.head post = self.head j = 0 while p.next!=0 and j<index: post = p p = p.next j+=1 if index ==j: q = Node(item,p) post.next = q q.prev = post q.next = p p.prev = q def delete(self,index): if self.is_empty() or index<0 or index >self.getlength(): print 'Linklist is empty.' return if index ==0: q = Node(item,self.head) self.head = q p = self.head post = self.head j = 0 while p.next!=0 and j<index: post = p p = p.next j+=1 if index ==j: post.next = p.next p.next.prev = post def index(self,value): if self.is_empty(): print 'Linklist is empty.' return p = self.head i = 0 while p.next!=0 and not p.data ==value: p = p.next i+=1 if p.data == value: return i else: return -1 l = LinkList() l.initlist([1,2,3,4,5]) print l.getitem(4) l.append(6) print l.getitem(5) l.insert(4,40) print l.getitem(3) print l.getitem(4) print l.getitem(5) l.delete(5) print l.getitem(5) l.index(5) # 結果 5 6 4 40 5 6
雙鏈表相對於單鏈表的優點:
刪除單鏈表中的某個節點時,一定要得到待刪除節點的前驅,得到其前驅的方法一般是在定位待刪除節點的時候一路儲存當前節點的前驅,這樣指標的總的的移動操作為2n次,如果是用雙鏈表,就不需要去定位前驅,所以指標的總的的移動操作為n次。
查詢時也是一樣的,可以用二分法的思路,從頭節點向後和尾節點向前同時進行,這樣效率也可以提高一倍,但是為什麼市場上對於單鏈表的使用要超過雙鏈表呢?從儲存結構來看,每一個雙鏈表的節點都比單鏈表的節點多一個指標,如果長度是n,就需要n*lenght(32位是4位元組,64位是8位元組)的空間,這在一些追求時間效率不高的應用下就不適用了,因為他佔的空間大於單鏈表的1/3,所以設計者就會一時間換空間。
單向連結串列的反轉
名如其意:將單鏈表進行反轉。
舉例:將 1234 反轉為 4321 怎麼操作
1234->2134->2314->2341->3241->3421->4321,這樣也太費勁了,類似氣泡排序了
應該是每次把n後面的數字放到前面來,1234n->1n234->21n34->321n4->4321n
那麼接下來用 Python 實現
第一種方法 迴圈方法
迴圈方法的思想是建立三個變數,分別指向當前結點,當前結點的前一個結點,新的head結點,從head開始,每次迴圈將相鄰兩個結點的方向反轉。當整個連結串列迴圈遍歷過一遍之後,連結串列的方向就被反轉過來了。
class ListNode: def __init__(self, x): self.val = x self.next = None def reverse(head): # 迴圈的方法反轉連結串列 if head is None or head.next is None: return head # 定義反轉的初始狀態 pre = None cur = head newhead = head while cur: newhead = cur tmp = cur.next cur.next = pre pre = cur cur = tmp return newhead if __name__ == '__main__': head = ListNode(1) # 測試程式碼 p1 = ListNode(2) # 建立連結串列1->2->3->4->None; p2 = ListNode(3) p3 = ListNode(4) head.next = p1 p1.next = p2 p2.next = p3 p = reverse(head) # 輸出連結串列 4->3->2->1->None while p: print(p.val) p = p.next
第二種呢 就是 遞迴方法
根據遞迴的概念,我們只需要關注遞迴的基例條件,也就是遞迴的出口或遞迴的終止條件,以及長度為n的連結串列與長度為n-1的連結串列的關係即可
長度為n的連結串列的反轉結果,只需要將長度為n-1的連結串列反轉後,將連結串列最後指向None修改為指向長度為n的連結串列的head,並讓head指向None(或者說在連結串列與None之間新增長度為n的連結串列的head結點)
即反轉長度為n的連結串列,首先反轉n-1連結串列,然後再操作反轉好的連結串列與head結點的關係;至於n-1長度的連結串列怎麼反轉,只需要把它再拆分成node1和n-2的連結串列…
class ListNode: def __init__(self, x): self.val = x self.next = None def reverse(head, newhead): # 遞迴,head為原連結串列的頭結點,newhead為反轉後連結串列的頭結點 if head is None: return if head.next is None: newhead = head else: newhead = reverse(head.next, newhead) head.next.next = head head.next = None return newhead if __name__ == '__main__': head = ListNode(1) # 測試程式碼 p1 = ListNode(2) # 建立連結串列1->2->3->4->None p2 = ListNode(3) p3 = ListNode(4) head.next = p1 p1.next = p2 p2.next = p3 newhead = None p = reverse(head, newhead) # 輸出連結串列4->3->2->1->None while p: print(p.val) p = p.next
陣列
所謂陣列,是有序的元素序列。若將有限個型別相同的變數的集合命名,那麼這個名稱為陣列名。組成陣列的各個變數稱為陣列的分量,也稱為陣列的元素,有時也稱為下標變數。用於區分陣列的各個元素的數字編號稱為下標。陣列是在程式設計中,為了處理方便, 把具有相同型別的若干元素按無序的形式組織起來的一種形式。 這些無序排列的同類資料元素的集合稱為陣列。
陣列是用於儲存多個相同型別資料的集合。
Python 沒有內建對陣列的支援,但可以使用 Python 列表代替。
一維陣列
一維陣列是最簡單的陣列,其邏輯結構是線性表。要使用一維陣列,需經過定義、初始化和應用等過程。
在陣列的宣告格式裡,“資料型別”是宣告陣列元素的資料型別,可以是java語言中任意的資料型別,包括簡單型別和結構型別。“陣列名”是用來統一這些相同資料型別的名稱,其命名規則和變數的命名規則相同。
陣列宣告之後,接下來便是要分配陣列所需要的記憶體,這時必須用運算子new,其中“個數”是告訴編譯器,所宣告的陣列要存放多少個元素,所以new運算子是通知編譯器根據括號裡的個數,在記憶體中分配一塊空間供該陣列使用。利用new運算子為陣列元素分配記憶體空間的方式稱為動態分配方式。
二維陣列
前面介紹的陣列只有一個下標,稱為一維陣列, 其陣列元素也稱為單下標變數。在實際問題中有很多量是二維的或多維的, 因此多維陣列元素有多個下標, 以標識它在陣列中的位置,所以也稱為多下標變數。
二維陣列在概念上是二維的,即是說其下標在兩個方向上變化, 下標變數在陣列中的位置也處於一個平面之中, 而不是象一維陣列只是一個向量。但是,實際的硬體儲存器卻是連續編址的, 也就是說儲存器單元是按一維線性排列的。如何在一維儲存器中存放二維陣列,可有兩種方式:一種是按行排列, 即放完一行之後順次放入第二行。另一種是按列排列, 即放完一列之後再順次放入第二列。在C語言中,二維陣列是按行排列的。在如上中,按行順次存放,先存放a[0]行,再存放a[1]行,最後存放a[2]行。每行中有四個元素也是依次存放。由於陣列a說明為
int型別,該型別佔兩個位元組的記憶體空間,所以每個元素均佔有兩個 位元組(圖中每一格為一位元組)。
三維陣列
三維陣列,是指維數為三的陣列結構。三維陣列是最常見的多維陣列,由於其可以用來描述三維空間中的位置或狀態而被廣泛使用。
三維陣列就是維度為三的陣列,可以認為它表示對該陣列儲存的內容使用了三個獨立參量去描述,但更多的是認為該陣列的下標是由三個不同的參量組成的。
陣列這一概念主要用在編寫程式當中,和數學中的向量、矩陣等概念有一定的差別,主要表現:在陣列內的元素可以是任意的相同資料型別,包括向量和矩陣。
對陣列的訪問一般是通過下標進行的。在三維陣列中,陣列的下標是由三個數字構成的,通過這三個數字組成的下標對陣列的內容進行訪問。
字元陣列
用來存放字元量的陣列稱為字元陣列。
字元陣列型別說明的形式與前面介紹的數值陣列相同。例如:char c[10]; 由於字元型和整型通用,也可以定義為int c[10]但這時每個陣列元素佔2個位元組的記憶體單元。
字元陣列也可以是二維或多維陣列,例如:char c[5][10];即為二維字元陣列
字典實現原理 NSDictionary
Python 中 dict 物件是表明了其是一個原始的Python資料型別,按照鍵值對的方式儲存,其中文名字翻譯為字典,顧名思義其通過鍵名查詢對應的值會有很高的效率,時間複雜度在常數級別O(1)
dict底層實現
在Python中,字典是通過 雜湊表 實現的。也就是說,字典是一個數組,而陣列的索引是鍵經過雜湊函式處理後得到的。
雜湊表
是根據關鍵碼值(Key value)而直接進行訪問的資料結構。它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度。
這個對映函式叫做雜湊函式,存放記錄的陣列叫做散列表。
給定表M,存在函式f(key),對任意給定的關鍵字值key,代入函式後若能得到包含該關鍵字的記錄在表中的地址,則稱表M為雜湊(Hash)表.
函式f(key)為雜湊(Hash) 函式。
>>> map(hash, (0, 1, 2, 3)) [0, 1, 2, 3] >>> map(hash, ("namea", "nameb", "namec", "named")) [-1658398457, -1658398460, -1658398459, -1658398462]
雜湊概念:雜湊表的本質是一個數組,陣列中每一個元素稱為一個箱子(bin),箱子中存放的是鍵值對。
雜湊函式
雜湊函式就是一個對映,因此雜湊函式的設定很靈活,只要使得任何關鍵字由此所得的雜湊函式值都落在表長允許的範圍之內即可;
並不是所有的輸入都只對應唯一一個輸出,也就是雜湊函式不可能做成一個一對一的對映關係,其本質是一個多對一的對映,這也就引出了下面一個概念–衝突。
衝突
只要不是一對一的對映關係,衝突就必然會發生,還是上面的極端例子,這時新加了一個員工號為2的員工,先不考慮我們的陣列大小已經定為2了,按照之前的雜湊函式,工號為2的員工雜湊值也是2,這與100000000001的員工一樣了,這就是一個衝突,針對不同的解決思路,提出三個不同的解決方法。
衝突解決方法
開放地址
開放地址的意思是除了雜湊函式得出的地址可用,當出現衝突的時候其他的地址也一樣可用,常見的開放地址思想的方法有線性探測再雜湊,二次探測再雜湊,這些方法都是在第一選擇被佔用的情況下的解決方法。
再雜湊法
這個方法是按順序規定多個雜湊函式,每次查詢的時候按順序呼叫雜湊函式,呼叫到第一個為空的時候返回不存在,呼叫到此鍵的時候返回其值。
鏈地址法
將所有關鍵字雜湊值相同的記錄都存在同一線性連結串列中,這樣不需要佔用其他的雜湊地址,相同的雜湊值在一條連結串列上,按順序遍歷就可以找到。
公共溢位區
其基本思想是:所有關鍵字和基本表中關鍵字為相同雜湊值的記錄,不管他們由雜湊函式得到的雜湊地址是什麼,一旦發生衝突,都填入溢位表。
裝填因子α
一般情況下,處理衝突方法相同的雜湊表,其平均查詢長度依賴於雜湊表的裝填因子。雜湊表的裝填因子定義為表中填入的記錄數和雜湊表長度的桌布,也就是標誌著雜湊表的裝滿程度。直觀看來,α越小,發生衝突的可能性就越小,反之越大。一般0.75比較合適,涉及數學推導。
雜湊儲存過程
1.根據 key 計算出它的雜湊值 h。
2.假設箱子的個數為 n,那麼這個鍵值對應該放在第 (h % n) 個箱子中。
3.如果該箱子中已經有了鍵值對,就使用開放定址法或者拉鍊法解決衝突。
在使用拉鍊法解決雜湊衝突時,每個箱子其實是一個連結串列,屬於同一個箱子的所有鍵值對都會排列在連結串列中。雜湊表還有一個重要的屬性: 負載因子(load factor),它用來衡量雜湊表的空/滿程度,一定程度上也可以體現查詢的效率,計算公式為:負載因子 = 總鍵值對數 / 箱子個數負載因子越大,意味著雜湊表越滿,越容易導致衝突,效能也就越低。因此,一般來說,當負載因子大於某個常數(可能是 1,或者 0.75 等)時,雜湊表將自動擴容。雜湊表在自動擴容時,一般會建立兩倍於原來個數的箱子,因此即使 key 的雜湊值不變,對箱子個數取餘的結果也會發生改變,因此所有鍵值對的存放位置都有可能發生改變,這個過程也稱為重雜湊(rehash)。雜湊表的擴容並不總是能夠有效解決負載因子過大的問題。假設所有 key 的雜湊值都一樣,那麼即使擴容以後他們的位置也不會變化。雖然負載因子會降低,但實際儲存在每個箱子中的連結串列長度並不發生改變,因此也就不能提高雜湊表的查詢效能。基於以上總結,細心的朋友可能會發現雜湊表的兩個問題:
1.如果雜湊表中本來箱子就比較多,擴容時需要重新雜湊並移動資料,效能影響較大。
2.如果雜湊函式設計不合理,雜湊表在極端情況下會變成線性表,效能極低。
Python中所有不可變的內建型別都是可雜湊的。
可變型別(如列表,字典和集合)就是不可雜湊的,因此不能作為字典的鍵。
樹
樹是一種資料結構,它是由n(n>=1)個有限結點組成一個具有層次關係的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:
每個結點有零個或多個子結點;沒有父結點的結點稱為根結點;每一個非根結點有且只有一個父結點;除了根結點外,每個子結點可以分為多個不相交的子樹;
樹是一種重要的非線性資料結構,直觀地看,它是資料元素(在樹中稱為結點)按分支關係組織起來的結構,很象自然界中的樹那樣。
樹的定義:
樹是由邊連線的節點或頂點的分層集合。樹不能有迴圈,並且只有節點和它的下降節點或子節點之間存在邊。同一父級的兩個子節點在它們之間不能有任何邊。每個節點可以有一個父節點除非是頂部節點,也稱為根節點。每棵樹只能有一個根節點。每個節點可以有零個或多個子節點。在下面的圖中,A是根節點,B、C和D是A的子節點。我們也可以說,A是B、C、D的父節點。B、C和D被稱為兄弟姐妹,因為它們是來自同一父節點A。
樹的種類:
無序樹:樹中任意節點的子結點之間沒有順序關係,這種樹稱為無序樹,也稱為自由樹;
有序樹:樹中任意節點的子結點之間有順序關係,這種樹稱為有序樹;
二叉樹:每個節點最多含有兩個子樹的樹稱為二叉樹;
完全二叉樹
滿二叉樹
哈夫曼樹:帶權路徑最短的二叉樹稱為哈夫曼樹或最優二叉樹;
樹的深度:
定義一棵樹的根結點層次為1,其他節點的層次是其父結點層次加1。一棵樹中所有結點的層次的最大值稱為這棵樹的深度。
二叉樹、滿二叉樹、完全二叉樹
二叉樹是一種特殊的樹:它或者為空,在二叉樹中每個節點最多有兩個子節點,一般稱為左子節點和右子節點(或左孩子和右孩子),並且二叉樹的子樹有左右之分,其次序不能任意顛倒。
滿二叉樹: 在一棵二叉樹中,如果所有分支結點都有左孩子和右孩子結點,並且葉子結點都集中在二叉樹的最下層,這樣的樹叫做滿二叉樹
完全二叉樹: 若二叉樹中最多隻有最下面兩層的結點的度數可以小於2,並且最下面一層的葉子結點都是依次排列在該層最左邊的位置上,則稱為完全二叉樹
區別: 滿二叉樹是完全二叉樹的特例,因為滿二叉樹已經滿了,而完全並不代表滿。所以形態你也應該想象出來了吧,滿指的是出了葉子節點外每個節點都有兩個孩子,而完全的含義則是最後一層沒有滿,並沒有滿。
二叉樹
在電腦科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查詢樹和二叉堆。
二叉樹是遞迴定義的,其結點有左右子樹之分,邏輯上二叉樹有五種基本形態:
(1)空二叉樹——如圖(a);
(2)只有一個根結點的二叉樹——如圖(b);
(3)只有左子樹——如圖(1);
(4)只有右子樹——如圖(3);
(5)完全二叉樹——如圖(3)。
注意:儘管二叉樹與樹有許多相似之處,但二叉樹不是樹的特殊情形。 [1]
hash樹
雜湊樹(或雜湊特里)是一種永續性資料結構,可用於實現集合和對映,旨在替換純函數語言程式設計中的雜湊表。 在其基本形式中,雜湊樹在trie中儲存其鍵的雜湊值(被視為位串),其中實際鍵和(可選)值儲存在trie的“最終”節點中
什麼是質數 : 即只能被 1 和 本身 整除的數。
為什麼用質數:因為N個不同的質數可以 ”辨別“ 的連續整數的數量,與這些質數的乘積相同。
也就是說如果有21億個數字的話,我們查詢的哪怕是最底層的也僅僅需要計算10次就能找到對應的數字。
所以hash樹是一棵為查詢而生的樹。
例如:
從2起的連續質數,連續10個質數就可以分辨大約M(10) =23571113171923*29= 6464693230 個數,已經超過計算機中常用整數(32bit)的表達範圍。連續100個質數就可以分辨大約M(100) = 4.711930 乘以10的219次方。
而按照目前的CPU水平,100次取餘的整數除法操作幾乎不算什麼難事。在實際應用中,整體的操作速度往往取決於節點將關鍵字裝載記憶體的次數和時間。一般來說,裝載的時間是由關鍵字的大小和硬體來決定的;在相同型別關鍵字和相同硬體條件下,實際的整體操作時間就主要取決於裝載的次數。他們之間是一個成正比的關係。
AAA雜湊樹參考原址
B-tree/B+tree
Btree
Btree是一種多路自平衡搜尋樹,它類似普通的二叉樹,但是Btree允許每個節點有更多的子節點。Btree示意圖如下:
由上圖可知 Btree 的一些特點:
所有鍵值分佈在整個樹中
任何關鍵字出現且只出現在一個節點中
搜尋有可能在非葉子節點結束
在關鍵字全集內做一次查詢,效能逼近二分查詢演算法
B+tree
B+樹是B樹的變體,也是一種多路平衡查詢樹,B+樹的示意圖為:
由圖可看出B+tree的特點 同時也是 Btree 和 B+tree的區別
所有關鍵字儲存在葉子節點,非葉子節點不儲存真正的data
為所有葉子節點增加了一個鏈指標 只有一個
總結:在資料儲存的索引結構上 Btree 更偏向於 縱向深度的儲存資料 而 B+tree 更青睞於 橫向廣度的儲存資料。
AAAbtree 的 參考原址