链表练习题

这段时间将会用这版面来记录一些链表的题目,脚踏实地去理解链表和使用链表。加油。(也许不是最优解,但是一定是符合题目要求的而且AC的)

1.移除链表元素

leetcode203题目:删除链表中等于给定值 val 的所有节点。

这是开始写的第一道,准备用3种方式来完成这道题目,记录自己的思路。

1.一般使用head节点完成

思路:既然是链表的删除元素,也只有next才能访问到其他元素,所以从head开始判断就好。

需要分别对head 和head.next进行判断.

其实就是遍历链表:时间复杂度O(N)

public ListNode removeElements(ListNode head, int val) {
         //如果头节点的val就是要找的val,
         while(head!=null && head.val ==val ){
             head = head.next;
         }
         //链表的val全是val
         if(head ==null){
             return head;
         }
         //删除head.next位置
         ListNode prev = head;
         while(prev.next!=null){
             if(prev.next.val == val){
                 prev.next = prev.next.next;
             }else{
                 prev = prev.next;
             }
         }
         return head;     
}

2.使用虚拟头结点

虚拟头结点的好处就在于可以不需要单独考虑head的情况,把head的情况也考虑在next里面去就好了。

时间复杂度O(N)

         //使用虚拟头节点的方法:不需要单独判断头结点的情况了。
         ListNode dummyHead = new ListNode(-1);
         dummyHead.next = head;
         //后面的和用head的方式一摸一样,但是要注意引用关系
         ListNode prev = dummyHead;
         while(prev.next!=null){
             if(prev.next.val == val){
                 prev.next = prev.next.next;
             }else{
                 prev = prev.next;
             }
         }
         return dummyHead.next;

3.递归的方式

由于递归具有天然的递归结构。如果不把链表看出一个一个节点,而是看成一个节点后面跟着比这个链表小一个规模的链表。null也可以当成最小规模的链表,这样就是一个递归的思想了。

所以对于这道移除严元素的题目来说,最终得到的是移除元素后的链表。

递归的关注点主要有2个。

1.最小规模的子问题的情况(即递归到底):最小规模子节点就是一个Null。 if(head == null) return head;

2.子问题怎么组合成比他大一个规模的问题:对于这个题目来说,当后面的链表已经是删除元素后的链表了(假设子链表头结点为res)。

与已知头结点的关系其实就是  如果head.val ==val,那头结点接上子链表就是所求解。head.next = res; 否则直接返回子链表就好了。 return res;

接下来看代码,思路理解后代码还是挺简单的。

递归的次数走遍整个链表,时间复杂度为O(N),比较相等为O(1),所以整体也是O(N)。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
     public ListNode removeElements(ListNode head, int val) {
         //使用递归的方式
         //最小规模的情况
         if(head == null) {
             return head;
         }
         //子问题组合成原问题的过程
         ListNode res=  removeElements(head.next,val);
         if(head.val ==val) {
             return res;
         }else {
             head.next = res;
         }
         return head;
     }
    
}

 

2.反转一个单链表

leetcode206:反转一个单链表。

思路:单链表的反转其实比较好理解的。主要是要理解链表反转时放在断链的问题。

1.非递归的方式

非递归的方式就要想好如果防止断链。这里采用3指针来标识前置结点,当前结点,下一个节点。然后进行判断

具体非递归的方式在剑指offer分类里面写的挺清楚的。剑指Offr:反转链表    跳过去看看就好。这里重点分析一下递归的方式。

2.递归的方式

递归的方式还是从宏观上关注问题。

1.最小规模的问题的结果:当最小链表是null或者只有1个节点的时候,直接返回head就可以了。不需要反转,if(head == null) return head;

2.关注子问题的解怎么组合成前一个规模的问题的解。

其实比较好理解,因为子链表已经是反转完成的了。所以要解决的就是子链表怎么指向头结点,然后头结点指向null的问题.

head.next.next =head;//head的下一个元素是子链表的最后一个元素。所以子链表最后一个元素.next=head就反转成功了。

