1 顺序表
1.1 顺序表的形式和结构
上图表示的是顺序表的基本形式,数据元素本身连续存储,每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址
,而元素存储的物理地址(实际内存地址)可以通过存储区的起始地址Loc (e0)加上逻辑地址(第i个元素)与存储单元大小(c)的乘积计算而得,即:
Loc(ei) = Loc(e0) + c*i
故,访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)
。
如果元素的大小不统一,则须采用图b的元素外置的形式,将实际数据元素另行存储,而顺序表中各单元位置保存对应元素的地址信息(即链接)。由于每个链接所需的存储量相同,通过上述公式,可以计算出元素链接的存储位置,而后顺着链接找到实际存储的数据元素。注意,图b中的c不再是数据元素的大小,而是存储一个链接地址所需的存储量,这个量通常很小。
图b这样的顺序表也被称为对实际数据的索引,这是最简单的索引结构
1.2 python的顺序表
- 概念
Python中的list
和tuple
两种类型采用了顺序表的实现技术,具有前面讨论的顺序表的所有性质。
tuple是不可变类型,即不变的顺序表,因此不支持改变其内部状态的任何操作,而其他方面,则与list的性质类似。
- list的基本实现技术
Python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,并在各种操作中维持已有元素的顺序(即保序),而且还具有以下行为特征:
-
基于下标(位置)的高效元素访问和更新,时间复杂度应该是O(1);
为满足该特征,应该采用顺序表技术,表中元素保存在一块连续的存储区中。
-
允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变。
为满足该特征,就必须能更换元素存储区,并且为保证更换存储区时list对象的标识id不变,只能采用分离式实现技术。
在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表
。这就是为什么用list.append(x) (或 list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的原因。
在Python的官方实现中,list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
1.3 顺序表的操作
1.3.1 顺序表的数据插入
- prepend(头部插入)
可以明显的知道,因为顺序表元素是连续的,如果你想要头部插入一个元素,则需要将之前所有元素整体往后移动,因此时间复杂度无疑为O(n)
- append(尾部插入)
尾部的话则只需要扩容在尾部加入元素即可,因此时间复杂度为O(1)
- insert(中间插入)
中间插入参考头部插入,后面的元素也需要往后移动,因此时间复杂度也为O(n)
1.3.2 删除数据
- 删除表尾数据
时间复杂度为O(1)
- 删除不为表尾数据
时间复杂度为O(n)
1.3.3 访问指定index元素
访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)
2.单链表
从上面顺序表的特性我们可以知道,由于顺序表的空间地址是连续的,所有做插入操作或者删除操作的话,后面的元素需要做移动操作,可能时间复杂度会达到O(n),当我们的程序需要做这种频繁的操作时,我们就不适合用这种数据结构了,而链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。这里我们先介绍一下单链表。
2.1 链表的定义
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)
。
下面是图示意:
可以看到每一个节点都有一个数据区和链接区,数据区可以是任何数据结构(元祖,字典,列表),链接区则保存一个地址指向下一个元素
2.2 单向链表
2.2.1 定义
单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
2.2.2 python实现单链表
- 节点实现
class SingleNode(object):
"""单链表的结点"""
def __init__(self,item):
# _item存放数据元素
self.item = item
# _next是下一个节点的标识
self.next = None
由于python没有指针的概念,因此用一个类来模拟表示一个节点,self.item保存值,self.next保存地址
- 基本功能实现
- is_empty() 链表是否为空
- length() 链表长度
- travel() 遍历整个链表
class SingleLinkList(object):
"""单链表"""
def __init__(self):
# 初始头节点为None
self._head = None
def is_empty(self):
"""判断链表是否为空,如果头节点为None,说明这个列表是为空的"""
return self._head == None
def length(self):
"""链表长度"""
# cur初始时指向头节点
cur = self._head
count = 0
# 尾节点指向None,当未到达尾部时
while cur != None:
count += 1
# 将cur后移一个节点
cur = cur.next
return count
def travel(self):
"""遍历链表"""
cur = self._head
# 判断最后一个节点是否为None,如果为None说明已经遍历到尾了
while cur != None:
print(cur.item)
cur = cur.next
print ""
- 头插入操作实现
def add(self, item):
"""头部添加元素"""
# 先创建一个保存item值的节点
node = SingleNode(item)
# 将新节点的链接域next指向头节点,即_head指向的位置
node.next = self._head
# 将链表的头_head指向新节点
self._head = node
新建一个节点,改变头节点的指向
- 尾插入实现
def append(self, item):
"""尾部添加元素"""
node = SingleNode(item)
# 先判断链表是否为空,若是空链表,则将_head指向新节点
if self.is_empty():
self._head = node
# 若不为空,则遍历找到尾部,将尾节点的next指向新节点
else:
cur = self._head
while cur.next != None:
cur = cur.next
cur.next = node
- 中间插入
def insert(self, pos, item):
"""指定位置添加元素"""
# 若指定位置pos为第一个元素之前,则执行头部插入
if pos <= 0:
self.add(item)
# 若指定位置超过链表尾部,则执行尾部插入
elif pos > (self.length() - 1):
self.append(item)
# 找到指定位置
else:
node = SingleNode(item)
count = 0
# pre用来指向指定位置pos的前一个位置pos-1,初始从头节点开始移动到指定位置
pre = self._head
while count < (pos - 1):
count += 1
pre = pre.next
# 先将新节点node的next指向插入位置的节点
node.next = pre.next
# 将插入位置的前一个节点的next指向新节点
pre.next = node
- 删除操作
def remove(self, item):
"""删除节点"""
cur = self._head
pre = None
while cur != None:
# 找到了指定元素
if cur.item == item:
# 如果第一个就是删除的节点
if not pre:
# 将头指针指向头节点的后一个节点
self._head = cur.next
else:
# 将删除位置前一个节点的next指向删除位置的后一个节点
pre.next = cur.next
break
else:
# 继续按链表后移节点
pre = cur
cur = cur.next
3.顺序表和单链表进行对比
顺序表特点:顺序表的优点是
访问元素的时间复杂度比较小
,缺点在于顺序表的空间必须是连续的,如果说一旦动态的改变,整个存储区都得改变。链表的特点:
可以对离散的存储空间得到充分的利用
,但是它的缺点为在于额外的开销比较大,利用链表之后,你对于存储的操作时(add,insert,append)时间复杂度会比顺序表小,可以说是空间复杂度换时间复杂度的做法
操作 | 链表 | 顺序表 |
---|---|---|
访问元素 | O(n) | O(1) |
在头部插入/删除 | O(1) | O(n) |
在尾部插入/删除 | O(1) | O(1) |
在中间插入/删除 | O(1) | O(n |