Leetcode链表题思路

链表

链表类的题有有很多相似的题目,主要可以使用递归和迭代两种方法。同时,在做题的时候也有一些小技巧,这里暂时列出几项新。

  1. 递归求解;
  2. 使用虚拟头结点求解;
  3. 创建一个临时结点,其next指向需要保存的结点,就像交换函数中的temp一个道理;
  4. 使用快慢指针,让快指针先走部分距离,之后快慢指针一起走,快指针到结尾时慢指针正好离尾指针相同距离,可以解决一轮访问单向链表的倒数n个结点的问题。
  5. 如果需要对链表表尾对其,可以使用栈实现。

(持续更新)


下面是题解。


1. 找出两个链表的交点。

链接:找出两个链表的交点(简单)
在这里插入图片描述
解答:

两个链表虽然在分开处不等长,但相交后等长。假设链表A相交前的长度为a,链表B相交前的长度为b,相交后的长度为c,那么链表A的总长度为a+c,链表B的总长度为b+c。

此时做如下操作:

  • 让访问链表A的指针ptr1在访问到尾部的时候继续访问链表B的头部;
  • 让访问链表B的指针ptr2在访问到尾部的时候继续访问链表A的头部。

那么当ptr1访问到相交点C时,ptr1访问长度为a+c+b;同理ptr2访问交点时访问长度为b+c+a,因此ptr1和ptr2的访问长度是相等的。那么,使用一个while循环每次让ptr1和ptr2访问一个节点,最后两个指针重合的时候(即ptr1==ptr2),重合的节点一定是相交点。
如果不存在重合,那么当两个指针都访问完两个链表后,都会指向Null,也会相等。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    ListNode ptr1 = headA;
    ListNode ptr2 = headB;
    
    while(ptr1 != ptr2){
        ptr1 = (ptr1!=null) ? ptr1.next : headB;
        ptr2 = (ptr2!=null) ? ptr2.next : headA;
    }
    return ptr1;
}

2. 反转链表

链接:反转链表(简单)
在这里插入图片描述
这道题有两种解答方式,递归和迭代。

递归

递归将子任务交给递归方程完成,本任务只需要衔接子任务的输出和初始的输入的结合,即可保证结果正确。这里子递归输入next节点,输出已反转的子链表,本任务实现head与递归输出的逆转即可。

public ListNode reverseList(ListNode head) {
    if(head == null || head.next == null)    return head;
    ListNode revList = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return revList;
}
迭代
  • 使用一个cur指针指向当前节点,由于需要反转链表,所以需要记录前一个节点pre(即反转后的链表头节点)。
  • 当前节点需要反转方向时,为了不丢失原本的下一个节点,需要用一个next指针事先保存。
  • 当前节点的指向反转后,这个节点就成了反转链表的头节点,此时pre节点和cur都往后移一位即可,先移pre后移cur,防止当前节点地址丢失。
