单链表的结构
单链表结点的组成:
- 元素
- 链接域(保存下一结点的地址)
在单链表里,表里的n 个结点通过链接形成一条结点链。从表里的任一个结点都可以找到保存下一个元素的结点。
链表中的一个重要部分就是头指针。头指针保存着链表首结点的标识,通过头指针可以十分方便的对链表进行:访问元素、遍历、增删改查等操作
TODO:补充单链表的特性
实现
定义节点类
节点是链表的基本组成部分
class Node : # 只定义初始化操作
def __init__(self, elm, next_=None):
self.elem = elem
# 因为Python中next是一个内置的函数,为区分,故在变量名后添加"_"
self.next_ = next_
定义单链表类
# 单链表类只有一个 _head 域,表示单链表的头指针
class SingleList:
def __init__(self):
self._head = None
变量前的单下划线表示私有变量,不要在对象的外部去访问。python语言没有为定义私有变量提供专门的机制,只能通过约定和良好的编程习惯来保护对象的私有属性。
定义异常类
为了能合理处理一些链表操作中遇到的错误状态,如在执行方法时遇到了无法操作的错误参数,首先定义一个异常类LinkedListUnderflow
,这里将它定义为标准异常类ValueError
的子类,pass
表示空语句,即什么都不做。
TODO: 在这里直接抛出
ValueError
也没问题,但定义了自己的异常类就可以写专门的异常处理器。
class LinkedListUnderflow(ValueError):
pass
对链表的具体操作
删除链表
在Python
中,如果一个对象未被引用,则该对象会被当做垃圾并进行回收。所以删除链表只需将头指针设为None
,使得链表对象不被引用
def del_list(self):
self._head = None
判断表空
在Python
里检查_head
值是否为None
,如果为None
,则说明_head
不指向任何对象
def is_empty(self):
return self._head is None
判断表满
因为链表没有一个固定的容量,可以动态扩充,所以在内容充足的情况下,链表是不会满的
链表长度
算法描述:
- 创建计数器
- 遍历至最后一个节点
代码实现:
def length(self):
'''计算链表长度'''
point = self._head
count = 1
while point.next_ != None:
point = point.next_
count += 1
return count
思考1:为什么需要新建一个point变量
遍历链表
算法描述:
- 创建计数器
- 遍历至最后一个节点
算法实现:
def traverse(self):
'''遍历链表'''
point = self._head
index = 0
while point.next_ != None:
index += 1
print("第", index, "个元素是:", point.elem)
point = point.next_
print("第", (index + 1), "个元素是:", point.elem)
思考2:如何确定循环结束的条件?
定位元素–按下标定位
算法描述:
- 判断下标的合法性
- 根据计数器找指定下标的元素
代码实现:
def index(self, index_):
'''定位元素--按下标定位'''
if index_ >= self.length() and index_ < 0:
raise LinkedListUnderflow("IndexError: list index out of range")
count = 0
point = self._head
while count != index_:
count += 1
point = point.next_
return point.elem
定位元素–按值定位
算法描述:
- 遍历节点中的值匹配目标
- 创建计数器,返回值对应的下标
代码实现:
def find(self, val):
'''定位元素--找值为x的元素'''
point = self._head
count = 0
# 此处需要判断链表是否为空
while point.elem != val and point != None:
count += 1
point = point.next_
return count
反转链表
算法描述:修改链表中节点的关系
单链表反转操作
注意:一定要将尾节点(反转前的首节点)的next_
(链接域)设为None,否则会出现死循环!!!
代码实现:
def rev(self):
'''反转链表'''
point = self._head
self._head = self._head.next_
# 必须要将原先的首节点的next_设为0,否则将在遍历时出现死循环
point.next_ = None
while self._head != None:
prepoint = point
point = self._head
self._head = self._head.next_
point.next_ = prepoint
self._head = point
插入
首端插入
算法步骤:
- 创建一个新结点并存入数据
- 让新结点的链接域指向原链表首结点
- 修改头指针使之指向新结点
思考3:能否交换上述算法步骤2、3
算法实现:
def prepend(self, elem):
'''首端插入元素
如果为空链表,则直接新建节点;如果为非空链表,则改变head指针和新节点的next_值'''
n = Node(elem)
if self._head == None:
self._head = n
else:
n.next_ = self._head
self._head = n
'''
进阶代码1:
n = Node(elem)
n.next_ = self._head
self._head = n
进阶代码2:
self._head = Node(elem, self._head)
'''
尾端插入
算法步骤:
- 创建一个新结点并存入数据,并将新结点的链接域设置为空链接
- 表是否为空
- 如果表空,直接让头指针指向这个新结点并结束
- 如果表不空,找到表尾结点
- 令表尾结点的链接域指向这一新结点
代码实现:
def append(self, elem):
'''尾端插入元素'''
n = Node(elem)
if self._head == None: # 此种情况容易疏漏
self._head = n
else:
point = self._head
while point.next_ != None:
point = point.next_
point.next_ = n
定位插入
算法步骤:
- 创建新结点并存入数据
- 找到插入位置的前一结点
- 修改前一结点和新结点链接
代码实现:
def insert(self, index, elem):
'''任意位置插入元素'''
n = Node(elem)
if index >= self.length() and index < 0:
raise LinkedListUnderflow('insert 下标越界,不合法', i)
elif index == 0:
self.prepend(elem)
elif index == self.length():
self.append(elem)
else:
count = 1
point = self._head
while count != index:
count += 1
point = point.next_
n.next_ = point.next_
point.next_ = n
# ===================================
# 方法二:简化
def insert(self, elem, index):
# 在下标为i的元素之前插入elem
n = LNode(elem)
if index < 0 or index >= self.length():
raise LinkedListUnderflow('insert 下标越界,不合法', index)
elif index == 0:
self.prepend(elem)
else:
point = self._head
while index != 1:
index -= 1
point = point.next_
n.next_ = point.next_
point.next_ = n
删除
首部删除
算法步骤:
- 判断链表是否为空,若为空,则抛出异常
- 保存首元素的地址
- 将首指针指向下一节点
- 返回被删除的元素
单链表首端删除操作
代码实现:
def pop(self):
'''首端删除并返回被删元素'''
if self._head == None:
raise LinkedListUnderflow("in pop")
else:
n = self._head.elem
self._head = self._head.next_
return n
尾部删除
算法步骤:
- 判断链表是否为空,若为空,则抛出异常
- 若只有一个节点,则将首指针设为None
- 若有两个及以上个节点,则遍历至倒数第二个节点
- 设置倒数第二个节点的next_属性(链接域)为None
代码实现:
def poplast(self):
'''尾端删除并返回删除的值'''
# 空链表
if self._head == None:
raise LinkedListUnderflow("in pop last")
# 只有一个元素
elif self._head.next_ == None:
e = self._head.elem
self._head.next_ = None
return e
# 链表中有两个或多个元素
else:
point = self._head
while point.next_.next_ != None:
point = point.next_
e = point.next_.elem
point.next_ = None
return e
定位删除
算法描述:
- 判断链表是否为空,若为空,则抛出异常
- 对索引进行合法性检查
- 如果索引为
0
,则调用pop()
方法;若索引为length-1
,则调用poplast()
方法 - 其余情况,通过计数器遍历至要删除节点的前一节点并删除
代码实现:
def remove(self, index):
'''定位删除'''
if self._head == None:
raise LinkedListUnderflow("链表为空")
if index < 0 and index >= self.length():
raise LinkedListUnderflow('remove 下标越界,不合法', i)
elif index == 0:
self.pop()
elif index == self.length() - 1:
self.poplast()
else:
count = 1
point = self._head
while count != index:
count += 1
point = point.next_
e = point.next_
point.next_ = point.next_.next_
return e
# ==========================================
# 方法二:简化
def remove(self, index):
#删除下标为index的元素
if self._head == None:
raise LinkedListUnderflow("链表为空")
if index < 0 or index >= self.length():
raise LinkedListUnderflow('remove 下标越界,不合法', index)
elif index == 0:
self.pop()
else:
point = self._head
while index != 1:
index -= 1
point = point.next_
e = point.next_
point.next_ = point.next_.next_
return e
思考与解答
思考1
为什么在遍历链表时需要新建一个point变量,而不是直接用self._head去遍历?
因为self._head
是首指针,首指针的移动会使得链表节点发生变化。如下图所以,当首指针移动到第三个节点,因为第1、2个节点没有被引用,所以这两个节点会被Python的内存管理机制清除(删除节点)
思考2
在遍历链表或访问特定的节点时,如何确定循环结束的条件?
依据与目标节点相邻的特殊节点的特点来设置循环条件。在遍历操作中,我们需要遍历到最后一个节点,最后一个节点本身就是一个特殊节点,其特点是next_
(链接域)为None;如果需要遍历到倒数第二个节点,同样借助最后一个节点的特殊性。
思考3
在链表中插入元素时能否先将首节点或上一节点的指针先指向新节点?
不能,如果将首节点或上一节点的指针指向新节点,那么后面的节点会失去引用,即被删除
参考文章:
- 《数据结构与算法Python语言描述》裘宗燕
- 老师整理ppt
- Python内存管理机制:没白熬夜,终于把Python的内存管理机制搞明白了