head =null;//将head的next也改变方向。最后会指向null

class Solution {
    public ListNode reverseList(ListNode head) {
        if(head==null||head.next ==null)
            return head;
//要注意问题的解是给出反转后链表的头结点,后面的处理是解决head和子链表的关系 ListNode res
= reverseList(head.next); head.next.next = head; head.next = null; return res; } }

递归的思路其实都差不多。关键是从宏观理解问题.

3.删除排序链表中的重复元素

leetcode83:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

1.非递归

链表非递归的方式要关注头结点和next节点的关系,结合题目分情况讨论

这个题目的是要删除重复元素的,所以一个节点或者节点为空都可以直接返回。

而在多个元素的情况,抓住链表的是排序的。。所以其实当前节点只需要和下一个节点比较就好 了。重复的越过他就好了。(leetCode解题就不将无用的节点.next = null了,)

代码比较简单,就不注释了。看上面介绍结合代码就好了。

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if(head ==null || head.next == null){
            return head;
        }
        ListNode prev = head;
        while(prev.next!=null){
            if(prev.val == prev.next.val){
                prev.next = prev.next.next;
            }else{
                prev = prev.next;
            }
        }
        return head;
    }
}

2.递归的方式

老规矩思考,递归要从宏观去理解题目

1.问题的最小规模:当只有一个节点或者节点为空的时候,直接返回head就好

2.子问题的解怎么组合成原问题的解。问题的解是一个没有重复元素的链表

这里的思考重点其实和非递归一样,抓住已排序。

而子链表是全不重复的,所以只需要和子链表的头节点比较就好了。相同则越过,不相同则接上。这样就组合成原问题的解了

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
//最小规模问题的解
if(head ==null || head.next == null){ return head; }
//要注意问题的解的意思。可以和上一道题目进行比较一下思考。 ListNode res
= deleteDuplicates(head.next); ListNode prev =head; if(prev.val == res.val){ prev.next=prev.next.next; }else{ head.next =res; } return head; } }

 4.排序链表

leetcode148:  在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。

思路:这道题目的重点就在于时间复杂度和空间复杂度都有要求,O(n log n)的算法有归并排序和快排。而由于一般都是练习的数组的排序算法的思维影响,归并和快排都是不能达到O(1)额外空间的。

但是对于链表来说,其实并不需要多余的空间来存储链表的节点。这是比较重要的。而且链表的merge的一端为Null以后,后面的就直接接上就可以了。

这道题目我选用归并来做。因为归并的3个要点:

1.选取中点位置。找到链表的中点位置也是一道题目。如果用遍历的方式,时间复杂度就是O(n+n/2)了.但是用快慢指针的方式(fast-slow)就可以用O(logn)的时间复杂度完成。

2.递归调用:还是老思路,递归一定要从宏观理解问题,明确终止条件和子问题的解是如何组成原问题的解的。

结合链表的结构和归并排序的要点。终止条件是只有一个元素的时候,而子问题组成原问题其实就是merge的过程。

3.merge:链表的归并过程和数组的归并并不一样。不需要O(n)空间。定义一个指针指向第一个结点,2个子链表的头结点比较大小向后移动,而定义的指针一直指向小的节点,最终2个链表都走完得出结果。

具体看代码:

