算法基础之数据结构
what's the 數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)是指相互之間存在著一種或多種關(guān)系的數(shù)據(jù)元素的集合和該集合中數(shù)據(jù)元素之間的關(guān)系組成。 簡單來說,數(shù)據(jù)結(jié)構(gòu)就是設(shè)計(jì)數(shù)據(jù)以何種方式組織并存儲在計(jì)算機(jī)中。 比如:列表、集合與字典等都是一種數(shù)據(jù)結(jié)構(gòu)。?
通常情況下,精心選擇的數(shù)據(jù)結(jié)構(gòu)可以帶來更高的運(yùn)行或者存儲效率。數(shù)據(jù)結(jié)構(gòu)往往同高效的檢索算法和索引技術(shù)有關(guān)。
數(shù)據(jù)結(jié)構(gòu)按照其邏輯結(jié)構(gòu)可分為線性結(jié)構(gòu)、樹結(jié)構(gòu)、圖結(jié)構(gòu):
- 線性結(jié)構(gòu):數(shù)據(jù)結(jié)構(gòu)中的元素存在一對一的相互關(guān)系
- 樹結(jié)構(gòu):數(shù)據(jù)結(jié)構(gòu)中的元素存在一對多的相互關(guān)系
- 圖結(jié)構(gòu):數(shù)據(jù)結(jié)構(gòu)中的元素存在多對多的相互關(guān)系
棧
棧(Stack)是一個數(shù)據(jù)集合,它是一種運(yùn)算受限的線性表。其限制是僅允許在表的一端進(jìn)行插入和刪除運(yùn)算。這一端被稱為棧頂,相對地,把另一端稱為棧底。可以將棧理解為只能在一端進(jìn)行插入或刪除操作的列表。
棧的特點(diǎn):后進(jìn)先出、先進(jìn)后出(類似于往箱子里放東西,要拿的時候只能拿最上面的而最上面的是最后進(jìn)的)
棧操作:進(jìn)棧push、出棧pop、取棧頂gettop
在Python中,不用自定義棧,直接用列表就行,進(jìn)棧函數(shù):append 出棧函數(shù):pop 查看棧頂函數(shù):li[-1]
棧的應(yīng)用
括號匹配問題:給一個字符串,其中包含小括號、中括號、大括號,求該字符串中的括號是否匹配。
基本思路:按順序遍歷字符串是左括號則進(jìn)棧,來的是右括號則將棧頂左括號pop,若來的右括號與棧頂左括號不匹配或空棧情況下來了右括號則返回錯誤信息
def brace_match(s):stack = []d = {'(':')', '[':']', '{':'}'}for ch in s:if ch in {'(', '[', '{'}:stack.append(ch)elif len(stack) == 0:print('多了右括號%s' % ch)return Falseelif d[stack[-1]] == ch:stack.pop()else:print('括號%s處不匹配' % ch)return Falseif len(stack) == 0:return Trueelse:print("剩余左括號未匹配")return Falseprint(brace_match('[]{{}[]{()})}'))用兩個棧實(shí)現(xiàn)隊(duì)列
class QueueStack(object):def __init__(self):self.l1 = []self.l2 = []def push(self,a):self.l1.append(a)def pop(self):if not self.l2:for i in range(len(self.l1)):a = self.l1.pop()self.l2.append(a)if self.l2:return self.l2.pop()else:return False隊(duì)列
隊(duì)列(Queue)是一個數(shù)據(jù)集合,僅允許在列表的一端進(jìn)行插入,另一端進(jìn)行刪除。
進(jìn)行插入的一端稱為隊(duì)尾(rear),插入動作稱為進(jìn)隊(duì)或入隊(duì);
進(jìn)行刪除的一端稱為隊(duì)頭(front),刪除動作稱為出隊(duì)。和棧一樣,隊(duì)列是一種操作受限制的線性表。
隊(duì)列的性質(zhì):先進(jìn)先出(可以將隊(duì)列理解為排隊(duì)買東西)
特殊情況——雙向隊(duì)列:隊(duì)列的兩端都允許進(jìn)行進(jìn)隊(duì)和出隊(duì)操作。
如何用列表實(shí)現(xiàn)隊(duì)列:
以上就是隊(duì)列實(shí)現(xiàn)的基本思路,但是隊(duì)列出隊(duì)之后,前面的空間被浪費(fèi)了,所以實(shí)際情況中隊(duì)列的實(shí)現(xiàn)原理是一個環(huán)形隊(duì)列
環(huán)形隊(duì)列:當(dāng)隊(duì)尾指針front == Maxsize + 1時,再前進(jìn)一個位置就自動到0。
- 實(shí)現(xiàn)方式:求余數(shù)運(yùn)算
- 隊(duì)首指針前進(jìn)1:front = (front + 1) % MaxSize
- 隊(duì)尾指針前進(jìn)1:rear = (rear + 1) % MaxSize
- 隊(duì)空條件:rear == front
- 隊(duì)滿條件:(rear + 1) % MaxSize == front
?
在Python中,有一個內(nèi)置模塊可以幫我們快速建立起一個隊(duì)列——deque模塊
使用方法:from collections import deque
創(chuàng)建隊(duì)列:queue = deque(li)
進(jìn)隊(duì):append()
出隊(duì):popleft()
雙向隊(duì)列隊(duì)首進(jìn)隊(duì):appendleft()
雙向隊(duì)列隊(duì)尾進(jìn)隊(duì):pop()
棧和隊(duì)列的應(yīng)用
求走出迷宮的路徑
用棧解決迷宮問題
基本思路:在一個迷宮節(jié)點(diǎn)(x,y)上,可以進(jìn)行四個方向的探查:maze[x-1][y](表示上), maze[x+1][y](下), maze[x][y-1](左), maze[x][y+1](右)
思路:思路:從一個節(jié)點(diǎn)開始,任意找下一個能走的點(diǎn),當(dāng)找不到能走的點(diǎn)時,退回上一個點(diǎn)尋找是否有其他方向的點(diǎn)
方法:創(chuàng)建一個空棧,首先將入口位置進(jìn)棧。當(dāng)棧不空時循環(huán):獲取棧頂元素,尋找下一個可走的相鄰方塊,如果找不到可走的相鄰方塊,說明當(dāng)前位置是死胡同,進(jìn)行回溯(就是講當(dāng)前位置出棧,看前面的點(diǎn)是否還有別的出路)
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 stack_solve_maze(x1, y1, x2, y2):""":param x1: 起點(diǎn)x坐標(biāo):param y1: 起點(diǎn)y坐標(biāo):param x2: 終點(diǎn)x坐標(biāo):param y2: 終點(diǎn)y坐標(biāo):return:"""stack = []stack.append((x1,y1)) # 起點(diǎn)maze[x1][y1] = 2 # 2表示已經(jīng)走過的點(diǎn),我們要將已經(jīng)走過的點(diǎn)進(jìn)行標(biāo)識,免得走重復(fù)的路while len(stack) > 0: # 當(dāng)棧不空循環(huán)cur_node = stack[-1] # 棧頂,即目前所在位置if cur_node == (x2,y2): # 到達(dá)終點(diǎn)for p in stack:print('==>',p,end='') # 依次輸出棧內(nèi)坐標(biāo)return True# 沒到終點(diǎn)時,在任意位置都要試探上下左右是否走得通for dir in dirs:next_node = dir(*cur_node)if maze[next_node[0]][next_node[1]] == 0: # 0是通道,說明找到一個能走的方向stack.append(next_node)maze[next_node[0]][next_node[1]] = 2 # 2表示已經(jīng)走過的點(diǎn)breakelse: # 如果一個方向也找不到,說明到死胡同了stack.pop()else:print("無路可走")return Falsestack_solve_maze(1,1,8,8) # ==> (1, 1)==> (1, 2)==> (2, 2)==> (3, 2)==> (3, 1)==> (4, 1)==> (5, 1)==> (5, 2)==> (5, 3)==> (6, 3)==> (6, 4)==>(6, 5)==> (5, 5)==> (4, 5)==> (4, 6)==> (4, 7)==> (3, 7)==> (3, 8)==> (4, 8)==> (5, 8)==> (6, 8)==> (7, 8)==> (8, 8)用隊(duì)列解決迷宮問題
思路:從一個節(jié)點(diǎn)開始,尋找所有下面能繼續(xù)走的點(diǎn)。繼續(xù)尋找,直到找到出口。
方法:創(chuàng)建一個空隊(duì)列,將起點(diǎn)位置進(jìn)隊(duì)。在隊(duì)列不為空時循環(huán):出隊(duì)一次。如果當(dāng)前位置為出口,則結(jié)束算法;否則找出當(dāng)前方塊的4個相鄰方塊中可走的方塊,全部進(jìn)隊(duì)。
?
from collections import dequemaze = [[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 deque_solve_maze(x1,y1,x2,y2):queue = deque()# 創(chuàng)建隊(duì)列path = [] # 記錄出隊(duì)之后的節(jié)點(diǎn)queue.append((x1,y1,-1))maze[x1][y1] = 2 # 2表示應(yīng)經(jīng)走過的點(diǎn)while len(queue) > 0:cur_node = queue.popleft()path.append(cur_node)if cur_node[0] == x2 and cur_node[1] == y2: # 到終點(diǎn)real_path = []x,y,i = path[-1]real_path.append((x,y)) # 將終點(diǎn)坐標(biāo)append到real_path中while i >= 0:node = path[i] # node是一個元祖(x坐標(biāo),y坐標(biāo),該點(diǎn)的leader)real_path.append(node[0:2]) # 只要坐標(biāo)i = node[2]real_path.reverse() # 反轉(zhuǎn)后順序才為從起點(diǎn)到終點(diǎn)for p in real_path:print('==>',p,end='')return Truefor 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 # 標(biāo)記為已經(jīng)走過else:print("無路可走")return Falsedeque_solve_maze(1,1,8,8) #==> (1, 1)==> (2, 1)==> (3, 1)==> (4, 1)==> (5, 1)==> (5, 2)==> (5, 3)==> (6, 3)==> (6, 4)==> (6, 5)==> (7, 5)==> (8, 5)==> (8, 6)==> (8, 7)==> (8, 8)總結(jié):
- 隊(duì)列解決迷宮問題找到的出路肯定是最短路徑,但是相對而言用隊(duì)列會比較占用內(nèi)存
- 隊(duì)列對應(yīng)的思想是廣度優(yōu)先,棧對應(yīng)的是深度優(yōu)先
鏈表
鏈表是一種物理存儲單元上非連續(xù)、非順序的存儲結(jié)構(gòu),數(shù)據(jù)元素的邏輯順序是通過鏈表中的指針鏈接次序?qū)崿F(xiàn)的。鏈表由一系列結(jié)點(diǎn)(鏈表中每一個元素稱為結(jié)點(diǎn))組成,結(jié)點(diǎn)可以在運(yùn)行時動態(tài)生成。
每個結(jié)點(diǎn)包括兩個部分:一個是存儲數(shù)據(jù)元素的數(shù)據(jù)域,另一個是存儲下一個結(jié)點(diǎn)地址的指針域。 相比于線性表順序結(jié)構(gòu),操作復(fù)雜。由于不必須按順序存儲,鏈表在插入的時候可以達(dá)到O(1)的復(fù)雜度,比另一種線性表順序表快得多,但是查找一個節(jié)點(diǎn)或者訪問特定編號的節(jié)點(diǎn)則需要O(n)的時間,而線性表和順序表相應(yīng)的時間復(fù)雜度分別是O(logn)和O(1)。
使用鏈表結(jié)構(gòu)可以克服數(shù)組鏈表需要預(yù)先知道數(shù)據(jù)大小的缺點(diǎn),鏈表結(jié)構(gòu)可以充分利用計(jì)算機(jī)內(nèi)存空間,實(shí)現(xiàn)靈活的內(nèi)存動態(tài)管理。但是鏈表失去了數(shù)組隨機(jī)讀取的優(yōu)點(diǎn),同時鏈表由于增加了結(jié)點(diǎn)的指針域,空間開銷比較大。鏈表最明顯的好處就是,常規(guī)數(shù)組排列關(guān)聯(lián)項(xiàng)目的方式可能不同于這些數(shù)據(jù)項(xiàng)目在記憶體或磁盤上順序,數(shù)據(jù)的存取往往要在不同的排列順序中轉(zhuǎn)換。鏈表允許插入和移除表上任意位置上的節(jié)點(diǎn),但是不允許隨機(jī)存取。鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環(huán)鏈表。鏈表可以在多種編程語言中實(shí)現(xiàn)。像Lisp和Scheme這樣的語言的內(nèi)建數(shù)據(jù)類型中就包含了鏈表的存取和操作。程序語言或面向?qū)ο笳Z言,如C,C++和Java依靠易變工具來生成鏈表。
鏈表中每一個元素都是一個對象,每個對象稱為一個節(jié)點(diǎn),包含有數(shù)據(jù)域key和指向下一個節(jié)點(diǎn)的指針next。通過各個節(jié)點(diǎn)之間的相互連接,最終串聯(lián)成一個鏈表。
# 結(jié)點(diǎn)的定義,單向 class Node(object):def __init__(self, item):self.item = itemself.next = None建立鏈表的方式有頭插法和尾插法兩種
- 頭插法:在一個結(jié)點(diǎn)的前面插入元素,head的指針由指向原來的結(jié)點(diǎn)變?yōu)橹赶蛐略?#xff0c;新元素的指針指向原來的結(jié)點(diǎn)
- 尾插法:在一個元素后面插入一個元素,原來結(jié)點(diǎn)的指針指向新元素
建立列表實(shí)現(xiàn)代碼如下:
# a = Node(1) # 頭結(jié)點(diǎn),是節(jié)點(diǎn) # b = Node(2) # c = Node(3) # a.next = b # b.next = c # head = a# 帶空頭結(jié)點(diǎn)的鏈表 # head = Node() # 頭結(jié)點(diǎn) # a = Node(1) # b = Node(2) # c = Node(3) # head.next = a # a.next = b # b.next = c # print(a.next.data)class Node:def __init__(self, data=None):self.data = dataself.next = Noneclass LinkList:def __init__(self, li, method='tail'):self.head = Noneself.tail = Noneif method == 'tail':self.create_linklist_tail(li)elif method == 'head':self.create_linklist_head(li)else:raise ValueError("Unsupport value %s" % method)def create_linklist_head(self, li):self.head = Node(0)for v in li:n = Node(v)n.next = self.head.nextself.head.next = nself.head.data += 1def create_linklist_tail(self, li):self.head = Node(0)self.tail = self.headfor v in li:p = Node(v)self.tail.next = pself.tail = pself.head.data += 1def traverse(self):p = self.head.nextwhile p:yield p.datap = p.nextdef __len__(self):return self.head.datall = LinkList([1,2,3,4,5], method='tail') print(len(ll))鏈表結(jié)點(diǎn)的插入
鏈表插入結(jié)點(diǎn)的操作的重點(diǎn)是指針的變換,首先我們有兩個結(jié)點(diǎn)A指向B,這時要在AB中間插入C,我們需要將C的指針指向B,然后將A的指針指向C,在刪除AB之間的指針,就完成了C的插入,由AB變?yōu)榱薃CB
# curNode為A結(jié)點(diǎn) c.next = curNode.next curNode.next = c鏈表結(jié)點(diǎn)的刪除
在鏈表中,要刪除一個結(jié)點(diǎn)不能直接刪掉就萬事大吉,我們需要將指向該結(jié)點(diǎn)的結(jié)點(diǎn)的指針指向該結(jié)點(diǎn)指針指向的結(jié)點(diǎn)(A指向B指向C,B為要刪除的該結(jié)點(diǎn),將A的指針指向C),然后才能刪除該節(jié)點(diǎn)(B)
?
# p為要刪除的結(jié)點(diǎn) curNode.next = curNode.next.next # 即p.next del p鏈表的特殊形態(tài)——雙鏈表
雙向鏈表也叫雙鏈表,是鏈表的一種,它的每個數(shù)據(jù)結(jié)點(diǎn)中都有兩個指針,分別指向直接后繼(后面結(jié)點(diǎn))和直接前驅(qū)(前面結(jié)點(diǎn))。所以,從雙向鏈表中的任意一個結(jié)點(diǎn)開始,都可以很方便地訪問它的前驅(qū)結(jié)點(diǎn)和后繼結(jié)點(diǎn)。一般我們都構(gòu)造雙向循環(huán)鏈表。?
雙向鏈表的節(jié)點(diǎn)定義
class Node(object):def __init__(self, item=None):self.item = itemself.next = Noneself.prior = None雙向鏈表結(jié)點(diǎn)的插入
與鏈表相同,雙向鏈表插入結(jié)點(diǎn)也需要將指針進(jìn)行變換。同樣是AB之間要插入C,我們需要先將C的指針指向B、B的指針由指向A轉(zhuǎn)變?yōu)橹赶駼,然后C的另一個指針指向A,A結(jié)點(diǎn)的指針由指向B轉(zhuǎn)變?yōu)橹赶駼。
# p為新插入的元素 p.next = curNode.next curNode.next.prior = p p.prior = curNode curNode.next = p雙向鏈表結(jié)點(diǎn)的刪除
刪除雙向鏈表的結(jié)點(diǎn)前需要建立起該結(jié)點(diǎn)前后兩個結(jié)點(diǎn)的指針關(guān)系,然后才能刪除結(jié)點(diǎn)
# p為要刪除的結(jié)點(diǎn) p = curNode.next curNode.next = p.next p.next.prior = curNode del p鏈表的復(fù)雜度分析
- 按元素值查找——O(n),因?yàn)闆]有下標(biāo)所以沒法做二分
- 按下標(biāo)查找——O(n),因?yàn)闆]有下標(biāo)
- 在某元素后插入——O(1)
- 刪除某元素——O(1)
總結(jié)
- 鏈表在插入和刪除的操作上明顯快于順序表
- 鏈表的內(nèi)存可以更靈活的分配。試?yán)面湵碇匦聦?shí)現(xiàn)棧和隊(duì)列
- 鏈表這種鏈?zhǔn)酱鎯Φ臄?shù)據(jù)結(jié)構(gòu)對樹和圖的結(jié)構(gòu)有很大的啟發(fā)性
鏈表的題
檢查給定的鏈表是否包含循環(huán),包含循環(huán)返回1,不包含循環(huán)則返回0。同時說明所實(shí)現(xiàn)的時間和空間復(fù)雜度是多少。
# 快慢指針法:一個指針每次移動一個單位,一個指針每次移動兩個單位,如果重合,說明有環(huán) def find_loop(list):p1 = p2 = listwhile p2 and p2.pnext:p1 = p1.pnextp2 = p2.pnext.pnextif p1 == p2:return 1return 0 # 時間復(fù)雜度O(n), 空間復(fù)雜度O(1).哈希表
哈希表的簡單概述
哈希表一個通過哈希函數(shù)來計(jì)算數(shù)據(jù)存儲位置的數(shù)據(jù)結(jié)構(gòu),通常支持如下操作 (高效的操作):python中的字典是通過哈希表實(shí)現(xiàn)的
- insert(key, value):插入鍵值對(key,value)
- get(key):如果存在鍵為key的鍵值對則返回其value,否則返回空值
- delete(key):刪除鍵為key的鍵值對
?直接尋址表
當(dāng)關(guān)鍵字的key 的?全域U(關(guān)鍵字可能出現(xiàn)的范圍)比較小時,直接尋址是一種簡單而有效的方法
- 存儲 : 如上圖將數(shù)組的下標(biāo)作為key,將數(shù)值存儲在對應(yīng)的下表位置? ? ??key為k的元素放到k位置上
- 刪除 : 當(dāng)要刪除某個元素時,將對應(yīng)的下標(biāo)的位置值置為空
直接尋址技術(shù)缺點(diǎn):
- 當(dāng)域U很大時,需要消耗大量內(nèi)存,很不實(shí)際
- 如果域U很大而實(shí)際出現(xiàn)的key很少,則大量空間被浪費(fèi)
- 無法處理關(guān)鍵字不是數(shù)字的情況,因?yàn)閗ey可以是其他的數(shù)據(jù)類型
哈希與哈希表
改進(jìn)直接尋址表:?哈希
- 構(gòu)建大小為m的尋址表T
- key為k的元素放到h(k)位置上
- h(k)是一個函數(shù),其將域U映射到表T[0,1,...,m-1]
哈希表
- 哈希表(Hash Table,又稱為散列表),是一種線性表的存儲結(jié)構(gòu)。哈希表由一個直接尋址表和一個哈希函數(shù)組成。
- 哈希函數(shù)h(k)將元素關(guān)鍵字k作為自變量,返回元素的存儲下標(biāo)。
簡單的hash函數(shù)
- 除法哈希:h(k) = k mod m
- 乘法哈希:h(k) = floor(m(kA mod 1)) 0<A<1
存儲機(jī)制
以除法哈希為例討論下存儲機(jī)制以及存在的問題
假設(shè)有一個長度為7的數(shù)組,哈希函數(shù)h(k)=k mod 7,元素集合{14,22,3,5}的存儲方式如下圖。
?
解釋:
- 存儲 : key對數(shù)組長度取余,余數(shù)作為數(shù)組的下標(biāo),將值存儲在此處
- 存在的問題 :比如:h(k)=k mod 7, h(0)=h(7)=h(14)=...?
?
哈希沖突
由于哈希表的大小是有限的,而要存儲的值的總數(shù)量是無限的,因此對于任何哈希函數(shù),都會出現(xiàn)兩個不同元素映射到同一個位置上的情況,這種情況叫做哈希沖突。
比如上圖中的哈希表就存在這哈希沖突——h(k)=k%7, h(0)=h(7)=h(14)=...
解決哈希沖突方法
方法一:開放尋址法——如果哈希函數(shù)返回的位置已經(jīng)有值,則可以向后探查新的位置來存儲這個值。
- 線性探查:如果位置i被占用,則探查i+1, i+2,……
- 二次探查:如果位置i被占用,則探查i+12,i-12,i+22,i-22,……
- 二度哈希:有n個哈希函數(shù),當(dāng)使用第1個哈希函數(shù)h1發(fā)生沖突時,則嘗試使用h2,h3,……
方法二:拉鏈法——哈希表每個位置都連接一個鏈表,當(dāng)沖突發(fā)生時,沖突的元素將被加到該位置鏈表的最后。
?
哈希表在Python中的應(yīng)用
字典與集合都是通過哈希表來實(shí)現(xiàn)的
在Python中的字典:a = {'name': 'Damon', 'age': 18, 'gender': 'Man'}
使用哈希表存儲字典,通過哈希函數(shù)將字典的鍵映射為下標(biāo)。假設(shè)h(‘name’) = 3, h(‘a(chǎn)ge’) = 1, h(‘gender’) = 4,則哈希表存儲為[None, 18, None, ’Damon’, ‘Man’]
在字典鍵值對數(shù)量不多的情況下,幾乎不會發(fā)生哈希沖突,此時查找一個元素的時間復(fù)雜度為O(1)。
二叉樹
樹
在了解二叉樹之前,首先我們得有樹的概念。
樹是一種數(shù)據(jù)結(jié)構(gòu)又可稱為樹狀圖,如文檔的目錄、HTML的文檔樹都是樹結(jié)構(gòu),它是由n(n>=1)個有限節(jié)點(diǎn)組成一個具有層次關(guān)系的集合。把它叫做“樹”是因?yàn)樗雌饋硐褚豢玫箳斓臉?#xff0c;也就是說它是根朝上,而葉朝下的。它具有以下的特點(diǎn):
- 每個節(jié)點(diǎn)有零個或多個子節(jié)點(diǎn);
- 沒有父節(jié)點(diǎn)的節(jié)點(diǎn)稱為根節(jié)點(diǎn);
- 每一個非根節(jié)點(diǎn)有且只有一個父節(jié)點(diǎn);
- 除了根節(jié)點(diǎn)外,每個子節(jié)點(diǎn)可以分為多個不相交的子樹;?
有關(guān)樹的一些相關(guān)術(shù)語:
節(jié)點(diǎn)的度:一個節(jié)點(diǎn)含有的子樹的個數(shù)稱為該節(jié)點(diǎn)的度; 葉節(jié)點(diǎn)或終端節(jié)點(diǎn):度為0的節(jié)點(diǎn)稱為葉節(jié)點(diǎn); 非終端節(jié)點(diǎn)或分支節(jié)點(diǎn):度不為0的節(jié)點(diǎn); 雙親節(jié)點(diǎn)或父節(jié)點(diǎn):若一個節(jié)點(diǎn)含有子節(jié)點(diǎn),則這個節(jié)點(diǎn)稱為其子節(jié)點(diǎn)的父節(jié)點(diǎn); 孩子節(jié)點(diǎn)或子節(jié)點(diǎn):一個節(jié)點(diǎn)含有的子樹的根節(jié)點(diǎn)稱為該節(jié)點(diǎn)的子節(jié)點(diǎn); 兄弟節(jié)點(diǎn):具有相同父節(jié)點(diǎn)的節(jié)點(diǎn)互稱為兄弟節(jié)點(diǎn); 樹的度:一棵樹中,最大的節(jié)點(diǎn)的度稱為樹的度; 節(jié)點(diǎn)的層次:從根開始定義起,根為第1層,根的子節(jié)點(diǎn)為第2層,以此類推; 樹的高度或深度:樹中節(jié)點(diǎn)的最大層次; 堂兄弟節(jié)點(diǎn):雙親在同一層的節(jié)點(diǎn)互為堂兄弟; 節(jié)點(diǎn)的祖先:從根到該節(jié)點(diǎn)所經(jīng)分支上的所有節(jié)點(diǎn); 森林:由m(m>=0)棵互不相交的樹的集合稱為森林;樹的種類有:無序樹、有序樹、二叉樹、霍夫曼樹。其中最重要應(yīng)用最多的就是二叉樹,下面我們來學(xué)習(xí)有關(guān)二叉樹的知識。
二叉樹
二叉樹的定義為度不超過2的樹,即每個節(jié)點(diǎn)最多有兩個叉(兩個分支)。上面那個例圖其實(shí)就是一顆二叉樹。
二叉樹是每個節(jié)點(diǎn)最多有兩個子樹的樹結(jié)構(gòu)。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用于實(shí)現(xiàn)二叉查找樹和二叉堆。
二叉樹的每個結(jié)點(diǎn)至多只有二棵子樹(不存在度大于2的結(jié)點(diǎn)),二叉樹的子樹有左右之分,次序不能顛倒。二叉樹的第i層至多有個結(jié)點(diǎn);深度為k的二叉樹至多有個結(jié)點(diǎn);對任何一棵二叉樹T,如果其終端結(jié)點(diǎn)數(shù)為,度為2的結(jié)點(diǎn)數(shù)為,則。
一棵深度為k,且有個節(jié)點(diǎn)的二叉樹,稱為滿二叉樹。這種樹的特點(diǎn)是每一層上的節(jié)點(diǎn)數(shù)都是最大節(jié)點(diǎn)數(shù)。而在一棵二叉樹中,除最后一層外,若其余層都是滿的,并且最后一層或者是滿的,或者是在右邊缺少連續(xù)若干節(jié)點(diǎn),則此二叉樹為完全二叉樹。具有n個節(jié)點(diǎn)的完全二叉樹的深度為log2n+1。深度為k的完全二叉樹,至少有個節(jié)點(diǎn),至多有個節(jié)點(diǎn)。
二叉樹的存儲方式分為鏈?zhǔn)酱鎯晚樞虼鎯?#xff08;類似列表)兩種
二叉樹父節(jié)點(diǎn)下標(biāo)i和左孩子節(jié)點(diǎn)的編號下標(biāo)的關(guān)系為2i+1,和右孩子節(jié)點(diǎn)的編號下標(biāo)的關(guān)系為2i+2
二叉樹有兩個特殊的形態(tài):滿二叉樹和完全二叉樹
滿二叉樹:一個二叉樹,如果除了葉子節(jié)點(diǎn)外每一個層的結(jié)點(diǎn)數(shù)都達(dá)到最大值,則這個二叉樹就是滿二叉樹。
完全二叉樹:葉節(jié)點(diǎn)只能出現(xiàn)在最下層和次下層,并且最下面一層的結(jié)點(diǎn)都集中在該層最左邊的若干位置的二叉樹為完全二叉樹。即右邊的最下層和次下層可以適當(dāng)缺一個右子數(shù)
完全二叉樹是效率很高的數(shù)據(jù)結(jié)構(gòu)
二叉樹的鏈?zhǔn)酱鎯?#xff1a;將二叉樹的節(jié)點(diǎn)定義為一個對象,節(jié)點(diǎn)之間通過類似鏈表的鏈接方式來連接。
二叉樹結(jié)點(diǎn)的定義
class BiTreeNode:def __init__(self, data):self.data = dataself.lchild = Noneself.rchild = None二叉樹的遍歷分為四種——前序遍歷、中序遍歷、后序遍歷和層級遍歷
前序+中序或者后序+中序 可以唯一確定一顆子樹(兩個節(jié)點(diǎn)除外)
設(shè)樹結(jié)構(gòu)為:
- 前序遍歷:先打印根,再遞歸其左子樹,后遞歸其右子數(shù) ? ?E ACBD GF
- 中序遍歷:以根為中心,左邊打印左子樹,右邊打印右子樹(注意,每個子樹也有相應(yīng)的根和子樹) ? A BCD E GF
- 后序遍歷:先遞歸左子樹,再遞歸右子樹,后打印根(注意,每個子樹也有相應(yīng)的根和子樹BDC A FG E
- 層次遍歷:從根開始一層一層來,同一層的從左到右輸出E AG CF BD
四種遍歷方法的代碼實(shí)現(xiàn):
# 結(jié)點(diǎn)的定義 class BiTreeNode:def __init__(self, data):self.data = dataself.lchild = Noneself.rchild = None # 二叉樹結(jié)點(diǎn) a = BiTreeNode('A') b = BiTreeNode('B') c = BiTreeNode('C') d = BiTreeNode('D') e = BiTreeNode('E') f = BiTreeNode('F') g = BiTreeNode('G') # 結(jié)點(diǎn)之間的關(guān)系 e.lchild = a e.rchild = g a.rchild = c c.lchild = b c.rchild = d g.rchild = froot = e# 前序遍歷:先打印根,再遞歸左孩子,后遞歸右孩子 def pre_order(root):if root:print(root.data, end='')pre_order(root.lchild)pre_order(root.rchild)# 中序遍歷:以根為中心,左邊打印左子樹,右邊打印右子樹(注意,每個子樹也有相應(yīng)的根和子樹) # (ACBD) E (GF)-->(A(CBD)) E (GF)-->(A (B C D)) E (G F) def in_order(root):if root:in_order(root.lchild)print(root.data, end='')in_order(root.rchild)# 后序遍歷:先遞歸左子樹,再遞歸右子數(shù),后打印根(注意,每個子樹也有相應(yīng)的根和子樹) # (ABCD)(GF)E-->((BCD)A)(GF)E-->(BDCA)(FG)E 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)pre_order(root)#EACBDGF print("") in_order(root)#ABCDEGF print("") post_order(root)#BDCAFGE print("") level_order(root)#EAGCFBD二叉搜索樹
二叉搜索樹(Binary Search Tree),它或者是一棵空樹,或者是具有下列性質(zhì)的二叉樹: 若它的左子樹不空,則左子樹上所有結(jié)點(diǎn)的值均小于它的根結(jié)點(diǎn)的值; 若它的右子樹不空,則右子樹上所有結(jié)點(diǎn)的值均大于它的根結(jié)點(diǎn)的值; 它的左、右子樹也分別為二叉搜索樹。
二叉搜索樹的中序遍歷得到的是原來列表按升序排序的列表
由列表生成二叉搜索樹、通過二叉搜索樹查詢值
# 結(jié)點(diǎn)定義 class BiTreeNode:def __init__(self, data):self.data = dataself.lchild = Noneself.rchild = None # 建立二叉搜索樹(循環(huán)列表,插入值) class BST:def __init__(self, li=None):self.root = Noneif li:self.root = self.insert(self.root, li[0]) # 列表的第一個元素是根for val in li[1:]:self.insert(self.root, val)# 生成二叉搜索樹遞歸版本def insert(self, root, val):if root is None:root = BiTreeNode(val)elif val < root.data: # 插入的值小于root,要放到左子樹中(遞歸查詢插入的位置)root.lchild = self.insert(root.lchild, val)else: # 插入的值大于root,要放到右子樹中(遞歸查詢插入的位置)root.rchild = self.insert(root.rchild, val)return root# 生成二叉搜索樹不遞歸的版本def insert_no_rec(self, val):p = self.rootif not p:self.root = BiTreeNode(val)returnwhile True:if val < p.data:if p.lchild:p = p.lchildelse:p.lchild = BiTreeNode(val)breakelse:if p.rchild:p = p.rchildelse:p.rchild = BiTreeNode(val)break# 查詢遞歸版本def query(self, root, val):if not root:return Falseif root.data == val:return Trueelif root.data > val:return self.query(root.lchild, val)else:return self.query(root.rchild, val)# 查詢非遞歸版本def query_no_rec(self, val):p = self.rootwhile p:if p.data == val:return Trueelif p.data > val:p = p.lchildelse:p = p.rchildreturn False# 中序遍歷,得到的是升序的列表def in_order(self, root):if root:self.in_order(root.lchild)print(root.data, end=',')self.in_order(root.rchild)tree = BST() for i in [1,5,9,8,7,6,4,3,2]:tree.insert_no_rec(i) tree.in_order(tree.root) #print(tree.query_no_rec(12))刪除操作
- 1. 如果要刪除的節(jié)點(diǎn)是葉子節(jié)點(diǎn):直接刪除
- 2. 如果要刪除的節(jié)點(diǎn)只有一個孩子:將此節(jié)點(diǎn)的父親與孩子連接,然后刪除該節(jié)點(diǎn)。
- 3. 如果要刪除的節(jié)點(diǎn)有兩個孩子:將其右子樹的最小節(jié)點(diǎn)(該節(jié)點(diǎn)最多有一個右孩子)刪除,并替換當(dāng)前節(jié)點(diǎn)。
二叉搜索樹效率:
- 平均情況下,二叉搜索樹進(jìn)行搜索的時間復(fù)雜度為O(nlogn)。
- 最壞情況下,二叉搜索樹可能非常偏斜。
- 解決方案:隨機(jī)化插入,AVL樹
二叉搜索樹的應(yīng)用——AVL樹、B樹、B+樹
AVL樹
AVL樹:AVL樹是一棵自平衡的二叉搜索樹。
AVL樹具有以下性質(zhì): 根的左右子樹的高度之差的絕對值不能超過1,根的左右子樹都是平衡二叉樹
插入一個節(jié)點(diǎn)可能會破壞AVL樹的平衡,可以通過旋轉(zhuǎn)操作來進(jìn)行修正。
插入一個節(jié)點(diǎn)后,只有從插入節(jié)點(diǎn)到根節(jié)點(diǎn)的路徑上的節(jié)點(diǎn)的平衡可能被改變。我們需要找出第一個破壞了平衡條件的節(jié)點(diǎn),稱之為K。K的兩顆子樹的高度差2。
不平衡的出現(xiàn)可能有4種情況
1.不平衡是由于對K的右孩子的右子樹插入導(dǎo)致的:左旋
2.不平衡是由于對K的左孩子的左子樹插入導(dǎo)致的:右旋
3.不平衡是由于對K的右孩子的左子樹插入導(dǎo)致的:右旋-左旋
4.不平衡是由于對K的左孩子的右子樹插入導(dǎo)致的:左旋-右旋
B樹
B樹是一棵自平衡的多路搜索樹。常用于數(shù)據(jù)庫的索引。
B+ 樹
B+ 樹是一種樹數(shù)據(jù)結(jié)構(gòu),是一個n叉排序樹,每個節(jié)點(diǎn)通常有多個孩子,一棵B+樹包含根節(jié)點(diǎn)、內(nèi)部節(jié)點(diǎn)和葉子節(jié)點(diǎn)。根節(jié)點(diǎn)可能是一個葉子節(jié)點(diǎn),也可能是一個包含兩個或兩個以上孩子節(jié)點(diǎn)的節(jié)點(diǎn)。
B+ 樹通常用于數(shù)據(jù)庫和操作系統(tǒng)的文件系統(tǒng)中。NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等文件系統(tǒng)都在使用B+樹作為元數(shù)據(jù)索引。B+ 樹的特點(diǎn)是能夠保持?jǐn)?shù)據(jù)穩(wěn)定有序,其插入與修改擁有較穩(wěn)定的對數(shù)時間復(fù)雜度。B+ 樹元素自底向上插入。
B+樹是應(yīng)文件系統(tǒng)所需而出的一種B樹的變型樹。一棵m階的B+樹和m階的B-樹的差異在于:
1.有n棵子樹的結(jié)點(diǎn)中含有n個關(guān)鍵字,每個關(guān)鍵字不保存數(shù)據(jù),只用來索引,所有數(shù)據(jù)都保存在葉子節(jié)點(diǎn)。
2.所有的葉子結(jié)點(diǎn)中包含了全部關(guān)鍵字的信息,及指向含這些關(guān)鍵字記錄的指針,且葉子結(jié)點(diǎn)本身依關(guān)鍵字的大小自小而大順序鏈接。
3.所有的非終端結(jié)點(diǎn)可以看成是索引部分,結(jié)點(diǎn)中僅含其子樹(根結(jié)點(diǎn))中的最大(或最小)關(guān)鍵字。
通常在B+樹上有兩個頭指針,一個指向根結(jié)點(diǎn),一個指向關(guān)鍵字最小的葉子結(jié)點(diǎn)。
https://www.cnblogs.com/zhuminghui/p/8414317.html
https://www.cnblogs.com/wcx666/p/10686964.html
總結(jié)
- 上一篇: 数据库理论知识
- 下一篇: 电脑出现问题,你的PIN不可用。请单击以