链表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)

posted @ 2018-07-09 22:21  TheAnswerer  阅读(317)  评论(2编辑  收藏  举报