本文的最新版本位于:https://github.com/iwhales/algorithms_notes
转载请注明出处:https://www.jianshu.com/u/5e6f798c903a
参考:《数据结构(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,它们都是基本数据结构,通常由计算机语言提供。
参考: Examples of Concrete and Abstract Data Types Concrete data type vs Abstract data type (data structures)
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 <variable> = array(<capacity>, <optional fill value>) 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(n2) 。考虑下述方案,每次将数组大小翻倍,从而将整体时间性能优化为O(log2n) ,但这样做会以浪费内存为代价。
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 Arrayclass 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 resultdef 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()
作者:曾翔翔
链接:https://www.jianshu.com/p/1fc797b2618e