1.链表的定义:
链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。
由于不必须按顺序存储,链表在插入的时候可以达到 O(1)O(1) 的复杂度,比另一种线性表 —— 顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n)O(n) 的时间,而顺序表相应的时间复杂度分别是 O(log\ n)O(log n) 和 O(1)O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(links)。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。
链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
链表通常可以衍生出循环链表,静态链表,双链表等。对于链表使用,需要注意头结点的使用。
2. 示例分析:
2.1例子1: leet-code:25. K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。示例:给你这个链表:1->2->3->4->5当 k = 2 时,应当返回: 2->1->4->3->5当 k = 3 时,应当返回: 3->2->1->4->5说明:你的算法只能使用常数的额外空间。你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路:
这个问题是链表反转的进阶题目,可以拆分成两个问题,一个是 K个节点反转一次的问题;一个是反转之前的头头节点转换指向到下一个k链表,循环直到都操作结束。
代码:
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */func reverseKGroup(head *ListNode, k int) *ListNode { if head == nil { return head } begin,end := head,head for i:=0; i<k;i++{ if end == nil { // 小于k的链表,直接返回 return head } end = end.Next } // end表示的是反转之后的头节点,其实就是没有做转换之前的k链表之后的那个节点 node := reverseList(begin,end) // 更换头节点的指向,方便下一次反转链表 begin.Next = reverseKGroup(end,k) return node}// 一个链表的反转,node表示反转之后的头节点func reverseList(head *ListNode, node *ListNode) *ListNode { if head == nil || head.Next == nil { return head } curr:= head pre := node for curr != node { tmp := curr.Next curr.Next=pre pre = curr curr= tmp } return pre}
执行结果:
2.2 例子2: leet-code:23. 合并K个排序链表
合并k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。示例:输入:[ 1->4->5, 1->3->4, 2->6]输出:1->1->2->3->4->4->5->6
解题思路:
分治+递归;
分治:两个一组,依次递减,k,k/2,k/4,k/8...1;
递归:两个列表一组,排序。
两个链表的合并:这个思路是,将小的元素指向除了这个元素之外排序好的链表即可,可以采用递归。
代码:
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */func mergeKLists(lists []*ListNode) *ListNode { if len(lists) == 0 { return nil } if len(lists) == 1 { return lists[0] }
for { // 一次性合并一半的链表 n := len(lists)/2 ok := len(lists)%2 == 1 for i:=0;i< n; i++ { lists[i]=mergeLists(lists[i],lists[i+n]) } if !ok { // 偶数的话,合并之后的链表作为新的链表数组 lists = lists[:n] } else { // 奇数的话,需要将链表最后一个元素合并过来 lists[n] = lists[len(lists)-1] lists = lists[:n+1] } if len(lists) == 1 { break } } return lists[0] // lists[0]是最终合并完成的链表}// 两个链表的递归排序操作func mergeLists(l1 *ListNode, l2 *ListNode) *ListNode { if l1 == nil { return l2 } if l2 == nil { return l1 } // 小的那个节点指向排序之后的两个链表结合之后的链表 if l1.Val < l2.Val { l1.Next = mergeLists(l1.Next, l2) return l1 } l2.Next = mergeLists(l1, l2.Next) return l2}
执行结果:
2.3 例子 leet-code:3:19. 删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。示例:给定一个链表: 1->2->3->4->5, 和 n = 2.当删除了倒数第二个节点后,链表变为 1->2->3->5.说明:给定的 n 保证是有效的。进阶:你能尝试使用一趟扫描实现吗?
解题思路:
这种问题都可以采用快慢链表的方式来解决,两个链表相差n个元素,等快的链表到达链表尾部的时候,慢的位置就是需要删除的元素。
详细:两个链表头遍历方法,header和header+n同时开始遍历,header+n到链表尾部,header正好到倒是第那个节点
代码:
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */func removeNthFromEnd(head *ListNode, n int) *ListNode { head1 := head // 需要复制,防止覆盖head head2 := head for i:=n; i>0; i-- { head2 = head2.Next } if head2 == nil { head = head.Next return head } for { if head2.Next == nil { break } head2 = head2.Next head1 = head1.Next } // 获取到 倒数第n-1位置的节点 head1.Next = head1.Next.Next return head}
执行结果:
2.4 例子4: leet-code:82. 删除排序链表中的重复元素 II
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。示例 1:输入: 1->2->3->3->4->4->5输出: 1->2->5示例 2:输入: 1->1->1->2->3输出: 2->3
解题思路:
这里的关键点在于如何判定重复的节点,以及首节点就开始重复的情况。
先说首节点开始重复的问题,这个需要用到头节点之前添加钩子节点的方法来解决。
重复节点的判断,循环遍历后续节点,直到出现不重复的节点位置,直接把重复节点之前的pre的下一个节点指向这个不同的节点即可。
代码:
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */func deleteDuplicates(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } n := new(ListNode) // 设置钩子节点,避免头节点都被删除不好操作 n.Next = head pre := n for pre != nil { // pre是当前节点的前一个节点 if pre.Next == nil { break } curr := pre.Next next := curr.Next for next != nil { // 比较重复的节点,将next最终指向补充的那个节点 if next.Val != curr.Val { break } next = next.Next } if curr.Next == next { // 这里不是重复的节点,移动pre的节点位置 pre = pre.Next } else { // 重复的,删除重复节点 pre.Next = next } } return n.Next}
执行结果:
2.5 例子5:leet-code:160. 相交链表
编写一个程序,找到两个单链表相交的起始节点。如下面的两个链表:在节点 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) 内存。
解题思路:
解法:双指针的办法操作,同时遍历链表A和B,
A到末尾之后,指向B;B到末尾之后指向A。
等到A== B的话,就是交点。
代码:
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */func getIntersectionNode(headA, headB *ListNode) *ListNode { if headA == nil || headB == nil { return nil } A := headA B := headB for headA != headB { if headA != nil { headA = headA.Next } else { headA = B } if headB != nil { headB = headB.Next } else { headB = A } } return headA}
执行结果:
2.6 例子6:141. 环形链表
给定一个链表,判断链表中是否有环。为了表示给定链表中的环,我们使用整数 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 || head.Next == nil { return false } slow := head fast := head.Next for slow != fast { if slow == nil || fast == nil { return false } slow = slow.Next fast = fast.Next if fast != nil { fast = fast.Next } } return true}
执行结果: