链表
链表是一种动态的数据结构,因为在创建链表时,我们不需要知道链表的长度,当插入一个结点时,只需要为该结点分配内存,然后调整指针的指向来确保新结点被连接到链表中。所以,它不像数组,内存是一次性分配完毕的,而是每添加一个结点分配一次内存。正是因为链表的内存不是一次性分配的,所以它没有闲置的内存,比起数组,空间效率更高。
单向链表:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
1.从尾到头打印链表
首先我们必须明确的一点,就是我们无法像是数组那样直接的逆序遍历,因为链表并不是一次性分配内存,我们无法使用索引来获取链表中的值,所以我们只能是从头到尾的遍历链表,然后我们的输出是从尾到头,也就是说,对于链表中的元素,是"先进后出",如果明白到这点,我们自然就能想到栈。
事实上,链表确实是实现栈的基础,所以这道题目的要求其实就是要求我们使用一个栈。
而递归在本质上就是一个栈,所以我们完全可以用递归来实现,但使用递归就意味着可能发生栈溢出的风险,尤其是链表非常长的时候。所以,基于循环实现的栈的鲁棒性要好一些。
#非递归
def printListFromTailToHead(self, listNode):
# 存入列表倒序打印 相当于栈结构 pop操作
l = []
while(listNode!=None):
l.append(listNode.val)
listNode=listNode.next
# while(l!=[]):
# print(l.pop())
return l[::-1] # 倒序打印
# 递归
def printListFromTailToHead(self, listNode):
if listNode is None:
return []
return self.printListFromTailToHead(listNode.next) + [listNode.val] # python里面list型添加元素可以直接+[num]
2.反转链表
迭代:设置pre,cur,lat三个指针,首先cur.next = pre,接着pre = cur,cur = lat,lat = lat.next,重复上述操作直到lat=None,最后cur.next = pre。
递归:因为reverseList(head)返回输入的链表反转后的head,所以先reverseList(head.next)递归到最后一个结点,再head.next.next=head,head.next=None,返回node即可。
def reverseList(self, head: ListNode) -> ListNode:
# 借助列表,时间复杂度O(n) 空间复杂度O(n)
if not head:
return None
mylist = []
while head:
mylist.append(head)
head = head.next
mylist = mylist[::-1]
for i in range(len(mylist) - 1):
mylist[i].next = mylist[i + 1]
mylist[-1].next = None # 一定要将原头结点的next设为none
return mylist[0]
# 迭代 时间复杂度O(n) 空间复杂度O(1)
pre = None
cur = head
while cur != None:
lat = cur.next
cur.next = pre
pre = cur
cur = lat
return pre
# 递归 时间复杂度O(n) 空间复杂度O(1)
if head == None or head.next == None: #空链表或只有一个节点的情况
return head
node = self.reverseList(head.next)
head.next.next = head # 递归到尾节点时,则令尾节点的next为头结点None,并指向原尾节点
head.next = None
return node
3.输入一个链表,然后删除它的倒数第K个结点的值
基本思路:遍历一次链表获得链表长度,再次遍历链表,至n-k+1出输出
进阶思路:设置2个指针,第一个指针走K步之后,第二个指针开始从头走,这样两个指针之间始终相隔K,当指针2走到链表结尾时,指针1的位置即倒数K个节点
问题分解:1.先找倒数第n个节点 2.删除该节点
def removeNthFromEnd(self, head, n):
"""
:type head: ListNode
:type n: int
:rtype: ListNode
"""
start = head
org = head
for i in range(1, n):
start = start.next
i = i + 1
while start.next != None:
start = start.next
org = org.next
# 删除该节点 分情况讨论
head_node = head
del_node = org
if not (head_node and del_node):
return False
if del_node.next:
# 删除的节点不是尾节点,而且不是唯一一个节点
del_next_node = del_node.next
del_node.val = del_next_node.val
del_node.next = del_next_node.next
del_next_node.val = None
del_next_node.next = None
elif del_node == head_node:
# 唯一一个节点,删除头节点
head_node = None
del_node = None
else:
# 删除节点为尾节点
node = head_node
while node.next != del_node:
node = node.next
node.next = None
del_node = None
return head_node
4.输入两个链表,找出它们的第一个公共结点
思路1:拿到这道题目,我们的第一个想法就是在每遍历一个链表的结点时,再遍历另一个链表。这样大概的时间复杂度将会是O(M * N)。如果是数组,或许我们可以考虑一下使用二分查找来提高查找的效率,但是链表完全不能这样。
思路2:想想我们判断一个结点是否是公共结点,不仅要比较值,还要比较它下一个结点是否是一样,也就是说,就算找到该结点,判断的依据还是要放在后面的结点是否相同,所以,可以倒过来思考:如果从尾结点开始,找到两个结点的值完全相同,则可以认为前面的结点是公共结点。
但链表是单链表,我们只能从头开始遍历,但是尾结点却要先比较,这种做法就是所谓的"后进先出",也就是所谓的栈。但使用栈需要空间复杂度,现在我们可以将时间复杂度控制在O(M + N),但是空间复杂度却是O(M + N)。要想办法将空间复杂度降到最低,也就是减少两个栈的比较次数。
思路3:注意到一个事情:两个链表的长度不一定相同,我们可以先遍历两个链表,得到它们的长度M和N,其中M < N,让较长的链表先行N - M,然后再同时遍历,这样时间复杂度就是O(M + N),但根本就不需要栈,节省了空间。
5.给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该结点
这个题目的要求是让我们能够像数组操作一样,实现O(1),而根据一般链表的特点,是无法做到这点的,这就要求我们想办法改进一般的删除结点的做法。
一般我们删除结点,就像上面的做法,是从头指针开始,然后遍历整个链表,之所以要这样做,是因为我们需要得到将被删除的结点的前面一个结点,在单向链表中,结点中并没有指向前一个结点的指针,所以我们才从链表的头结点开始按顺序查找。
知道这点后,我们就可以想想其中的一个疑问:为什么我们一定要得到将被删除结点前面的结点呢?事实上,比起得到前面的结点,我们更加容易得到后面的结点,因为一般的结点中就已经包含了指向后面结点的指针。我们可以把下一个结点的内容复制到需要删除的结点上覆盖原有的内容,再把下一个结点删除,那其实也就是相当于将当前的结点删除。
特殊情况:如果要删除的结点位于链表的尾部,那么它就没有下一个结点,这时我们就必须从链表的头结点开始,顺序遍历得到该结点的前序结点,并完成删除操作。还有,如果链表中只有一个结点,而我们又要删除;;链表的头结点,也就是尾结点,这时我们在删除结点后,还需要把链表的头结点设置为NULL,这种做法重要,因为头指针是一个指针,当我们删除一个指针后,如果没有将它设置为NULL,就不能算是真正的删除该指针。
下面的代码还是有缺点,就是基于要删除的结点一定在链表中,事实上,不一定,但这份责任是交给函数的调用者。
def deleteNode(self, node):
node.val = node.next.val
node.next = node.next.next
对于n- 1个非尾结点而言,我们可以在O(1)时把下一个结点的内存复制覆盖要删除的结点,并删除下一个结点,但对于尾结点而言,由于仍然需要顺序查找,时间复杂度为O(N),因此总的时间复杂度为O[((N - 1) * O(1) + O(N)) / N] = O(1),这个也是需要我们会计算的,不然我们无法向面试官解释,为什么这段代码的时间复杂度就是O(1)。
6.合并两个排序的链表
这种题目最直观的做法就是将一个链表的值与其他链表的值一一比较。考察链表的题目不会要求我们时间复杂度,因为链表并不像是数组那样,可以方便的使用各种排序算法和查找算法。因为链表涉及到大量的指针操作,所以链表的题目考察的主要是两个方面:代码的鲁棒性和简洁性。
def mergeTwoLists(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
# # 借助列表重新排序,时间复杂度O(nlgn) 空间复杂度O(n)
# if not (l1 or l2):
# return None
# list1, list2 = [], []
# while l1:
# list1.append(l1)
# l1 = l1.next
# while l2:
# list2.append(l2)
# l2 = l2.next
# mylist = (list1 + list2)
# mylist.sort(key=lambda ListNode: ListNode.val) # 时间复杂度O(nlgn),list中放的ListNode对象,需要按ListNode对象的val属性排列.sorted是创建副本,sort改变原值
# for i in range(len(mylist) - 1):
# mylist[i].next = mylist[i + 1]
# mylist[-1].next = None # 一定要将原头结点的next设为none
# return mylist[0]
# 迭代,借助两个指针
p = merge = ListNode(0)
while l1 and l2:
if l1.val < l2.val:
merge.next = l1
l1 = l1.next
else:
merge.next = l2
l2 = l2.next
merge = merge.next
merge.next= l1 or l2 # 注意:当由于其中一链表l1或者l2为空导致跳出while循环时,还需要将另一不为null的链表的后续部分赋给合并链表。
return p.next
# 递归
if not l1:
return l2
if not l2:
return l1
if l1.val <= l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
思路:初始化两个链表头,其中一个表头用以记录两个单调递增链表比较后的结果,另一个用以返回结果。
用while循环:
①如果两个链表不为空,比较进行,并将小的那个赋给合并的链表头。小表头继续走一步,合并表头继续走一步。
②如果两个链表有其一为空,那么跳出循环,并将另一不为null的链表的后续部分赋给合并链表。
7.复杂链表的复制
对自定义结点组成的链表,其中m_pSibling指向的是链表中任意一个结点或者NULL。
struct ComplexListNode
{
int m_nValue;
ComplexListNode* m_pNext;
ComplexListNode* m_pSibling;
};
思路1:第一步肯定是要复制每个结点并按照m_pNext连接起来,第二步就是设置每个结点的m_pSibling。我们可以在第一步遍历的时候就保存每个节点的m_pSibling,这样就可以节省第二步的遍历,将时间复杂度控制在O(N),但是这样子的空间复杂度就是O(N),事实上,链表的问题求解和数组不一样,数组更多考虑的是时间复杂度能否足够低,而链表则考虑空间复杂度能否足够低。
一个链表的求解如果不能将空间复杂度控制在O(1),完全不能通过面试。
思路2:我们完全可以不用专门用辅助空间来存放m_pSibling,直接就是将复制后的结点连接在原本结点后面,然后将这个链表按照奇数和偶数位置拆成两个子链表,其中,偶数位置就是我们要的复制后的链表。
8.0,1,2…,n - 1这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字
这就是有名的约瑟夫环问题,它有一个简洁的数学公式,但除非我们有很深的数学素养和数学灵敏性,否则是很难一下子想出来的。
f(n,m)=0, n=1
f(n,m)=[f(n-1,m) + m]%n n>1
既然是一个圆圈,我们自然就会联想到环形链表.
def LastRemaining_Solution(self, n, m):
# write code here
# # 常规算法 时间复杂度O(mn) 超时
# if n<1 or m<1:
# return False
# temp = m
# mOvern = False
# if m>n:
# m = m%n
# mOvern = True
# leftCount = n
# count = 0
# index = 0
# arr = [False]*n
# while leftCount>1:
# count+=1
# if count==m:
# count = 0
# leftCount-=1
# arr[index]=True
# if mOvern:
# m = temp % leftCount
# m=leftCount if m==0 else m
# index = (index+1)%n
# while arr[index]:
# index = (index + 1) % n
# return index
# # 创新解法,递归实现
if n < 0 or m < 1:
return False
if n==1:
return 0
if n>1:
return (self.LastRemaining_Solution(n-1,m)+m)%n
# 创新解法,循环实现
# if n < 0 or m < 1:
# return False
#
# last = 0
# for i in range(2,n+1):
# last = (last + m) % i
#
# return last
# 另一种python解法
# if n < 0 or m < 1:
# return False
# s = [x for x in range(n)] # 产生等差数列
# p = m - 1
# while len(s) != 1:
# # 这里用while而不是if 因为处理一次后的p可能仍>len(s)-1. 所以必须处理到p值满足list的index条件为止
# while p > len(s) - 1: # 超过了尾数的index
# # 这个条件要放在最前面,为了防止p一上来就设置的大于len(s)-1, 如last_reaining_number(5, 8)
# p = p - (len(s) - 1) - 1 # -1 减1 是因为index从0计数..
# s.pop(p)
# p += (m - 1)
# return s[0]
编程细节
注意链表尾节点的next设为None;
当涉及删除、插入等操作时,注意头结点、尾节点、唯一一个节点等特殊情况。
考虑无效输入,如链表为空
sorted()是创建副本,sth.sort()改变原值
参考网址:https://www.cnblogs.com/wenjiang/p/3310233.html