1.什么是链表(单向链表)
链表(link list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。单向链表中的每一个节点都包含两部分,一部分是存放数据的变量,一部分是指向下一个节点的指针next。
class Node(object):
"""单链表的结点"""
def __init__(self, item):
# _item存放数据元素
self.item = item
# _next是下一个节点的标识
self.next = None
链表的第一个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指针指向空。对于链表的其中一个节点A,我们只能根据节点A的next指针来找到该节点的下一个节点B,再根据节点B的next指针找到下一个节点C……
1.1虚拟节点(还未使用过)
在做题以及工程中会看到虚拟节点的概念,其next指针指向head,是为了方便处理首部节点,否则我们需要在代码里单独处理首部节点的问题。
2.链表的随机存储
链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。
让我们看一看下面两张图,对比一下数组和链表在内存中分配方式的不同。
图中的箭头代表链表节点的next指针。
3.创建链表
Python类中,__init__方法用于初始化对象,getData和setData方法用于获取和设置data属性,get_next和set_next方法用于获取和设置next属性。请注意,Python类中的属性是使用self.data和self.next的语法进行访问和设置的。
class ListNode:
def __init__(self, data):
self.data = data
self.next = None
def get_data(self):
return self.data
def set_data(self, data):
self.data = data
def get_next(self):
return self.next
def set_next(self, next):
self.next = next
但是在LeetCode中算法题是经常使用这样的方式来创建链表(这一方法为什么违背面向对象的设计要求?):
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
listnode = ListNode(1)
这里的val就是当前节点的值,next指向下一个节点。因为两个变量都是public的,创建对象后能直接使用listnode.val和listnode.next来操作,虽然违背了面向对象的设计要求,但是上面的代码更为精简,因此在算法题目中应用广泛。
4.链表的增删改查
4.1查找节点
对于单向链表,链表中的数据只能按顺序进行访问,一定是从头开始逐个向后访问。
4.2 链表插入
单链表的插入情况需要考虑三种情况:首部、中部和尾部。
4.2.1 尾部插入
尾部插入是最简单的一种情况,把最后一个节点的next指针指向新插入的节点即
可。
4.2.2 头部插入
头部插入可以分为两步:第一步,把新节点的next指针指向原先的头节点。第二步,,把新节点变为链表的头节点。
4.2.3 中间插入
中间插入也可以分为两步:第1步,新节点的next指针,指向插入位置的节点。
第2步,插入位置前置节点的next指针,指向新节点。
这里插入教材中的叙述,感觉会更偏向实用。
链表的遍历代码如下:
def length(self): """链表长度""" # cur初始时指向头节点 cur = self._head count = 0 # 尾节点指向None,当未到达尾部时 while cur != None count += 1 # 将cur后移一个节点 cur = cur.next return count
在中间位置插入
在中间位置插入,我们必须遍历找到要插入的位置,然后将当前位置接入到前驱节点和后继节点之间,但是到了该位置之后我们却不能获得前驱节点了,也就无法将节点接入进来了。这就好比一边过河一边拆桥,结果自己也回不去了。
为此,我们要在目标节点的前一位置停下来,也就是使用cur.next的值而不是cur的值来判断,这是链表最常用的策略。
例如下图中,如果要在7的前面插入,当cur.next=node(7)了就应该停下来,此时cur.val=15。然后需要将newNode前后接两根线,此时只能先让new.next=node(15).next(图中虚线),然后node(15).next=new,而且顺序还不能错。
想一下为什么不能调到顺序?
由于每个节点都只有一个next,因此执行了node(15).next=new之后,节点15和7之间的连线就自动断开了,如下图所示:
4.3 链表删除
同样分为删除尾部元素,删除头部元素和删除中间元素。
4.3.1 尾部删除
尾部删除,是最简单的情况,把倒数第2个节点的next指针指向空即可。
4.3.2 头部删除
头部删除,也很简单,把链表的头节点设为原先头节点的next指针即可。
4.3.3 中间删除
把要删除节点的前置节点的next指针,指向要删除元素的下一个节点即可。
这里需要注意的是,许多高级语言拥有自动化的垃圾回收机制,所以我们不用刻意去释放被删除的节点,只要没有外部引用指向它们,被删除的节点会被自动回收。
完整代码
class Node:
"单链表的节点"
def __init__(self, data) -> None:
# data存放数据元素
self.data = data
# next是下一个节点的标识
self.next = None
class SingleLinkList:
def __init__(self):
self._head = None
def is_empty(self):
"""判断链表是否为空"""
return self._head == None
def length(self):
"""链表长度"""
# cur初始时指向头节点
cur = self._head
count = 0
# 尾节点指向None, 当未到达尾部时
while cur != None:
count += 1
# 将cur后移一个节点
cur = cur.next
return count
def travel(self):
"""遍历链表"""
cur = self._head
while cur != None:
print(cur.data)
cur = cur.next
def get(self, index):
"""获得指定索引的节点"""
# 先判断链表是否为空,若是空链表,则返回错误信息
if self.is_empty():
raise Exception('链表为空!')
# 将变量 p 赋值为链表的头节点,即链表中的第一个节点。通过这个赋值操作,变量 p
# 现在指向了链表的头节点,可以用来遍历链表或者访问链表中的其他节点。
else:
p = self._head
for i in range(index):
p = p.next
return p
def add(self, data):
"""头部插入元素"""
# 先创建一个保存data值的节点
node = Node(data)
# 将新节点的next指向头节点,即_head指向的位置
node.next = self._head
# 将链表的头节点设为新的节点
self._head = node
def append(self, data):
"""尾部插入元素"""
node = Node(data)
# 先判断链表是否为空,若是空链表,则将_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, data):
"""在指定位置插入元素"""
# 若指定位置pos为第一个元素之前,则执行头部插入
if pos == 0:
self.add(data)
# 若指定位置超过链表尾部,则执行尾部插入
elif pos > (self.length() - 1):
self.append(data)
# 找到指定位置
else:
node = Node(data)
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, data):
"""删除节点"""
cur = self._head
pre = None
while cur != None:
# 找到了指定元素
if cur.data == data:
# 如果第一个就是要删除的节点
if not pre:
# 将头指针指向头节点的后一个节点
self._head = cur.next
else:
# 将删除位置前一个节点的next指向删除位置的后一个节点
pre.next = cur.next
break
else:
pre = cur
cur = cur.next
def search(self, data):
"""链表查找节点是否存在,并返回True或者False"""
cur = self._head
while cur != None:
if cur.data == data:
return True
else:
cur = cur.next
return False
if __name__ =="__main__":
ll = SingleLinkList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.append(4)
ll.append(5)
ll.remove(1)
ll.insert(2, 4)
ll.travel()
print("length: ", ll.length())
本文中出现的理论为本人总结通关村教材书写,由于是新手,难免有疏漏,出现的图多数为《漫画算法 小灰的算法之旅》中图片。