day03 第二章 链表part01
今日任务
● 链表理论基础
● 203.移除链表元素
● 707.设计链表
● 206.反转链表
1. 链表理论基础
-
基础结构
- 通过指针串联的结构
- 两部分组成
- 数据域:存储该链表数据类型的具体数据
- 指针域:存储下一个节点的指针【长度固定】
- 两部分组成
- 除了最后一个节点以外,每个节点都会存储下一个节点的位置信息,最后一个节点指向null
- 一定能定位到头节点【否则就是无法追踪的链表,要么被GC,要么导致Mem leak】
- 通过指针串联的结构
-
常见类型
-
单链表
-
图片来自代码随想录官网
-
-
双链表
-
相较普通单链表多了一个指向上一个节点的指针
-
查询效率优于单链表
-
不用从头节点开始遍历查找
-
图片来自代码随想录官网
-
-
循环链表
-
将链表的首位相连成一个环
-
图片来自代码随想录官网
-
-
-
在内存中的存储方式
- 不是连续分布的【区别于数组】
- 散乱分布在内存在,因为有下一个节点的地址,可以通过读取内存位置来精准找到下一个节点
-
定义实现
-
C/CPP
// 单链表 struct ListNode { int val; // 节点上存储的元素 ListNode *next; // 指向下一个节点的指针 ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 };
-
Golang
// 单链表 type ListNode struct { Val int Next *ListNode }
-
-
常见操作
-
删除 O(1)
-
将指针从 被删除节点的前一个节点 直接连接 被删除节点的后一个节点 即可
-
图片来自代码随想录官网
-
-
添加 O(1)
-
将指针从 待添加节点的前一个节点 直接连接 待添加节点,待添加节点 指向 后一个节点
-
图片来自代码随想录官网
-
-
注意
- 删除、添加操作本身是O(1), 但是查询的过程是 O(n)
-
2. 移除链表元素
关联 leetcode 203.移除链表元素
-
思路
- 普通单链表始终保持找到 线头【头节点即可】
- 使用虚拟节点保证 原链表头节点和其他节点的操作一致,降低复杂度
-
使用虚拟头节点后,比较操作都是一样的了
- 都是比较 虚拟头节点的Next节点值与 target 是否匹配 ===》一定是一个循环操作
-
代码实现
func removeElements(head *ListNode, val int) *ListNode { // 虚拟头指针, 线头的作用,不可变的固定头节点位置 vHead := ListNode{ Next: head, } // 操作指针 cHead := &vHead //链表不为空 for cHead.Next != nil { if cHead.Next.Val == val { cHead.Next = cHead.Next.Next continue } // 下一位不是目标值,则操作指针向后顺移一位 cHead = cHead.Next } return vHead.Next }
3. 设计链表
关联 leetcode 707.设计链表
-
注意避坑
- 链表节点在操作前一定要注意
- 单链表节点插入时,一定是把新节点先指向链表内节点,否则会导致游离节点产生,插入新节点后的所有节点都会丢失,因为后续的节点无法定位
- 新节点插入时,先把新节点的后续节点关联上,再断开原有链表链接
- 链表节点在操作前一定要注意
-
代码实现:
type MyLinkedList struct { vHead *LinkNode Size int } // 单链表 type LinkNode struct { Val int Next *LinkNode } // 初始化链表,分配虚拟头节点,链表长度 func Constructor() MyLinkedList { return MyLinkedList{ vHead: &LinkNode{ Val: 0, Next: nil, }, Size: 0, } } // 注意 index 从0开始,链表的index,因此要注意 排除 虚拟头节点 // 考虑极端情况,只有一个 真实头节点 func (this *MyLinkedList) Get(index int) int { if index < 0 || index >= this.Size { return -1 } cHead := this.vHead.Next for index > 0 { cHead = cHead.Next index-- } return cHead.Val } func (this *MyLinkedList) AddAtHead(val int) { oHead := this.vHead.Next // 原始头节点 insertNode := LinkNode{ Val: val, Next: oHead, } this.vHead.Next = &insertNode this.Size++ } func (this *MyLinkedList) AddAtTail(val int) { cHead := this.vHead for cHead.Next != nil { cHead = cHead.Next } cHead.Next = &LinkNode{Val: val} this.Size++ } func (this *MyLinkedList) AddAtIndex(index int, val int) { if index > this.Size { return } if index == this.Size { this.AddAtTail(val) return } if index == 0 { this.AddAtHead(val) return } cHead := this.vHead for index > 0 { cHead = cHead.Next index-- } insertNode := LinkNode{Val: val} insertNode.Next = cHead.Next cHead.Next = &insertNode this.Size++ } func (this *MyLinkedList) DeleteAtIndex(index int) { if this.Get(index) == -1 { return } cHead := this.vHead for index > 0 { cHead = cHead.Next index-- } cHead.Next = cHead.Next.Next this.Size-- }
4. 反转链表
关联 leetcode 206.反转链表
-
双指针解法
- 注意循环终止条件
- 终止条件用 cur 不用 cur->next 考虑极端,原链表是一个空链表,则cur->next 会报空指针异常
- 注意节点丢失问题
- 如果在保存 cur节点地址前, 先改动了 cur节点的前一个节点的Next指向,则 cur节点地址将会丢失,导致内存泄漏
// 单链表节点定义 type ListNode struct { Val int Next *ListNode } func reverseList(head *ListNode) *ListNode { // 双指针解法 var pre *ListNode // 空节点 cur := head //循环终止条件 for cur != nil { next := cur.Next cur.Next = pre // 反转 // 移动指针 pre = cur cur = next } return pre }
- 注意循环终止条件
-
递归解法【由双指针可推导】
- 使用递归替代循环
- 无论递归还是循环都是 重复一样的劳动
- 递归的内存占用会高于循环,计算资源使用优于循环
// 单链表节点定义 type ListNode struct { Val int Next *ListNode } // 反转目的, 将当前节点的Next 指向前一个节点 // cur: 当前节点 // pre: 当前节点的前置节点 func reverse(cur, pre *ListNode) *ListNode { if cur == nil { return pre } // 当前节点的下一个节点 next := cur.Next // 反转指向 cur.Next = pre // 移动节点 pre = cur cur = next // 进入下一次递归 return reverse(cur, pre) } func reverseList(head *ListNode) *ListNode { // 递归解法 return reverse(head, nil) }
- 使用递归替代循环
参考引用网站: 代码随想录 (programmercarl.com)