链表应该是面试时被提及最频繁的数据结构,链表的结构很简单,它由指针把若干个节点连接成链状结构。
链表的创建、插入节点、删除节点等操作都只需要20行左右的代码就能实现,因此适合面试时提问。
常见的面试题有:
1)从尾到头打印链表;
2)删除链表的节点;
3)删除链表中倒数第k个结点;
4)反转链表;
5)合并两个排序的链表;
6)两个链表的第一个公共结点;
7)判断链表中是否有环;找出环的入口
8)判断一个链表是否为回文;
对应的解题思路为:
1
1)首先从头到尾打印这个链表,将打印的值放入一个栈中,然后从栈中依次弹出元素。依赖栈的后入先出的结构实现了从尾到头打印链表。
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
'''
需要注意的是,python没有自带的链表结构,也没有自带的栈结构,所以要实现已上思路的话,需要先了解题目中listNode的定义,其具有两个属性,next表示接下来的节点,val表示其值。栈结构需要自己实现。
'''
class Solution:
# 返回从尾部到头部的列表值序列,例如[1,2,3]
def printListFromTailToHead(self, listNode):
# write code here
class Stack(object):
def __init__(self):
self.item=[]
def push(self,value):
self.item.append(value)
def pop(self):
if self.item:
return self.item.pop()
else:
return
def isEmpty(self):
return len(self.item)==0
if not listNode:
return []
stack=Stack()
while listNode:
stack.push(listNode.val)
listNode=listNode.next
re=[]
while not stack.isEmpty():
re.append(stack.pop())
return re
2
2)输入:将被删除的节点Node
将这个节点的数值赋值为下一个节点的数值,然后让它指向下下个节点.
Node.val=Node.next.val
Node.next=Node.next.next
也就是将下一个节点赋值给这个节点来实现删除这个节点的操作。
要考虑的特殊情况是:要删除的点是尾结点,那么只能从头结点开始遍历,找到这个结点的前一个结点;如果这个链表只有一个结点,那么就是删除投头结点,返回头结点为空。
3
-
在链表中,因为内存不是连续的,表示节点的位置需要一个辅助的指针来实现。
比如说要找倒数第k个结点,那么就使用两个指针,fast在slow指针的前k个位置点,当fast指向none时,slow就指向倒数第k个结点。class Solution(object):
def removeNthFromEnd(self, head, n):
slow=fast=dummy=ListNode(0)
dummy.next=head
for i in range(n+1):
fast=fast.next
while fast:
slow=slow.next
fast=fast.next
slow.next=slow.next.next
return dummy.next #考虑删除的是head的情况
4
4)使用迭代的方法,依次保存下一个节点–>让这个节点指向上一个节点–>继续遍历下一个保存的节点。
pre=None
while head:
Next=head.next
head.next=pre
pre=head
head=Next
return pre
5
5)采用递归的方法,每次比较链表最开始的两个节点,并将较小的一个值接在后面,然后依次比较后面的节点。
if not l1:
return l2
if not l2:
return l1
if l1.val>l2.val:
a,b=l2,l1
else:
a,b=l1,l2
a.next=self.sort(a.next,b)
return a
思考:如果是对两个已排序的数组合并呢?要求空间复杂度为O(1)
在合并两个数组时,如果从前往后幅值每个数字(字符)则需要重复移动数字(字符)多次,那么我们可以考虑从后往前幅值,这样就能减少移动的次数,从而提高效率。
6
6)当两个单链表有公共结点之后,它们后面的所有结点都是一样的。因此如果我们可以从最后面的数据开始比较,找到最后一个相同的节点就是第一个公共结点,可以使用栈来存储两个链表的节点来实现,可是这样需要O(M+N)的空间复杂度。不能从头开始比较的一个重要原因是:两个链表的长度不一样。因此可以先遍历两个链表得到他们的长度,就能知道哪个链表更长,以及长的链表比短的链表长多少;在第二次遍历的时候,先让长的链表多走若干步,接着同时在两个链表上遍历,找到的第一个相同的节点就是他们的第一个公共结点。
class Solution(object):
def getIntersectionNode(self, headA, headB):
if not headA or not headB:
return None
a,b=headA,headB
while a is not b:
a=a.next if a else headB #注意这里是if a而不是a.next
b=b.next if b else headA
return a
为了考虑两者之间没有公共结点的情况,所以a=b=None的情况在第二次遍历时也存在。
7
7)从一个新颖的角度出发,如果链表中有环,那么两个人以不同的速度在环中跑,总有一天会遇到,如果直到跑得快指向空了还没遇到说明没有环。
如何找到环的入口呢?
首先我们已经找到位于环中的某一个节点,也就是当slow=fast时指向的节点,然后根据这个节点我们可以得到环的长度n,方法是从这个节点出发,一边继续向前移动一边计数,当再次回到这个节点的时候,就可以得到环的长度n;然后定义两个节点p1,p2为头结点,从头结点开始,p1节点先走n步,然后两个节点同时向前走,相遇的点就是环的入口。
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def EntryNodeOfLoop(self, pHead):
# write code here
slow,fast=pHead,pHead
while fast and fast.next:
fast=fast.next.next
slow=slow.next
if fast==slow: #1.判断是否有环:如果快慢指针相遇,说明有环
aNode=slow.next
n=1 #2.计算环的长度
while slow!=aNode: #aNode回到slow的步数n就是环的长度
n=n+1
aNode=aNode.next
slow,fast=pHead,pHead #3.计算入环点:让快指针fast先走n步,慢指针从表头出发,相遇点即入环点
while n:
n=n-1
fast=fast.next
while slow!=fast:
slow=slow.next
fast=fast.next
return slow
当不需要计算环的长度n时,一个指针从相遇点即slow=fast时指向的节点出发,另一个指针从表头出发,然后两个节点同时向前走,相遇的点就是环的入口。
class Solution:
def EntryNodeOfLoop(self, pHead):
# write code here
slow,fast=pHead,pHead
while fast and fast.next:
fast=fast.next.next
slow=slow.next
if fast==slow: #如果快慢指针相遇,说明有环
aNode=pHead
while slow!=aNode: #一个指针从相遇点出发,另一个指针从表头出发,会在入环点相遇
slow=slow.next
aNode=aNode.next
return slow
8
8)回文的性质是从前往后读与从后往前读相同,也就是链表数值是对称的,那么可以将链表的前半部分逆转,然后比较这个逆转后的部分与后半部分的数值是否一致。
class Solution(object):
def isPalindrome(self, head):
pre=None
slow=fast=head
while fast and fast.next:
fast=fast.next.next
Next=head.next
head.next=pre
pre=head
head=Next
if fast:
head=head.next
while pre and pre.val == head.val:
pre=pre.next
head=head.next
if not pre and not head:
return True
else:
return False
注意
1.代码的鲁棒性
要考虑链表的头结点为空,链表只有一个节点或链表长度小于k的情况,防止代码奔溃。