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 的本意同样,可能是用于顺序任务处理的缓存。

相关文章
相关标签/搜索