链表
链表是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针。由于不必须按顺序存储,链表在插入的时候可以达到O(1)复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
链表结构种类
从结构上分链表大体可分为单向链表、双向链表、循环链表。
单向链表
单向链表分为两部分,一个是当前节点的值,另外一个是指向下一个节点的链接。
双向链表
双向链表分为三部分,数值、向前的节点链接、向后的节点链接。
循环链表
将单向链表首尾相连即可构成一个循环链表。
链表题型
- 链表增删改查的操作
- 链表反转(递归法、迭代法)
- 合并链表
链表解题归纳
- 链表在操作时候一般会使用一个哑节点
- 哑节点可以记录链表的头部指针,单向链表只记录了next节点的位置
- 方便操作,数组的下标是从0开始的。链表在操作时候把哑节点作为下标为0的节点,可以和大家日常使用数组习惯一致
- 链表中会出现head.Next这种用法,有时候容易绕晕了。这时候记住一条head.Next看作一个整体,如果怕记乱可以多写一步用变量替换法
例如:删除head节点的下一个节点。找到下个节点指向的位置,让head指向下个节点指向的位置即可
1. 找到下个节点,声明tmp为head下个节点
tmp := head.Next
2. 让head指向tmp的下个节点
head.Next = tmp.Next
- 链表操作时候,一般先找到待操作节点的前一个位置
707. 设计链表
type MyLinkedList struct {
size int // 维护一个链表长度,对于无效的下标操作不用遍历链表
head *ListNode
}
func Constructor() MyLinkedList {
return MyLinkedList{
head: &ListNode{}, // 链表操作时候一般会使用一个哑节点
size: 0,
}
}
func (l *MyLinkedList) Get(index int) int {
if index < 0 || index >= l.size {
return -1
}
cur := l.head // 可以把这里理解为数组的下标0位置,方便操作
// 1. 首先找到待操作位置
for i := 0; i<=index; i++ {
cur = cur.Next
}
// 2. 返回查询结果
return cur.Val
}
func (l *MyLinkedList) AddAtHead(val int) {
l.AddAtIndex(0, val)
}
func (l *MyLinkedList) AddAtTail(val int) {
l.AddAtIndex(l.size, val)
}
func (l *MyLinkedList) AddAtIndex(index int, val int) {
if index > l.size {
return
}
if index < 0 {
index = 0
}
cur := l.head
// 1. 找到待操作位置
// 这里条件是i<index。此时下标是(index-1),注意观察题目说的是之前还是之后,加上新插入节点恰好下标是index
for i := 0; i < index; i++ {
cur = cur.Next
}
// 2. 生成新节点
node := &ListNode{
Val: val,
Next: cur.Next,
}
// 3. 插入到链表中
cur.Next = node
// 4. 链表长度记得+1
l.size++
}
func (l *MyLinkedList) DeleteAtIndex(index int) {
// 删除最后一个元素没有意义,所以index也不能等有l.size
if index < 0 || index >= l.size {
return
}
l.size--
cur := l.head
// 1. 找到待操作位置
for i := 0; i<index; i++ {
cur = cur.Next
}
// 2. 删除节点
cur.Next = cur.Next.Next
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* obj := Constructor();
* param_1 := obj.Get(index);
* obj.AddAtHead(val);
* obj.AddAtTail(val);
* obj.AddAtIndex(index,val);
* obj.DeleteAtIndex(index);
*/
203. 移除链表元素
解题思路:
- 找到符合条件的节点,代码上的实现就是(cur.Val == val)
- 让符合条件的节点前一个节点直接指向当前节点的下一个节点即可
- 由于单向链表只记录了next节点位置,这里我们还要维护一个pre节点,记录前一个节点
定义一个哑节点,让哑节点指向链表头部节点,因为会对头节点做for循环迭代,需要保存记录头节点位置
func removeElements(head *ListNode, val int) *ListNode {
// 定义哑节点,记录链表的头指针
dummy := &ListNode{}
dummy.Next = head
// 记录上一个节点,找到满足条件的节点时候需要知道前一个节点
pre := dummy
// 当前节点
cur := dummy.Next
for cur != nil {
if cur.Val == val {
pre.Next = cur.Next
} else {
pre = cur
}
cur = cur.Next
}
return dummy.Next
}
206. 反转链表
解题思路:
假如你左手拿着📱,右手拿着🍗。现在你想右手拿📱,左手拿🍗。你一定是先把手机放下或者把鸡腿放下,或者都放下。。。
两个变量交换肯定要借助于第三个变量
循环遍历链表,按照以下顺序一个节点一个节点反转
- 保存下个节点位置(把手机放到桌子上)
- 反转当前节点,让当前节点指向pre(上个节点)。(右手的鸡腿🍗给左手)
- 当前节点反转完成,更新pre节点(当前节点变为了pre节点)。
- 移动到下一个节点(从桌子上拿起🍗)
func reverseList(head *ListNode) *ListNode {
var pre *ListNode
for head != nil {
// 保存下个节点指针
next := head.Next
// 当前节点反转,指向前一个节点
head.Next = pre
// 当前节点完成操作,pre前移一步(当前节点变为pre节点)
pre = head
// head移向下一个操作节点
head = next
}
return pre
}
24. 两两交换链表中的节点
解题思路:
把前两个节点位置交换写出来就可以了,后面节点都是循环的。由于第一个节点是没有上一个节点的,这里我们可以使用哑节点这样就和后面的保持一致了
- pre节点指向新的节点
- node1指向node3节点
- node2指向node1节点
- 当前节点后移
func swapPairs(head *ListNode) *ListNode {
dummy := &ListNode{0, head}
cur := dummy
for cur.Next != nil && cur.Next.Next != nil {
node1 := cur.Next
node2 := cur.Next.Next
cur.Next = node2
node1.Next = node2.Next
node2.Next = node1
cur = node1
}
return dummy.Next
}
19. 删除链表的倒数第 N 个结点
解题思路:
使用快慢指针,即先让快指针走n个节点和头部保持n,此时慢指针开始。等快指针遍历完链表时候,此时慢指针指向的位置即是我们要操作目标节点的前一个节点
整体思路不难,但是在遍历时候有些小技巧和边界条件理解起来比较麻烦。下面结合代码整体看下。
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 声明一个哑节点,方便操作。对于只有一个元素的情况也不用特殊处理了
dummy := &ListNode{0, head}
// 快慢指针,注意这里慢指针是从哑节点开始的,会影响到下面找待操作位置的条件
fast, slow := head, dummy
// i<n 由于数组下标是从0开始,题目中限制了n>=1。我们平时说的倒数第n个在计数时候是从1开始的
for i := 0; i<n; i++ {
fast = fast.Next
}
// 这里为什么不用fast.Next != nil的条件呢,因为我们加入了哑节点,所以要让slow节点多移动一步
// 此时slow所在的位置恰好是待操作节点的前一个位置
for fast != nil {
fast = fast.Next
slow = slow.Next
}
// 哑节点加入可以省去对一个元素的特殊情况的判空处理
slow.Next = slow.Next.Next
return dummy.Next
}