文章目录
基本概念和顺序表的形式
数据是由连续的地址单元组成的,每个地址单元都有它的地址.内存的基本单位是字节,一个字节有8位,在存储整型数据的时候,需要4个字节,例如整数1,需要转换成二进制,然后放到4个字节的位置上;一个字符(char)占一个字节,如下图所示
如果要把一组数据元素放到一起作为一个整体,将这组元素看作一个序列,这种组织形式叫做***线性表***,线性表又分为***顺序表***和***链表***,顺序表将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示;链表将元素存放在通过链接构造起来的一系列存储块中.
图a表示的是顺序表的基本形式,数据元素本身连续存储,每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址,而元素存储的物理地址(实际内存地址)可以通过存储区的起始地址Loc (e0)加上逻辑地址(第i个元素)与存储单元大小(c)的乘积计算而得,即:
Loc(ei) = Loc(e0) + c*i
故,访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)。
如果元素的大小不统一,则须采用图b的元素外置的形式,将实际数据元素另行存储,而顺序表中各单元位置保存对应元素的地址信息(即链接)。由于每个链接所需的存储量相同,通过上述公式,可以计算出元素链接的存储位置,而后顺着链接找到实际存储的数据元素。注意,图b中的c不再是数据元素的大小,而是存储一个链接地址所需的存储量,这个量通常很小。
图b这样的顺序表也被称为对实际数据的索引,这是最简单的索引结构。
对于上图两种顺序表的存储形式,有一个具体的示意图,如下所示:
以元素外置的情况为例,要存储列表li
,需要存储4个地址,存储4个地址需要16个字节的内存空间,这4个地址是连续的,li
的地址是这4个地址的起始地址,如果要找到li[3]
,首先找到的是li
的地址,即0x324
,然后计算出第4个元素的地址,即0x324+3*4Byte
,第4个元素中存储了一个地址,即0x110
,最后通过0x110
读取到1000
顺序表的结构
基本结构
一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需记录的信息,即有关表的整体情况的信息,这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项。
顺序表的两种存储方式
图a为一体式结构,存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,两部分数据的整体形成一个完整的顺序表对象。
一体式结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。
图b为分离式结构,表对象里只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。
元素存储区替换
一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。
分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。
下面作出示意图供参考:
元素存储区扩充的两种策略
采用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变表对象的前提下对其数据存储区进行了扩充,所有使用这个表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。
扩充的两种策略
-
每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。
特点:节省空间,但是扩充操作频繁,操作次数多。
-
每次扩充容量加倍,如每次扩充增加一倍存储空间。
特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。
顺序表的操作
增加元素
a. 尾端加入元素,时间复杂度为O(1)
b. 非保序的加入元素(不常见),时间复杂度为O(1)
c. 保序的元素加入,时间复杂度为O(n)
删除元素
a. 删除表尾元素,时间复杂度为O(1)
b. 非保序的元素删除(不常见),时间复杂度为O(1)
c. 保序的元素删除,时间复杂度为O(n)
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),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
链表
单向链表
单向链表***也叫***单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。表元素域elem
用来存放具体的数据。链接域next
用来存放下一个节点的位置(python中的标识).变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。最后一个节点叫尾结点.
图中最后尾结点中一竖一横的标志,代表最后尾结点指向空
下面给出一个具体的示意图:
单链表的创建和操作
首先观察一个代码
a = 10
b = 20
a, b = b, a
print(a, b)
输出结果
20 10
代码的第三句运行时,首先等号右边根据b,a的id找到b,a的值,如下所示
a, b = 20, 10
然后,a的地址指向对象20,b的地址指向对象10,如下图所示
在python中,变量可以指向任意一类对象,可以是函数,可以是字符串,也可以是一个整数,而不像其他一些语言把变量的类型固定死,比如int a = 10
这样的语句.
所以要创建一个单链表,只需要让一个变量指向下一个节点,就可以起到存储地址的效果,如下图所示,节点1中的next=node2
意思就是变量next
指向了下一个节点node2
然后用代码构造单链表,需要创建一个单链表对象和一个节点对象,二者的关系如下图所示,其中,__head是单链表对象的一个私有属性,它指向头节点对象
接下来用代码实现这个结构
class Node:
# 创建一个节点,elem存放数据
# next存放下一个节点的地址
def __init__(self, elem):
self.elem = elem
self.next = None
class SingleLinkList(object):
def __init__(self, node = None):
# 链表的head属性指向第一个节点
self.__head = None
SingleLinkList
需要实现一系列的方法,如下所示
is_empty()
链表是否为空
def is_empty(self):
"""判断链表是否为空"""
return self._head == None
自己写的代码显然不够简洁,经过测试可以运行
def is_empty(self):
if self.__head == None:
return True
else:
return False
- length() 链表长度(已掌握)
def length(self):
"""链表长度"""
# 要获取链表长度,需要用指针来计算节点的个数
# cur代表一个指针/游标
# cur初始时指向头节点
cur = self._head
count = 0
# 尾节点指向None,当未到达尾部时
while cur != None:
count += 1
# 将cur后移一个节点
cur = cur.next
return count
效果如下图所示:
- travel() 遍历整个链表
def travel(self):
"""遍历链表"""
cur = self.__head
while cur != None:
# 游标指向的节点不为空,则输出节点的数据
print(cur.elem, end='\t')
cur = cur.next
print()
append(item)
链表尾部添加元素(要考虑到链表为空的情况,因为这种情况下,要对__head__
进行操作)
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
- add(item) 链表头部添加元素
def add(self, item):
"""头部添加元素"""
# 先创建一个保存item值的节点
node = SingleNode(item)
# 将新节点的链接域next指向头节点,即_head指向的位置
node.next = self._head
# 将链表的头_head指向新节点
self._head = node
- insert(pos, item) 指定位置添加元素
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
- remove(item) 删除节点
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
- search(item) 查找节点是否存在
def search(self,item):
"""链表查找节点是否存在,并返回True或者False"""
cur = self._head
while cur != None:
if cur.item == item:
return True
cur = cur.next
return False
整合所有的内容,代码如下所示
class SingleNode(object):
def __init__(self,elem):
self.elem = elem
self.next = None
class SingleLinkList(object):
def __init__(self):
self.__head = None
def is_empty(self):
return self.__head == None
def length(self):
cur = self.__head
count = 0
while cur != None:
count += 1
cur = cur.next
return count
def append(self, item):
cur = self.__head
node = SingleNode(item)
if self.is_empty():
self.__head = node
else:
while cur.next != None:
cur = cur.next
cur.next = node
def travel(self):
cur = self.__head
while cur != None:
print(cur.elem, end='\t')
cur = cur.next
print()
def add(self, item):
node = SingleNode(item)
node.next = self.__head
self.__head = node
def insert(self, pos, item):
if pos <= 0:
self.add(item)
elif pos > self.length() - 1:
self.append(item)
else:
cur = self.__head
count = 0
node = SingleNode(item)
while count + 1 < pos:
count += 1
cur = cur.next
node.next = cur.next
cur.next = node
def remove(self, item):
cur = self.__head
if cur.elem == item:
self.__head = cur.next
else:
while cur.next.elem != item:
cur = cur.next
cur.next = cur.next.next
def search(self, item):
cur = self.__head
while cur != None:
if cur.elem != item:
cur = cur.next
return True
return False
# 测试
if __name__ == '__main__':
li = SingleLinkList()
# 判断列表是否为空
print(li.is_empty())
# 判断空列表长度是否为0
print(li.length())
# # 追加一个元素1
li.append(1)
print(li.is_empty())
print(li.length())
li.append(2)
li.append(3)
# 测试遍历
li.travel() # 结果1 2 3
# 在链表头部添加元素
li.add(7)
li.travel() # 结果7 1 2 3
# 测试在中间插入元素
li.insert(-1, 5)
li.insert(2, 4)
li.insert(100, 6)
li.travel() # 结果5 7 4 1 2 3 6
li.remove(5)
li.travel()
print(li.search(5), end='')
输出结果
True
0
False
1
1 2 3
7 1 2 3
5 7 4 1 2 3 6
7 4 1 2 3 6
False
答疑:为什么指针cur
可以使用next
属性?
那什么时候会用到while内部的计算呢,也就是cur游标指向一个不是None的对象。就是SingleLinkList()
中不为空,已经有了节点值。
注意:add()
,append()
,insert()
操作内部,都会有首先有一个node=Node(item)
对象的初始化。length()
,travel()
函数内部没有node对象初始化。但是要注意,length()
,travel()
只有在SingleLinkList()
已经有值的时候才会执行。也就是在经过插入操作add()
,append()
,insert()
之后才会执行。此时才会运行while内部的next属性(因为此时链表内部已经有了Node对象)。
链表和顺序表的复杂度对比
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的使用要相对灵活。
链表与顺序表的各种操作复杂度如下所示:
操作 | 链表 | 顺序表 |
---|---|---|
访问元素 | O(n) | O(1) |
在头部插入/删除 | O(1) | O(n) |
在尾部插入/删除 | O(n) | O(1) |
在中间插入/删除 | O(n) | O(n) |
注意虽然表面看起来复杂度都是 O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
单向循环链表
单项循环链表和单项链表的唯一区别就是单项循环列表的最后一个结点的链接域指向第一个节点
代码
class Node(object):
def __init__(self,elem):
self.elem = elem
self.next = None
class SinCycLinkedlist(object):
def __init__(self, node = None):
self.__head = node
if node:
node.next = node
def is_empty(self):
return self.__head == None
def length(self):
if self.is_empty():
return 0
cur = self.__head
# 因为单链表的指针可以一直到None
# 所以count初始值可以为0
# 而单项循环链表必须在最后一个结点停下
# 所以初始值必须为1
count = 1
# 单项循环链表的判断条件改变
while cur != self.__head:
count += 1
cur = cur.next
return count
def append(self, item):
node = Node(item)
if self.is_empty():
self.__head = node
node.next = node
else:
cur = self.__head
while cur.next != self.__head:
cur = cur.next
# 下面的两句话是可以调换顺序的
cur.next = node
# 这句话还可以写作
# node.next = cur.next
node.next = self.__head
def add(self, item):
node = Node(item)
if self.is_empty():
self.__head = node
node.next = node
else:
cur = self.__head
while cur.next != self.__head:
cur = cur.next
# 退出循环,cur指向尾结点
node.next = self.__head
self.__head = node
cur.next = node
def travel(self):
if self.is_empty():
return
cur = self.__head
while cur != self.__head:
print(cur.elem, end='\t')
cur = cur.next
# 因为循环不会打印最后一个结点的内容
# 所以需要单独打印一次
print(cur.elem)
def insert(self, pos, item):
if pos <= 0:
self.add(item)
elif pos > self.length() - 1:
self.append(item)
else:
cur = self.__head
count = 0
node = Node(item)
while count + 1 < pos:
count += 1
cur = cur.next
node.next = cur.next
cur.next = node
def remove(self, item):
if self.is_empty():
return
cur = self.__head
pre = None
while cur.next != self.__head:
if cur.elem == item:
# 判断要删除的节点是否是头结点
if cur == self.__head:
# 找到尾结点
# 建立一个新的游标
rear = self.__head
while rear.next != self.__head:
rear = rear.next
self.__head = cur.next
# 也可以写作
# rear.next = self.__head
rear.next = cur.next
else:
# 下面的语句没有包含删除尾部的情况
# 代表的是删除中间结点的情况
pre.next = cur.next
# 如果满足if cur.elem == item
# 则终止while循环
return
else:
pre = cur
cur = cur.next
# 退出循环指向尾结点
if cur.elem == item:
if cur == self.__head:
self.__head = None
else:
pre.next = cur.next
def search(self, item):
if self.is_empty():
return False
cur = self.__head
while cur.next != self.__head:
if cur.elem != item:
cur = cur.next
return True
# 退出循环时不能漏掉最后一个结点
if cur.elem == item:
return True
return False
if __name__ == "__main__":
ll = SinCycLinkedlist()
print(ll.length())
ll.add(1)
ll.travel()
ll.add(2)
ll.travel()
# ll.append(3)
# ll.insert(2, 4)
# ll.insert(4, 5)
# ll.insert(0, 6)
# print("length:",ll.length())
# ll.travel()
# print(ll.search(3))
# print(ll.search(7))
# ll.remove(1)
# print("length:",ll.length())
# ll.travel()
双向链表
一种更复杂的链表是“双向链表”或“双面链表”。每个节点有两个链接:一个指向前一个节点,当此节点为第一个节点时,指向空值;而另一个指向下一个节点,当此节点为最后一个节点时,指向空值。
示意图如下所示
双向列表的操作如下所示
is_empty()
链表是否为空length()
链表长度travel()
遍历链表add(item)
链表头部添加append(item)
链表尾部添加insert(pos, item)
指定位置添加remove(item)
删除节点search(item)
查找节点是否存在
其中,__init__(self, node=None)
,is_empty
(),length()
,travel()
,search()
不需要更改,所以双链表可以继承单链表的方法.首先改写add(self, item)
方法
代码
class DoubleNode(object):
def __init__(self,elem):
self.elem = elem
self.next = None
self.prev = None
class DoubleLinkList(object):
def __init__(self):
self.__head = None
def is_empty(self):
return self.__head == None
def length(self):
cur = self.__head
count = 0
while cur != None:
count += 1
cur = cur.next
return count
def travel(self):
cur = self.__head
while cur != None:
print(cur.elem, end='\t')
cur = cur.next
print()
def add(self, item):
node = DoubleNode(item)
node.next = self.__head
# 这句可以和下一句调换位置
self.__head = node
# 这个地方漏掉了
# node后面的节点的前驱结点需要指向node
node.next.prev = node
def append(self, item):
cur = self.__head
node = DoubleNode(item)
if self.__head is None:
self.__head = node
else:
while cur.next is not None:
cur = cur.next
cur.next = node
node.prev = cur
def insert(self, pos, item):
if pos <= 0:
self.add(item)
elif pos > self.length() - 1:
self.append(item)
else:
cur = self.__head
count = 0
node = DoubleNode(item)
while count < pos:
count += 1
cur = cur.next
# 退出的时候cur指向pos的位置
# 前两句没有打断原有的链接
node.next = cur
node.prev = cur.prev
cur.prev = node
cur.prev.next = node
def remove(self, item):
cur = self.__head
while cur is not None:
if cur.elem is item:
# 判断链表是否是头结点
self.__head = cur.next
# 判断链表是否只有一个节点
if cur.next:
cur.prev = None
else:
cur.prev.next = cur.next
# 判断是否是最后一个节点
if cur.next:
cur.next.prev = cur.prev
# 判断出当前节点的值就是item之后
# 终止循环,如果去掉break会死循环
break
else:
cur = cur.next
def search(self, item):
cur = self.__head
while cur != None:
if cur.elem == item:
return True
cur = cur.next
return False
if __name__ == '__main__':
# 测试
if __name__ == '__main__':
li = DoubleLinkList()
# 判断列表是否为空
print(li.is_empty())
# 判断空列表长度是否为0
print(li.length())
# # 追加一个元素1
li.append(1)
print(li.is_empty())
print(li.length())
li.append(2)
li.append(3)
# 测试遍历
li.travel() # 结果1 2 3
# 在链表头部添加元素
li.add(7)
li.travel() # 结果7 1 2 3
# 测试在中间插入元素
li.insert(-1, 5)
li.insert(2, 4)
li.insert(100, 6)
li.travel() # 结果5 7 4 1 2 3 6
li.remove(5)
li.travel()
print(li.search(5), end='')
输出结果
True
0
False
1
1 2 3
7 1 2 3
5 7 1 2 3 6
7 1 2 3 6
False
栈和队列
栈和队列的介绍
栈和队列的本质是两种特殊的线性表,可以用顺序表或链表来实现
栈(stack),有些地方称为堆栈,是一种容器,可存入数据元素、访问元素、删除元素,它的特点在于只能允许在容器的一端(称为栈顶端指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。没有了位置概念,保证任何时候可以访问、删除的元素都是此前最后存入的那个元素,确定了一种默认的访问顺序。
由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
栈就像一个杯子,入栈和出栈的操作就像往杯子里倒水
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出的(First In First Out)的线性表,简称FIFO。允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!假设队列是q=(a1,a2,……,an),那么a1就是队头元素,而an是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,总是在队列最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后。
双端队列(deque,全名double-ended queue),是一种具有队列和栈的性质的数据结构。
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
栈和队列的实现
栈的实现
Stack()
创建一个新的空栈push(item)
添加一个新的元素item到栈顶pop()
弹出栈顶元素peek()
返回栈顶元素is_empty()
判断栈是否为空size()
返回栈的元素个数
代码:
class Stack:
def __init__(self):
self.list = []
def push(self, item):
"""加入元素"""
# 最后一个元素就是栈顶
self.list.append(item)
def pop(self):
"""弹出元素"""
return self.list.pop()
def peek(self):
"""返回栈顶元素"""
return self.list[len(self.list) - 1]
def is_empty(self):
"""判断是否为空"""
return self.list == []
def size(self):
return len(self.list)
if __name__ == "__main__":
stack = Stack()
stack.push("hello")
stack.push("world")
stack.push("itcast")
print(stack.size())
print(stack.peek())
print(stack.pop())
print(stack.pop())
print(stack.pop())
输出结果
3
itcast
itcast
world
hello
队列的实现
Queue()
创建一个空的队列enqueue(item)
往队列中添加一个item元素dequeue()
从队列头部删除一个元素is_empty()
判断一个队列是否为空size()
返回队列的大小
代码
class Queue:
def __init__(self):
self.list = []
def enqueue(self, item):
self.list.append(item)
def dequeue(self):
return self.list.pop(0)
def size(self):
return len(self.list)
def is_empty(self):
return len(self.list) == []
if __name__ == "__main__":
q = Queue()
q.enqueue("hello")
q.enqueue("world")
q.enqueue("itcast")
print(q.size())
print(q.dequeue())
print(q.dequeue())
print(q.dequeue())
输出结果
3
hello
world
itcast
双端队列的实现
Deque()
创建一个空的双端队列add_front(item)
从队头加入一个item元素add_rear(item)
从队尾加入一个item元素remove_front()
从队头删除一个item元素remove_rear()
从队尾删除一个item元素is_empty()
判断双端队列是否为空size()
返回队列的大小
代码
class Deque:
def __init__(self):
self.list = []
def add_front(self, item):
self.list.insert(0, item)
def add_rear(self, item):
self.list.append(item)
def remove_front(self):
return self.list.pop(0)
def remove_rear(self):
return self.list.pop()
def is_empty(self):
return self.list == []
def size(self):
return len(self.list)
if __name__ == "__main__":
deque = Deque()
deque.add_front(1)
deque.add_front(2)
deque.add_rear(3)
deque.add_rear(4)
print(deque.size())
print(deque.remove_front())
print(deque.remove_front())
print(deque.remove_rear())
print(deque.remove_rear())
输出结果
4
2
1
4
3