/*
归并排序的3个要点:
1.划分中点:这里用快慢指针法来确定中点。快慢指针法的理解可以百度一下。(这里需要多一个节点记录中点的前一个节点,方便链表断开)
2.递归调用方法:理解递归子问题的返回值和原问题的意义一样,是已排序好的链表的头结点。
3.merge:归并操作就是将子问题组合成原问题的方法。可以理解一下。

*/
class Solution {
    public ListNode sortList(ListNode head) {
        if(head ==null){
            return head;
        }
        return mergeSort(head);
    }
    private ListNode mergeSort(ListNode head){
        //递归的终止条件
        if(head.next== null){
            return head;
        }
        //利用快慢指针法找到中点 slow走1步,fast走2步,当fast走到终点slow刚好到达终点,以这个为中点,递归调用。
        ListNode slow = head;
        ListNode fast = head;
        ListNode prev =null;
        while(fast!=null && fast.next!=null){//注意这个判断语句,fast!=null在前,避免出现空指针的情况
            prev = slow;
            fast =fast.next.next;
            slow =slow.next;
        }
        //将2个节点断开
        prev.next = null;
        ListNode res1 = mergeSort(head);
        ListNode res2 = mergeSort(slow);
        return merge(res1,res2);
    }
    private ListNode merge(ListNode res1,ListNode res2){
        //用虚拟头结点的方式来简化对于头结点的判断
        ListNode dummyHead = new ListNode(-1);
        ListNode prev = dummyHead;
        
        while(res1 !=null && res2 !=null){
             if(res1.val >=res2.val){
                prev.next = res2;
                res2 = res2.next;
                prev =prev.next;
            }else{
                prev.next = res1;
                res1 = res1.next;
                prev =prev.next;
            }
        }
        if(res1==null){
            prev.next =res2;
        }
        if(res2 ==null){
            prev.next =res1;   
        }
        return dummyHead.next;
    }
    
}

这里如果懂归并排序的话,应该不难理解。我在merge的过程使用了虚拟的头结点,这样不需要对链表头结点进行单独的判断,很实用的操作。

注:题目还是有难度的。做了一定量的题目(找链表中点,快慢指针理解)和对排序算法的理解更深(基于原归并排序的改写)以后,还是有点用的。

对于一些提示就能很快有理解写出点东西,对很多基本的算法和数据结构都能比较快的写出来。感觉这才是最大的进步。不一定记得住全部,但是最根本的常用的记住了。以后看别人代码或者自己去写代码思路都会比较清晰,加油!!

5.判断链表是否有环

Leetcode141 :给定一个链表,判断链表中是否有环。

快慢指针法又用上了。如果链表有环的话,快指针就一定会追上慢指针的

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head ==null){
            return false;
        }
        if(head.next ==null){
            return false;
        }
        ListNode slow =head;
        ListNode fast =head.next;
        while(fast!=null && fast.next !=null){
            //分2种情况的追上,当快慢指针都走一次,快指针在慢指针的前面,一个是快指针刚好跳到慢指针的位置,
            if(fast == slow || fast.next ==slow){
                return true;
            }
            fast =fast.next.next;
            slow = slow.next;
        }
        return false;
    }
    
}

6.两两交换链表的节点

leetcode24 :给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 注意:

  • 你的算法只能使用常数的额外空间。
  • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

思路:两两交换其实和反转链表的思路是很相似的,但是要注意反转后链表的指向的变化情况。

所以一样是定义3个指针prev,cur,pNext来表示节点之间的关系。

由于是两两的交换,所以我定义一个变量来控制是否需要交换。

遍历链表是O(N),交换是O(1),整体是O(N)的时间复杂度。

看代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        //一样是虚拟头节点。
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        //至少2个才能交换撒
        if(head == null || head.next ==null){
            return head;
        }
        //控制是否需要交换的变量
        int count= 0;
        //3个指针标识节点关系
        ListNode cur =dummyHead.next;
        ListNode pNext = dummyHead.next.next;
        ListNode prev = dummyHead;
        //遍历链表就好
        while(pNext!=null){
            if(count%2==0){
                //节点反转
                prev.next = cur.next;
                cur.next = pNext.next;
                pNext.next =cur;
                //节点反转完成后记得把指针也要交换
                ListNode temp =cur;
                cur=pNext;
                pNext=temp;
                //反转完成后整体指针向前移动
                prev = prev.next;
                cur=cur.next;
                pNext=pNext.next;
                //下一轮就不用交换了
                count++;
            }else{
                //不需要交换,指针往后走就可以了。
                prev = prev.next;
                cur=cur.next;
                pNext=pNext.next;
                count++;
            }
        }
        //注意是虚拟头结点
        return dummyHead.next;
    }
}

 

posted @ 2019-01-31 02:09  发包哥哥  阅读(735)  评论(0编辑  收藏  举报