数据结构-单链表
链表(list)
与数组相似,链表也是一种线性数据结构。如下图所示:
由图可知,链表中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。与数组不同,我们无法在常量时间内访问单链表中的随机元素。 如果我们想要获得第 i 个元素,我们必须从头结点逐个遍历。 我们按索引
来访问元素
平均要花费 O(N)
时间,其中 N 是链表的长度。
链表有两种类型:单链表和双链表。上图为单链表,下图为双链表:
链表的添加与删除元素功能十分方便
设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev
以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index)
:获取链表中第index
个节点的值。如果索引无效,则返回-1
。addAtHead(val)
:在链表的第一个元素之前添加一个值为val
的节点。插入后,新节点将成为链表的第一个节点。addAtTail(val)
:将值为val
的节点追加到链表的最后一个元素。addAtIndex(index,val)
:在链表中的第index
个节点之前添加值为val
的节点。如果index
等于链表的长度,则该节点将附加到链表的末尾。如果index
大于链表长度,则不会插入节点。如果index
小于0,则在头部插入节点。deleteAtIndex(index)
:如果索引index
有效,则删除链表中的第index
个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
提示:
- 所有
val
值都在[1, 1000]
之内。 - 操作次数将在
[1, 1000]
之内。 - 请不要使用内置的 链码 库。
- 哨兵节点在树和链表中被广泛用作伪头、伪尾等,通常不保存任何数据。我们将使用伪头来简化我们简化插入和删除。
解:
/*
构造函数时设定哨兵节点,头节点的地址为head.next
新节点插入最后一个节点之后,只需要把链表最后一个节点的指针指向新节点,新节点的指针再指向None即可
节点插入在X、Y之间,只要将X节点的指针指向新节点,新节点指针指向Y即可
*/
// 定义链表结构
type MyLinkedList struct {
Val int // 链表上数据
Next *MyLinkedList // 指向下一个节点的指针
}
// 构造函数
// 在这里设定哨兵节点
func Constructor() MyLinkedList {
return MyLinkedList{
Val: 0,
Next: nil,
}
}
// 获取链表中第 `index` 个节点的值。如果索引无效,则返回`-1`。
// 链表的头节点(哨兵节点) 和 第一个存储节点不是一个节点,头节点的地址为head.Next
func (head *MyLinkedList) Get(index int) int {
// p 初始化指向列表第二个节点
p := head.Next
// 只要指针指向的下一个节点p非空,便一直循环
// 每循环一次index-1,指针前进一位,直到index=0,
// 此时到达要获取的节点位置,返回val
for p != nil {
if index == 0 {
return p.Val
}
p = p.Next
index -= 1
}
return -1
}
// 在链表的第一个元素之前添加一个值为 `val` 的节点。插入后,新节点将成为链表的第一个节点。
func (head *MyLinkedList) AddAtHead(val int) {
// 要插入的第一个节点的值
node := &MyLinkedList{Val:val}
// 新节点的指针指向链表原来的第一个节点
node.Next = head.Next
// 此时head.next已经又向前移动了一位,然后将链表头指针指向新节点,新节点成为第一个存储节点
head.Next = node
}
// 将值为 `val` 的节点追加到链表的最后一个元素。
// 新节点插入最后一个节点之后,只需要把链表最后一个节点的指针指向新节点,新节点的指针再指向None即可
func (head *MyLinkedList) AddAtTail(val int) {
// p 初始化 为指向 链表第一个节点的指针
p := head.Next
// 循环结束后,p.next指向最后一个节点的下一个节点
for p.Next != nil {
p = p.Next
}
// 赋值
p.Next = &MyLinkedList{Val: val, Next: nil}
}
// 在链表中的第 `index` 个节点之前添加值为 `val` 的节点。
// 如果 `index` 等于链表的长度,则该节点将附加到链表的末尾。
// 如果 `index` 大于链表长度,则不会插入节点。
// 如果`index`小于0,则在头部插入节点。
// 节点在X、Y之间只要将X节点的指针指向新节点,新节点指针指向Y即可
func (head *MyLinkedList) AddAtIndex(index int, val int) {
// p 初始化 为指向 链表伪头节点的指针
p := head
// 插入值地址
node := &MyLinkedList{Val: val, Next: nil}
// 若当前节点非空(即没有到链表尾部),便一直循环
for p != nil {
if index == 0 {
node.Next = p.Next
p.Next = node
break
}
index--
p = p.Next
}
}
// 如果索引 `index` 有效,则删除链表中的第 `index` 个节点
func (head *MyLinkedList) DeleteAtIndex(index int) {
// p 初始化 为指向 链表伪头节点的指针
p := head
for p.Next != nil {
if index == 0 {
p.Next = p.Next.Next
break
}
index -= 1
p = p.Next
}
}
Golang中list包
在Go语言中,链表使用 container/list
包来实现,内部的实现原理是双链表,在实际中应用并不多。
package main
import (
"container/list"
"fmt"
)
func main() {
// 创建一个链表对象
l := list.New()
// 尾部添加元素
l.PushBack("hello")
// 头部添加元素
l.PushFront("boy")
// 尾部添加后保存元素索引
element := l.PushBack("see")
// 在"see"之后添加"you"
l.InsertAfter("you", element)
// 在"see"之前添加"again"
l.InsertBefore("again", element)
// 删除"see"
l.Remove(element)
fmt.Println(l)
}
输出:
&{{0xc000076390 0xc0000763f0 <nil> <nil>} 4}
这里具体的0x
实际记录的是对象的地址,可能会变动,但是最后的"4"
不会变动,其表示当前的链表对象里面有4个元素。
那我们要怎么输出呢,这里就需要采用循环遍历的形式:
//输出list的值
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
下面是所有常见list包中函数:
// 返回该元素的下一个元素,如果没有下一个元素则返回nil
func (e *Element) Next() *Element
// 返回该元素的前一个元素,如果没有前一个元素则返回nil。
func (e *Element) Prev() *Element
type List
// 返回一个初始化的list
func New() *List
// 获取list l的最后一个元素
func (l *List) Back() *Element
// 获取list l的第一个元素
func (l *List) Front() *Element
// list l初始化或者清除list l
func (l *List) Init() *List
// 在list l中元素mark之后插入一个值为v的元素,并返回该元素,如果mark不是list中元素,则list不改变。
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
// 在list l中元素mark之前插入一个值为v的元素,并返回该元素,如果mark不是list中元素,则list不改变。
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
// 获取list l的长度
func (l *List) Len() int
// 将元素e移动到元素mark之后,如果元素e或者mark不属于list l,或者e==mark,则list l不改变。
func (l *List) MoveAfter(e, mark *Element)
// 将元素e移动到元素mark之前,如果元素e或者mark不属于list l,或者e==mark,则list l不改变。
func (l *List) MoveBefore(e, mark *Element)
// 将元素e移动到list l的末尾,如果e不属于list l,则list不改变。
func (l *List) MoveToBack(e *Element)
// 将元素e移动到list l的首部,如果e不属于list l,则list不改变。
func (l *List) MoveToFront(e *Element)
// 在list l的末尾插入值为v的元素,并返回该元素。
func (l *List) PushBack(v interface{}) *Element
// 在list l的尾部插入另外一个list,其中l和other可以相等。
func (l *List) PushBackList(other *List)
// 在list l的首部插入值为v的元素,并返回该元素。
func (l *List) PushFront(v interface{}) *Element
// 在list l的首部插入另外一个list,其中l和other可以相等。
func (l *List) PushFrontList(other *List)
// 如果元素e属于list l,将其从list中删除,并返回元素e的值。
func (l *List) Remove(e *Element) interface{}
双指针在链表中的使用
让我们从一个经典问题开始:给定一个链表,判断链表中是否有环。
想象一下,有两个速度不同的跑步者。如果他们在直路上行驶,快跑者将首先到达目的地。但是,如果它们在圆形跑道上跑步,那么快跑者如果继续跑步就会追上慢跑者。
这正是我们在链表中使用两个速度不同的指针时会遇到的情况:
如果没有环,快指针将停在链表的末尾。
如果有环,快指针最终将与慢指针相遇。
所以剩下的问题是:这两个指针的适当速度应该是多少?
一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。下面我们用练习题来进一步理解这种算法。
环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
解:
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func hasCycle(head *ListNode) bool {
/*
双指针法
快慢指针
*/
if head == nil {
return false
}
fast := head
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
head = head.Next
if fast == head {
return true
}
}
return false
}
环形链表 Ⅱ
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。
说明:不允许修改给定的链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。
解:
用到了数学知识
解:
func detectCycle(head *ListNode) *ListNode {
/*
双指针法
首先判断是否为环形链表,如上题
然后采用运用数学公式推导找规律,详细看上图
*/
fast := head
slow := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if fast == slow {
for fast != head {
fast = fast.Next
head = head.Next
}
return head
}
}
return nil
}
// 改进版:
func detectCycle(head *ListNode) *ListNode {
slow = head
fast = head
// 空列表
for {
if fast==nil||fast.Next==nil{
return nil
}
slow = slow.Next
fast = fast.Next.Next
// 相交则跳出循环,相比上面降低了时间复杂度
if slow == fast{
break
}
}
fast = head
for fast != slow{
slow = slow.Next
fast = fast.Next
}
return slow
}
相交链表
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:
- 如果两个链表没有交点,返回
null
. - 在返回结果后,两个链表仍须保持原有的结构。
- 可假定整个链表结构中没有循环。
- 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
解:
// 暴力破解法
func getIntersectionNode(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
p1 := headA
p2 := headB
for p1 != nil {
for p2 != nil {
if p1 == p2 {
return p1
}
p2 = p2.Next
}
p1 = p1.Next
p2 = headB // 关键所在
}
return nil
}
// 双指针法
func getIntersectionNode(headA, headB *ListNode) *ListNode {
/*
两个链表长度分别为a和b,那么a+b = b+a
因此,两个指针分别遍历这两个链表,在第一次遍历到链表a尾部的时候,指向链表b头部继续遍历,这样会抵消长度差。
如果链表有相交,那么会在中途相等,返回相交节点(画图更为明显);
如果链表不相交,那么最后会 nil == nil,返回 nil;
*/
p1 := headA
p2 := headB
for p1 != p2 {
if p1 == nil {
p1 = headB
} else {
p1 = p1.Next
}
if p2 == nil {
p2 = headA
} else {
p2 = p2.Next
}
}
return p1
}
删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的 n 保证是有效的。
解:
func removeNthFromEnd(head *ListNode, n int) *ListNode {
/*
使用快慢两个指针,两个指针之间有n+1个结点,那么当前面的指针指向列表末尾时,
慢指针指向倒数第n+1个结点(即被删除结点的前一个结点)
*/
// 定义哨兵节点
p := &ListNode{}
p.Next = head
// 快慢节点都指向哨兵节点
fast := p
slow := p
// 快指针到达距离慢指针n+1的节点
for i:=0;i<=n;i++ {
fast = fast.Next
}
// 之后快指针与慢指针同时开始移动
// 当快指针到达链表尾部时,慢指针指向要删除节点的前一个节点
for fast != nil {
fast = fast.Next
slow = slow.Next
}
// 删除倒数第n个节点
slow.Next = slow.Next.Next
// 返回头节点
return p.Next
}
// 进阶版:
func removeNthFromEnd(head *ListNode, n int) *ListNode {
/*
只需对链表进行一次遍历,快慢指针相邻
*/
// 哨兵节点
p := &ListNode{}
p.Next = head
// 快慢指针
var slow *ListNode
fast := p
i := 1
for head != nil {
// 此时快指针距离head指针n,当head到达nil时,fast即指向倒数第n个节点
if i >= n {
slow = fast
fast = fast.Next
}
head = head.Next
i++
}
// 当上面循环结束后,head指向nil,fast指向所要删除的节点
slow.Next = slow.Next.Next
return p.Next
}
删除链表节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
示例 1:
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
解:
func deleteNode(head *ListNode, val int) *ListNode {
/*
方法同上题(在这里繁琐了)
*/
p := &ListNode{}
p.Next = head
fast := p
var slow *ListNode
for fast.Next != nil {
slow = fast
fast = fast.Next
if fast.Val == val {
slow.Next = slow.Next.Next
break
}
}
return p.Next
}
// 简易版
func deleteNode(head *ListNode, val int) *ListNode {
p := &ListNode{}
p.Next = head
cur := p
for cur.Next != nil {
if cur.Next.Val == val {
cur.Next = cur.Next.Next
break
} else {
cur = cur.Next
}
}
return p.Next
}
删除链表中的节点(头脑风暴)
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
现有一个链表 -- head = [4,5,1,9],它可以表示为:
示例 1:
输入: head = [4,5,1,9], node = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], node = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
链表至少包含两个节点。
链表中所有节点的值都是唯一的。
给定的节点为非末尾节点并且一定是链表中的一个有效节点。
不要从你的函数中返回任何结果。
解:
func deleteNode(node *ListNode) {
/*
如何让自己在世界上消失,但又不死?
—— 将自己完全变成另一个人,再杀了那个人就行了。
*/
node.Val = node.Next.Val
node.Next = node.Next.Next
}
经典问题 ---> 反转链表问题
让我们从一个经典问题开始:
反转一个单链表。
一种解决方案是按原始顺序迭代结点
,并将它们逐个移动到列表的头部
。似乎很难理解。我们先用一个例子来说明我们的算法。
让我们看一个例子:
请记住,黑色结点 23 是原始的头结点。
- 首先,我们将黑色结点的下一个结点(即结点 6)移动到列表的头部:
- 然后,我们将黑色结点的下一个结点(即结点 15)移动到列表的头部:
- 黑色结点的下一个结点现在是空。因此,我们停止这一过程并返回新的头结点 15。
注意:
在该算法中,每个结点只移动一次
。
因此,时间复杂度为 O(N)
,其中 N 是链表的长度。我们只使用常量级的额外空间,所以空间复杂度为 O(1)。
反转链表
这个题是基础中的基础:反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
解:
func reverseList(head *ListNode) *ListNode {
/*
迭代法
当前节点指向老的表头,然后把当前节点修改为新的表头
为了保证迭代正常,使用node变量缓存head.Next节点
*/
// 空链表情况
if head == nil {
return nil
}
var preHead *ListNode // 老表头
curHead := head // 当前表头
for curHead != nil {
tmp := curHead.Next // 暂时缓存新表头
curHead.Next = preHead // 当前表头指向preHead(老表头),在这里转换指针方向
// 老表头 和 当前表头 都前进一位
preHead = curHead // 老表头成为当前表头
curHead = tmp
}
return preHead
}
递归法相对难理解,这里上一张leetcode上图片,我觉得十分传神:
总结一下就是,初始化当前表头在老表头前面一位,迭代开始后,若当前表头不指向nil,则:
-
当前表头指向老表头,当前表头、老表头向前移动;
-
当前表头指向老表头,当前表头、老表头向前移动;
-
当前表头指向老表头,当前表头、老表头向前移动;
-
当前表头指向老表头,当前表头、老表头向前移动.......
直到当前表头指向nil,证明迭代完毕,链表完全反转。
移出链表元素
删除链表中等于给定值 *val* 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
解:
func deleteNode(head *ListNode, val int) *ListNode {
p := &ListNode{}
p.Next = head
cur := p
for cur.Next != nil {
if cur.Next.Val == val {
cur.Next = cur.Next.Next
} else {
cur = cur.Next
}
}
return p.Next
}
奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:
- 应当保持奇数节点和偶数节点的相对顺序。
- 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
解:
func oddEvenList(head *ListNode) *ListNode {
/*
结点odd作为奇数链的头 结点even作为偶数链的头,此时记录偶数链的头evenHead
从第3个点开始遍历,依次轮流附在奇、偶链的后面
遍历完后,即偶链的尾为空时,奇数链的尾连向偶链的头evenHead,返回奇数链的头
*/
// 空链表情况
if head == nil {
return nil
}
odd := head // 奇数在第一位
even := head.Next // 偶数在第二位
evenHead := even // 记录偶数头
for even != nil && even.Next != nil {
odd.Next = even.Next // 奇数下一位等于偶数的下一位,即下一个奇数
odd = odd.Next // 奇数指针指到下一奇数
even.Next = odd.Next // 偶数的下一位等于奇数的下一位,即下一个偶数
even = even.Next // 偶数的指针指到下一个偶数
}
odd.Next = evenHead // 奇偶相接
return head
}
参考下图:
回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
解:
func isPalindrome(head *ListNode) bool {
/*
使用快慢指针寻找链表中点,然后反转后半部分,再分别从开头和中点处遍历比较
*/
// 空链表情况
if head == nil {
return true
}
// 1. 快慢指针寻找链表中点
slow, fast := head, head
for fast != nil && fast.Next != nil {
fast = fast.Next.Next // 快指针一次走两步
slow = slow.Next // 慢指针一次走一步
}
// 2. 从中点开始反转链表后半部分,见这一部分第一个习题 -- 反转链表
var pre, cur *ListNode = nil, slow
for cur != nil {
next := cur.Next // 先记录下下一个节点,不然一会就没了
cur.Next = pre // 当前节点指向上一个节点
pre = cur // 指针后移
cur = next
}
// 3. 分别从开头和中点处遍历比较,pre此时指向原链表的链尾,即反抓后链表的链首
mid := pre
for mid != nil {
if head.Val != mid.Val { // 比较每一个元素,但凡有一个不同,则不是回文链表
return false
}
mid = mid.Next // 指针后移
head = head.Next
}
return true
}