【程序人生】數據結構雜記(八)
說在前面
個人讀書筆記
排序算法的穩定性Stable
穩定排序:對於相等的元素,在排序後,原來靠前的元素依然靠前。相等元素的相對位置沒有發生改變。
可以實現穩定排序:插入排序、歸併排序
無法實現穩定排序:快速排序、堆排序
優先級隊列
按照事先約定的優先級,可以始終高效查找並訪問優先級最高數據項的數據結構,統稱作優先級隊列(priority queue)。
優先級隊列,這類結構將操作對象限定於當前的全局極值者。比如,在全體北京市民中,查找年齡最長者;或者在所有鳥類中,查找種羣規模最小者,等等。這種根據數據對象之間相對優先級對其進行訪問的方式,與此前的訪問方式有着本質區別,稱作循優先級訪問(call-by-priority)。
“全局極值”本身就隱含了“所有元素可相互比較”這一性質。然而,優先級隊列並不會也不必忠實地動態維護這個全序,卻轉而維護一個偏序(partial order)關係。
優先級隊列中的數據項稱作詞條(entry);而與特定優先級相對應的數據屬性,也稱作關鍵碼(key)。不同應用中的關鍵碼,特點不盡相同:
有時限定詞條的關鍵碼須互異,有時則允許詞條的關鍵碼雷同;有些詞條的關鍵碼一成不變,有些則可動態修改;有的關鍵碼只是一個數字、一個字符或一個字符串,而複雜的關鍵碼則可能由多個基本類型組合而成;多數關鍵碼都取作詞條內部的某一成員變量,而有的關鍵碼則並非詞條的天然屬性。
無論具體形式如何,作爲確定詞條優先級的依據,關鍵碼之間必須可以比較大小——注意,這與詞典結構完全不同,後者僅要求關鍵碼支持判等操作。因此對於優先級隊列,必須以比較器的形式兌現對應的優先級關係。
堆
堆多用於動態數據的維護。
基於列表或向量等結構的實現方式,之所以無法同時保證插入操作和刪除優先級最高成員的操作的高效率,原因在於其對優先級的理解過於機械,以致始終都保存了全體詞條之間的全序關係。實際上,儘管優先級隊列的確隱含了“所有詞條可相互比較”這一條件,但從操作接口層面來看,並不需要真正地維護全序關係。比如執行刪除優先級最高成員的操作時,只要能夠確定全局優先級最高的詞條即可;至於次高者、第三高者等其餘詞條,目前暫時不必關心。
有限偏序集的極值必然存在,故此時藉助堆(heap)結構維護一個偏序關係即足矣。堆有多種實現形式,最基本的一種形式是完全二叉堆(complete binary heap)。
完全二叉堆
完全二叉堆應滿足兩個條件:
- 首先,其邏輯結構須等同於完全二叉樹,此即所謂的“結構性”。如此,堆節點將與詞條一一對應。
- 其次,就優先級而言,堆頂以外的每個節點都不高(大)於其父節點,此即所謂的“堆序性”。
大頂堆與小頂堆
由堆序性不難看出,堆中優先級最高的詞條必然始終處於堆頂位置。因此,堆結構的得到優先級最大成員的操作總是可以在時間內完成。
堆序性也可對稱地約定爲“堆頂以外的每個節點都不低(小)於其父節點”,此時同理,優先級最低的詞條,必然始終處於堆頂位置。爲以示區別,通常稱前(後)者爲大(小)頂堆。(優先級最高的是堆頂——大堆頂;優先級最低的是堆頂——小堆頂)
小頂堆和大頂堆是相對的,而且可以相互轉換。
高度
結構等同於完全二叉樹的堆,必然不致太高。n個詞條組成的堆的高度。
基於向量的緊湊表示
儘管二叉樹不屬於線性結構,但作爲其特例的完全二叉樹,卻與向量有着緊密的對應關係。
由上圖可見,完全二叉堆的拓撲聯接結構,完全由其規模n確定。按照層次遍歷的次序,每個節點都對應於唯一的編號;反之亦然。故若將所有節點組織爲一個向量,則堆中各節點(編號)與向量各單元(秩)也將彼此一一對應。
這一實現方式的優勢首先體現在,各節點在物理上連續排列,故總共僅需O(n)空間。而更重要地是,利用各節點的編號(或秩),也可便捷地判別父子關係。
具體地,若將節點的編號(秩)記作,則根節點及其後代節點的編號分別爲:
…
更一般地,不難驗證,完全二叉堆中的任意節點,必然滿足:
向上取整,用數學符號 ⌈⌉表示。
向下取整,用數學符號 ⌊⌋表示。
完全二叉堆的模板類
爲簡化後續算法的描述及實現,可如下圖所示預先設置一系列的宏定義。
藉助多重繼承的機制,定義完全二叉堆模板類如下:
既然全局優先級最高的詞條總是位於堆頂,故只需返回向量的首單元,即可在時間內完成getMax()操作。
c++實現
#include<iostream>
#include<algorithm>
#include<cassert>
using namespace std;
template<typename Item>
class MaxHeap{
private:
Item* data;
int count;
int capacity;
// 上濾操作
void shiftUp(int k)
{
while(k>1 && data[k/2] < data[k])
{
swap(data[k/2], data[k]);
k /= 2;
}
}
// 下濾操作
void shiftDown(int k)
{
while(2*k <= count)
{
int j = 2*k;
if (j + 1 <= count && data[j+1] > data[j])
{
j += 1;
}
if (data[k] >= data[j])
{
break;
}
swap(data[k], data[j]);
k = j;
}
}
public:
// 聲明堆的最大容量
MaxHeap(int capacity)
{
data = new Item[capacity + 1];
count = 0;
this->capacity = capacity;
}
~MaxHeap()
{
delete [] data;
}
// 返回堆的元素個數
int size()
{
return count;
}
// 返回堆是否爲空
bool isEmpty()
{
return count == 0;
}
// 插入元素
void insert(Item item)
{
assert(count + 1 <= capacity);
data[count+1] = item;
count++;
shiftUp(count);
}
// 刪除元素
Item extractMax()
{
assert(count > 0);
Item ret = data[1];
swap(data[1], data[count]);
count --;
shiftDown(1);
return ret;
}
};
int main()
{
MaxHeap<int> maxheap = MaxHeap<int>(100);
srand(time(NULL));
for (int i = 0; i < 15; i++)
{
maxheap.insert(rand()%100);
}
while( !maxheap.isEmpty())
{
cout << maxheap.extractMax() << " ";
}
cout<<endl;
return 0;
}
python實現
# -*- coding:utf-8 -*-
class MaxHeap:
def __init__(self, data):
"""
輸入一個列表, 建堆
:param data: 列表
"""
self.__data = data
self.__count = len(data)
self.__floyd()
# 交換self.__data中的兩個索引對應的值
def __swap(self, a, b):
c = self.__data[a]
self.__data[a] = self.__data[b]
self.__data[b] = c
# 對索引爲k(第k+1個值)的值, 上濾操作
def __shift_up(self, k):
while (k > 0) and (self.__data[(k-1)//2] < self.__data[k]):
self.__swap((k-1)//2, k)
k = (k-1)//2
# 對索引爲k(第k+1個值)的值, 下濾操作
def __shift_down(self, k):
while 2*k + 1 < self.__count:
j = 2*k + 1
if (2*k + 2 < self.__count) and (self.__data[j] < self.__data[2*k + 2]):
j = 2*k + 2
if self.__data[k] > self.__data[j]:
break
self.__swap(k, j)
k = j
# Floyd算法建堆
def __floyd(self):
k = list(range((self.__count - 2) // 2 + 1))[::-1]
for each in k:
self.__shift_down(each)
# 打印堆內容
def print_heap(self):
for each in self.__data:
print(each, end=" ")
# 堆插入操作
def insert(self, num):
self.__data.append(num)
self.__count = self.__count + 1
self.__shift_up(self.__count - 1)
# 刪除堆頂操作
def extract_max(self):
assert self.__count > 0, "The Max Heap is Empty"
num = self.__data[0]
self.__swap(0, self.__count - 1)
self.__data = self.__data[:-1]
self.__count = self.__count - 1
self.__shift_down(0)
return num
max_heap = MaxHeap([23, 5, 3, 345, 2, 544, 34, 56, 3, 5, 5, 234, 56, 24, 75, 3, 56])
# 測試打印
max_heap.print_heap()
print("\n")
# 測試刪除堆頂
print(max_heap.extract_max())
# 測試打印
max_heap.print_heap()
print("\n")
# 測試刪除堆頂
print(max_heap.extract_max())
# 測試打印
max_heap.print_heap()
print("\n")
# 測試插入
max_heap.insert(12)
# 測試打印
max_heap.print_heap()
print("\n")
# 測試股層噶任意
max_heap.insert(123)
# 測試打印
max_heap.print_heap()
print("\n")
# 測試刪除堆頂
while True:
print(max_heap.extract_max())
元素插入
插入算法分爲兩個步驟:
首先,調用向量的標準插入接口,將新詞條接至向量的末尾。
儘管此時上圖所示,新詞條的引入並未破壞堆的結構性,但只要新詞條不是堆頂,就有可能與其父親違反堆序性。當然,其它位置的堆序性依然滿足。故以下將調用percolateUp()函數,對新接入的詞條做適當調整,在保持結構性的前提下恢復整體的堆序性。
上濾
不妨假定原堆非空,於是新詞條的父親(深色節點)必然存在。根據在向量中對應的秩,可以簡便地確定詞條對應的秩,即。
此時,若經比較判定,則堆序性在此局部以至全堆均已滿足,插入操作因此即告完成。
反之,若,則可在向量中令和互換位置。如上圖所示,如此不僅全堆的結構性依然滿足,而且和之間的堆序性也得以恢復。
當然,此後與其新的父親,可能再次違背堆序性。若果真如此,不妨繼續套用以上方法,如上圖所示令二者交換位置。當然,只要有必要,此後可以不斷重複這種交換操作。
每交換一次,新詞條都向上攀升一層,故這一過程也形象地稱作上濾(percolate up)。
當然,至多上濾至堆頂。一旦上濾完成,則如上圖所示,全堆的堆序性必將恢復。
由上可見,上濾調整過程中交換操作的累計次數,不致超過全堆的高度。而在向量中,每次交換操作只需常數時間,故上濾調整乃至整個詞條插入算法整體的時間複雜度,均爲。
示例
實現
元素刪除
刪除算法也分爲兩個步驟:
首先,既然待刪除詞條總是位於堆頂,故可直接將其取出並備份。此時如上圖所示,堆的結構性將被破壞。爲修復這一缺陷,可如圖所示,將最末尾的詞條轉移至堆頂。
新的堆頂可能與其孩子(們)違背堆序性——儘管其它位置的堆序性依然滿足。故調用percolateDown()函數調整新堆頂,在保持結構性的前提下,恢復整體的堆序性。
下濾
若新堆頂不滿足堆序性,則可如上圖所示,將與其(至多)兩個孩子中的大者(圖中深色節點)交換位置。與上濾一樣,由於使用了向量來實現堆,根據詞條的秩可便捷地確定其孩子的秩。此後,堆中可能的缺陷依然只能來自於詞條——它與新孩子可能再次違背堆序性。
若果真如此,不妨繼續套用以上方法,將與新孩子中的大者交換,結果如圖所示。實際上,只要有必要,此後可如圖和不斷重複這種交換操作。
因每經過一次交換,詞條都會下降一層,故這一調整過程也稱作下濾(percolate down)。
與上濾同理,這一過程也必然終止。屆時如圖所示,全堆的堆序性必將恢復;而且,下濾乃至整個刪除算法的時間複雜度也爲。
示例
實現
建堆
很多算法中輸入詞條都是成批給出,故在初始化階段往往需要解決一個共同問題:
給定一組詞條,高效地將它們組織成一個堆。這一過程也稱作“建堆”(heapification)。
蠻力法
從空堆起反覆調用標準insert()接口,即可將輸入詞條逐一插入其中,並最終完成建堆任務,但其消耗的時間卻過多。
時間複雜度:
自上而下的上濾
對任何一棵完全二叉樹,只需自頂而下、自左向右地針對其中每個節點實施一次上濾,即可使之成爲完全二叉堆。
時間複雜度:
Floyd算法
爲得到更快的建堆算法,先考查一個相對簡單的問題:
任給堆和,以及另一獨立節點,如何高效地將轉化爲堆?從效果來看,這相當於以爲中介將堆和合二爲一,故稱作堆合併操作。
如上圖,首先爲滿足結構性,可將這兩個堆當作的左、右子樹,聯接成一棵完整的二叉樹。此時若與孩子和滿足堆序性,則該二叉樹已經就是一個不折不扣的堆。
實際上,此時的場景完全等效於,在delMax()操作中摘除堆頂,再將末位詞條轉移至堆頂。故只需對p實施下濾操作,即可將全樹轉換爲堆。
如果將以上過程作爲實現堆合併的一個通用算法,則在將所有詞條組織爲一棵完全二叉樹後,只需自底而上地反覆套用這一算法,即可不斷地將處於下層的堆逐對地合併成更高一層的堆,並最終得到一個完整的堆。按照這一構思,即可實現Floyd建堆算法。
可見,該算法的實現十分簡潔:
只需自下而上、由深而淺地遍歷所有內部節點,並對每個內部節點分別調用一次下濾算法percolateDown()
示例如下:
時間複雜度:
蠻力算法與Floyd算法恰好相反——若將前者理解爲“自上而下的上濾”,則後者即是“自下而上的下濾”
c++實現
#include<iostream>
#include<algorithm>
#include<cassert>
using namespace std;
template<typename Item>
class MaxHeap{
private:
Item* data;
int count;
int capacity;
// 上濾操作
void shiftUp(int k)
{
while(k>1 && data[k/2] < data[k])
{
swap(data[k/2], data[k]);
k /= 2;
}
}
// 下濾操作
void shiftDown(int k)
{
while(2*k <= count)
{
int j = 2*k;
if (j + 1 <= count && data[j+1] > data[j])
{
j += 1;
}
if (data[k] >= data[j])
{
break;
}
swap(data[k], data[j]);
k = j;
}
}
public:
// 聲明堆的最大容量
MaxHeap(int capacity)
{
data = new Item[capacity + 1];
count = 0;
this->capacity = capacity;
}
// Floyd算法建堆
MaxHeap(Item arr[], int n)
{
data = new Item[n+1];
capacity = n;
for (int i = 0; i < n; i++)
{
data[i+1] = arr[i];
}
count = n;
for (int i = count/2; i >= 1; i--)
{
shiftDown(i);
}
}
~MaxHeap()
{
delete [] data;
}
// 返回堆的元素個數
int size()
{
return count;
}
// 返回堆是否爲空
bool isEmpty()
{
return count == 0;
}
// 插入元素
void insert(Item item)
{
assert(count + 1 <= capacity);
data[count+1] = item;
count++;
shiftUp(count);
}
// 刪除元素
Item extractMax()
{
assert(count > 0);
Item ret = data[1];
swap(data[1], data[count]);
count --;
shiftDown(1);
return ret;
}
};
int main()
{
int a [15];
srand(time(NULL));
for (int i = 0; i < 15; i++)
{
a[i] = rand()%100;
}
MaxHeap<int> maxheap = MaxHeap<int>(a, 15);
while( !maxheap.isEmpty())
{
cout << maxheap.extractMax() << " ";
}
cout<<endl;
return 0;
}
python實現
# -*- coding:utf-8 -*-
class MaxHeap:
def __init__(self, data):
"""
輸入一個列表, 建堆
:param data: 列表
"""
self.__data = data
self.__count = len(data)
self.__floyd()
# 交換self.__data中的兩個索引對應的值
def __swap(self, a, b):
c = self.__data[a]
self.__data[a] = self.__data[b]
self.__data[b] = c
# 對索引爲k(第k+1個值)的值, 上濾操作
def __shift_up(self, k):
while (k > 0) and (self.__data[(k-1)//2] < self.__data[k]):
self.__swap((k-1)//2, k)
k = (k-1)//2
# 對索引爲k(第k+1個值)的值, 下濾操作
def __shift_down(self, k):
while 2*k + 1 < self.__count:
j = 2*k + 1
if (2*k + 2 < self.__count) and (self.__data[j] < self.__data[2*k + 2]):
j = 2*k + 2
if self.__data[k] > self.__data[j]:
break
self.__swap(k, j)
k = j
# Floyd算法建堆
def __floyd(self):
k = list(range((self.__count - 2) // 2 + 1))[::-1]
for each in k:
self.__shift_down(each)
# 打印堆內容
def print_heap(self):
for each in self.__data:
print(each, end=" ")
# 堆插入操作
def insert(self, num):
self.__data.append(num)
self.__count = self.__count + 1
self.__shift_up(self.__count - 1)
# 刪除堆頂操作
def extract_max(self):
assert self.__count > 0, "The Max Heap is Empty"
num = self.__data[0]
self.__swap(0, self.__count - 1)
self.__data = self.__data[:-1]
self.__count = self.__count - 1
self.__shift_down(0)
return num
max_heap = MaxHeap([23, 5, 3, 345, 2, 544, 34, 56, 3, 5, 5, 234, 56, 24, 75, 3, 56])
# 測試打印
max_heap.print_heap()
print("\n")
# 測試刪除堆頂
print(max_heap.extract_max())
# 測試打印
max_heap.print_heap()
print("\n")
# 測試刪除堆頂
print(max_heap.extract_max())
# 測試打印
max_heap.print_heap()
print("\n")
# 測試插入
max_heap.insert(12)
# 測試打印
max_heap.print_heap()
print("\n")
# 測試股層噶任意
max_heap.insert(123)
# 測試打印
max_heap.print_heap()
print("\n")
# 測試刪除堆頂
while True:
print(max_heap.extract_max())
就地堆排序
對於向量中的n個詞條,如何藉助堆的相關算法,實現高效的排序?這類算法也稱作堆排序(heapsort)算法。
算法的總體思路和策略與選擇排序算法基本相同:
將所有詞條分成未排序和已排序兩類,不斷從前一類中取出最大者,順序加至後一類中。算法啓動之初,所有詞條均屬於前一類;此後,後一類不斷增長;當所有詞條都已轉入後一類時,即完成排序。
這裏的待排序詞條既然已組織爲向量,不妨將其劃分爲前綴和與之互補的後綴,分別對應於上述未排序和已排序部分。與常規選擇排序算法一樣,在算法啓動之初覆蓋所有詞條,而爲空。新算法的不同之處在於,整個排序過程中,無論包含多少詞條,始終都組織爲一個堆。
另外,整個算法過程始終滿足如下不變性:
中的最大詞條不會大於中的最小詞條——除非二者之一爲空,比如算法的初始和終止時刻。
首先如上圖,取出首單元詞條,將其與末單元詞條交換。既是當前堆中的最大者,同時根據不變性也不大於中的任何詞條,故如此交換之後必處於正確的排序位置。故如圖,此時可等效地認爲向前擴大了一個單元,相應地縮小了一個單元。然後需對實施一次下濾調整,即可使整體的堆序性重新恢復。
示例
- Floyd算法建堆
- 就地堆排序
實現
c++實現
#include<iostream>
#include<algorithm>
#include<cassert>
using namespace std;
template<typename T>
void __shiftDown(T arr[], int n, int k)
{
while(2*k + 1 < n)
{
int j = 2*k + 1;
if (j+1 < n && arr[j+1] > arr[j])
{
j += 1;
}
if (arr[k] >= arr[j])
{
break;
}
swap(arr[k], arr[j]);
k = j;
}
}
template<typename T>
void heapSort(T arr[], int n)
{
for(int i = (n-2)/2; i>=0; i--)
{
__shiftDown(arr, n, i);
}
for (int i = n -1; i>0; i--)
{
swap(arr[0], arr[i]);
__shiftDown(arr, i, 0);
}
}
int main()
{
int a [15];
srand(time(NULL));
for (int i = 0; i < 15; i++)
{
a[i] = rand()%100;
}
heapSort(a, 15);
for (int i = 0; i<15; i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
return 0;
}
python實現
# -*- coding:utf-8 -*-
test_data = [23, 5, 3, 345, 2, 544, 34, 56, 3, 5, 5, 234, 56, 24, 75, 3, 56]
def swap(data, i, j):
a = data[i]
data[i] = data[j]
data[j] = a
def shift_down(data, k, n):
while 2 * k + 1 < n:
j = 2 * k + 1
if (2 * k + 2 < n) and (data[j] < data[2 * k + 2]):
j = 2 * k + 2
if data[k] > data[j]:
break
swap(data, k, j)
k = j
def heap_sort(data):
# floyd建堆
k = list(range((len(data) - 2) // 2 + 1))[::-1]
for each in k:
shift_down(data, each, len(data))
for each in range(len(data)):
i = len(data) - 1 - each
swap(data, 0, i)
shift_down(data, 0, i)
heap_sort(test_data)
print(test_data)
結語
如果您有修改意見或問題,歡迎留言或者通過郵箱和我聯繫。
手打很辛苦,如果我的文章對您有幫助,轉載請註明出處。