推荐先看看大话数据结构这本书,一点都不枯燥,写的很好.
牛客网算法直通车总结,感谢.
链表
链表基本知识
- 线性结构, 不是连续的储存空间
- 链表空间不能保证连续有序, 为临时分配的
- 无法使用下标来访问元素, 只能从头结点, 一个一个往下遍历
链表的分类:
- 按照方向分类
- 单链表
- 双链表
- 按照无环分类
- 普通链表
- 循环链表
代码实现的关键点:
- 链表调整函数的返回值类型, 根据要求往往是结点类型, 调整链表可能换了头结点
- 处理链表问题, 辅佐画图保证逻辑清晰, 环境需要先保存下来, 防止断线的情况
- 边界条件要讨论严格
插入和删除的情况:
- 注意链表为空, 或者长度为 1 的情况
- 注意插入的操作的调整过程
- 找到插入位置的前元素和后元素
- 前节点的 next 指针指向插入的元素
- 插入元素的 next 指针指向后元素
- 注意删除节点的调整过程
- 单链表中: 找到删除节点的前元素
- 前节点的 next 指针指向删除节点的后继节点
- 在头和尾插入和删除的情况下需要考虑空节点
- 双链表的插入和删除和单链表类似, 但是要考虑 previous 指针的指向
链表翻转的注意事项:
- 当链表为空或者长度为 1 的特殊情况
- 单链表的翻转操作:
head->now->...->null
- now 的 next 指针指向 head
- 将 now 设置为翻转部分的新的头部
- 在步骤 1 发生之前, 先记录下 now 节点的下一个节点是什么, 然后载进行步骤 1 和步骤 2, 前两个步骤进行完后, 在根据刚才记录 now 节点的下一个节点继续向下处理
使用递归
- 时间复杂度:
O(n)
,假设n
是列表的长度,那么时间复杂度为O(n)
。 - 空间复杂度:
O(n)
,由于使用递归,将会使用隐式栈空间。递归深度可能会达到n
层。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func reverseList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
next := head.Next
head.Next = nil
newHead := reverseList(next)
next.Next = head
return newHead
}
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode next = head.next;
head.next = null;
ListNode newHead = reverseList(next);
next.next = head;
return newHead;
}
public ListNode reverseList2(ListNode head) {
if (head == null || head.next == null) return head;
ListNode p = reverseList(head.next);
head.next.next = head;
head.next = null;
return p;
}
使用迭代:
- 时间复杂度
O(n)
- 空间复杂符
O(1)
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr!=nil {
tmp := curr.Next
curr.Next = prev
prev = curr
curr = tmp
}
return prev
}
大量链表问题可以使用额外的数据结构来简化处理, 但是最有解往往不需要额外的数据结构.
例题
一. 给定一个整数 num, 如何在节点值有序的环形链表中插住一个节点值为 num 的节点, 然后保证这个环形链表依然后续?
- 生成 node, 值为 num
- 如果链表为空, 则让 node 自己成为环形链表
- 如果链表不为空
- 让变量 p 头结点为 head 结点
- 变量 c 为当前结点
- 让 p 和 c 同步移动下去
- 如果遇到
p.val<=node.val
并且c.var>=node.val
插入在 p 和 c 之间 - 插入 node, 后返回 head
- 如果转了一圈都没有发现该插入的位置, 此时 node 应该插入头结点的前面
- 如果 node 的值比链表中每一个值都大, 返回原来的 head
- 如果比每一个值都小, 则返回 node 作为 head, 因为从 node 出发的循环链表才有序
二. 给定一个链表中的节点 node, 但是不给定整个链表的头结点, 怎么在链表中删除 node? 代码实现, 并且时间复杂度为 O(1)
.
- 双链表好实现, 找到 pre 找到 next, 然后链接就行
- 单链表中, 要删除节点 2, 但是不知道 head, 只需要把 2 的值变成 node3 的值, 然后删除节点 3 就行了
- 但是当要删除的是最后一个元素的时候, 没有办法删除最后一个节点, 因为需要把前面节点的 next 指针指向 null, 但是没有办法获得前面的元素
- 也不能把最后节点设置为 null, 因为 null 在内存中有特殊的含义, 必须找到前一个元素 (这不是删除节点, 而是进行了值得拷贝)
- 总之就是, 这个节点在结构复杂的时候开销很大, 在工程上会影响外部的依赖
实现一种算法,删除单向链表中间的某个节点(即不是第一个或最后一个节点),假定你只能访问该节点。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func deleteNode(node *ListNode) {
toDeleteNode := node.Next
node.Val = toDeleteNode.Val
node.Next = toDeleteNode.Next
}