zoukankan      html  css  js  c++  java
  • 链表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)

  • 相关阅读:
    HDOJ_ACM_超级楼梯
    HDOJ_ACM_下沙的沙子有几粒?
    杭电_ACM_How Many Trees
    HDOJ_ACM_Tiling_easy version
    HDOJ_ACM_母牛的故事
    HDOJ_ACM_小兔的棋盘
    HDOJ_ACM_一只小蜜蜂
    杭电_ACM_Count the Trees
    HDOJ_ACM_折线分割平面
    Sun Solaris下JAVA以及JSP开发环境的配制小记
  • 原文地址:https://www.cnblogs.com/10zhang/p/9286433.html
Copyright © 2011-2022 走看看