ADT - 序列

ADT


Abstract Data Type 是一些操做的集合。他們是數學層面上的抽象。ADT 的定義中只含有這些操做的行爲而不涉及它們的實現。這些具體定義的行爲也能夠當作是一種約束(如棧就是設計爲 LIFO 的),設計者指望經過這些約束獲取必定的好處。python

某種 ADT 須要擁有哪些操做取決於具體的需求,但存在一些特別通用的類型,將在後面逐一描述。算法

實現的部分包括數據結構和算法,不一樣的數據結構每每決定了不一樣的算法。編程

另外關於數據結構和算法之間的關係:某些數據結構的屬性致使它們在某些需求上擁有優秀的算法性能,如數組的按序取值(FindKth),哈希的隨機取值都擁有 O(1) 的複雜度。所以把一個具體問題拆分紅若干基本需求,而後把基本需求化歸到使用一種已知數據結構的屬性來求解是一種典型的算法思路。數組

本篇描述 ADT 中的一類:序列類型,和其三個典型的 ADT:列表、棧和隊列。緩存

序列二字的**序,**表示本類型的元素排列是有序的,則表示這是一種線性結構。所以全部的序列類型均可以當作一串元素,區別僅顯示在他們某些特定屬性(約束)上。數據結構

列表 list


列表是型如 A1, A2, A3,... An 的最基本的序列類型,咱們只定義它的大小,爲 n,和每兩個元素之間的相對位置,即 A<sub>n-1</sub> 前驅 A<sub>n</sub>,A<sub>n</sub> 後繼 A<sub>n-1</sub>。app

基本實現有數組鏈表,其中鏈表又能夠實現爲 單鏈表(singly linked list)雙鏈表(doubly linked list)循環鏈表(circularly linked list)。他們的 Find 方法都是 O(N) 的,但 Insert/DeleteFindKth 依實現各有不一樣。dom

數組 array

數組實現的空間由於是預分配的,因此在建立前大小必須已知。數據結構和算法

其插入/刪除的複雜度爲 O(N),由於須要移動插入位置後面的所有元素。而 FindKth 的複雜度爲 O(1),這一點在某些需求下尤爲好用,但主要由於空間的問題,通常通用實現更偏心鏈表。函數

鏈表 linked list

鏈表的結構更接近本節第一句話對列表的定義,由於它確實在每一個元素裏記錄了其相鄰元素的地址,這使其能夠不佔用連續的地址並支持動態大小。

但也所以其 FindKth 的複雜度爲 O(N),由於你沒法像數組同樣經過計算偏移量來直接找到你要的元素,必須從頭遍歷。但一旦找到位置,其插入/刪除的複雜度是 O(1),由於只須要改變兩個指針就能夠了。

多數地方,如 Wikipedia,對鏈表的插入複雜度解釋爲 O(1),是由於沒有把找到這個位置的過程算進來。不算的理由是鏈表的使用一般伴隨着一次遍歷,在遍歷的過程當中按需增刪。所以若是是在通用概念的列表增刪操做下,複雜度的優點(較數組)並不存在。

在編程細節上,雙鏈表實現增長了反向查詢的便利;而循環鏈表或循環雙鏈表使得鏈表連成了一個圈,又增長了某些狀況下的便利性,好比負數索引

>>> a = [1, 2, 3]
>>> a[-1]
3

基數排序 radix sort

基數的意思是一種進制下獨立數字的個數,即 n 進制的 n。

基數排序的原型是桶排序(bucket sort)。即假設要給 [0, M) 範圍內的 N 個整數排序,那麼咱們就造一個大小爲 M 的數組,而後初始化爲 0。而後遍歷待排序的數字,將數組對應數字索引的元素 +1,即記爲該數字遇到了一次。遍歷完後掃一遍桶就獲得了排序後的結果。例

lang:python
from array import array

M = 1000

numbers = [random.randint(0, M) for i in range(12)]
print(numbers)

buckets = array('i', [0] * M)

for num in numbers:
	buckets[num] += 1

sorted = []
for i in range(M):
	sorted.extend([i] * buckets[i])
print(sorted)

執行可得: 注意重複元素 264 也獲得了正確處理

[242, 823, 986, 704, 28, 428, 442, 185, 859, 482, 264, 264]
[28, 185, 242, 264, 264, 428, 442, 482, 704, 823, 859, 986]

後面再也不特地使用 array 對象,一概以 Python 默認的 list 代替,具體應該用數組仍是鏈表,依上下文而定。

桶排序的問題在於構建桶的時候咱們在一個數組裏窮舉了全部可能的元素,但只排序了少許值,形成了空間的浪費。另外一方面在機器資源有限的前提下,單是窮舉全部元素這件事均可能是危險的。

解決這個問題的算法方法和人類對天然數的處理方法一模一樣。就像人並無爲數一千頭羊發明一千個數字,而是選擇了基於位置的基數表示法同樣,咱們在這個問題上也能夠選擇構建 log<sub>R</sub>M 個數組。其中每一個數組的大小都是 R,即 基數。而後從低位到高位屢次排序。

計算係數的公式:

coefficient = (number / (radix ** index)) % radix

e.g.

number = 123
radix = 10

coefficient_0 = 3
coefficient_1 = 2
coefficient_2 = 1

基數排序與桶排序還有一點不一樣,由於此次的桶只含有原數字的一位,咱們須要在排序時把原數帶上:

M = 1000
radix = 12
numbers = [242, 823, 986, 704, 28, 428, 442, 185, 859, 482, 264, 264]

for index in range(int(math.ceil(math.log(M, radix)))):
	sorted = [[] for i in range(radix)]
	for num in numbers:
		coefficient = (num / (radix ** index)) % radix
		sorted[coefficient].append(num)
	numbers = reduce(lambda x, y: x + y, sorted)

print(numbers)

棧(stack) & 隊列(queue)


棧和隊列是對列表添加特定約束後的數據結構。棧要求每次存取元素都要在列表的一頭進行,這一端稱爲其頂(top)。而隊列要求存取元素分別在其兩端進行,分別叫作對頭(front)和隊尾(rear)。

棧的後入先出特性使其適合用做實現某些流程的分步暫存,好比函數調用。遞歸調用裏面有一種情形是總在代碼最後進行遞歸調用,這被稱爲尾遞歸,以下

def feb(n):
	if n == 0:
		return 0
	elif n == 1:
		return 1
	else:
		return feb(n-1) + feb(n-2)

這是一種性能很是很差的遞歸用法,由於遞歸開始時暫存的變量在遞歸結束後都沒用了,等於純浪費。所以這種情形有時會被編譯器或虛擬機優化掉,稱爲尾調用優化(Tail Call Optimization, TCO)。這時的調用不會修改調用棧。

手動優化尾遞歸的方式是把它變成一個循環:

def feb1(n):
	if n < 2:
		return n
	else:
		x = 0
		y = 1
		for i in range(n-1):
			x, y = y, x + y
		return y

使用 timeit 測試的話會發現 feb1 比 feb 快不少不少。

隊列的應用場景就像 queue 的本意同樣,可能是用於順序任務處理的緩存。

相關文章
相關標籤/搜索