数据结构(Python实现)------ 链表
数据结构(Python实现)------ 链表
单链表
基本概念
添加操作 - 单链表
如果我们想在给定的结点 prev 之后添加新值,我们应该:
与数组不同,我们不需要将所有元素移动到插入元素之后。因此,您可以在 O(1) 时间复杂度中将新结点插入到链表中,这非常高效。
在开头添加结点
删除操作 - 单链表
删除第一个结点
设计链表
class MyLinkedList(object):
def __init__(self):
self.linkedlist = list()
def get(self,index):
if index < 0 or index >= len(self.linkedlist):
return -1
else:
return self.linkedlist[index]
def addAtHead(self,val):
self.linkedlist.insert(0,val)
def addAtTail(self,val):
self.linkedlist.append(val)
def addAtIndex(self,index,val):
if index <= len(self.linkedlist):
self.linkedlist.insert(index,val)
def deleteAtIndex(self,index):
if 0 <= index <len(self.linkedlist):
del self.linkedlist[index]
双指针技巧
基本概念
让我们从一个经典问题开始:
给定一个链表,判断链表中是否有环。
你可能已经使用哈希表提出了解决方案。但是,使用双指针技巧有一个更有效的解决方案。在阅读接下来的内容之前,试着自己仔细考虑一下。
想象一下,有两个速度不同的跑步者。如果他们在直路上行驶,快跑者将首先到达目的地。但是,如果它们在圆形跑道上跑步,那么快跑者如果继续跑步就会追上慢跑者。
这正是我们在链表中使用两个速度不同的指针时会遇到的情况:
如果没有环,快指针将停在链表的末尾。
如果有环,快指针最终将与慢指针相遇。
所以剩下的问题是:
这两个指针的适当速度应该是多少?
一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。
那其他选择呢?它们有用吗?它们会更高效吗?
环形链表
# Definition for singly-linked list.
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
class Solution(object):
def detectCycle(self,head):
if head is None or head.next is None or head.next.next is None:
return None
fast = head.next
slow = head.next.next
while fast != slow:
if slow.next == None or slow.next.next == None:
return None
fast = fast.next
slow = slow.next.next
slow = head
while fast != slow:
fast = fast.next
slow = slow.next
return fast
相交链表
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
方法一:
class Solution(object):
def getIntersectionNode(self, headA, headB):
"""
:type head1, head1: ListNode
:rtype: ListNode
"""
p,q = headA,headB
countA = countB = 0
while p != None:
p = p.next
countA += 1
while q != None:
q = q.next
countB += 1
m,n = headA,headB
if countA > countB:
for i in range(countA - countB):
m = m.next
else:
for i in range(countB - countA):
n = n.next
while m != n:
m = m.next
n = n.next
return m
删除链表的倒数第N个节点
经典问题(反转链表)
基本概念
让我们从一个经典问题开始:
反转一个单链表。
一种解决方案是按原始顺序迭代结点,并将它们逐个移动到列表的头部。似乎很难理解。我们先用一个例子来说明我们的算法。
Python 实现
反转链表
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
class Solution(object):
def reverseList(self,head):
if head is None or head.next is None:
return head
p = head
d = {}
i = 0
while p:
d[i] = p
p = p.next
i += 1
l = len(d)
for i in range(l-1,0,-1):
d[i].next = d[i-1]
d[0].next = None
return d[l-1]
解法二:
class Solution(object):
def reverseList(self,head):
if head is None:
return None
cur = head
pre = None
nxt = cur.next
while nxt:
cur.next = pre
pre = cur
cur = nxt
nxt = nxt.next
cur.next = pre
head = cur
return head
移除链表元素
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
class Solution(object):
def removeElements(self, head, val):
"""
:type head: ListNode
:type val: int
:rtype: ListNode
"""
if head:
while head.val == val:
head = head.next
if head is None:
return head
first = head
second = first.next
while second:
if second.val == val:
first.next = second.next
else:
first = first.next
second = second.next
return head
奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:
应当保持奇数节点和偶数节点的相对顺序。
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
class Solution(object):
def isPalindrome(self,head):
if head is None or head.next is None:
return True
l = []
p = head
while p.next:
l.append(p.val)
p = p.next
l.append(p.val)
return l == l
小结 - 链表经典问题
我们为你提供了几个练习。你可能已经注意到它们之间的相似之处了。这里我们提供一些提示:
- 通过一些测试用例可以节省您的时间。
使用链表时不易调试。因此,在编写代码之前,自己尝试几个不同的示例来验证您的算法总是很有用的。
- 你可以同时使用多个指针。
有时,当你为链表问题设计算法时,可能需要同时跟踪多个结点。您应该记住需要跟踪哪些结点,并且可以自由地使用几个不同的结点指针来同时跟踪这些结点。
如果你使用多个指针,最好为它们指定适当的名称,以防将来必须调试或检查代码。
- 在许多情况下,你需要跟踪当前结点的前一个结点。
你无法追溯单链表中的前一个结点。因此,您不仅要存储当前结点,还要存储前一个结点。这在双链表中是不同的,我们将在后面的章节中介绍。
双链表
基本概念
我们在前面的章节中介绍了单链表。
单链接列表中的结点具有 Value 字段,以及用于顺序链接结点的“Next”引用字段。
在本文中,我们将介绍另一种类型的链表:双链表。
定义
双链表以类似的方式工作,但还有一个引用字段,称为“prev”字段。有了这个额外的字段,您就能够知道当前结点的前一个结点。
让我们看一个例子:
操作
与单链表类似,我们将介绍在双链表中如何访问数据、插入新结点或删除现有结点。
我们可以与单链表相同的方式访问数据:
我们不能在常量级的时间内访问随机位置。
我们必须从头部遍历才能得到我们想要的第一个结点。
在最坏的情况下,时间复杂度将是 O(N),其中 N 是链表的长度。
对于添加和删除,会稍微复杂一些,因为我们还需要处理“prev”字段。在接下来的两篇文章中,我们将介绍这两个操作
之后,我们提供练习,让你使用双链表重新设计链表。
添加操作 - 双链表
示例
删除操作 - 双链表
如果我们想从双链表中删除一个现有的结点 cur,我们可以简单地将它的前一个结点 prev 与下一个结点 next 链接起来。
与单链表不同,使用“prev”字段可以很容易地在常量时间内获得前一个结点。
因为我们不再需要遍历链表来获取前一个结点,所以时间和空间复杂度都是O(1)。
示例
小结 - 链表
基本概念
复习
让我们简要回顾一下单链表和双链表的表现。
它们在许多操作中是相似的。
它们都无法在常量时间内随机访问数据。
它们都能够在 O(1) 时间内在给定结点之后或列表开头添加一个新结点。
它们都能够在 O(1) 时间内删除第一个结点。
但是删除给定结点(包括最后一个结点)时略有不同。
在单链表中,它无法获取给定结点的前一个结点,因此在删除给定结点之前我们必须花费 O(N) 时间来找出前一结点。
在双链表中,这会更容易,因为我们可以使用“prev”引用字段获取前一个结点。因此我们可以在 O(1) 时间内删除给定结点。
对照
经过这次比较,我们不难得出结论:
如果你需要经常添加或删除结点,链表可能是一个不错的选择。
如果你需要经常按索引访问元素,数组可能是比链表更好的选择。
Python实现
合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
class ListNode(object):
def __init__(self,x):
self.val = x
self.next = None
class Solution(object):
def mergeTwoLists(self,l1,l2):
if l1 == None:
return l2
elif l2 == None:
return l1
if l1.val<l2.val:
l1.next = self.mergeTwoLists(l1.next,l2)
return l1
else:
l2.next = self.mergeTwoLists(l2.next,l1)
return l2
两数相加
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
class Solution(object):
def addTwoNumbers(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
carry=0
head=ListNode(0)
node=head
while l1 and l2:
value=(l1.val+l2.val+carry)%10
carry=(l1.val+l2.val+carry)//10
l1.val=value
node.next=l1
node=node.next
l1=l1.next
l2=l2.next
while l1:
value=(l1.val+carry)%10
carry=(l1.val+carry)//10
l1.val=value
node.next=l1
node=node.next
l1=l1.next
while l2:
value=(l2.val+carry)%10
carry=(l2.val+carry)//10
l2.val=value
node.next=l2
node=node.next
l2=l2.next
if carry:
node.next=ListNode(1)
return head.next
旋转链表
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
示例 2:
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
算法过程
1)定义2个指针,一快一慢
2)快的先走k步,慢的不动
3)接着快慢一起动,当快的走到节点的时候,慢的的下一项就是新的头部节点
class Solution(object):
def rotateRight(self,head,k):
if not head or not head.next or k == 0:
return head
ListLen = 0
p = head
#数有几个节点
while(p):
ListLen+=1
p=p.next
#优化
k = k%ListLen
if k == 0:
return head
#快节点先走k步
p = head
while(k>0):
k-=1
p=p.next
slow = head
fast = p
#接着让fast走到最后一个节点,slow与它有相同的速度
while fast.next:
slow = slow.next
fast = fast.next
new_head = slow.next
fast.next = head
slow.next = None
return new_head