数组
定义
a=[]
数组的基本特点:支持随机访问数组的关键:索引与寻址
访问
a[i]
数组在内存中是一段连续的存储空间
插入元素
删除元素
时间复杂度
| 操作 | 时间复杂度 |
|---|---|
| 访问 | O(1) |
| 插入元素 | O(n) |
| 删除元素 | O(n) |
| 插入元素(在最后一个结点) | O(1) |
| 插入元素(在第一个结点) | O(n) |
变长数组
list
思考
如何实现一个变长数组?
- 支持索引与随机访问分配多长的连续空间?
- 空间不够用了怎么办?
- 间乘余很多如何回收?
一个简易的实现方法
- 初始:空数组,分配常数空间
- Push back:若空间不够,重新申请⒉倍大小的连续空间,拷贝到新空间,释放旧空间.
- Pop back:若空间利用率不到25%,释放一半的空间
- 均摊O(1)
- 在空数组中连续插入n个元素,总插入/拷贝次数为n+n/2+n/4+ ...<2n.
以初始值为1个存储单位为例,插入N个元素时
第一次申请后的连续空间:2
第二次申请后的连续空间:4
第三次申请后的连续空间:8
...
第N次申请后的连续空间:2^n
2^0+2^1+2^2+...+2^n<2^(n+1)
- 一次扩容到下一次释放,至少需要再删除(1-2*0.25)n=0.5n次
思考:若释放空间的阈值设定为50%,会发生什么情况?
顺序表
在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传进传出函数等。一组数据中包含的元素个数可能发生变化(可以增加或删除元素)。
对于这种需求,最简单的解决方案便是将这样一组元素看成一个序列,用元素在序列里的位置和顺序,表示实际应用中的某种有意义的信息,或者表示数据之间的某种关系。
这样的一组序列元素的组织形式,我们可以将其抽象为线性表。一个线性表是某类元素的一个集合,还记录着元素之间的一种顺序关系。线性表是最基本的数据结构之一,在实际程序中应用非常广泛,它还经常被用作更复杂的数据结构的实现基础。
根据线性表的实际存储方式,分为两种实现模型:
- 顺序表,将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示。
- 链表,将元素存放在通过链接构造起来的一系列存储块中。
顺序表的基本形式
图a表示的是顺序表的基本形式,数据元素本身连续存储,每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址,而元素存储的物理地址(实际内存地址)可以通过存储区的起始地址Loc (e(0))加上逻辑地址(第i个元素)与存储单元大小(c)的乘积计算而得,即:
Loc(e(i)) = Loc(e(0)) + c*i
故,访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)。
如果元素的大小不统一,则须采用图b的元素外置的形式,将实际数据元素另行存储,而顺序表中各单元位置保存对应元素的地址信息(即链接)。由于每个链接所需的存储量相同,通过上述公式,可以计算出元素链接的存储位置,而后顺着链接找到实际存储的数据元素。注意,图b中的c不再是数据元素的大小,而是存储一个链接地址所需的存储量,这个量通常很小。
图b这样的顺序表也被称为对实际数据的索引,这是最简单的索引结构。
顺序表的结构
一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需记录的信息,即有关表的整体情况的信息,这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项。
顺序表的两种基本实现方式
图a为一体式结构,存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,两部分数据的整体形成一个完整的顺序表对象。
一体式结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。
图b为分离式结构,表对象里只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。
元素存储区替换
一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。
分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。
元素存储区扩充
采用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变表对象的前提下对其数据存储区进行了扩充,所有使用这个表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。
扩充的两种策略
- 每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。
- 特点:节省空间,但是扩充操作频繁,操作次数多。
- 每次扩充容量加倍,如每次扩充增加一倍存储空间。
- 特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。
无序表
定义
一种数据项按照相对位置存放的数据集特别的,被称为“无序表unordered list”其中数据项只按照存放位,置来索引,如第1个、第2个、...、最后一个等。(为了简单起见,假设表中不存在重复数据项)
无序表的操作
无序表List的操作如下:
List():创建一个空列表
add(item):添加一个数据项到列表中,假设item原先不存在于列表中
remove(item):从列表中移除item,列表被修改,item原先应存在于表中
search(item):在列表中查找item,返回布尔类型值
isEmpty():返回列表是否为空
size():返回列表包含了多少数据项
- 为了实现无序表数据结构,可以采用链接表的方案。
- 虽然列表数据结构要求保持数据项的前后相对位置,但这种前后位置的保持,并不要求数据项依次存放在连续的存储空间
有序表
有序表中的“有序”是逻辑意义上的有序,指表中的元素按某种规则已经排好了位置。顺序表中的“顺序”是物理意义上的,指线形表中的元素一个接一个的存储在一片相邻的存储区域中
有序表的定义
有序表是一种数据项依照其某可比性质(如整数大小、字母表先后)来决定在列表中的位置
越“小”的数据项越靠近列表的头,越靠“前”
有序表的操作
OrderedList所定义的操作如下:
OrderedList():创建一个空的有序表
add(item):在表中添加一个数据项,并保持整体顺序,此项原不存在
remove(item):从有序表中移除一个数据项,此项应存在,有序表被修改
search(item):在有序表中查找数据项,返回是否存在
isEmpty():是否空表
size():返回表中数据项的个数
index(item):返回数据项在表中的位置,此项应存在
pop():移除并返回有序表中最后一项,表中应至少存在.一项
pop(pos):移除并返回有序表中指定位置的数据项,此位置应存在
链表
单链表
定义
单链表的元素分布在内存的各个角落,用一条“链子”对各个元素进行相连,形成单链表。我们可以通过箭头方向对链表数据进行访问。
在链表中,数据项存放位置并没有规则,但如果在数据项之间建立链接指向,就可以保持其前后相对位置。第一个和最后一个数据项需要显式标记出来,一个是队首,一个是队尾,后面再无数据了。
实现
链表实现的最基本元素是节点Node,每个节点至少要包含2个信息:数据项本身,以及指向下一个节点的引用信息
注意:next为None的意义是没有下一个节点了,这个很重要
class Node:
def __init__ (self,initdata):
self.data = initdata
self.next = None
def getData(self):
return self.data
def getNext(self):
return self.next
def setData(self,newdata):
self.data = newdata
def setNext ( self, newnext):
self.next = newnext
插入元素
创建新结点,让
Node.next=NewNode
NewNode.next = Node.next
顺序不能换
删除元素
Node.next=TargetNode.next
双链表
定义
保护节点
链表实现无序表
无序表定义的实现
我们可以通过链表实现无序表
- 可以采用链接节点的方式构建数据集来实现无序表
- 链表的第一个和最后一个节点最重要
如果想访问到链表中的所有节点,就必须从第一个节点开始沿着链接遍历下去
所以无序表必须要有对第一个节点的引用信息
设立一个属性head,保存对第一个节点的引用空表的head为None
class UnorderedList:
def __init__(self):
self.head = None
随着数据项的加入,无序表的head始终指向链条中的第一个节点
注意!!!无序表mylist对象本身并不包含数据项(数据项在节点中),其中包含的head只是对首个节点Node的引用判断空表的isEmpty()很容易实现
def isEmpty(self):
return self.head == None
无序表的操作实现
接下来,考虑如何实现向无序表中添加数据项,实现add方法。
-
由于无序表并没有限定数据项之间的顺序
-
新数据项可以加入到原表的任何位置
-
按照实现的性能考虑,应添加到最容易加入的位置上
- 由链表结构我们知道要访问到整条链上的所有数据项,都必须从表头head开始沿着next链接逐个向后查找,所以添加新数据项最快捷的位置是表头,整个链表的首位置。
def add(self, item):
temp = Node(item)
temp.setNext(self.head)
self.head =temp
size:从链条头head开始遍历到表尾同时用变量累加经过的节点个数。
def size(self):
current = self. head
count = 0
while current != None:
count = count + 1
current = current.getNext ( )
return count
search:从链表头head开始遍历到表尾,同时判断当前节点的数据项是否目标
def search(self, item):
current = self.head
found = False
while current != None and not found:
if current.getData() == item:
found = True
else:
current = current.getNext()
return found
remove:首先要找到item,这个过程跟search一样,但在删除节点时,需要特别的技巧
current指向的是当前匹配数据项的节点,而删除需要把前一个节点的next指向current的下一个节点所以我们在search current的同 时,还要维护前一个(previous)节点的引用
找到item之后, current指向item节点,previous指向前一个节点,开始执行删除,需要区分两种情况:
- current是首个节点
- current是位于链条中间的节
def remove(self, item):
current = self.head
previous = None
found = False
while not found:
if current.getData) == item:
found = True
else:
previous = current
current = current.getNext()
if previous == None:
self.head = current.getNext()
else:
previous.setNext(current.getNext())
链表实现有序表
在实现有序表的时候,需要记住的是,数据项的相对位置,取决于它们之间的“大小”比较
有序表的定义实现
Node定义相同,OrderedList也设置一个head来保存链表表头的引用
class OrderedList:
def __init__(self):
self.head = None
有序表的操作实现
对于isEmpty/size/remove这些方法与节点的次序无关,所以其实现跟UnorderedList是一样的。search/add方法则需要有修改
search:在无序表的search中,如果需要查找的数据项不存在,则会搜遍整个链表,直到表尾,对于有序表来说,可以利用链表节点有序排列的特性,来为search节省不存在数据项的查找时间,一旦当前节点的数据项大于所要查找的数据项,则说明链表后面已经不可能再有要查找的数据项,可以直接返回False
def search(self, item):
current = self.head
found = False
stop = False
while current != None and not found and not stop:
if current.getData() == item:
found = True
else:
if current.getData() > item:
stop = True
else:
current = current.getNext()
return found
add:相比无序表,改变最大的方法是add,因为add方法必须保证加入的数据项添加在合适的位置,以维护整个链表的有序性,由于涉及到的插入位置是当前节点之前,而链表无法得到“前驱”节点的引用所以要跟remove方法类似,引入一个previous的引用,跟随当前节点current
def add(self, item):
current = self.head
previous = None
stop = False
while current != None and not stop:
if current.getData() > item:
stop = True
else:
previous = current
current = current.getNext( )
temp = Node(item)
if previous == None:
temp.setNext(self.head)
self.head = temp
else:
temp.setNext(current)
previous.setNext(temp)
栈与双端队列
栈
栈的定义
栈是一种后进先出(LIFO) 的数据结构,一种有次序的数据项集合,在栈中,数据项的加入和移除都仅发生在同一端这一端叫栈“顶top”,另一端叫栈“底base',距离栈底越近的数据项,留在栈中的时间就越长。
栈的特性
进栈和出栈的次序正好相反。这种访问次序反转的特性,我们在某些计算机操作上碰到过
浏览器的“后退back"按钮,最先back的是最近访问的网页
Word的“Undo”按钮,最先撤销的是最近操作
根据栈的特性,我们抽象一个数据类型Stack:
- 必抽象数据类型"栈”是一个有次序的数据集,每个数据项仅从"栈顶”一端加入到数据集中、从数据集中移除,栈具有后进先出LIFO的特性
栈的操作
抽象数据类型"栈"定义为如下的操作:
Stack():创建一个空栈,不包含任何数据项
push(item):将item加入栈顶,无返回值
pop():将栈顶数据项移除,并返回,栈被修改
peek( ):“窥视”栈顶数据项,返回栈顶的数据项但不移除,栈不被修改
isEmpty():返回栈是否为空栈
size():返回栈中有多少个数据项
栈的实现
-
将ADT Stack 实现为Python的一个Class
-
将ADT Stack的操作实现为Class的方法
-
由于Stack是一个数据集,所以可以采用Python的原生数据集来实现,我们选用最常用的数据集List来实现
- 可以将List的任意一端(index=0或者-1) 设置为栈顶
- 我们选用List的末端(index=-1) 作为栈顶(如下图所示)
- 这样栈的操作就可以通过对list的append和pop来实现,很简单!
class Stack:
def __init__ (self):
self.items =[]
def isEmpty(self):
return self.items ==[ ]
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items)-1]
def size(self):
return len(self.items)
如果我们把List的另一端(首端index=0)作为Stack的栈顶,同样也可以实现Stack
class Stack:
def __init__ (self):
self.items =[]
def isEmpty(self):
return self.items ==[ ]
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items) - 1]
def size(self):
return len(self.items)
不同的实现方案保持了ADT接口的稳定性,但性能有所不同,栈顶首端的版本,其push/pop的复杂度为0(n),而栈顶尾端的实现其push/pop的复杂度为0(1)
队列
队列的定义
队列是一种有次序的数据集合,其特征是新数据项的添加总发生在一端(通常称为“尾rear”端),而现存数据项的移除总发生在另一端(通常称为“首front”端), 当数据项加入队列,首先出现在队尾,随着队首数据项的移除,它逐渐接近队首。
队列的特性
新加入的数据项必须在数据集末尾等待,而等待时间最长的数据项则是队首,这种次序安排的原则称为先进先出(FIFO) 或“先到先服务first-come first-served”
日常生活中,我们排队的现象可以抽象成一个队列,打印机的打印文件缓存区也可以抽象成一个队列
队列仅有一个入口和一个出口,不允许数据项直接插入队中,也不允许从中间移除数据项
抽象数据类型Queue是一个有次序的数据集合
队列的操作
抽象数据类型Queue由如下操作定义:
Queue():创建一个空队列对象,返回值为Queue对象;
enqueue(item):将数据项item添加到队尾,无返回值;
dequeue():从队首移除数据项,返回值为队首数据项,队列被修改;
isEmpty():测试是否空队列,返回值为布尔值;
size():返回队列中数据项的个数。
队列的实现
- 采用List来容纳Queue的数据项
- 将List首端作为队列尾端
- List的末端作为队列首端
- enqueue()复杂度为0(n)
- dequeue()复杂度为0(1)
- 首尾倒过来的实现复杂度也倒过来
class Queue:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def enqueue(self, item):
self.items.insert(0, item)
def dequeue(self):
return self.items.pop( )
def size(self):
return len(self.items)
双端队列
双端队列的定义
双端队列Deque是一种有次序的数据集跟队列相似,其两端可以称作“首”“尾”端,但deque中数据项既可以从队首加入,也可以从队尾加入;数据项也可以从两端移除。
某种意义上说,双端队列集成了栈和队列的能力
但双端队列并不具有内在的LIFO或者FIFO特性,如果用双端队列来模拟栈或队列需要由使用者自行维护操作的一致性
双端队列的操作
deque定义的操作如下:
Deque():创建一个空双端队列
addFront(item):将item加入队首
addRear(item):将item加入队尾
removeFront():从队首移除数据项,返回值为移除的数据项
removeRear():从队尾移除数据项,返回值为移除的数据项
isEmpty():返回deque是否为空
size():返回deque中包含数据项的个数
双端队列的实现
-
采用List实现
-
List下标0作为deque的尾端
-
List下标-1作为deque的首端
-
操作复杂度
- addFront/removeFront:0(1)
- addRear/removeRear:0(n)
class Deque:
def __init__ (self):
self.items =[]
def isEmpty(self):
return self.items == []
def addFront(self, item):
self.items.append( item)
def addRear(self, item):
self.items.insert(0, item)
def removeFront(self):
return self.items.pop()
def removeRear(self) :
return self.items.pop(0)
def size(self):
return len(self.items)