链表
理论基础
1.基础
链表是一种通过指针串联在一起的线性结构,每一个节点是又两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。数组需要一块连续的内存空间来存储,对内存的要求比较高。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。
链接的入口点称为列表的头结点也就是head。
2. 链表类型
- 单链表
上图 - 双链表
单链表中的节点只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
如图所示:
- 循环链表
它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。循环链表可以用来解决约瑟夫环问题。
3. 链表的定义
class ListNode{
int val;//节点存储的元素
ListNode *next;//指向下一个节点的指针
ListNode(int x):val(x),next(nullptr){}//节点构造函数
}
4. 链表的操作
删除节点
删除D节点,如图所示:
只要将C节点的next指针 指向E节点就可以了。
D节点依然存留在内存里, 只不过是没有在这个链表里而已.在C++里最好是再手动释放这个D节点,释放这块内存。其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
添加节点
![](https://i-blog.csdnimg.cn/blog_migrate/45ce1e7d717d4c64df60d8bbe1a087df.png)
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
5. 性能分析
![](https://i-blog.csdnimg.cn/blog_migrate/71ccf815ad570e3b973d7b71169531d6.png)
刷题
JZ24/LC206 反转链表
题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
- 正规法
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
#
# @param head ListNode类
# @return ListNode类
#
class Solution:
def ReverseList(self , head: ListNode) -> ListNode:
# write code here
if head is None or head.next is None:
return head
pre=None # 上一个节点
while head:
# 先用tmp保存phead下一个节点信息,保证单链表不会因为失去head的next
# 就此断裂
tmp=head.next
# 保存完next就可以让head 的next指向pre
head.next=pre
# 让pre和head以此向后
pre=head
head=tmp
return pre
- 递归法
可以把问题划分为头结点与剩余结点
地址: https://leetcode-cn.com/problems/reverse-linked-list/solution/shi-pin-jiang-jie-die-dai-he-di-gui-hen-hswxy/
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
#
# @param head ListNode类
# @return ListNode类
#
class Solution:
def ReverseList(self , head: ListNode) -> ListNode:
# write code here
# 递归终止条件
if head is None or head.next is None:
return head
#这个是递
p=self.ReverseList(head.next)
head.next.next=head
head.next=None
return p
JZ52 两个链表的第一个公共结点
使用两个指针N1,N2,一个从链表1的头节点开始遍历,我们记为N1,一个从链表2的头节点开始遍历,我们记为N2。
让N1和N2一起遍历,当N1先走完链表1的尽头(为null)的时候,则从链表2的头节点继续遍历,同样,如果N2先走完了链表2的尽头,则从链表1的头节点继续遍历,也就是说,N1和N2都会遍历链表1和链表2。
因为两个指针,同样的速度,走完同样长度(链表1+链表2),不管两条链表有无相同节点,都能够到达同时到达终点。
(N1最后肯定能到达链表2的终点,N2肯定能到达链表1的终点)。
所以,如何得到公共节点:
有公共节点的时候,N1和N2必会相遇,因为长度一样嘛,速度也一定,必会走到相同的地方的,所以当两者相等的时候,则会第一个公共的节点
无公共节点的时候,此时N1和N2则都会走到终点,那么他们此时都是null,所以也算是相等了。
下面看个动态图,可以更形象的表示这个过程~
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
#
#
# @param pHead1 ListNode类
# @param pHead2 ListNode类
# @return ListNode类
#
class Solution:
def FindFirstCommonNode(self , pHead1 , pHead2 ):
# write code here
if pHead1 is None or pHead2 is None:
return None
l1=pHead1
l2=pHead2
while l1!=l2:
if l1 is None:
l1=pHead2
else:
l1=l1.next
if l2 is None:
l2=pHead1
else:
l2=l2.next
# 如果不存在的话最后同时为None 也跳出了循环
return l2
JZ23/LC142 链表中环的入口结点/环形链表II
题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
![](https://i-blog.csdnimg.cn/blog_migrate/017868f384dac17c89c4ea397696af78.png)
- 普通法
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def EntryNodeOfLoop(self, pHead):
# write code here
listnode=[]
if pHead is None or pHead.next is None:
return None
while pHead:
if pHead in listnode:
return pHead
else:
listnode.append(pHead)
pHead=pHead.next
return None
- 快慢指针
(1)初始化:快指针fast指向头结点, 慢指针slow指向头结点
(2)让fast一次走两步, slow一次走一步,第一次相遇在C处,停止
(3)然后让fast指向头结点,slow原地不动,让后fast,slow每次走一步,当再次相遇,就是入口结点。
如上解释:
// cpp
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
ListNode *fast = pHead;
ListNode *slow = pHead;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) break;
}
if (!fast || !fast->next) return nullptr;
fast = pHead;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return fast;
}
};
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 如果相遇
if slow == fast:
p = head
q = slow
while p!=q:
p = p.next
q = q.next
#你也可以return q
return p
return None
JZ14/LC19 删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 k 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
- 快慢指针
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。思路是这样的,但要注意一些细节。分为如下几步:
- 首先这里我推荐大家使用虚拟头结点,这样方面处理删除实际头结点的逻辑。
- 定义fast指针和slow指针,初始值为虚拟头结点,如图:
- fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:
- fast和slow同时移动,直到fast指向末尾
- 删除slow指向的下一个节点,如图:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummy=ListNode()
dummy.next=head
slow= dummy
fast=dummy
while (n!=0): #fast先往前走n步
fast=fast.next
n-=1
while fast.next!=None:
slow=slow.next
fast=fast.next
slow.next=slow.next.next #fast 走到结尾后,slow的下一个节点为倒数第N个节点
return dummy.next
//cpp
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummyHead->next;
}
};
LC203 移除链表元素
题意:删除链表中等于给定值 val 的所有节点。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
- 迭代法
如果不添加虚拟头结点,删除头结点就需要另做考虑。
# Python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
dummy=ListNode()
dummy.next=head
p=dummy
while (p.next!=None):
# 如果不加p.next这个判定 则会报错 因为p.next可能为None,none没用val这个attribute
if p.next.val==val:
# 跳过 p.next 节点
p.next=p.next.next
else:
p=p.next
return dummy.next
// c++
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
struct ListNode* dummyHead = new ListNode(0, head);
struct ListNode* temp = dummyHead;
while (temp->next != NULL) {
if (temp->next->val == val) {
temp->next = temp->next->next;
} else {
temp = temp->next;
}
}
return dummyHead->next;
}
};
- 递归法
链表的定义具有递归的性质,因此链表题目常可以用递归的方法求解。这道题要求删除链表中所有节点值等于特定值的节点,可以用递归实现。
对于给定的链表,首先对除了头节点 head以外的节点进行删除操作,然后判断 head 的节点值是否等于给定的val。如果 head 的节点值等于 val,则 head 需要被删除,因此删除操作后的头节点为head.next;如果head 的节点值不等于val,则head 保留,因此删除操作后的头节点还是head。上述过程是一个递归的过程。
递归的终止条件是head 为空,此时直接返回 head。当head 不为空时,递归地进行删除操作,然后判断head 的节点值是否等于val 并决定是否要删除head。
# Python
class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
if head is None:
return head
# removeElement方法会返回下一个Node节点
head.next = self.removeElements(head.next, val)
if head.val == val:
next_node = head.next
else:
next_node = head
return next_node
//c++
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if (head == nullptr) {
return head;
}
head->next = removeElements(head->next, val);
return head->val == val ? head->next : head;
}
};
LC707 设计链表
题意:
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
链表操作的两种方式:直接使用原来的链表来进行操作。设置一个虚拟头结点在进行操作。
下面采用的设置一个虚拟头结点,这样更方便一些。
class Node:
def __init__(self,val,next=None):
self.val=val
self.next=next
class MyLinkedList:
def __init__(self):
self._head= Node(0) # 虚拟头部节点
self._count=0 #添加的节点数
# -> 是返回函数注释
def get(self, index: int) -> int:
if 0<=index<self._count:
node=self._head
for _ in range(index+1):
node=node.next
return node.val
else:
return -1
def addAtHead(self, val: int) -> None:
self.addAtIndex(0,val)
def addAtTail(self, val: int) -> None:
self.addAtIndex(self._count,val)
def addAtIndex(self, index: int, val: int) -> None:
if index<0:# 如果index小于0,则在头部插入节点。
index=0
elif index>self._count: #如果 index 大于链表长度,则不会插入节点。
return
self._count+=1
add_node=Node(val)
prev_node,current_node =None, self._head
for _ in range(index+1):
prev_node=current_node
current_node = current_node.next
else:
# for 正常运行完再运行else for如果跳出 else 也会跳出
prev_node.next=add_node
add_node.next=current_node
def deleteAtIndex(self, index: int) -> None:
if 0<=index<self._count:
self._count-=1
prev_node = None
current_node = self._head
for _ in range(index+1):
prev_node=current_node
current_node =current_node.next
else:
prev_node.next=current_node.next
current_node.next=None
# Your MyLinkedList object will be instantiated and called as such:
# obj = MyLinkedList()
# param_1 = obj.get(index)
# obj.addAtHead(val)
# obj.addAtTail(val)
# obj.addAtIndex(index,val)
# obj.deleteAtIndex(index)
LC24 两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
输入: head=[1,2,3,4]
输出: [2,1,4,3]
输入: head=[]
输出 = []
建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。
接下来就是交换相邻两个元素了,此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序
初始时,cur指向虚拟头结点,然后进行如下三步:
操作之后,链表如下
代码的顺序先从步骤3,再从步骤2,再从步骤1
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
dummy=ListNode()
dummy.next=head
pre=dummy
# 必须有pre的下一个和下下一个才能交换 否则说明交换已经结束
while pre.next and pre.next.next:
cur=pre.next
post=pre.next.next
# pre cur 和post分别对应最左中间和最右边的节点
cur.next=post.next # 步骤三
post.next=cur # 步骤二
pre.next=post #步骤一
pre=pre.next.next
return dummy.next
LC141 环形链表
Reference
- 代码随想录 https://www.programmercarl.com/
- 数据结构与算法之美