public ListNode reverseList(ListNode head) {
    ListNode pre = null;
    ListNode cur = head;
    while(cur!=null){
        ListNode next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

3. 归并两个有序的链表

链接:归并两个有序的链表(简单)
在这里插入图片描述

递归

比较两个头结点大小,选择小结点的next与大结点的链表做递归的归并,再做一个小结点next指向递归结果即可。

public ListNode mergeTwoLists(ListNode l1, ListNode l2){
    if(l1==null) return l2;
    if(l2==null) return l1;
    if(l1.val<l2.val){
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else{
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
}
迭代
  • 使用一个虚拟头结点dummyHead作为归并的头结点,使用一个指针ptr3表示尾指针。
  • 每次让尾指针next接到链表1和链表2访问指针ptr1、ptr2较小的值,再移动较小值的指针即可。
  • 最后其中一个链表访问完,剩下的直接接上即可。
public ListNode mergeTwoLists(ListNode l1, ListNode l2){
    ListNode ptr1 = l1, ptr2 = l2;
    ListNode ptr3 = new ListNode(-1), dummyHead = ptr3;
    while(ptr1!=null && ptr2!=null){
        if(ptr1.val<ptr2.val){
            ptr3.next = ptr1;
            ptr1 = ptr1.next;
        } else{
            ptr3.next = ptr2;
            ptr2 = ptr2.next;
        }
        ptr3 = ptr3.next;
    }
    ptr3.next = ptr1 !=null ? ptr1 : ptr2;
    return dummyHead.next;
}

4. 从有序链表中删除重复节点

在这里插入图片描述
链接:从有序链表中删除重复节点(简单)

递归

递归解决子任务,head和subHead比较是否相同,若相同则跳过subHead。

public ListNode deleteDuplicates(ListNode head) {
    if(head == null || head.next == null)    return head;
    ListNode subHead = deleteDuplicates(head.next);
    if(head.val == subHead.val){
        subHead = subHead.next;
    }
    head.next = subHead;
    return head;
}
迭代

这里使用一个指针即可,若该指针下一个节点和本结点的值相等,那么跳过。

public ListNode deleteDuplicates(ListNode head) {
    if(head == null)    return head;
    ListNode ptr = head;
    while(ptr.next!=null){
        if(ptr.val == ptr.next.val){
            ptr.next = ptr.next.next;
        } else{
            ptr = ptr.next;
        }
    }
    return head;
}

5. 删除链表的倒数第 n 个节点

在这里插入图片描述
链接:删除链表的倒数第 n 个节点(中等)

这里使用了一个叫快慢指针的技巧,先让快指针往前走距离n,然后快指针和慢指针同时移动,当快指针到尾部的时候,慢指针此时正好离尾部距离n。

public ListNode removeNthFromEnd(ListNode head, int n) {
    //快慢指针
    if(head == null)    return null;
    ListNode dummyHead = new ListNode(-1, head);
    ListNode slow = dummyHead, fast = dummyHead.next;
    int count = n;
    while(count>0){
        fast = fast.next;
        count--;
    }
    while(fast!=null){
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return dummyHead.next;
}

6. 交换链表中的相邻结点

在这里插入图片描述
链接:交换链表中的相邻结点(中等)

递归

和普通递归单向链表题一样的做法。

public ListNode swapPairs(ListNode head) {
   if(head == null || head.next == null)   return head;
   ListNode subHead = swapPairs(head.next.next);
   ListNode ptr = head.next;
   head.next = subHead;
   ptr.next = head;
   return ptr;
}
迭代

这里使用一个虚拟头结点,设置三个指针pre、first、second,让first和second的结点next改变后,再改变三个指针的指向。

public ListNode swapPairs(ListNode head) {
    ListNode dummy = new ListNode(-1, head);
    ListNode pre = dummy;
    while(pre.next != null && pre.next.next != null){
        ListNode first = pre.next, second = first.next;
        first.next = second.next;
        second.next = pre.next;
        pre.next = second;
        pre = first;
    }
    return dummy.next;
}

7. 链表求和

在这里插入图片描述
链接:链表求和(中等)

像这种做加减操作而位数没有对齐的题目,就可以使用栈来解决,通过压栈使得两个数的位数对齐。若其中一个数没有高位数,直接置0即可。

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    Stack<Integer> stack1 = getStackFromList(l1);
    Stack<Integer> stack2 = getStackFromList(l2);

    int carry = 0;
    ListNode ptr = null;
    while(!stack1.empty() || !stack2.empty() || carry!=0){
        int val1 = !stack1.empty() ? stack1.pop() : 0;
        int val2 = !stack2.empty() ? stack2.pop() : 0;

        int val = val1 + val2 + carry;
        carry = val/10;
        val %= 10;
        ListNode node = new ListNode(val);
        node.next = ptr;
        ptr = node;
    }
    return ptr;
}

private Stack<Integer> getStackFromList(ListNode head){
    Stack<Integer> stack = new Stack<Integer>();
    while(head!=null){
        stack.push(head.val);
        head = head.next;
    }
    return stack;
}

8. 回文链表

在这里插入图片描述
链接:回文链表(简单)

方法一:使用栈,但是时间复杂度会很高,O(n)。

public boolean isPalindrome(ListNode head) {
    Stack<Integer> stack = new Stack<>();
    ListNode node = head;
    int count = 0;
    while(node!=null){
        stack.push(node.val);
        count++;
        node = node.next;
    }
    int i = 0;
    while(i<count/2){
        int val1 = head.val;
        int val2 = stack.pop();
        if(val1!=val2)  return false;
        head = head.next;
        i++;
    }
    return true;
}

方法二:

  • 先用快慢指针找到链表的中间结点,再将链表断开。
  • 将后半段链表反转。
  • 用前半段的链表的值和后半段反转后的链表值比较。
  • 如果需要恢复链表,再反转一次后半段,并重新接上即可。

这种方法的空间复杂度为O(1),是方法一的优化。

public boolean isPalindrome(ListNode head) {
    if(head == null || head.next == null)    return true;
    Stack<Integer> stack = new Stack<>();
    ListNode slow = new ListNode(-1, head), fast = head;
    //将链表分成两段
    //slow停在第一段的最后一个节点
    while(fast!=null && fast.next!=null){
        fast = fast.next.next;
        slow = slow.next;
    }
    //奇数节点,让slow指向下一个节点(中间节点)
    if(fast!=null){
        slow = slow.next;
    }

    //断开两端节点,slow指向第二段第一个节点
    ListNode tmp = slow;
    slow = slow.next;
    tmp.next = null;

    //反转后半段链表
    ListNode revHead = reverse(slow);

    while(revHead!=null){
        if(head.val!=revHead.val)  return false;
        head = head.next;
        revHead = revHead.next;
    }
    return true;
}

private ListNode reverse(ListNode head){
    ListNode pre = null;
    while(head!=null){
        ListNode next = head.next;
        head.next = pre;
        pre = head;
        head = next;
    }
    return pre;
}

8. 分割链表

在这里插入图片描述
链接:分割链表(中等)

  • 由于分隔成的每个部分的长度和原始链表的长度有关,因此需要首先遍历链表,得到链表的长度。
  • 得到长度后可以通过整除和取余的方式得到每部分的size。
  • 之后用for循环遍历单向链表,每到一个size的大小做切割即可。
public ListNode[] splitListToParts(ListNode head, int k) {
     int count = 0;
     ListNode ptr = head;
     //计算链表长度
     while(ptr!=null){
         count++;
         ptr = ptr.next;
     }

     int size = count / k;
     int mod = count % k;

     ListNode[] ret = new ListNode[k];
     ptr = head;
     for(int i = 0; ptr!=null && i<k; i++){
         ret[i] = ptr;
         int curSize = size + (mod-- > 0 ? 1 : 0);
         for(int j = 0; j<curSize-1; j++){
             ptr = ptr.next;
         }
         ListNode tmp = ptr;
         ptr = ptr.next;
         tmp.next = null;
     }
     return ret;
 }

10. 链表元素按奇偶聚集

在这里插入图片描述
链接:链表元素按奇偶聚集(中等)
用两个指针,一奇一偶,while循环时不断变换指向即可,记得要把偶数链表头先记录下来。

public ListNode oddEvenList(ListNode head) {
    if(head == null || head.next == null)    return head;
    ListNode odd = head, even = head.next, secHead = head.next;

    while(even!=null && even.next!=null){
        odd.next = even.next;
        odd = odd.next;
        even.next = odd.next;
        even = even.next;
    }

    odd.next = secHead;
    return head;
}

11. 链表中环的入口节点

在这里插入图片描述
链接:链表中环的入口节点

一个非常直观的思路是:我们遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。借助哈希表可以很方便地实现。使用HashSet避免重复。

public ListNode detectCycle(ListNode head) {
    ListNode pos = head;
    Set<ListNode> visited = new HashSet<ListNode>();
    while (pos != null) {
        if (visited.contains(pos)) {
            return pos;
        } else {
            visited.add(pos);
        }
        pos = pos.next;
    }
    return null;
}

另一个方法:这道题本质上就是求一个相交点,一般来说这种题尝试使用快慢指针常常可以解决问题。我们设置一个快指针fast,一个慢指针slow,fast的速度是slow的两倍,两个指针都是从head出发。
在这里插入图片描述
我们假设两个指针在上图紫色点处相遇,设定路程a, b, c。

  • 我们可以列出,fast所走的路程为a+n*(b+c)+b,slow所走的路程为a+b,n为fast在环中走过的圈数。
  • 又因为fast的速度是slow的两倍,我们可以根据上述公式得到:a+n*(b+c)+b = 2*(a+b)。
  • 对上式化简得a = (n-1)*(b+c)+c。也就是说a的距离是n-1倍圈的路程+c的距离。
  • 那此时我们在head新建立一个指针ptr,让slow和ptr同时移动。ptr走完a时走到环的入口结点,此时slow正好走完n-1倍圈的路程+c的距离,也是环的入口节点。他们相交的点就必然是环的入口节点。
public ListNode detectCycle(ListNode head) {
    if(head==null) return null;
    ListNode fast = head, slow = head;
    while(fast!=null){
        slow = slow.next;
        if(fast.next!=null){
            fast = fast.next.next;
        } else{
            return null;
        }

        if(fast == slow){
            ListNode ptr = head;
            while(slow!=ptr){
                slow = slow.next;
                ptr = ptr.next;
            }
            return ptr;
        }

    }
    return null;
}

这里需要注意的是,题目并没有保证一定有环,所以需要对fast判断是否为null。而且在快指针移动的时候,一定要判断是否会跳过null,否则很可能会把null当作节点调用方法,结果报错。同时也要注意边界条件,例如节点为null或者只有一个的情况。

12. LRU缓存

在这里插入图片描述
链接:LRU缓存

  • LRU缓存指的是最近最少使用(Least Recently Used)缓存,是一种页面置换算法。当某段内存页被访问或者被刚刚创建到缓存时,就会放在头部;当缓存空间不够时,就会将最不常用的尾部数据清除。
  • 这种特性使用HashMap和双向链表结合可以实现。HashMap以O(1)的时间复杂度来找key,双向链表用来来改变访问后的缓存顺序。HashMap的value用来装Node。
  • 这里使用虚拟头节点和虚拟尾节点,可以少考虑一些边界条件,写起程序来更高效。
class LRUCache {

    class Node{
        int key;
        int value;
        Node left;
        Node right;

        public Node(){}

        public Node(int key, int value){
            this.key = key;
            this.value = value;
        }
    }

    Map<Integer, Node> map = new HashMap<>();

    int capacity;
    int size;
    Node head;
    Node tail;


    public LRUCache(int capacity) {
        this.capacity = capacity;
        size=0;
        head = new Node();
        tail = new Node();
        head.right = tail;
        tail.left = head;
    }
    
    public int get(int key) {
        if(!map.containsKey(key)) return -1;
        Node node = map.get(key);
        moveToHead(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        //如果已经有这个key
        if(map.containsKey(key)){
            Node node = map.get(key);
            //那么更新对应value
            node.value = value;
            //并把该Node放到链表头
            moveToHead(node);
        } else{
   			//没有key那么添加节点
            Node node = new Node(key, value);
            map.put(key, node);
            addToHead(node);
            size++;
            //如果容量已满
            if(capacity < size){
                //把map中的<k, v>去掉
                map.remove(tail.left.key);
                //把链表尾的节点去掉
                removeTail();
                size--;
            }
        }
    }

    public void addToHead(Node node){
        node.right = head.right;
        node.left = head;
        head.right.left = node;
        head.right = node;
    }

    public void removeTail(){
        Node node = tail.left;
        removeNodeLink(node);
        node.left = null;
        node.right = null;    
    }

    public void removeNodeLink(Node node){
        node.left.right = node.right;
        node.right.left = node.left;
    }

    public void moveToHead(Node node){
        removeNodeLink(node);
        addToHead(node);
    }

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值