这是本公号的第127篇原创。
近期看到一个数据结构题目,翻转链表。动手写了下代码,手生了不少,发现好铁不用也会生锈,大脑也如此。
于是就整体回顾了一下链表的常见操作和数据结构题,整理下分享出来,万一对读者有帮助呢?想必那是极好的!目录如下~
- 链表基础知识
- 链表常见操作
- 链表常见题目
链表基础知识
链表和顺序表相对应,顺序表将表中的元素按照顺序放置于连续的存储区间,便于访问和查找。而链表则将元素放置于一系列结点中,各结点空间不要去连续,虽然给访问和查找带来了一定不便,但是在元素的删除和插入等操作上有着得天独厚的优势。(数组和链表结构的在复杂度方面的优缺点你get到了吗?)
链表的结点包括数据域和指针域两部分,顾名思义,数据域保存着元素的数据信息,指针域保存着下一个结点的指针标识。当然 Python 中没有指针这个概念,刷题中也是模拟指针的概念进行操作。
谈到链表一定不能避开的两个定义:头结点和头指针。
头结点的设立是为了便于操作,指的是放在第一个元素结点前的结点。不是链表结构必不可少的一部分。头指针则不一样,属于必不可少的一部分,指向链表结构的第一个结点(头结点存在的时候指向头结点)。通常我们对链表的访问和各操作都是基于头指针进行的。
链表分类则可以根据不同方式:
从内存角度出发: 链表可分为 静态链表、动态链表。
从链表存储方式的角度出发: 链表可分为 单链表、双链表、以及循环链表。
链表常见操作
链表的常见操作包括但不局限于求链表长度、结点的删除、插入等,但小詹之前文章说过许多次,Python 中由于不存在指针,所以指针和链表指的都是模拟指针和链表。所以这里的常见操作应当加上链表的构建操作。
1.首先是用 Python 构建链表
如上所述,链表由一系列结点组成,所以构建链表的第一步是定义一个结点类。
class Node(object): """ 定义一个节点类 """ def __init__(self,data): self.data = data self.next = None
有了结点,下一步就是构建链表了。如下的 InitList() 方法用一个非空 data 构建的链表,并定义了一个 PrintList() 方法打印出链表结果。
class LinkList(object): """ 创建一个链表类,包括判断是否为空、打印链表 """ def __init__(self): self.head = Node(None) def InitList(self, data): if len(data) == 0: print('\nIt is a empty link list') return False self.head = Node(data[0])#头结点,data的一个元素为数据域信息 p = self.head #头指针,指向第一个结点 for i in data[1:]: node = Node(i) p.next = node p = p.next def IsEmpty(self): p = self.head #指向第一个结点 def IsEmpty(self): p = self.head #指向第一个结点 if p == None: # print('\nIt is a empty link list') return True #是空链表 # print('\nIt is not empty') return False #非空链表 def PrintList(self): p = self.head while p: #注意不是p.next print(p.data, end = ' ') #end表示结束的标识,不添加说明时默认为换行符 p = p.next
这是一个简单的构建链表方法并将其打印出来,可以测试下结果。
def main(): """ 测试 """ data = [1, 2, 3, 4, 5] lst = LinkList() lst.InitList(data) lst.PrintList() main()
对应的结果为:
1 2 3 4 5
2.其次是链表长度的求取
链表长度的求取在使用中经常遇到,所以构建了一个链表后,我们应当把一些常见的操作封装到一起,为此,可以创建一个求表长的方法,通过便历链表所有结点即可。
def Size(self): """ 求取链表长度 """ p = self.head size = 0 while p: size += 1 p = p.next return size
3.还有常见的结点删除与插入
开篇提到顺序表对元素的查找和访问比较便捷,但是涉及到元素删除和插入就比较鸡肋了,链表中就相反,非常的便捷!
需要注意情况分类,有大概 3 种情况。且删除位置 n 应当有区间限制。
- 当链表只有一个结点的时候
- 当删除的是第一个结点的时候
- 其他普适情况
对于第一种,只需要直接将头结点为 None 即可;第二种情况和普适情况类似,只是普适情况的前驱结点在第二种情况为头结点。重点还在普适情况的删除操作,可以简单的将前一个结点的指针指向待删除结点的下一个结点即可跳过该结点。代码如下所示:
def Del(self,n): """ 删除第n个结点,注意特殊边界情况 """ if self.IsEmpty() or n < 0 or n > self.Size(): print('error occured') return p = self.head if p.next is None: # 当只有一个节点的链表时 self.head = None return if n == 1: # 当删除第一个节点时 self.head = p.next return #普通情况 p = self.head index = 1 while index < n:#找到要删除的结点位置 pre = p index += 1 p = p.next pre.next = p.next#跳过该结点即可删除 p = None
以上是删除操作,考虑了各种删除情况。插入操作和删除有点类似倒转的感觉,只需要找到待插入位置,打断前后两个结点的指针,改变 3 者的指向关系即可。插入位置n范围应当在闭区间(0,链表长度)之间。值得注意的是这里也需要分情况讨论,许多教程忽略了边缘情况,这里着重讨论下。
- 插入的链表为空链表
- 插入在链表的头结点处
- 普适情况
前俩种情况类似,只用将插入结点做头结点即可,普适情况只需要将该结点的前后位置结点指针做些调整即可,代码给出如下,可以手动画图方便理解噢。
def Insert(self,n,data): """ 把data插入第n个结点之后的位置 """ if n < 0 or n > self.Size() + 1: #n最小值为0,且插入位置超过长度+1时实际只能插入到最后,即最大值为链表长【0,size】区间内有效 print('error occured') return p = self.head if self.IsEmpty():#空链表时插入头指针之后即可 node = Node(data) p.next = node return if n == 0: node = Node(data) lat = self.head self.head = node #新插入的做头结点 node.next = lat #新插入结点后续指向之前的头结点 return index = 1 while index < n: index += 1 p = p.next #插入操作关键之处,注意指针变换顺序 node = Node(data) node.next = p.next p.next = node
链表常见题目
这里重要讲解 2 个题型。
1.链表翻转问题
简单的说就是将一个单链表翻转过来,这是 LeetCode 的第 206 题,在链表题目中很有分量。
输入:1->2->3->4->5 输出:5->4->3->2->1
这个题目其实不算难,思路比较清晰,只需要从前往后依次将结点指向颠倒,例如:
原 始:1->2->3->4->5 第1轮:5<-4 3->2->1 第2轮:5<-4<-3 2->1 第3轮:5<-4<-3<-2 1 第4轮:5<-4<-3<-2<-1
关键在于指针的转换顺序不能颠倒变换,建议自己按照我上边给的示例进行绘图帮助理解,代码如下:
class Solution: """ leetcode题目中提交函数都只用放在此类下 """ def reverseList(self, head): """ 翻转操作leetcode题目206 """ cur, pre = head, None while cur: cur.next, pre, cur = pre, cur, cur.next #这一行等同于如下 4 行: #lat = cur.next #cur.next = pre #pre = cur #cur = lat return pre
2.链表有环问题(LeetCode第141题)
链表有环问题也是一个常见的问题了,之前在总结双指针类型问题(见文末推荐阅读)时候讲过,可以去回顾下。思路如下:
- 定义两个指针 ,一快一慢 。比如慢指针一次移动 1 个位置 ,快指针移动 2 个 。
- 初始快慢指针放在一个位置 ,并开始循环移动 。
- 如果有环 ,那么随着移动的进行 ,终有快指针经过环遇到并超过慢指针的时候 ,那么这就可以用来判断是否存在环的依据啦 。