提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
一、介绍链表
二、题目分析与心得
总结
1.什么是链表呢?
链表是一种我们常用的数据结构,用来存储数据,链表通常由数据域和指针域构成,数据域存储数据,指针域存储指向别的链表节点的指针。
2.链表的类别
我们常见的链表通常有以下几种:
单链表:具有数据域和一个指针域,该指针域指向链表的下一个节点,链表的最后一个节点的指针域为null。单链表是我们最常使用的链表。代码表达与图式如下
class ListNode{
int val;
ListNode next;
}
双链表:双链表具有数据域和两个指针域,其中一个指针域指向链表的前一个结点,另一个指针域指向链表的后一个节点,头节点的前指针域和后指针域都为null。双链表与单链表最大的不同就是当我们具有一个单链表节点的指针的时候,只能去访问它下一个节点的位置,而不能去访问它的上一个节点,双链表解决了这个问题,因为前指针的存在,从而可以方便的访问某一节点的上一个节点,但是代价是每一个节点都需要多一个前指针域。
循环链表:当我们去访问单链表时,只能单向访问,访问双链表时,虽然可以双向访问,但两者的本质都是一条线,而循环链表不不同,它把线的头尾相接,变成了一个圆,只要我们想要,可以从某一个节点开始,无限的访问下去。循环链表在单链表中的体现是,尾节点的指针域不再是null,而是指向了头节点。在双链表中的体现是,尾节点的后指针域指向头节点,头节点的前指针域指向尾节点。
3.链表的特点
链表的最大特点就是它存储数据的时候可以地址不连续,这样是它和数组的最大区别,数组存储数据时是连续存储的,我们可以通过下标来快速访问某一位置的值,但是链表通过指针域,把一个个节点连接起来,这样的优点是我们不需要再去寻找一大片连续的空间去存储数据,正是因为这一特点,也导致了链表的缺点和优点
优点:
(1)地址不需要连续,这就让我们可以不用再寻找一大片连续的空间去存储数据,减少了存储空间的碎片化(当然,这只是从逻辑存储结构来分析,是否在底层真的是非连续存储,还需要考虑选择的物理存储结构)
(2)对数据的增删更为方便,在数组中,我们对数据的增删需要大量的移动其他元素的位置,而在链表中,可以这是很方便的,换句话说,在数组中对元素的增删的时间复杂度为O(n),在链表中为O(1),当然也有前提,在知道要增删位置前一个节点的指针的情况下。
缺点:
天下没有免费的午餐,好处都是有代价的
(1)还记得为什么链表可以非连续存储吗,是因为我们添加了指针域,所以这也就导致了每一个节点的空间不单单只用来存储我们需要的数据,必须还要空出位置去记录下一个节点的位置,这就导致了空间的增大
(2)同样因为地址不连续,这就导致了我们想要访问链表的某一个位置的节点的时候,必须要从头开始一个一个寻找,而数组可以通过下标进行随机存取操作,而链表只能顺序存取,也就是访问某一个位置的值,数组的时间复杂度是O(1),链表是O(n)。
(1).
203移除链表
分析:
本题主要考察了删除链表中的元素,核心就是寻找要删除节点的前一个结点,易错点在于对边界位置的把控和循环条件的设置,很容易出现最开始的节点和最后一个节点没有删除或者出现空指针异常的情况
代码实现:
# 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: Optional[ListNode], val: int) -> Optional[ListNode]:
dummy_head = ListNode(0, head)
cur = dummy_head
while cur.next != None:
if cur.next.val == val:
cur.next = cur.next.next
else:
cur = cur.next
return dummy_head.next
先创建一个头节点,便于我们对头指针位置进行操作,然后让cur指向要判断节点的前一个位置,根据是否删除,来进行cur.next的设置和cur的移动
心得:
当链表没有头节点时,我们可以先自己设立一个头节点,从而减少了对头指针位置的单独分析。
(2).
707设计链表
分析:
此题是对链表的一个综合性考察,涵盖了增删改查四个方面,个人认为易错点不在对增删改查操作编写方面,二是要意识到,题目给的不是一个链表节点,二是一个宏观的链表结构,其中包含的不是链表的数据域和指针域,二是链表长度和头指针位置,所以我们需要先自己设计一个链表节点的结构。
代码实现:
class Node{
int val;
Node next;
Node(){
}
Node(int val){
this.val = val;
}
Node(int val, Node next){
this.val = val;
this.next = next;
}
}
class MyLinkedList {
int size;
Node head;
public MyLinkedList() {
this.size = 0;
this.head = new Node(0);
}
public int get(int index) {
if(index < 0 || index >= size){
return -1;
}
Node cur = this.head.next;
for(int i = 0;i < index;i++ ){
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
Node cur = new Node(val);
cur.next = this.head.next;
this.head.next = cur;
this.size++;
}
public void addAtTail(int val) {
Node cur = head;
while(cur.next != null){
cur = cur.next;
}
Node node =new Node(val);
cur.next = node;
this.size++;
}
public void addAtIndex(int index, int val) {
if(index <= 0){
addAtHead(val);
return;
}
if(index > this.size){
return;
}
if(index == this.size){
addAtTail(val);
return;
}
Node cur = head;
for(int i = 0;i < index;i++){
cur = cur.next;
}
Node node = new Node(val, cur.next);
cur.next = node;
this.size++;
}
public void deleteAtIndex(int index) {
if(index < 0 || index >=this.size){
return;
}
Node cur = head;
for(int i = 0;i < index;i++){
cur = cur.next;
}
cur.next = cur.next.next;
this.size--;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
心得:
此题是综合性考察,具体到不是太难,易错点还是前边所说的对题意的理解.
(3).
206反转链表
分析:
此题最直观的想法就是利用尾插法,此种方法不再赘述,以下介绍双指针法
利用双指针,一快一慢,慢指针指向前一个节点,快指针指向后一个节点,然后让快指针的next域指向慢指针
代码实现
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre, cur = None, head
while cur != None:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return pre
注意最开始的时候,让慢指针为null
心得:此题算是快慢指针很简单的应用,以后做题要多多利用此种方法
(4).
24.两两交换链表中的节点
分析:
此题与上题反转链表比较像,都是改变节点的指针域,不同的是,上题的只需要改变后节点的next域即可,此题还需要改变前节点的next域,所以,我们要站在前节点的前节点上去操作。易错点就是对循环条件的判断,容易造成空指针异常。
代码实现:
# 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: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode(0, head)
key = dummy_head
while key.next != None and key.next.next != None:
pre = key.next
cur = key.next.next
tmp = cur.next
key.next = cur
cur.next = pre
pre.next = tmp
key = pre
return dummy_head.next
key为前节点的前节点,pre为前节点,cur为后节点。注意循环条件,因为我们在交换时,是对key后边的两个节点交换,所以要保证key.next和key.next.next都不为空。
心得:
首先是设置头节点方便操作,再就是对链表指针域改变的理解
(5).
19.删除链表的倒数第N个节点
分析:
最直观的思路,先遍历一边链表,得到链表的长度len,倒数第n个节点,就是正数第(len - n)个节点,然后再从头开始查找。
此题依然可以用双指针方法,首先让pre和cur都指向头节点,我们要明白这样一个现象,我们要找到倒数第n个节点,那么如果先让cur向后移动n次,之后pre和cur再同时向后移动,当cur指向尾节点时,pre正好指向倒数第n个节点前边的那个节点,这时在进行删除操作就非常简单了
代码实现:
# 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: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy_head = ListNode(0, head)
pre, cur, val = dummy_head, dummy_head, n
while val != 0:
cur = cur.next
val -= 1
while cur.next != None:
cur = cur.next
pre = pre.next
pre.next = pre.next.next
return dummy_head.next
只要理解了分析中说的现象,代码是非常好写的
心得:
我们在寻找倒数第n个节点的时候,可以设置快慢指针,先让快指针走n步,之后快慢指针再一起向后走,当快指针指向尾节点时,慢指针指向倒数第n个节点的前一个节点。
(6).
160.相交链表
分析:
此题与上题有些类似,我们要注意一点,如链表A长度为6,链表B长度为4,则在相交位置只有可能在A的第三个节点及之后。所以,我们依然可以利用双指针,先分别指向两个链表的head,然后让较长的链表的指针向后先移动x个位置(x为两链表长度差值)。之后再让两个指针一起向后走,看能否找到两指针相等的位置。
代码实现:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
A, B = headA, headB
a, b = 0, 0
while A != None:
A = A.next
a += 1
while B != None:
B = B.next
b += 1
if a >= b:
val = a - b
A, B = headA, headB
x = 0
while x < val:
A = A.next
x += 1
while A != None:
if A == B:
return A
A = A.next
B = B.next
return None
else:
val = b - a
A, B = headA, headB
x = 0
while x < val:
B = B.next
x += 1
while A != None:
if A == B:
return A
A = A.next
B = B.next
return None
此题如果能想到利用长度差值解题,那么代码撰写方面是比较简单的。
心得:
双指针!双指针!双指针!
(7).
142环形链表Ⅱ
分析:
此题分为两大部分,一是先确定是否具有环,二是如果有环的话,寻找入环节点。
先看第一部分,确定是否有环,我们可以利用快慢指针,开始都先指向头指针,然后快指针每次走两步,慢指针每次走一步,如果存在环,则快慢指针总会相遇(因为进入环后,相当于慢指针不动,快指针每次逼近慢指针一步),
再说第二部分,此时需要一些数学计算,具体过程可参考代码随想录
代码实现:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
low, quick = head, head
while quick != None and quick.next != None:
quick = quick.next.next
low = low.next
if quick == low:
pre = head
cur = quick
while pre != cur:
cur = cur.next
pre = pre.next
return cur
return None
主要实在数学计算方面,如果理解的话,代码很容易编写。注意一点,快指针每次走两步,避免空指针异常的条件是quick != none and quick.next != none
心得:
利用快慢指针来判断链表是否存在环,快指针每次走两步,慢指针走一步
876链表的中间节点
分析:
此题结合了快慢指针和之前的寻找倒数第n个节点的精髓,只要认识到一点,先让快慢指针都指向head,之后快指针每次走两步,慢指针每次走一步,当快指针指向尾节点或none时,慢指针指向链表的中间节点(如果有两个的话指向后边的)
代码实现:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre, cur = head, head
while cur != None and cur.next != None:
cur = cur.next.next
pre = pre.next
return pre
代码分析:
如果理解思想,代码并不难写,注意循环条件,避免空指针异常
心得:
利用快慢指针求链表中间位置节点
链表是数据结构中非常重要的存储结构,首先要一定要掌握链表的特点,之后多练题来加深算法方面的
个人感觉链表中的双指针用到的非常非常多,在此片文章中的应用
求解是否有环
求解倒数第n个节点(求解中间位置节点)
再就是对节点指针域的改变
易错的部分除了一些新思想很难想到和理解,编写代码时空指针异常和断链的情况也很容易发生。