概念
-
链表的基础概念:了解链表结构,如节点(Node)和指针(next,或在双链表中的prev)。
-
链表操作的实现:
- 插入:在链表的头部、尾部、或中间插入一个新节点。
- 删除:从链表的头部、中间、或尾部删除一个节点。
- 检索:根据索引查找链表中的节点。
-
链表与数组的不同:链表在插入和删除操作时通常更高效,因为它不需要移动元素。但是,链表在访问元素时不如数组高效,因为它不支持随机访问。
-
指针和引用的操作:如何使用指针或引用来遍历链表、插入新节点、删除节点等。
-
边界和异常情况的处理:例如,当索引超出链表长度时,或当链表为空时,如何处理。
-
哑节点(Dummy Node)的使用:哑节点可以简化某些链表操作,特别是插入和删除,因为你不需要为头节点和其他节点编写不同的逻辑。
个人易错点
- 未处理特殊情况:在链表操作中,头节点和尾节点的处理通常与中间节点不同。例如,在
addAtIndex方法中,当index为0时,应该在头部添加节点,但是在原始代码中这一点被遗漏了。 - 遍历的条件设置:在多个函数中,特别是
addAtIndex和deleteAtIndex,遍历链表以找到指定位置时,应确保遍历条件正确。例如,为了找到索引前一个节点,应该使用number < index - 1而不是number < index。 - 边界条件的检查:在执行操作之前,检查索引或节点是否有效非常重要。例如,在
get方法中,应检查索引是否小于0,并在deleteAtIndex中检查当前节点的下一个节点是否存在。 - 冗余代码:在某些地方,可能会重复执行相同的操作。例如,在
addAtIndex中,有多处设置newNode.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。
解题思路🙋🏻 ♀️
为什么是 while current != nil && number < index - 1 { number < index - 1, 而不是 index ?
当需要在链表的指定索引位置插入一个新节点或删除一个节点时,我们实际上需要找到该索引前面的节点(也就是索引位置的前一个节点)。这样,我们可以通过修改这个前一个节点的next指针来插入新节点或删除节点。
考虑以下情况:
链表为: 1→2→3→4
如果我们想在索引为2的位置插入一个新节点5,那么链表应该变为: 1→2→5→3→4
注意,为了在索引2处插入新节点,我们实际上需要找到索引1处的节点,然后修改这个节点的next指针,使其指向新节点。
因此,我们需要遍历链表,直到我们到达索引位置的前一个节点。这就是为什么我们在while循环中使用number < index - 1条件的原因。这样,当循环结束时,current会指向索引位置的前一个节点。
为什么 while current?.next != nil && number < index - 1 { 是< 而不是 <=
使用<而不是<=,主要是为了确保在遍历结束后,current指针指向的是指定索引前一个节点。这样我们可以在该位置插入或删除节点。
为什么这样做呢?考虑以下几点:
- 插入操作:如果我们想在
指定的索引位置插入一个新节点,我们需要修改前一个节点的next指针,使其指向新节点。为了完成这一操作,我们需要找到前一个节点。 - 删除操作:如果我们想删除指定索引的节点,我们同样需要修改前一个节点的
next指针,使其跳过要删除的节点,直接指向下一个节点。
为了更好地理解,考虑以下示例:
链表为:1→2→3→4
假设我们想在索引2的位置插入新节点5。我们希望链表变为:1→2→5→3→4
为了实现这一操作,我们需要:
- 找到索引
1的节点(也就是值为2的节点)。 - 修改这个节点的
next指针,使其指向新节点5。
如果我们在遍历时使用<=,那么current将会指向索引2的节点(值为3的节点),而不是我们所需要的前一个节点。
因此,我们使用number < index - 1来确保在循环结束后,current指向的是指定索引前一个节点。这样,我们可以方便地进行插入或删除操作。
边界思考🤔
代码
// 定义自定义链表类
class MyLinkedList {
// 头节点,初始为nil
private var head: ListNode?
// 初始化函数,设置头节点为nil
init() {
head = nil
}
// 获取链表中下标为index的节点的值。如果下标无效,则返回-1
func get(_ index: Int) -> Int {
// 设置当前节点为头节点
var current = head
// 如果索引小于0,则返回-1
if index < 0 {
return -1
}
var number = 0
// 遍历链表,直到达到指定的索引或链表末尾
while current != nil && number < index {
current = current?.next
number += 1
}
// 返回找到的节点的值,如果节点为nil,则返回-1
return current?.val ?? -1
}
// 在链表头部添加一个新节点
func addAtHead(_ val: Int) {
// 创建新节点
let newNode = ListNode(val)
// 新节点的下一个节点指向原头节点
newNode.next = head
// 更新头节点为新节点
head = newNode
}
// 在链表尾部添加一个新节点
func addAtTail(_ val: Int) {
// 创建新节点
let newNode = ListNode(val)
// 设置当前节点为头节点
var current = head
// 如果链表为空,直接将头节点设置为新节点
if head == nil {
head = newNode
return
}
// 遍历链表,直到找到最后一个节点
while current?.next != nil {
current = current?.next
}
// 将最后一个节点的下一个节点设置为新节点
current?.next = newNode
}
// 在指定索引添加一个新节点
func addAtIndex(_ index: Int, _ val: Int) {
// 创建新节点
let newNode = ListNode(val)
// 设置当前节点为头节点
var current = head
var number = 0
// 如果索引为0,直接在头部添加新节点
if index == 0 {
newNode.next = head
head = newNode
return
}
// 遍历链表,直到找到索引前一个节点
while current != nil && number < index - 1 {
current = current?.next
number += 1
}
// 如果找到了该节点,将新节点插入到它后面
if current != nil {
newNode.next = current?.next
current?.next = newNode
}
}
// 在指定索引删除节点
func deleteAtIndex(_ index: Int) {
// 如果索引为0,直接删除头节点
if index == 0 {
head = head?.next
return
}
// 设置当前节点为头节点
var current = head
var number = 0
// 遍历链表,直到找到索引前一个节点
while current != nil && number < index - 1 {
current = current?.next
number += 1
}
// 如果找到了该节点,并且它的下一个节点不为nil,删除它的下一个节点
if current?.next != nil {
current?.next = current?.next?.next
}
}
}
错题集
func get(_ index: Int) -> Int {
if index < 0 { return -1 }
var current = head
var number = 0
// ❌ 错误的部分:您在返回`current`的值之前已经更新了它。
while current != nil && number <= index {
current = current?.next
number += 1
}
// ✅ 正确的做法:在更新`current`之前返回它的值。
while current != nil && number < index {
current = current?.next
number += 1
}
return current?.val ?? -1
}
func addAtIndex(_ index: Int, _ val: Int) {
let newNode = ListNode(val)
if index == 0 {
newNode.next = head
head = newNode
return
}
var current = head
var number = 0
// ❌ 错误的部分:代码逻辑重复和混淆。
while current != nil && number <= index - 1 {
if number == index - 1 {
newNode.next = current?.next
head = newNode // ❌ 这里不应该更新head。
}
newNode.next = current?.next // ❌ 多次连接newNode是不必要的。
number += 1
}
// ✅ 正确的做法:在找到正确的位置后只连接一次。
while current != nil && number < index - 1 {
current = current?.next
number += 1
}
if current != nil {
newNode.next = current?.next
current?.next = newNode
}
}
func deleteAtIndex(_ index: Int) {
if index < 0 { return }
// ❌ 错误的部分:当`index`为0时,您没有删除头节点。
if index == 0 {
head = head?.next
return
}
var current = head
var number = 0
// ❌ 错误的部分:您的删除逻辑有误。
while current != nil && number < index {
if number == index && current?.next != nil {
current?.next = current?.next?.next
}
number += 1
}
// ✅ 正确的做法:在找到索引前一个节点后执行删除操作。
while current != nil && number < index - 1 {
current = current?.next
number += 1
}
if current?.next != nil {
current?.next = current?.next?.next
}
}
时空复杂度分析
引用
本系列文章部分概念内容引用 www.hello-algo.com/
解题思路参考了 abuladong 的算法小抄, 代码随想录... 等等
Youtube 博主: huahua 酱, 山景城一姐,