基本数据结构学习-线性表
线性表(顺序表和链表)
什么是线性表?
在程序中将一组数据(通常同为某种类型的)数据作为整体去管理和使用,需要创建这种元素组,用变量去记录它们,传递传出函数等。一组数据中包含的元素个数可能发生改变(增加和删除元素),这样一组序列元素的组织,可以将其抽象成线性表。
一个线性表是某类元素的一个集合,还记录着元素之间的一种顺序关系,线性表是最基本的数据结构之一。
根据线性表的实际存储方式,分为两种实现模型:
顺序表:将元素顺序地放到一块连续的存储区里,元素间的顺序关系由它们存储顺序自然表示?
链表:将元素放在通过链接构造起来的一系列存储块中
顺序表
顺序表的基本形式和元素外置形式
基本形式:数据本身连续存储,且每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址,而元素的物理地址(实际内存地址)可以通过存储区的起始位置(Loce0)加上逻辑地址(第i 个元素)与存储单元大小(c)的乘积计算而得,即:Loc(ei) = Loc(e0)+ (c * i),因此访问指定元素时,不需要从头开始遍历,通过计算就可以获得对应的地址,其时间复杂度位O(1)
元素外置形式:元素的大小不统一,将实际的数据元素另行存储,而顺序表中各单元位置保存对应的元素的地址信息(即链接)。由于每个链接所需要的存储量相同,通过上述公式可以计算出元素链接的存储位置,然后顺着链接找到实际存储的数据元素。注意:这时候公式中的c 不再表示元素的大小,而是存储一个链接地址所需要的存储量,这个量通常很小。
元素外置形式的顺序表,也被称为实际数据的索引,是最简单的索引结构。
顺序表结构
一个顺序表完整的信息包括两部分,一部分是表中元素的集合,另一部分是实际正确操作而需要记录的信息,即有关表的整体情况信息,这部分信息主要包括元素存储区的容量(最大存储元素的个数)和当前已有的元素的个数。
顺序表的两种基本实现方式:一体式结构和分离式结构
一体式结构:存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,两部分数据的整体形成了一个完整的顺序表对象。
一体式结构整体性强,易于管理,但是由于数据元素存储区域是表的一部分,顺序表创建后,元素存储区域就固定了,因此,如果更换数据区,则只能整体迁移,即整个顺序表对象(存储信息表结构的区域)改变了。
分离式结构:表对象中只保存与整个表有关的信息(及元素的容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。
分离式结构更加灵活,若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。
对于分离式结构的顺序表,实现扩容,只要程序的运营环境(计算机系统)还有空闲的存储,这种表结构就不会因为满了而导致操作无法继续进行,这种技术实现的顺序表被称为动态顺序表,其容量可以在使用中动态的变化。
两种扩容策略:
1.每次扩容增加固定数目的存储位置,比如每次扩容增加10。
特点:节省空间,但是扩容操作频繁,操作次数多
2.每次扩容,容量加倍,比如每次扩容增加一倍
特点:减少扩容次数,但可能浪费空间资源,以空间换时间,推荐的扩容方式。
Python中的顺序表 (python 天然支持元素外置,永远支持元素外置)
python 中有list和tuple两种类型采用了顺序表的实现技术,具有前面讨论的所有顺序表的性质。
tuple 是不可变类型,即不变的顺序表, 因此不支持改变其内部状态的任何操作,其他方面和list 的性质类似。
list 的基本实现技术
Python标准类型list是一种元素个数可变的线性表,加入和删除元素,并在各个操作中维持已有的元素的顺序(保序)而且还具有以下特征:
1.基于下标(位置)的高效元素的访问与更新,时间复杂度应该是O(1),为满足该特征,应采用顺序表技术,表中的元素保存在一块连续的存储区中
2.允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变,为满足该特征,就必须能更换元素存储区,而为了保证更换存储区时,list 的对象的标识id不变,只能采用分离式结构实现。
在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表,这就是为什么在在尾部插入元素比在指定位置插入元素效率高的原因。
采用了如下策略:在建立空表(或很小的表)时,系统分配一块能容纳8个元素的存储区,在执行插入(append或者insert)操作的时候,如果元素满就换一块四倍大的存储区,但此时的表已经很大了(目前的阈值(yu值:临界值)为50000),则改变策略,采用增加一倍的方法,引入这种改变策略的方式,是为了避免出现过多的空闲空间。
知识扩展(python中的dict的数据结构)
dict 类似 hash表,不是线性表(不是一对一的数据结构)
建立一个固定空间(总长度固定),比如这个空间有八个坑,称为这个hash表的桶长度是8,存数据Value 的时候,需要指定一个Key值,将Key值通过hash算法,得到一个不大于桶长度的hash值(比如5),则会将这个数据Value 的地址引用存到6这个坑里面去。
取数据的时候,也会将你输入的Key值通过hash算法,得到一个固定的hash值(hash算法固定,所以,存和取得时候,相同的Key值,通过hash算法得到得结果一定是相同的)。
如果通过不同的Key 算出相同的hash值,则6 这个坑中存的是个hash链,这个hash链中包括了所有hash值为6 的数据Value。对应着取数据Value的步骤就是:1.根据Key 值计算出hash值,通过hash值取出hash链。2.遍历判断hash链中的元素是否是待查元素
以空间换时间(尽量大的桶长度),则取值的时候,算出相同hash值几率小,即可取出数据。相反,如果桶长度很小,则算出hash值重复的几率大,也就是对应坑放的是hash链,取出后,还有耗时挨个判断。
链表
为什么需要链表?
顺序表的结构需要预先知道数据的大小,来申请连续的存储空间,而在进行扩容的时候又要进行数据迁移(一体式结构),使用不是很灵活。
1.顺序表容易造成空间浪费(扩容到很大的内存空间后,若不用后面的空间)
比如当你使用一个顺序表存储很多的信息,需要跟内存申请一个足够大的空间,但是后来你又将这个顺序表后面绝大部分数据给删除了,而且以后都不怎么会用到了,那么此时的状态就是,你用了一个足够大的空间,去存储了数量极少的数据,造成了内存空间的浪费。
2.顺序表需要连续的空间(内存中很难有大片连续的内存空间,内存碎片化)
顺序表的空间必须是整体的,里面所存储的数据也是连续的,扩大了说,就是比如你的电脑是4g的,现在你需要使用1g的空间去存储顺序表,但是由于内存的调用是CPU自动分配的,可能没有连续的1g的内存空间,可能有空闲的内存空间是0.3g,0.5g,0.8g,0.2g这个样子,这时候就需要用到链表,而不是顺序表了。
链表的结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
链表的定义
链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里面放下一个节点的位置信息(即地址)。
链表分为单向链表和双向链表
单向链表:也叫单链表,是链表中最简单的一种形式,它的每个节点包括两个域,一个是信息域(元素域),一个是链接域,这个链接指向链表的下一个节点,而最后一个节点的链接域则指向一个空值。
python 中的标识
1.表元素域elem用来放具体的数据
2.链接域next用来放下一个节点的位置
3.变量p指向链表的头节点(首节点)的位置,从p出发,能找到表中的任意节点。
单链表节点的实现
class SingleNode(object):
"""单链表的结点"""
def __init__(self,item):
# item存放数据元素
self.item = item
# next是下一个节点的标识
self.next = None
单链表的操作
is_empty() 链表是否为空
length() 链表长度
travel() 遍历整个链表
add(item) 链表头部添加元素
append(item) 链表尾部添加元素
insert(pos, item) 指定位置添加元素
remove(item) 删除节点
search(item) 查找节点是否存在
单链表的实现
class SingleLinkList(object):
"""单链表"""
def __init__(self):
self.__head = None
def is_empty(self):
"""判断链表是否为空"""
return self.__head is None
def length(self):
"""链表长度"""
# cur初始时指向头节点
cur = self.__head
count = 0
# 尾节点指向None,当未到达尾部时
while cur is not None:
count += 1
# 将cur后移一个节点
cur = cur.next
return count
def travel(self):
"""遍历链表"""
cur = self.__head
while cur is not 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
查找节点是否存在
def search(self,item):
"""链表查找节点是否存在,并返回True或者False"""
cur = self.__head
while cur != None:
if cur.item == item:
return True
cur = cur.next
return False
链表与顺序表对比
链表失去了顺序表随机读取的优点,同时链表增加了节点的指针域,空间开销大,但对于存储空间使用要相对灵活。
操作 | 链表 | 顺序表 |
---|---|---|
访问元素 | O(n) | O(1) |
在头部插入/删除 | O(1) | O(n) |
在尾部插入/删除 | O(n) | O(1) |
在中间插入/删除 | O(n) | O(n) |
虽然表面看起来的复杂度都是O(n),但是链表和顺序表在插入和删除时进行的操作完全不同,链表主要耗时的操作时遍历查找,删除和插入操作本身的复杂度都时O(1)。顺序表的查找很快,主要的耗时操作是拷贝和覆盖,因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作点之后所有的元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。