前言
在之前的博客中有为各位同学提到通过列表这种参考数据类型去实现一些抽象数据类型(ADT Abstract Data Type)例如栈、队列,Python中的list是经过了高度优化的,通常也是考虑存储问题的最优解,但是列表也有缺点:
(1)一个动态数组的有效长度可能超过实际存储数组元素的所需长度
(2)在μOS中对操作的摊销边界是不可接受的
(3)对于数组内部执行插入和删除操作的代价太高
链表:
概念:
链表是一种逻辑连续,空间大小待定的抽象数据结构,其依赖于更多的分布式表示方法,采用称为节点的轻量级对象,分配给每一个元素,每个节点维护一个指向它的元素的引用,并含有一个或者多个指向相邻节点的引用,这样做的目的是为了集中地表示序列的线性顺序。链表避免了上述基于数组序列的3个缺点。
链表的分类:
1、单向链表:
单向链表最简单的实现形式就是由多个节点的集合共同构成一个线性序列。每个节点存储一个对象的引用。这个引用指向序列中的一个元素,即存储指向列表中的下一个节点,对于链表的学习,推荐初学者经常画图,将抽象概念更加直观化,有益于知识的吸收。
所以单向链表的结构如图:
链表的第一个节点和最后一个节点称为头节点和尾节点。从头结点开始,可以有每个节点的next引用找到下个节点,从而到达尾节点,尾节点的next引用为None。这个过程叫做遍历链表,也称链表跳跃或者指针跳跃。
链表中的每一节点被表示成链表的一个实例对象,该对象存储着指向其元素成员的引用和指向下一个节点的引用(或者为空)。另外一个对象用于表示整个链表。链表的实例对象至少包含一个指向链表头节点的引用(还可以包含尾节点,但是同过遍历就能够找到尾节点,通常保存尾节点实例的引用是为了查询链表尾节点时不用遍历整张链表)。链表实例一般会保存节点总数,这样就可以避免为了得到链表中节点总数而去遍历整张链表。
为了简单起见,我们将上图中提到的 “元素成员引用的一个任意对象” 直接添加在节点的结构中,尽管元素实际上是一个独立的对象。如图:
1.1 链表头部插入一个元素(头插法):
基本思路:创建一个新的节点,将新节点的元素域设置为新元素,将该节点的next指针指向当前的头结点,然后设置列表的头指针直线新的节点。
伪代码:
Algorithm add_first(L,e):# L为链表实例,e为元素对象
newset = Node(e)# 产生新节点
newset.next = L.head# 将新节点的next指针指向链表的头节点,
#目的是为了让新节点和旧的头节点连接
L.head = newset# 将链表的新的头结点刷新为新节点
L.size = L.size + 1 # 链表长度加 1
1.2 链表尾部插入一个元素(尾插法):
只要保存了尾节点的引用,就可以很容易的在链表的尾部插入一个元素。创建新的节点将其next设置为None,并且设置尾节点的next指向新节点,刷新尾节点的引用为这个新节点。
伪代码:
Algorithm add_last(L,e):# L为链表实例,e为元素对象
newset = Node(e)#创建新节点对象
newset.next = None# 新节点的尾节点置空
L.tail.next = newset# 将旧的尾节点和新节点相连
L.tail = newset# 刷新尾节点
L.size += 1 # 链表长度加 1
1.3 从链表中删除一个元素:
伪代码:
Algorithm remove_list:
if L.head is None then:# 加入头结点都为空,切记此时并没有节点,链表为空
raise Error: the list is empty# 抛出异常
L.head = L.head.next# 将链表的头节点的引用指向当前头节点的下一个节点,这样相当于删除了头结点的内容
L.size -= 1
1.4 用单向链表实现栈:
兄弟们,别忘记了栈啊,他很重要,是一种LIFO的ADT。:-)所以啊,栈顶对应链表头即可了,为啥啊,因为单向列表从头部删除是最方便的。其余的删除方式太费时间(比如删除结尾行不行?行是行,就是得编列的尾节点的前一个节点吧?对不对,多浪费时间,切记链表可没有列表那种0索引哦)
为了表示链表内的单个节点,我们将节点作为链表内的一个轻量级非公开的内置类_NodeSet。
所以呢,废话不多说,上才(代)艺(码)。句句都是自己的理解,可能不是很精确,但是很容易理解。
class LinkStack:
class _NodeSet:
__slot__ = '_element','_next'# 限定属性
def __init__(self, element, next):
self._element = element# 节点中元素对象
self._next = next# 节点的next指针
def __init__(self):
self._head = None# 链表的头节点
self._size = 0# 节点计数(不是必须,如果不设置,当要求链表长度时就需要遍历整张链表)
def __len__(self):
return self._size
def is_empty(self):# 判空
if self._head is None:
return True
else:
return False
def push(self,e):# 压栈
self._head = self._NodeSet(e, self._head)# 将链表头节点的引用指向一个新节点对象,该对象的_next指向原始的头节点
self._size += 1 # 计数加一
def top(self):
if self.is_empty():
raise TypeError('linkStack is empty')
return self._head._element# 返回栈顶(头节点)
def pop(self):# 出栈
if self.is_empty():
raise TypeError('linkStack is empty')
res = self._head._element# 保留出栈的元素
self._head = self._head._next# 将头节点指向现在头节点的下一个元素
self._size -= 1# 长度自减1
return res# 返回弹出的元素
1.5 用单向链表实现队列:
之前已经讨论过队列的特点了,所以这里不费文章,不清楚的同学请关注前几篇文章中有提及其概念(通过列表实现的);其实就是FIFO就好了。这个能明白,那么逻辑上就没有任何问题。
由于队列需要对两端进行操作,所以我们在链表类中维护两个节点_head和_tail,逻辑上最简单的方式就是就是链表尾对应队列尾,链表头对应队列头,这样子就能明白,插入元素采用尾插法,弹出元素从头弹出。是不是一想就明白了?
那么,接下来,上才(代)艺(码):
class LinkQueue:
class _Node:
__slot__ = '_element','_next'
def __init__(self, element, next):
self._element = element
self._next = next
def __init__(self):
self._head = None
self._tail = None
self._size = 0
def __len__(self):
return self._size
def is_empty(self):
return self._size == 0
def first(self):
if self._size():
raise TypeError('Queue is empty!')
return self._head._element
def dequeue(self):
if self._size():
raise TypeError('Queue is empty!')
res = self._head
self._head = self._head._next
self._size -= 1
if self.is_empty():# 删除节点之后如果队列为空:
self._tail = None# 则要将尾节点置空
return res
def enqueue(self,e):
newset = self._Node(e,None)# 创建新成员
if self.is_empty():# 如果队列为空
self._head = newset# 队头即是新元素
self._tail = None# 队尾为空
else:
self._tail._next = newset# 当前队尾节点的下一个元素是newset
self._tail = newset# 将尾节点更新为新节点
self.size += 1
已经习惯了深夜放毒,可能是因为安静,我是一个编程讲师,为了自己的未来努力加油!接下来为各位持续更新循环链表和双向链表的相关内容。