链表
链表理论基础
链表:
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的 入口节点 称为 链表的头结点 也就是 head 。
链表的类型:
单链表
上述描述。
注意:单链表中的 指针域 只能 指向节点的下一个节点。
双链表
每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表
顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
链表的存储方式
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针 链接 在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制 取决于 操作系统的内存管理。
这个链表起始节点为2, 终止节点为7, 各个节点 分布在 内存的不同地址空间上,通过指针串联在一起。
链表的定义
其中,ListNode是链表节点的类名,__init__是类的构造函数,self.val表示节点的值,self.next表示下一个节点的指针。
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
# 创建链表节点
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
# 设置链表指针
node1.next = node2
node2.next = node3
# 打印链表
curr = node1
while curr:
print(curr.val)
curr = curr.next
# 输出
1
2
3
链表的操作
例:删除D节点
只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
添加节点
链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
性能分析
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
1、移除链表元素(leetcode203)
思路:
以链表 1 4 2 4 来举例,移除元素4。
这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了
那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢?
这里就涉及如下链表操作的两种方式:
直接使用原来的链表来进行删除操作。
设置一个虚拟头结点在进行删除操作。
第一种操作:直接使用原来的链表来进行移除。
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。
依然别忘将原头结点从内存中删掉。{python、java可以省略这步}
这样移除了一个头结点,是不是发现,在单链表中移除头结点 和 移除其他节点的操作方式是不一样,其实在写代码的时候也会发现,需要单独写一段逻辑来处理移除头结点的情况。
可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。
这里来给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1。
这样是不是就可以使用和移除链表其他节点的方式统一了呢?
来看一下,如何移除元素1 呢,还是熟悉的方式,然后从内存中删除元素1。
最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;, 这才是新的头结点。
1、设置 虚拟节点 dummy_head_next节点 指向 head节点
2、定义cur 临时指针 用来遍历整个链表 cur = dummy_head 因为要删除 head节点,而删除这个节点,只能通过这个节点的前一个节点直接指向 head的next节点,删除head节点。
3、设置了cur临时节点=head节点,要删除cur_next(head)节点,首先要判断cur_next是否为空,如果不是空就一直遍历列表
4、如果cur_next_val的数值等于tarket,就让cur_next = cur_next_next
5、如果没有找到目标值,继续遍历,cur = cur_next
6、遍历结束后,return dummy_head_next,不返回head,是因为head可能被删了。
代码实现
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
dummy_head = ListNode(next=head) #添加一个虚拟节点 并指向head节点
cur = dummy_head # 临时指针指向 dummy_head
while(cur.next!=None): # 判断 head的节点是否为空
if(cur.next.val == val):
cur.next = cur.next.next # 删除cur.next节点
else:
cur = cur.next
return dummy_head.next # 不返回 head :可能head 节点 被删除
2、设计链表(leetcode707)
反转链表(leetcode206)
思路
定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。
1、初始化:cur = head pre = null
2、当cur = Null时,遍历结束。注意 当cur 指向pre时, 1 -> 2间没有链接,所以2 -> 1 不能指向,所以需要在没赋值前,创建临时指针temp 指向cur的下一个节点 temp = cur.next
3、赋值:cur.next = pre
4、改变完方向后,pre与cur 指针整体向后移动以为 :pre = cur, cur = temp
5、返回新链表的头节点,pre 指向head节点
代码实现
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
cur, pre = head, None
while cur:
temp = cur.next # 创建临时指针 = cur的下一个节点,保存节点
cur.next = pre # 第一次翻转
pre = cur
cur = temp
return pre