链表Linked List
链表问题
在链表问题中,最常见的方法就是“双指针”,“快慢指针”。
最常用的技巧就是加“fakehead”
删除系列
删除链表中的节点 203题(easy):
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
分析:
最基础的删除操作,用到fakehead,定义一个指针一趟遍历,判断指针的下一位是否是目标val。
代码:
public ListNode removeElements(ListNode head, int val) {
ListNode i=new ListNode(0);
i.next=head;
ListNode s=i;
while (i.next!=null){
if (i.next.val==val){
i.next=i.next.next;
}
else i=i.next;
}
return s.next;
}
删除链表中的节点 237题(easy):
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
示例:
输入: head = [4,5,1,9], node = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
分析:
和203题的区别在于只给了我们这个要被删除的节点,不能像之前一样通过从头节点遍历定位到要被删除的前一节点。所以我们只能操作当前这个节点和他的下一节点,而操作不了他的前一个节点。
思路是把下一节点的值覆盖到当前要被删除的节点上,然后不删除当前节点而是删除下一节点。这样删除的效果的一样的。
代码:
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
删除链表的倒数第N个节点 19题:
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:给定的 n 保证是有效的。
进阶:你能尝试使用一趟扫描实现吗?
分析:
用双指针的方法做;两指针从头节点开始,一个快指针先走n步,然后两个指针同时走,等到快指针走到尾节点时,这时的慢支针就指在倒数第n个节点。注意这里的边界,多试试。
代码:
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head==null) return null;
ListNode slow=new ListNode(-1);
ListNode fast=new ListNode(-1);
slow.next=head;
fast.next=head;
ListNode res=slow;
for (int i=0;i<n;i++){//快指针先走n步
fast=fast.next;
}
while (fast.next!=null){
fast=fast.next;
slow=slow.next;
}
slow.next=slow.next.next;
return res.next;
}
删除排序链表中的重复元素 83题(easy):
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例:
输入: 1->1->2
输出: 1->2
输入: 1->1->2->3->3
输出: 1->2->3
分析:
一趟扫描就可以;i挪向下一节点前,看当前节点和它的下一个节点是不是相等;如果相等执行i.next=i.next.next
代码:
public ListNode deleteDuplicates(ListNode head) {
if (head==null) return head;
ListNode i=head;
while (i.next!=null){
if (i.next.val==i.val){
i.next=i.next.next;
}
else i=i.next;
}
return head;
}
删除排序链表中的重复元素II 82题:
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
示例:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
输入: 1->1->1->2->3
输出: 2->3
分析:
(1)这里用到了双指针的方法。一个pre,一个cur
(2)这里的头节点可能被删去,所以操作时在头节点前加一个“假头节点”是必要的。
(3)和I的区别在于只保留没有重复出现的数字,我们要把重复的数字都删掉,因为链表的单向性,要保存一个前驱节点pre。这样才能保证跳过所有重复的数字。
代码:
public ListNode deleteDuplicates(ListNode head) {
if(head==null) return null;
ListNode FakeHead=new ListNode(0);
FakeHead.next=head;
ListNode pre=FakeHead;
ListNode cur=head;
while(cur!=null){
while(cur.next!=null&&cur.val==cur.next.val){
cur=cur.next; //有重复cur挪动
}
if(pre.next==cur){//成立代表cur没有多挪动跳过重复元素
pre=pre.next; //pre挪动一个
}
else{
pre.next=cur.next;//否则连接节点,删掉重复元素
}
cur=cur.next;//迭代扫描中cur每次都挪一个
}
return FakeHead.next;
}
交换旋转反转系列
两两交换链表中的节点 24题:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
*你的算法只能使用常数的额外空间。
*你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
分析:
按要求操作就行 注意加fakehead
代码:
public ListNode swapPairs(ListNode head) {
ListNode header=new ListNode(-1);
header.next=head;
ListNode l=header;
while (l.next!=null&&l.next.next!=null){//判断条件注意
ListNode temp1=l.next.next.next;
ListNode temp2=l.next;
l.next=temp2.next;
l.next.next=temp2;
temp2.next=temp1;
l=l.next.next;
}
return header.next;
}
旋转链表 61题:
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
示例:
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
分析:
1.首先k可能大于节点个数,即k>len,此时旋转的位置是k%len个
2.关键找到旋转后的尾节点tail是哪个节点。观察后发现应该是从头节点数len-k%len个节点
3.最后一步,将原来链表首尾相接成一个环,将旋转后的尾节点tail的后一节点置为NULL
用到双指针(一个确定len,一个确定tail)和fakehead
代码:
public ListNode rotateRight(ListNode head, int n) {
if (head==null||head.next==null) return head;
ListNode dummy=new ListNode(0);
dummy.next=head;
ListNode fast=dummy,slow=dummy;
int i;
for (i=0;fast.next!=null;i++)//得到总长度i
fast=fast.next;
for (int j=i-n%i;j>0;j--) //得到第i-k%i个节点
slow=slow.next;
fast.next=dummy.next; //做旋转
dummy.next=slow.next;
slow.next=null;
return dummy.next;
}
反转链表 206题(easy):
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
分析:
迭代法一趟遍历,保存当前节点curr的前驱prev和后继nextTemp。
递归法思路一致,传参为nextTemp和prev,递归基是curr==null时,返回它的prev
代码:
public ListNode reverseList(ListNode head) {
/* 迭代法 */
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
public ListNode reverseList(ListNode head) {
/* 递归法*/
return reverseListInt(head, null);
}
private ListNode reverseListInt(ListNode curr, ListNode prev) {
if (curr == null)
return prev;
ListNode nextTemp = curr.next;
curr.next = prev;
return reverseListInt(nextTemp, prev);
}
反转链表 II 92题:
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:
1 ≤ m ≤ n ≤ 链表长度。
示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
分析:
思路和I的类似,但是这题是反转m到n的一段,而且要一趟扫描,所以要分段(0~m-1, m, m+1~n)不同处理,额外的变量也是必须的。
下面解法中,m到n这段中定义的subhead实际相当于I中的prev的作用;而这里的pre_cur只是为了将它最后定位在m-1位置上。扫描结束 将pre_cur,subtail,subhead,cur按pre_cur->subhead,
subtail->cur接起来.
代码:
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummyhead = new ListNode(0);
dummyhead.next = head;
ListNode sublisthead = new ListNode(0);
ListNode sublisttail = new ListNode(0);
int count = 1;
ListNode pre_cur = dummyhead, cur = head;
while(count <=n){
ListNode temp = cur.next;
if (count < m) //这段实际不进行操作,只是保存每次cur的前后两个节点
pre_cur = cur;
else if (count == m){ //在m这点上定义 subtail和subhead的指向
sublisttail = cur;
sublisthead.next = cur;
}else if (count > m){。 //在m+1~n这段上,方法就同I了,subhead就是I中的prev
cur.next = sublisthead.next;
sublisthead.next = cur;
}
cur = temp; //cur转向下一节点
++count; //计数器
}
//连接
pre_cur.next = sublisthead.next;
sublisttail.next = cur;
return dummyhead.next;
}
重排链表 143题:
给定一个单链表 L:L0→L1→…→Ln-1→Ln ,
将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
给定链表 1->2->3->4, 重新排列为 1->4->2->3.
给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
分析:
这题用到了反转链表I,可以分为三个步骤来操作:
1.利用快慢指针定位到中间节点(长度为偶数时靠左的中间节点)
2.将后一半reverse
3.将后一半依次交替插入
代码:
public void reorderList(ListNode head) {
if(head==null||head.next==null) return;
//Find the middle of the list
ListNode p1=head;
ListNode p2=head;
while(p2.next!=null&&p2.next.next!=null){
p1=p1.next;
p2=p2.next.next;
}
//Reverse the half after middle 1->2->3->4->5->6 to 1->2->3->6->5->4 Reverse Linked List I
ListNode preMiddle=p1;
ListNode preCurrent=p1;
ListNode current=p1.next;
while (current!=null){
ListNode temp=current.next;
current.next=preCurrent;
preCurrent=current;
current=temp;
}
preMiddle.next.next=null;
preMiddle.next=preCurrent;
//Start reorder one by one 1->2->3->6->5->4 to 1->6->2->5->3->4
p1=head;
p2=preMiddle.next;
while(p1!=preMiddle){
preMiddle.next=p2.next;
p2.next=p1.next;
p1.next=p2;
p1=p2.next;
p2=preMiddle.next;
}
}
判断回文链表 234题:
请判断一个链表是否为回文链表。
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
示例:
输入: 1->2
输出: false
输入: 1->2->2->1
输出: true
分析:
可以用到206题的反转链表来把原链表的后一半反转,然后依次比对。
注意要考虑节点长度为奇数和偶数的两种情况。
代码:
public boolean isPalindrome(ListNode head){
ListNode fast=head,slow=head;
//快慢指针用法,将slow定位到中间的节点上
while (fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
if (fast!=null){ //fast!=null说明节点总长为奇数
slow=slow.next;
}
slow=reverseList(slow);
fast=head;
while (slow!=null){
if (fast.val!=slow.val){
return false;
}
fast=fast.next;
slow=slow.next;
}
return true;
}
public ListNode reverseList(ListNode head) { //206题的reverseList函数
ListNode pre=null;
while (head!=null){
ListNode next=head.next;
head.next=pre;
pre=head;
head=next;
}
return pre;
}
循环问题
环形链表 141题(easy):
给定一个链表,判断链表中是否有环。
你能否不使用额外空间解决此题?
分析:
这类判断循环的方法就是“快慢指针”,快指针每次两步,慢指针每次一步。如果有循环的话,快慢指针最后一定会汇合。
代码:
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
环形链表II 142题:
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
说明:不允许修改给定的链表。
进阶:你是否可以不用额外空间解决此题?
分析:
这题首先像判断循环一样用快慢指针定位到汇合点,然后就是数学问题了...
假设:
1.L1定义为头点和入口点之间的距离
2.L2定义为入口点和会合点之间的距离
3.C定义为循环的长度
4.n被定义为快速指针绕循环的圈数
则有:
2*(L1+L2)=L1+L2+n*C => L1+L2=n*C =>L1=(n-1)*C+(C-L2)
那么一个指针从头节点,一个指针从汇合点沿着正向每次都只走一步,两指针最后的汇合点就是循环入口点。
代码:
public ListNode detectCycle(ListNode head) {
if (head==null||head.next==null) return null;
ListNode entry=head;
ListNode slow=head;
ListNode fast=head;
while (fast.next!=null&&fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
if (slow==fast){
while (entry!=slow){
entry=entry.next;
slow=slow.next;
}
return slow;
}
}
return null;//没有循环
}
其他问题
分割链表 86题:
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
你应当保留两个分区中每个节点的初始相对位置。
示例:
输入: head = 1->4->3->2->5->2, x = 3
输出: 1->2->2->4->3->5
分析:
基本思想是维护两个队列,第一个队列存储val小于x的所有节点,第二个队列存储所有其余节点。 然后连接这两个队列。 请记住将第二个队列的尾部设置为null,否则你将获得Time Limit Exceeded,即有循环
For this list: 5->6->1->2, x=3, at last cur2 points to 6, cur1 points to 2, we must set 6->1 to 6->null, otherwise there will be a cycle.
代码:
public ListNode partition(ListNode head, int x) {
ListNode d1=new ListNode(-1);
ListNode d2=new ListNode(-1);
ListNode cur1=d1,cur2=d2;
while (head!=null){
if (head.val<x){
cur1.next=head;
cur1=head;
}else {
cur2.next=head;
cur2=head;
}
head=head.next;
}
cur2.next=null; //很重要
cur1.next=d2.next;
return d1.next;
}
相交链表 160题:
编写一个程序,找到两个单链表相交的起始节点。
例如,下面的两个链表:
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
在节点 c1 开始相交。
注意:
1.如果两个链表没有交点,返回 null.
2.在返回结果后,两个链表仍须保持原有的结构。
3.可假定整个链表结构中没有循环。
4.程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存
分析:
这题两个链表的长度可能不一样,所以要先让两个链表各自指针指在“对齐”的位置,然后两指针每次都走一步判断是否相等就能找到相交点。
discuss里有个巧妙的方法,代码如下,记住吧...
代码:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//boundary check
if(headA == null || headB == null) return null;
ListNode a = headA;
ListNode b = headB;
//if a & b have different len, then we will stop the loop after second iteration
while( a != b){
//for the end of first iteration, we just reset the pointer to the head of another linkedlist
a = a == null? headB : a.next;
b = b == null? headA : b.next;
}
return a;
}
合并两个有序链表 21题:
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
分析:
类似于二路归并的思路。
注意这题是通过拼接的方式,不能重新再建一条链表把对应的值存里。
代码:
//迭代版本
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode fakehead=new ListNode(-1);
ListNode tail=fakehead;
while (l1!=null&&l2!=null){
if (l1.val<l2.val){
tail.next=l1;
l1=l1.next;
}else {
tail.next=l2;
l2=l2.next;
}
tail=tail.next;
}
tail.next=l1==null?l2:l1;
return fakehead.next;
}
//递归版本
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null){
return l2;
}
if(l2 == null){
return l1;
}
ListNode mergeHead;
if(l1.val < l2.val){
mergeHead = l1;
mergeHead.next = mergeTwoLists(l1.next, l2);
}
else{
mergeHead = l2;
mergeHead.next = mergeTwoLists(l1, l2.next);
}
return mergeHead;
}
复制随机指针的链表 138题:
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的深度拷贝。
//定义的随机指针节点
class RandomListNode {
int label;
RandomListNode next, random;
RandomListNode(int x) { this.label = x; }
}
分析:
这题和克隆图那道类似,算是那题的一个简化版。使用克隆图那题的BFS方法可以做,但是复杂没必要。这题的一个高票答案只用了三趟扫描,常数空间复杂度。三趟扫描思路如下:
1.在next指针相连的两个节点之间插入一个前一节点的拷贝
如1->2->3->null 插入后为 1->1->2->2->3->3->null
2.这次扫描确定拷贝的节点的random指针指向;因为拷贝的节点就在原节点的后面,所以能够直接定位操作到。
3.再把拷贝的节点从原来的链表中取出来,用next连接起来。
代码:
public RandomListNode copyRandomList(RandomListNode head) {
RandomListNode iter = head, next;
// 第一轮:做好每一个节点的拷贝
// 插入到原连表中
while (iter != null) {
next = iter.next;
RandomListNode copy = new RandomListNode(iter.label);
iter.next = copy;
copy.next = next;
iter = next;
}
// 第二轮:标记拷贝节点的random指针指向
iter = head;
while (iter != null) {
if (iter.random != null) {
iter.next.random = iter.random.next;
}
iter = iter.next.next;
}
// 第三轮:恢复原链表,把拷贝链表中原链表中取出来。
iter = head;
RandomListNode pseudoHead = new RandomListNode(0);
RandomListNode copy, copyIter = pseudoHead;
while (iter != null) {
next = iter.next.next;
// 取出拷贝
copy = iter.next;
copyIter.next = copy;
copyIter = copy;
// 恢复原链表
iter.next = next;
iter = next;
}
return pseudoHead.next;
}
遗留的题目:
109 有序链表转换二叉搜索树(DFS)
147 对链表进行插入排序(sort)
148 排序链表(sort)