保證一週更兩篇吧,以此來督促自己好好的學習!代碼的很多地方我都給予了詳細的解釋,幫助理解。好了,幹就完了~加油!
聲明:本python數據結構與算法是imooc上liuyubobobo老師java數據結構的python改寫,並添加了一些自己的理解和新的東西,liuyubobobo老師真的是一位很棒的老師!超級喜歡他~
如有錯誤,還請小夥伴們不吝指出,一起學習~
一、二叉堆
- 二叉堆是一棵【完全二叉樹】(不一定是滿樹,除最底層外,其它層一定是滿樹,但是在最底層的節點一定是從左到右連續排列的)
- 既然是完全二叉樹,就可以用數組的方式來表示這棵完全二叉樹,數組索引按樹的廣度優先遍歷的順序那樣來設置(即按層序標註,每層從左到右來標註)
這樣就會有着明顯的索引關係:(元素從索引0開始盛放,相應的若從索引1開始盛放元素需要做出一點更改,也很簡單)
parent(i) = (i - 1) // 2 # 獲取位於索引i的元素的父親的索引
left_child(i) = i * 2 + 1 # 獲取位於索引i的元素的左孩子的索引,可以畫圖看一下,很簡單的
right_child(i) = i * 2 + 2 # 獲取位於索引i的元素的右孩子的索引
- 注意,雖然叫二叉堆,但是在真正的實現中並沒有Node一說,只有一個數組作爲底層的數據結構,因爲堆一定是一棵完全二叉樹,因此用索引就已經能夠表徵一個二叉堆了。
二、最大堆
堆中某個節點的值總是【不大於】其父節點的值(除非沒有父節點)————最大堆(相應的可以定義最小堆),本節來實現最大堆。
三、實現
# -*- coding: utf-8 -*-
# Author: Annihilation7
# Data: 2018-10-20 10:58 pm
# Python version: 3.6
from array.Array import Arr # 用我們以前實現的數組類作爲二叉堆的底層數據結構
class MaxHeap:
def __init__(self, capacity_=20):
"""二叉堆的構造函數"""
self._data = Arr(capacity=capacity_) # 調用數組的構造函數就可以了,別忘了我們的數組是支持索引操作的
def isEmpty(self):
"""
Description: 判空
Returns:
bool值,空爲True
"""
return self._data.isEmpty() # 直接調用數組的判空函數
def getSize(self):
"""
Description: 獲取當前二叉堆中有效元素的個數
Returns:
有效元素的個數
"""
return self._data.getSize()
def add(self, elem):
"""
Description: 向堆中添加元素elem
時間複雜度:O(logn)
Params:
- elem: 待添加元素
"""
self._data.addLast(elem) # 由於是一棵完全二叉樹,因此直接向數組尾部添加元素就完事了,
# 但是必須要對新添加的這個元素進行相應的維護操作,使其繼續滿足堆的性質。
# 空間問題不用考慮,有自動擴容
self._shiftUp(self.getSize() - 1) # addLast操作中已經維護了self._size,所以此時新添加的元素位於self.getSize()-1索引處
# 調用self._shiftUp來上移(shiftup)位於這個索引的元素,使其滿足堆的性質
def findMax(self):
"""
Description: 看一下堆中的最大值(也就是優先級最高的元素)
時間複雜度:O(1)
Returns:
優先級最高的元素的值
"""
if self.isEmpty():
raise Exception('Error! The maxheap is empty!')
return self._data[0] # 就是0索引處的元素的值
def extractMax(self):
"""
Description: 將堆中最大元素取出來,並返回其相應的值。
問題: 取出後,爲了維護堆的性質,由誰來代替最大值的位置呢?如果要將兩個子樹進行合併,非常的複雜, 我們這樣來做:
將數組中的最後一個元素放到索引0的位置,然後對索引0處的元素進行shiftDown操作,顧名思義,與shiftUp的操作是
處於對立關係的,從而維護堆的性質
Returns:
堆中最大的元素的值(索引0處的元素的值)
"""
ret = self.findMax() # 先找到最大值,便於返回,這裏不用判空了哈,findMax已經做了。
self._data.swap(0, self.getSize() - 1) # 將數組尾部的元素和索引0處的元素交換
self._data.removeLast() # 此時將數組尾部的元素刪除(也就是最大值,我們已經記錄過了,刪除就完事了)
self._shiftDown(0) # 此時對新來的索引0處的元素進行shiftDown操作,從而滿足堆的性質
return ret # 將最大值返回
def replace(self, elem):
"""
Description: 取出堆頂的元素,然後在添加一個元素elem,返回原先堆頂的元素。
實現這個方法可以從我們已經實現的方法組合來實現,先extacrmax(),再add()操作,這樣兩側O(logn)的操作,
在這裏我們通過一個O(logn)的方法來實現。
時間複雜度:O(logn)
Params:
- elem: 待添加元素
Returns:
原先堆頂的元素
"""
ret = self.findMax() # 找到棧頂元素並用ret記錄,便於返回
self._data.set(0, elem) # 直接將堆頂元素設爲elem,調用數組的set函數,時間複雜度爲O(1)
self._shiftDown(0) # 新設置的值極有可能破壞了堆的性質,所以將索引0處的元素進行shiftDown操作,使其最後滿足堆的性質
return ret # 將原先堆頂元素返回
def printMaxHeap(self):
"""對堆元素進行打印操作"""
self._data.printArr() # 直接調用數組的printArr()函數即可
# private
def _parent(self, index):
"""
Description: 獲取index索引處元素的父親節點的index
Params:
- index: 傳入的索引值
Returns:
其父親所在的位置
"""
if index == 0: # 非法index,這個位置是二叉堆根節點的位置,沒有父親節點。
raise Exception('index-0 doesn\'t have parent.')
return (index - 1) // 2 # 根據公式返回就好了,建議畫圖理解下,很簡單
def _leftChild(self, index):
"""
Description: 獲取index索引處元素的左孩子節點的index
Params:
- index: 傳入的索引值
Returns:
其左孩子所在的位置
"""
return index * 2 + 1
def _rightChild(self, index):
"""
Description: 獲取index索引處元素的右孩子節點的index
Params:
- index: 傳入的索引值
Returns:
其右孩子所在的位置
"""
return index * 2 + 2
def _shiftUp(self, k):
"""
Description: 對位於索引k的元素進行上移操作,移動到合適的位置,從而滿足堆的性質
Params:
- k: 傳入的索引值
"""
while k > 0 and self._data[k] > self._data[self._parent(k)]: # 如果k>0(沒到根節點,也就是還有父親節點),並且索引k處的元素大於其父親的元素
self._data.swap(k, self._parent(k)) # 交換
k = self._parent(k) # k移動到父親節點的索引處,看是否還需要上移,反正只要不到根節點就一直判斷嘛,直到合適的位置
def _shiftDown(self, k):
"""
Description: 對位於索引k的元素進行下移操作,移動到合適的位置,從而滿足堆的性質
Params:
- k: 傳入的索引值
"""
while(self._leftChild(k) < self.getSize()): # 如果左孩子的索引還有效(想一想爲什麼不是右孩子,如果判斷的是右孩子的索引值,就會少判斷一種情況:
# 就是右孩子的索引無效,但是左孩子的索引還有效)
j = self._leftChild(k) # 此時左孩子的索引有效,用j來標記這個索引
if self._rightChild(k) < self.getSize(): # 還沒有判斷右孩子,肯定是要和兩個孩子的最大值進行交換,這樣在交換後才能滿足堆的定義:孩子節點的值
# 不大於父親節點的值,所以要找到兩個孩子的最大值,如果右孩子有效:
if self._data[self._rightChild(k)] > self._data[j]: # 看一下右孩子的值是否大於左孩子的值
j = self._rightChild(k) # 如果大於,j就等於右孩子的索引,否則什麼也不做
# 此時索引j等於位於索引k處元素的兩個孩子的最大值的索引
if self._data[k] >= self._data[j]: # 此時索引k的值已經大於兩個孩子的最大值了
break # 直接退出循環
self._data.swap(k, j) # 否則就和最大值交換
k = j # k繼續往下走,看是否需要繼續進行交換操作來滿足最大堆的定義
@staticmethod
# 獨立的一個實現
def heapify(alist):
"""
Description: 傳入一個列表,並自動對列表中的所有元素進行堆排列。實在是因爲python
沒有構造函數的重載功能,我迫不得已才把這個功能單獨抽出來作爲一個靜態函數,有一點不方便。。。
分析: 實現這個功能很簡單,新建一個空的MaxHeap,然後將列表中的所有元素一個一個的add
到MaxHeap中,這樣做時間複雜度爲O(nlogn)。本函數要實現一個時間複雜度爲O(n)的堆
排列功能,至於爲什麼是O(n)的,我也不知道,感興趣的可以看一下heapify的時間複雜度的
數學推導,但是要知道O(n)與O(nlogn)之間性能是質的飛躍!
時間複雜度:O(n)
Params:
- alist: 一個列表,裏面的元素是要進行堆排列的全部元素
Returns:
一個滿足堆排列(最大堆)的list,注意這個函數是in-place操作哦。
"""
def shiftDown(alist, index):
"""
Description: 和MaxHeap的self._shiftDown操作是一樣,只不過我需要在這裏再實現一個,才
可以調用--!
Params:
- alist: 一個列表
- index: 要shiftDown的索引
"""
length = len(alist) # 就不詳細講解了,跟前面的self._shiftDown是一樣的~
while index * 2 + 1 < length:
j = index * 2 + 1
if j + 1 < length:
if alist[j + 1] > alist[j]:
j += 1
if alist[index] >= alist[j]:
break
alist[index], alist[j] = alist[j], alist[index]
index = j
length = len(alist)
# 這裏要記住堆有一個極其重要的性質~就是堆的第一個非葉子節點的索引一定是
# ((length - 1) - 1) // 2
# 其實想一想也很簡單,length - 1這個索引一定是數組的最後一個元素,最後一個元素的父親
# 節點的索引一定是第一個非葉子節點的索引呀,即parent(length - 1),這裏並沒有parent函數
# 所以展開就是 ((length - 1) - 1) // 2
# 從第一個非葉子節點開始,一直到索引0處(二叉堆根的索引),都執行shiftDown操作,最後
# 這個數組也就變成了一個堆。爲什麼不用管 (((length - 1) - 1) // 2, length - 1]左
# 開右閉區間內的點呢?原因也很簡單,在非葉子節點shiftDown的過程中自動就將這些點維護了!
for i in range(((length - 1) - 1) // 2, -1, -1): # 遍歷第一個非葉子節點開始一直到根節點
shiftDown(alist, i) # 都shiftDown一次就完事了
return alist
四、測試
from Priorityqueue.maxheap import MaxHeap # 我們的MaxHeap寫在了maxheap.py文件中
import numpy as np
np.random.seed(7)
test_maxheap = MaxHeap()
nums = 100000 # 操作數
for i in range(nums - 1):
test_maxheap.add(np.random.randint(1000)) # 99999次添加隨機數操作
test_maxheap.add(6666) # 再添加一個最大的,便於驗證
print('添加元素操作後的Size:', test_maxheap.getSize())
print('此時堆中最大值:', test_maxheap.findMax())
record = []
for i in range(nums):
record.append(test_maxheap.extractMax()) # 100000次提取最大元素操作並append到record中
# 不出意外,record中的元素應該是完全降序排列的
for i in range(1, len(record)): # 測試record是否真的是降序排列
if record[i - 1] < record[i]:
raise Exception('Error, the list is not absolutely a reversed list.')
print('Test MaxHeap completed.')
print('將元素全部抽取後是否爲空堆?', test_maxheap.isEmpty())
print('---------------------------------------------')
print('新建一個堆完成replace的測試,10次添加操作後:')
#######################################################
# 9 #
# / \ #
# 8 5 #
# / \ / \ #
# 6 7 1 4 #
# / \ / #
# 0 3 2 #
#######################################################
test_maxheap2 = MaxHeap()
for i in range(10):
test_maxheap2.add(i)
test_maxheap2.printMaxHeap()
print('抽取堆頂元素並添加一個0.7:') # 0.7這裏選的不好,因爲浮點數一旦涉及到判等操作會出現問題,但是字在這裏沒有判等操作
test_maxheap2.replace(0.7)
test_maxheap2.printMaxHeap()
print('測試heapify函數')
test_list = [i for i in range(30)]
print('待堆排列的元素:', test_list)
print('堆排列後:')
print(MaxHeap.heapify(test_list)) # 可以動手畫一下是滿足堆排列性質的。
五、輸出
添加元素操作後的Size: 100000
此時堆中最大值: 6666
Test MaxHeap completed.
將元素全部抽取後是否爲空堆? True
---------------------------------------------
新建一個堆完成replace的測試,10次添加操作後:
9 8 5 6 7 1 4 0 3 2
Size: 10-----Capacity: 20
抽取堆頂元素並添加一個0.7:
8 7 5 6 2 1 4 0 3 0.7
Size: 10-----Capacity: 20
測試heapify函數
待堆排列的元素: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
堆排列後:
[29, 22, 28, 18, 21, 26, 27, 16, 17, 20, 10, 24, 25, 13, 14, 15, 7, 3, 8, 19, 9, 4, 1, 23, 11, 5, 12, 2, 0, 6]
六、總結
- 本節實現的是一個最大堆,相應的也可以更改成一個最小堆,定義就是某個節點的值都【不小於】其父親節點的值,相應的操作大同小異,很簡單。
- 根據最大堆特有的性質,最大值一定位於索引0處,因此可以很輕鬆的實現一個全部操作時間複雜度爲O(logn)的優先隊列,下一小節我們一起動手來實現一個基於最大堆的優先隊列,性能非常的好,因爲是O(logn)的時間複雜度!!
- 可能基礎知識講解的不夠到位,畢竟沒有圖解神馬的,這方面小夥伴們還是需要去額外的查一下資料呢。因爲怕侵權,還是不貼老師上課ppt的圖了,有需要的小夥伴直接去聽他的課就好,很值得的!講的非常非常好!
若有還可以改進、優化的地方,還請小夥伴們批評指正!