1. 链表理论基础
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表的入口节点称为链表的头结点也就是head。
链表的类型:
- 单链表:每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思。
- 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表既可以向前查询也可以向后查询。
- 循环链表:头尾相接的链表,可用于解决内瑟夫问题。
图源:代码随想录
链表的存储方式::
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
这个链表起始点为2,终止点为7,各个节点分布在内存的不同地址空间上,通过指针串联在一起。
链表的定义:
C++版本
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
python 版本
class ListNode:
def __init__(self, val, next=None):
self.val = val
self.next = next
链表的操作:
-
删除节点
只要将C节点的next指针 指向E节点就可以了。
Python有自己的内存回收机制,就不用自己手动内存中存储的D节点释放。
-
添加节点
链表的添加以及删除都是O(1)操作,不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)
链表性能分析:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
2. 移除链表元素(leetcode 203)
由于删除头节点和删除其他节点的方式不一样,这题我们可以运用到虚拟头的办法,这样便于我们统一删除的方法,判断原有头的数据是否符合要求。我们用current和while loop来遍历我们的链表,若节点符合条件则向下遍历,若节点不符合条件则跳过该节点以删除它。
# 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(next = head)
current = dummy_head
while current.next:
if current.next.val == val:
current.next = current.next.next
else:
current = current.next
return dummy_head.next
这题是本人第一次系统接触链表,感觉链表看起来规则比较复杂,但其实操作并没有想象中难,关键点是要记住他的每一个节点并不是连续的,需要通过指针来连接,并也可以通过调整指针的方式来增加或者删除节点。在写代码的过程中我们也要记住留一个dummy_head来记录他的头,由current来对后续的节点进行判断和操作。
3. 设计链表(leetcode 707)
这题非常考验对链表这个数据结构的理解以及基本操作方法。
class MyLinkedList:
def __init__(self):
self.dummy_head = ListNode()
self.size = 0
def get(self, index: int) -> int:
if index < 0 or index >= self.size: #注意index是从零算起的,所以等于时也会超出长度
return -1
current = self.dummy_head.next
for i in range(index):
current = current.next
return current.val
def addAtHead(self, val: int) -> None:
self.dummy_head.next = ListNode(val, self.dummy_head.next #在头前加一个,在虚拟头后
self.size += 1
def addAtTail(self, val: int) -> None:
current = self.dummy_head
while current.next: #一直过到最后一个
current = current.next
current.next = ListNode(val)
self.size += 1
def addAtIndex(self, index: int, val: int) -> None:
if index < 0 or index > self.size:
return
current = self.dummy_head
for i in range(index):
current = current.next
current.next = ListNode(val, current.next)
self.size += 1
def deleteAtIndex(self, index: int) -> None:
if index < 0 or index >= self.size:
return
current = self.dummy_head
for i in range(index):
current = current.next
current.next = current.next.next
self.size -= 1
# 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)
还记得我们在对链表进行操作时,我们的增添/删除节点的操作为O(1),但我们查询位置的操作为O(n),因此我们在操作时要有设置一个current来为我们查询需要操作的节点位置。
4. 反转链表(leetcode 206)
这题我们可以选择定义一个新的新的链表,然后一个个查询旧链表的节点,但这样非常的浪费时间(1/2*O(n^2))以及空间(新定义链表)。
因此我们可以尝试在原有链表上反转指针的位置,这样不仅节省了空间,时间上也只需要遍历一遍,即O(n)。
我们可以在这里设置两个指针,current指向head,记录当前节点并向下遍历,prev指向上个节点,目标是让current指向prev。为了在这个过程中保留current原有的指针域并向下遍历,我们用一个temp来进行储存。
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev = None
current = head
while current:
temp = current.next
current.next = prev
prev = current
current = temp
return prev
参考资料: