手记

数组和链表结构(python)_1

本文的最新版本位于: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. 获取数组内存块的基本地址——数组第1项的机器地址

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

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)O(n)O(n) 。如果需要给数组添加nnn 项,那么整体的时间性能是n(n+1)/2n(n+1)/2n(n+1)/2 ,即O(n2)O(n^2)O(n2) 。考虑下述方案,每次将数组大小翻倍,从而将整体时间性能优化为O(log2n)O(log_2n)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 复杂度分析

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

操作运行时间
访问第iii 个位置的元素O(1)O(1)O(1),最好情况和最坏情况
替换第iii 个位置的元素O(1)O(1)O(1),最好情况和最坏情况
从逻辑末尾插入O(1)O(1)O(1),平均情况
从逻辑末尾删除O(1)O(1)O(1),平均情况
在第iii 个位置插入O(n)O(n)O(n),平均情况
从第iii 个位置删除O(n)O(n)O(n),平均情况
增加容量O(n)O(n)O(n),最好情况和最坏情况
减小容量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

1人推荐
随时随地看视频
慕课网APP