学习内容
学习文档:
收获总结
链表概述
链表是一种基础的数据结构,由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针(或引用)。链表的最后一个节点指向 null,表示链表的末尾。链表动态扩展性强,适合频繁插入和删除操作。
链表的定义
链表有多种类型,最常见的是单链表和双链表。
-
单链表:每个节点包含数据和指向下一个节点的指针
next。链表的头节点指向第一个节点,最后一个节点的next指向null。Golang 单链表定义:
type ListNode struct { Val int Next *ListNode } -
双链表:每个节点包含数据、指向下一个节点的指针
next和指向前一个节点的指针prev,允许双向遍历。Golang 双链表定义:
type ListNode struct { Val int Next *ListNode Prev *ListNode }
注意事项
- 循环的时候参考数组,初始条件是i,j; 链表就是cur, cur.Next
- 链表的第一个节点比较特殊,处理的时候需要特殊处理;引入一个空的头节点dummyHead,可以简化很多操作(一视同仁)
- 链表的指针就是ListNode本身,因为任何一个ListNode都可以根据Next进行移动; 双指针解法的时候每一个指针都应该是一个ListNode
链表元素的内存分布
链表节点的内存分布不连续,节点在内存中的位置是随机分配的。这使得链表可以灵活地增长或缩小,但查找元素的时间复杂度较高,因为需要从头节点开始逐一遍历。
节点的插入与删除
-
插入节点:
- 头部插入:新节点的
next指向当前头节点,并将链表头节点更新为新节点,时间复杂度为O(1)。 - 尾部插入:单链表需要遍历链表找到最后一个节点,时间复杂度为
O(n),双链表则直接访问尾节点,时间复杂度为O(1)。
- 头部插入:新节点的
-
删除节点:
- 删除头节点:将头节点更新为下一个节点,时间复杂度为
O(1)。 - 删除指定节点:需要遍历链表找到待删除节点,时间复杂度为
O(n)。
- 删除头节点:将头节点更新为下一个节点,时间复杂度为
题目解析
题目1:203.移除链表元素
-
题目描述: 给定一个链表的头节点
head和一个整数val,请删除链表中所有满足Node.val == val的节点,并返回新的头节点。 -
示例:
输入: head = [1,2,6,3,4,5,6], val = 6 输出: [1,2,3,4,5] -
解法总结: 使用虚拟头节点
dummyHead方便处理可能需要删除头节点的情况。遍历链表时,如果当前节点的下一个节点的值等于val,则跳过该节点(即将当前节点的next指向下下个节点),否则继续向下遍历。 -
代码实现:
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func removeElements(head *ListNode, val int) *ListNode { dummyHead := &ListNode{} dummyHead.Next = head // 初始为第一个元素 cur := dummyHead for cur != nil && cur.Next != nil { // 如果即将要看的下一位的值等于要删除的值 if cur.Next.Val == val { // 移除就是跳过两位 cur.Next = cur.Next.Next } else { // 跳过一位 cur = cur.Next } } // 移除dummyHead return dummyHead.Next } -
时间复杂度:
O(n),其中n是链表中的节点数。每个节点最多访问一次。 -
空间复杂度:
O(1),只使用了常量级别的额外空间。
题目2:707.设计链表
-
题目描述: 设计链表实现增删查等基本操作。需要支持在链表头、尾以及指定索引位置进行元素的添加和删除,并能够获取指定索引位置的元素值。
-
示例:
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 -
解法总结: 通过虚拟头节点
dummyHead简化在头部进行插入和删除操作的实现。链表的设计包括获取指定索引位置的值、在头部或尾部添加元素、在指定索引位置插入或删除元素。注意要处理索引越界的情况。 -
代码实现:
// type ListNode struct { // Val int // Next *ListNode // } type MyLinkedList struct { DummyHead *ListNode Size int } func Constructor() MyLinkedList { DummyHead := &ListNode{} MyLinkedList := MyLinkedList{ DummyHead: DummyHead, Size: 0, } return MyLinkedList } func (this *MyLinkedList) Get(index int) int { // 先判断index是否有效 if this == nil || index < 0 || index >= this.Size { return -1 } curNode := this.DummyHead for i := 0; i <= index; i++ { curNode = curNode.Next } return curNode.Val } func (this *MyLinkedList) AddAtHead(val int) { currHead := this.DummyHead.Next this.DummyHead.Next = &ListNode{ Val: val, Next: currHead, } this.Size++ } func (this *MyLinkedList) AddAtTail(val int) { currNode := this.DummyHead for i := 0; i < this.Size; i++ { currNode = currNode.Next } currNode.Next = &ListNode{ Val: val, Next: nil, } this.Size++ } func (this *MyLinkedList) AddAtIndex(index int, val int) { // 先判断index是否有效 if this == nil || index < 0 || index > this.Size { return } curNode := this.DummyHead for i := 0; i < index; i++ { curNode = curNode.Next } tmp := curNode.Next fmt.Println(tmp) curNode.Next = &ListNode{ Val: val, Next: tmp, } this.Size++ } func (this *MyLinkedList) DeleteAtIndex(index int) { // 先判断index是否有效 if this == nil || index < 0 || index >= this.Size { return } curNode := this.DummyHead for i := 0; i < index; i++ { curNode = curNode.Next } tmp := curNode.Next.Next fmt.Println(tmp) curNode.Next = tmp this.Size-- } /** * 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); */ -
时间复杂度:
Get操作:O(n),需要遍历链表直到指定索引位置。AddAtHead和AddAtTail操作:O(1),在链表头或尾进行插入操作。AddAtIndex和DeleteAtIndex操作:O(n),需要遍历链表找到指定位置。
-
空间复杂度:
O(1),只使用了常量级别的额外空间。
题目3:206. 反转链表
-
题目描述: 给定一个单链表的头节点
head,将链表反转并返回反转后的链表。 -
示例:
输入: head = [1,2,3,4,5] 输出: [5,4,3,2,1] -
解法总结: 使用双指针法反转链表。初始化
pre指针为nil,cur指针为链表的头节点。在遍历过程中,通过临时变量tmp保存cur.Next,将cur.Next指向pre实现链表反转,最后将pre移动到cur位置,cur移动到tmp位置,直到遍历结束。 一定要注意双指针的时候要根据下面的图解梳理清楚,先是cur后移,然后pre后移,然后cur回指到pre! -
图解:
-
代码实现:
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func reverseList(head *ListNode) *ListNode { cur := head var pre *ListNode for cur != nil { // 下次循环cur后移一位 tmp := cur.Next // cur回指到前一个 cur.Next = pre // pre后移一位 pre = cur cur = tmp } return pre } -
时间复杂度:
O(n),其中n是链表的长度。每个节点被访问一次。 -
空间复杂度:
O(1),只使用了常量级别的额外空间。