算法工程师重生之第三天( 链表理论基础 移除链表元素 设计链表 反转链表 )
参考文献 代码随想录
一、 链表理论基础
什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
如图所示:
#链表的类型
接下来说一下链表的几种类型:
#单链表
刚刚说的就是单链表。
#双链表
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
如图所示:
#循环链表
循环链表,顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
#链表的存储方式
了解完链表的类型,再来说一说链表在内存中的存储方式。
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
如图所示:
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
链表的操作
#删除节点
删除D节点,如图所示:
只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
#添加节点
如图所示:
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
#性能分析
再把链表的特性和数组的特性进行一个对比,如图所示:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
二、移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == 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 输出:[]
提示:
- 列表中的节点数目在范围
[0, 104]
内 1 <= Node.val <= 50
0 <= val <= 50
问题分析
注意事项
1. 不能直接操作虚拟头结点,如果操作,那么头节点会不断的被更改掉,必须定义一个临时的变量指向虚拟头结点
2. 使用while判断时,要保证,操作的指针不能为空,那为什么这里可以使用if呢,首先有虚拟头头结点的存在,其次,if和else只要满足一个,那么他就会回到while里,所以也可以使用if,相当于这样的链表的时候[7,7,7],存在了虚拟头结点,那么此时链表就变成了[0,7,7,7],如果cur.next.val == val,则需要删除,那么就不会执行else,直接到上一层的while循环,再次判断cur.next.val == val是否相等,所以可以使用if判断。
while版本
# Definition for singly-linked list. # class ListNode(object): # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution(object): def removeElements(self, head, val): """ :type head: ListNode :type val: int :rtype: ListNode """ # 不为空,寻找目标值 # 使用虚拟头结点,便于操作(针对于头节点等于val) dummy_head = ListNode(next = head) cur = dummy_head # 不能直接操作dummy_head, 如果你操作这个,那么原理的结构就会发生改变 while cur != None and cur.next != None: # 如果下一个元素存在,那么就要往下,遍历,寻找等于val的值,并删除 while cur.next != None and cur.next.val == val : cur.next = cur.next.next else: cur = cur.next return dummy_head.next
if版本
# Definition for singly-linked list. # class ListNode(object): # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution(object): def removeElements(self, head, val): """ :type head: ListNode :type val: int :rtype: ListNode """ # 不为空,寻找目标值 # 使用虚拟头结点,便于操作(针对于头节点等于val) dummy_head = ListNode(next = head) cur = dummy_head # 不能直接操作dummy_head, 如果你操作这个,那么原理的结构就会发生改变 while cur.next: # 如果下一个元素存在,那么就要往下,遍历,寻找等于val的值,并删除 if cur.next.val == val : cur.next = cur.next.next else: cur = cur.next return dummy_head.next
三、设计链表
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
示例:
输入 ["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"] [[], [1], [3], [1, 2], [1], [1], [1]] 输出 [null, null, null, null, 2, null, 3] 解释 MyLinkedList myLinkedList = new MyLinkedList(); myLinkedList.addAtHead(1); myLinkedList.addAtTail(3); myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3 myLinkedList.get(1); // 返回 2 myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3 myLinkedList.get(1); // 返回 3
提示:
0 <= index, val <= 1000
- 请不要使用内置的 LinkedList 库。
- 调用
get
、addAtHead
、addAtTail
、addAtIndex
和deleteAtIndex
的次数不超过2000
。
问题分析
删除链表节点:
添加链表节点:
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目
链表操作的两种方式:
- 直接使用原来的链表来进行操作。
- 设置一个虚拟头结点在进行操作。
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class MyLinkedList(object): def __init__(self): self.dummy_head = ListNode() # 初始化虚拟节点 self.size = 0 # 记录链表的长度 def get(self, index): """ :type index: int :rtype: int """ if index < 0 or index >= self.size: return -1 cur = self.dummy_head.next for i in range(index): # 举例一个极端,那么添加到下标为0时候是否为正确 cur = cur.next return cur.val def addAtHead(self, val): """ :type val: int :rtype: None """ self.dummy_head.next = ListNode(val, self.dummy_head.next) self.size += 1 def addAtTail(self, val): """ :type val: int :rtype: None """ cur = self.dummy_head while cur.next: cur = cur.next cur.next = ListNode(val) self.size += 1 def addAtIndex(self, index, val): """ :type index: int :type val: int :rtype: None """ if index < 0 or index > self.size: return cur = self.dummy_head for i in range(index): cur = cur.next cur.next = ListNode(val, cur.next) self.size += 1 def deleteAtIndex(self, index): """ :type index: int :rtype: None """ if index < 0 or index >= self.size: return cur = self.dummy_head for i in range(index): cur = cur.next cur.next = cur.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)
四、反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2] 输出:[2,1]
示例 3:
输入:head = [] 输出:[]
提示:
- 链表中节点的数目范围是
[0, 5000]
-5000 <= Node.val <= 5000
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。
那么接下来看一看是如何反转的呢?
我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur)
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
双指针
声明一个空指针,指向None,另外一个指针指向head,因为要使用cur来接受head,不能直接操作在head,所以需要判断是否为空指针,即while cur,翻转,那么就是每个节点的下一给都指向上一个节点,那么就需要存储,cur的下一个,如果你存,那么等下你,操作cur.next指向空指针prev的话,那么原来链表的元素就找不到了,所以需要使用一个临时变量存储,然后在做next方向的改变,然后就要移动这个2个指针,使prev指针指向cur, cur指向tmp。
class Solution(object): def reverseList(self, head): """ :type head: ListNode :rtype: ListNode """ prev = None # cur = head while cur: tmp = cur.next cur.next = prev prev = cur cur = tmp return prev
迭代
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。
class Solution: def reverseList(self, head: ListNode) -> ListNode: return self.reverse(head, None) def reverse(self, cur: ListNode, pre: ListNode) -> ListNode: if cur == None: return pre temp = cur.next cur.next = pre return self.reverse(temp, cur)