参考:《数据结构(Python 语言描述)》- 第4章 数组和链表

在编程语言中,最常用来实现集合(collection)的两种数据结构是数组和链表结构。这两种类型的结构采用不同的方法在计算内存中存储和访问数据。也正是由于这些不同的方法,导致了操作这两种结构的算法存在不同的时间/空间取舍。由此,我们需要了解数据结构,以明白不同的数据结构对算法的影响。

本文内容目录如下,会分拆为两篇笔记,另一则笔记是 "数组和链表结构(python)_2"。

目录.jpg

1. 术语

本节中的一些概念并非特定于 Python,比如 Python 中并没有类似于 C/C++ 中的数组(array)类型,所以在看到这些类型时,请以 C/C++ 的视角进行思考。

"数据结构"和"具体数据类型(CDT)"这两个术语,指的是集合的数据(collection's data)的内部表示——集合是一种抽象数据类型(ADT)。比如对于栈(stack)这种 ADT 集合,在其内部可使用数组或链存储数据,那么此时讨论的"数据结构"和"具体数据类型(CDT)"便是针对数组或链表进行的。另外,随着视角的变换也可将 ADT 集合直接视作"数据结构",比如我们常说栈是一种先进后出的数据结构,便是在思考栈这种"数据结构"的特征,而非是在讨论栈内部数据元素具体采用的"数据结构"。

1.1 数据结构

数据结构(data structure)是一种组织和存储数据的方式,它包含了数据值的集合、数据间的关系,以及可应用于数据之上函数或操作。常见数据结构如:数组、链表、栈、队列。

下图展示了哈希表(hash table)的数据结构:

Hash_table.png

Tips:为什么数组即是数据结构,又是数据类型? 数据结构是一种解决问题的思路,而数据类型是这种思路的具体实现。在讨论"数据结构"时,我们是在研究这种结构的具体特性,比如使用了连续的内存、支持随机访问等;在讨论数据类型时,我们是在讨论这种类型使用方法,比如支持索引操作。

1.2 具体数据类型

具体数据类型(Concrete Data Type,CDT)是 ADT 的实际实现方式,比如栈(ADT 类型)可由数组(CDT 类型)实现。数组(array)、链表(linked list)和树(tree) 均属于 CDT,它们都是基本数据结构,通常由计算机语言提供。

1.3 抽象数据类型

抽象数据类型(Abstract Data Type,ADT)不单纯是一组值的集合,还包括作用在值集上的操作的集合,在"构造数据类型(Structured data types)"的基础上同时增加了对数据的操作,并且类型的表示细节及操作的实现细节对外是不可见得——在 C 语言中,构造数据类型包括 数组(array)、结构体(struct)、联合体 (union)、枚举(enum)。

之所以命名为抽象数据类型,就是因为外部只知道它做什么,而不知道它如何做,更不知道数据的内部表示细节。 这样,即使改变数据的表示和操作的实现,也不会影响程序的其他部分。抽象数据类型可达到更好的信息隐藏效果,因为它使程序不依赖于数据结构的具体实现方法,只要提供相同的操作,换用其他方法实现时,程序无需修改,这个特征对于系统的维护很有利。

同一个数据结构可以实现为 1 或多个抽象数据类型。比如,哈希表这种数据结构,便可能由于哈希函数的不同而产生不同的实现,从而形成多个不同的抽象数据类型。栈(stack)、队列(queue)和堆(heap) 也属于抽象数据结构。

要实现某个 ADT 必须以某个恰当的 CDT 为基础,进行实现。比如,栈和队列可以由数组或链表实现;堆可以由数组或二叉树(binary tree)实现。

2. 数组数据结构

Array Data Structure

由于在许多编程语言中,集合中的实现结构主要是数组,而不是列表。因此,我们需要熟悉用数组的方式来思考问题,本节内容就是在要帮助我们了解如何利用数组实现集合,以及对部分操作进行复杂度分析。

Python 的 array 模块虽提供了 array 类,但该类与其它编程语言中的数组存在很大的区别,其行为更类似于列表,而且仅能存储数值。因此,为了讨论数组数据结构,我们将自定义一个 Array 类(下文中提及的数组都特指该自定义类),具体定义如下:

"""

File: arrays.py

An Array is a restricted list whose clients can use

only [], len, iter, and str.

To instantiate, use

= array(, )

The fill value is None by default.

"""

class Array(object):

"""Represents an array."""

def __init__(self, capacity, fillValue = None):

"""Capacity is the static size of the array.

fillValue is placed at each position."""

self._items = list()

for count in range(capacity):

self._items.append(fillValue)

def __len__(self):

"""-> The capacity of the array."""

return len(self._items)

def __str__(self):

"""-> The string representation of the array."""

return str(self._items)

def __iter__(self):

"""Supports iteration over a view of an array."""

return iter(self._items)

def __getitem__(self, index):

"""Subscript operator for access at index."""

return self._items[index]

def __setitem__(self, index, newItem):

"""Subscript operator for replacement at index."""

self._items[index] = newItem

2.1 随机访问和连续内存

计算机通过为数组分配一段连续的内存单位,从而支持对数组的随机访问。

索引操作便属于随机访问,其步骤如下:

获取数组内存块的基本地址——数组第1项的机器地址

给这个地址加上索引,返回最终结果。

2.2 物理尺寸和逻辑尺寸

物理尺寸:表示数组的最大容量,就是在创建数组时用于指定数组容量的数值。

逻辑尺寸:表示在数组中已填充了多少项。如果逻辑尺寸为了0,则表示数组为空;如果逻辑尺寸不为 0,则最后一项的索引为逻辑尺寸减 1。

在操作数组时,由程序员负责记录数组的逻辑尺寸和物理尺寸。 逻辑尺寸和物理尺寸的比值被称作数组的装载因子(load factor),

2.3 数组的操作

这一节会在数组上实现几种常见操作,这些操作数组的方法应位于包含数组的集合中,集合利用这些方法操作其内部的数组。分析这些常见操作的目的是为了解其运行时间的复杂度,以便在利用不同的数据结构实现集合时,可以有效对比不同数据结构间的优劣。

本节之后的示例中,会假设存在如下初始数据设置:

class ListCollection(object):

DEFAULT_CAPACITY = 5 # 默认容量

def __init__(self):

self.logical_size = 0 # 逻辑尺寸

self._array = Array(ListCollection.DEFAULT_CAPACITY)

# --snip--

a. 增加数组的尺寸

当插入新的项时,数组的逻辑尺寸等于物理尺寸,便需要增加数组的尺寸。

def increase_size(self):

if self.logical_size == len(self._array):

temp = Array(len(self._array)+1)

for i in range(self.logical_size):

temp[i] = self._array[i]

self._array = temp

# 旧数组的内存留给垃圾回收程序处理

采用上述代码增加数组尺寸时,复制操作的次数是线性的 O(n) 。如果需要给数组添加 n 项,那么整体的时间性能是 n(n+1)/2 ,即 O(n^2) 。考虑下述方案,每次将数组大小翻倍,从而将整体时间性能优化为 O(log_2n) ,但这样做会以浪费内存为代价。

temp = Array(len(a_array)*2)

b. 减少数组的尺寸

当数组的逻辑尺寸小于某个阈值时,就应当缩小其物理尺寸,以避免浪费大量内存。

def decrease_size(self):

if self.logical_size <= len(self._array)//4 and \

len(self._array) >= ListCollection.DEFAULT_CAPACITY*2:

temp = Array(len(self._array)//2)

for i in range(self.logical_size):

temp[i] = self._array[i]

self._array = temp

c. 在数组中插入一项

def insert_item(self, target_index, target_item):

self.increase_size() # 检查是否需要增加数组尺寸

# 从最后一项开始以逐个向后移动

for i in range(self.logical_size, target_index, -1):

self._array[i] = self._array[i-1]

self._array[target_index] = target_item

self.logical_size += 1

在插入过程中,移动项的时间性能在平均情况下是线性的,因此,插入操作的时间性能是线性的。

e. 从数组中删除一项

def remove_item(self, target_index, target_item):

# 从目标项之后的一项开始,逐个向前移动

for i in range(target_index, self.logical_size-1):

self._array[i] = self._array[i+1]

self.logical_size -= 1

# 检测是否需要减少物理尺寸

self.decrease_size()

由于移动一项的事件性能平均是线性的,因此,删除操作的时间性能是线性的。

2.4 复杂度分析

下表给出了数组操作的运行时间:

操作

运行时间

访问第 i 个位置的元素

O(1),最好情况和最坏情况

替换第 i 个位置的元素

O(1),最好情况和最坏情况

从逻辑末尾插入

O(1),平均情况

从逻辑末尾删除

O(1),平均情况

在第 i 个位置插入

O(n),平均情况

从第 i 个位置删除

O(n),平均情况

增加容量

O(n),最好情况和最坏情况

减小容量

O(n),最好情况和最坏情况

可见数组提供了对已经存在的项的快速访问,并且提供了在逻辑末尾位置的快速插入和删除。然而,在任意位置的插入和删除可能会慢上一个量级。调整大小所需时间也是线性阶,但是如果调整每次变化的倍率,则能够优化所需时间。

2.5 完整示例

综合上面的代码,以下是利用数组实现集合的一个完整示例:

class ListCollection(object):

DEFAULT_CAPACITY = 5 # 默认容量

def __init__(self):

self.logical_size = 0 # 逻辑尺寸

self._array = Array(ListCollection.DEFAULT_CAPACITY)

def increase_size(self):

if self.logical_size == len(self._array):

temp = Array(len(self._array)+1)

for i in range(self.logical_size):

temp[i] = self._array[i]

self._array = temp

# 旧数组的内存留给垃圾回收程序处理

def decrease_size(self):

if self.logical_size <= len(self._array)//4 and \

len(self._array) >= ListCollection.DEFAULT_CAPACITY*2:

temp = Array(len(self._array)//2)

for i in range(self.logical_size):

temp[i] = self._array[i]

self._array = temp

def insert_item(self, target_index, target_item):

self.increase_size()

for i in range(self.logical_size, target_index, -1):

self._array[i] = self._array[i-1]

self._array[target_index] = target_item

self.logical_size += 1

def remove_item(self, target_index):

# 从目标项之后的一项开始,逐个向前移动

for i in range(target_index, self.logical_size-1):

self._array[i] = self._array[i+1]

self.logical_size -= 1

# 检测是否需要减少物理尺寸

self.decrease_size()

def __len__(self):

# 逻辑长度

return self.logical_size

def __getitem__(self, item):

if item < self.logical_size:

return self._array[item]

def __setitem__(self, key, value):

self.increase_size()

self._array[key] = value

self.logical_size += 1

def __str__(self):

return str(self._array[:self.logical_size])

2.6 二维数组

利用之前定义的 Array 类,可以构建出二维数组 Grid 类,代码如下:

"""

File: grid.py

仅能使用 [], str, getHeight 和 getWidth, 不提供迭代器

"""

from 数组数据结构 import Array

class Grid(object):

"""Represents a two-dimensional array."""

def __init__(self, rows, columns, fillValue=None):

self._data = Array(rows)

for row in range(rows):

self._data[row] = Array(columns, fillValue)

def getHeight(self):

"""Returns the number of rows."""

return len(self._data)

def getWidth(self):

"Returns the number of columns."""

return len(self._data[0])

def __getitem__(self, index):

"""Supports two-dimensional indexing with [][]."""

return self._data[index]

def __str__(self):

"""Returns a string representation of the grid."""

result = ""

for row in range(self.getHeight()):

for col in range(self.getWidth()):

result += str(self._data[row][col]) + " "

result += "\n"

return result

def main():

g = Grid(10, 10, 1)

print(g)

# 重置二维列表

for row in range(g.getHeight()):

for column in range(g.getWidth()):

g[row][column] = row*10 + column

print(g)

if __name__ == "__main__":

main()