【LeetCode】链表专题总结

链表总结

  1. 对于链表问题,返回结果为头结点时,通常需要先初始化一个预先指针 pre,该指针的下一个节点指向真正的头结点 head。使用预先指针的目的在于链表初始化时无可用节点值,而且链表构造过程需要指针移动,进而会导致头指针丢失,无法返回结果。
ListNode pre = new ListNode(0);
ListNode cur = pre;

github对应的链表专题

先分割再合并:

  • 23.合并K个升序链表
  • 148.排序链表(归并排序)

双指针

找中间节点

109. 有序链表转换成二叉搜索树

在这里插入图片描述
这个单链表是按升序排序的,我们只需找到他的中间节点,让他成为树的根节点,中间节点前面的就是根节点左子树的所有节点,中间节点后面的就是根节点右子树的所有节点。然后递归分别对左右子树进行相同操作

class Solution {
    public TreeNode sortedListToBST(ListNode head) {
        // write code here
        if(head==null){
            return null;
        }
        if(head.next==null){
            return new TreeNode(head.val);
        }
        //找到中间节点
        ListNode fast = head;
        ListNode slow = head;
        ListNode prev = null;
        while(fast!=null && fast.next!=null){
            prev = slow;
            fast = fast.next.next;
            slow = slow.next;
        }
        //将链表断开为两部分
        prev.next = null;
        TreeNode node = new TreeNode(slow.val);
        node.left = sortedListToBST(head);
        node.right = sortedListToBST(slow.next);
        return node;
    }
}

通过快慢指针解决不知道链表总长度的问题

由于不知道链表总长度,但又希望通过一次遍历解决问题,所以需要通过两个指针找到等式来解决问题

找链表的中间结点(快慢指针)

问题的关键在于我们无法直接得到单链表的长度 n,常规方法也是先遍历链表计算 n,再遍历一次得到第 n / 2 个节点,也就是中间节点。
如果想一次遍历就得到中间节点,使用「快慢指针」的技巧:
我们让两个指针 slow 和 fast 分别指向链表头结点 head。

每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点。

class Solution {
    public ListNode middleNode(ListNode head) {

        ListNode fast = head;
        ListNode low = head;
        while(fast!=null && fast.next!=null){
            fast = fast.next.next;
            low = low.next;
        }
        return low;
    }
}

获取倒数第k个元素

如果找正数第k个元素则直接一次遍历到k即可,如果知道链表的个数n,倒数第k个元素就是n-k+1,但我们不知道链表的个数n
但如果用两个指针,第一个指针p先走到k的位置,则他剩余的节点是n-k,此时我们让指向head的另一个指针q和p一起移动,当p指向null时q指向的位置就是n-k+1的节点,即倒数第 k 个节点

class Solution {
// 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
    ListNode p1 = head;
    // p1 先走 k 步
    for (int i = 0; i < k; i++) {
        p1 = p1.next;
    }
    ListNode p2 = head;
    // p1 和 p2 同时走 n - k 步
    while (p1 != null) {
        p2 = p2.next;
        p1 = p1.next;
    }
    // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
    return p2;
}

}

19. 删除链表的倒数第 N 个结点

  1. 先找到倒数第N+1的节点
  2. 删除倒数第N个节点
  3. 需要用的dummy节点
    因为如果要删的是第一个节点,我们找N+1的时候就空指针了
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 如果要删的是倒数第n个节点,即第一个节点,那我们要找的是倒数第n+1个节点,如果不用dummy就会空指针
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode x = findNthFromEnd(dummy, n+1);
        x.next = x.next.next;
        return dummy.next;
    }

    public ListNode findNthFromEnd(ListNode head, int n){
        ListNode p = head;
        ListNode q = head;
        for (int i=0;i<n;i++){
            p = p.next;
        }
        while (p!=null){
            p = p.next;
            q = q.next;
        }
        return q;
    }

环形链表

判断链表是否有环

  1. 判断链表是否有环
    当链表有环时,快慢指针陷入环中,快指针总会追上慢指针。
    若不存在环,那么快指针一定会先走到链表尾部。
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head;
        ListNode low = head;
        while(fast!=null && fast.next!=null){
            fast = fast.next.next;
			low = low.next;
            if(fast==low){ //快的追上慢的了 说明有环
                return true;
            }
        }
        return false;
    }
}

返回链表开始入环的第一个节点

  1. 返回链表开始入环的第一个节点。如果链表无环,则返回null。

用快慢指针:

  • 第一步:快指针走两步慢指针走一步找到相遇点
  • 第二步:head和相遇点同时向后走,相遇点即入环节点
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        boolean hasCycle = false;
        // 判断是否有环:找到第一次相遇点
        while(fast!=null && fast.next!=null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast==slow){
                hasCycle = true;
                break;
            }
           
        }
        if(hasCycle){
            slow = head;
            //head 和 相遇点 一起往后走 相遇点即为入环点
            while(fast!=slow){
                fast = fast.next;
                slow = slow.next;
            }
            return fast;
        }
        else{
            return null;
        }
    }
}

判断环的长度

  1. 如果存在环,如何判断环的长度
    快慢指针
  • 第一步:快慢指针相遇
  • 第二步:相遇后继续走 直到第二次相遇,两次相遇间的移动次数即为环的长度

反转链表

反转整个链表

方法一:迭代

设置prev为null,curr为head,当curr不为null时:

  • tmp = curr.next
  • curr.next=prev
  • prev = curr
  • curr = tmp
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while(curr!=null){
            ListNode tmp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = tmp;
        }
        return prev;
    }
}

方法二:递归

class Solution {
    public ListNode reverseList(ListNode head) {
        //base case
        if(head==null || head.next==null){ //只有当前结点一个结点则返回
            return head;
        }
        ListNode last = reverseList(head.next); //得到反转后的头结点
        head.next.next = head;
        head.next=null;
        return last;
    }
}

反转链表前N个节点

ListNode successor = null; // 后驱节点

// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
    if (n == 1) { 
        // 记录第 n + 1 个节点
        successor = head.next;
        return head;
    }
    // 以 head.next 为起点,需要反转前 n - 1 个节点
    ListNode last = reverseN(head.next, n - 1);

    head.next.next = head;
    // 让反转之后的 head 节点和后面的节点连起来
    head.next = successor;
    return last;
}

反转单链表的一部分

92. 反转链表 II
在这里插入图片描述

方法一:迭代

  • 先用一个for循环找到第m个位置
    prev 和curr一直向后迭代left-1次,这时的curr为反转链表的起始位置,prev应该记录为之后连接的起始结点
  • 将m和n之间的元素反转
    反转后的prev为反转链表的起始结点,此节点应该为之前prev节点的下一个结点(如果之前节点为空则head为prev)
    反转后的curr为反转链表的结束结点的下一个结点,此节点应该为之前curr结点的下一个结点
  • 将反转后的和原来的连接起来
class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode prev = null;
        ListNode cur = head;
        // 1、先找到left cur指向left
        for(int i=1;i<left;i++){
            prev = cur;
            cur = cur.next;
        }
        // 反转链表的前一个节点,用来连接反转后的链表
        ListNode firstNode = prev;
        // 反转后的最后一个节点,用来连接 反转链表的后一个节点
        ListNode lastNode = cur;
        // 2、开始反转left到right
        while (left<=right && cur!=null){
            ListNode tmp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = tmp;
            left++;
        }
        // 3、连接
        if (firstNode!=null){
            // 反转的前部分有节点
            firstNode.next = prev;
        }else{
            // 反转的前部分无节点
            head = prev;
        }
        lastNode.next = cur;
        return head;
    }
}

带dummy节点的迭代

    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode dummyHead =  new ListNode(0);
        dummyHead.next = head;
        ListNode cur = head;
        ListNode prev =  dummyHead;
        // 找到要反转的第一个节点
        for (int i = 0; i < left-1; i++) {
            cur = cur.next;
            prev = prev.next;
        }
        ListNode newHead = reverseN(cur, right-left+1);
        prev.next = newHead;
        return dummyHead.next;
    }

    public ListNode reverseN(ListNode head, int n){
        ListNode oldHead = head;
        ListNode prev = null;
        ListNode cur = head;

        for (int i = 0; i < n; i++) {
            ListNode tmp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = tmp;
        }

        // 连接
        oldHead.next = cur;
        return prev;
    }

递归

  • 一直head.next 前进到left减到1
  • 就可以用reverseN 反转前n个链表
class Solution {
    ListNode successor = null;//后驱节点
    public ListNode reverseBetween(ListNode head, int left, int right) {
        if(left==1){
            return reverseN(head,right);
        }

        // 前进到反转的起点 触发base case
        head.next = reverseBetween(head.next,left-1,right-1);
        return head;
    }

    public ListNode reverseN(ListNode head,int n){
        if(n==1){
            successor = head.next;
            return head;
        }
        // 以head.next为起点,需要反转前n-1个节点
        ListNode last = reverseN(head.next,n-1);
        head.next.next = head;
        //让反转之后的head节点和后面的节点连起来
        head.next = successor;
        return last;
    }
}

25. K 个一组翻转链表

在这里插入图片描述

  • 先看剩余部分长度如果小于k则不用翻转直接返回
  • 找到要翻转的部分 head tail,进行翻转 注意翻转的条件是prev!=tail
  • 将翻转后的head和tail 和原链表拼接
class Solution {
    public ListNode reverseKGroup (ListNode head, int k) {
        // write code here
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode pre = dummyHead;
        while(head!=null){
            ListNode tail = pre;
            //查看剩余部分长度是否大于等于k ,小于k的话直接保持原样
            for(int i = 0;i<k;i++){
                tail=tail.next;
                if(tail==null){
                    return dummyHead.next;
                }
            }
            // 从pre 到tail 进行翻转
            ListNode nex = tail.next;
            ListNode[] reverse = myReverse(head,tail);
            head = reverse[0];
            tail = reverse[1];
            // 把子链表接回原链表
            pre.next = head;
            tail.next = nex;
            // 继续进行接下来的翻转
            pre = tail;
            head = tail.next;
        }
        return dummyHead.next;
    }
    
    public ListNode[] myReverse(ListNode head,ListNode tail){
        ListNode prev = null;
        ListNode curr = head;
        while(prev!=tail){// 结束条件
            ListNode tmp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = tmp;
        }
        return new ListNode[]{tail,head};
    }
}

回文链表

  • 第一步:通过快慢指针得到链表的中间节点
  • 通过双指针将后一段链表反转
  • 判断前半部分链表和后半部分链表是否相等
class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode fast=head;
        ListNode slow=head;
        //通过快慢指针得到链表的中间节点
        while(fast!=null && fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
        }
        //将后一段链表反转
        ListNode prev = null;
        ListNode curr = slow;
        while(curr!=null){
            ListNode tmp = curr.next;//存下一个节点
            curr.next = prev;//当前结点的下一个节点是前一个结点
            prev = curr;
            curr = tmp;
        }

        //判断两个链表是否相等
        while(prev!=null){
            if(head.val!=prev.val){
                return false;
            }
            head = head.next;
            prev = prev.next;
        }

        return true;
    }
}

重排链表

方法一: 找到中间结点+后半段逆序+合并链表

class Solution {
    public void reorderList(ListNode head) {
        if(head==null){
            return ;
        }
        
        // 找到中间节点
        ListNode fast = head;
        ListNode slow = head;
        while(fast!=null && fast.next!=null){
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode l1 = head;
        ListNode l2 = slow.next;
        slow.next= null; //要断开
        //后半部分链表反转
        ListNode prev = null;
        ListNode curr = l2;
        while(curr!=null){
            ListNode tmp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = tmp;
        }

        // 合并链表
        mergeList(l1,prev);
        
    }

    public void mergeList(ListNode l1, ListNode l2){
        ListNode l1_tmp;
        ListNode l2_tmp;
        while(l1!=null && l2!=null){
            l1_tmp = l1.next;
            l2_tmp = l2.next;
            
            l1.next = l2;
            l1 = l1_tmp;

            l2.next = l1;
            l2 = l2_tmp;

        }
    }

}

方法二: 将原始链表放到数组中

首先将原始链表的每一个节点存放在一个数组中,然后我们取首尾指针向中间遍历,每次循环我们需要将左指针的节点连上右指针的节点,在节点连上之后,我们需要将右指针连上未排序的首节点。

class Solution {
    public void reorderList(ListNode head) {
        if (head == null) {
            return;
        }
        List<ListNode> list = new ArrayList<ListNode>();
        ListNode node = head;
        while (node != null) {
            list.add(node);
            node = node.next;
        }
        int i = 0, j = list.size() - 1;
        while (i < j) {
            list.get(i).next = list.get(j);
            i++;
            if (i == j) {
                break;
            }
            list.get(j).next = list.get(i);
            j--;
        }
        list.get(i).next = null;
    }
}

相交链表

在这里插入图片描述

用la和lb两个指针 走两轮

  • 第一轮:抹除长度差 当走到了尾部,则指向另一个的头节点
  • 第二轮:两个同时向后走,就走到相交处
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode la = headA;
        ListNode lb = headB;
        while(la!=lb){//la 走到了尾部,则就指向headB 开始第二轮
            if(la==null){
                la = headB;
            }
            else{ 
                la = la.next;
            }

            if(lb==null){
                lb = headA;
            }
            else{
                lb = lb.next;                
            }
        }
        return la;
    }
}

链表排序问题

147. 对链表进行插入排序

在这里插入图片描述
插入排序的算法复杂度是O(n2)

思路:

  • 用lastSorted指向已排序部分的最后一个节点
  • curr 指向待插入元素,和lastSorted进行比较
    • curr的值大于等于lastSorted 则lastSorted直接后移
    • 小于 要从头遍历找到 curr插入的地方,即 prev.next.val>curr.val的地方
class Solution {
    public ListNode insertionSortList(ListNode head) {
        if(head==null){
            return head;
        }
        // write code here
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        // 已排序部分的最后一个节点
        ListNode lastSorted = head;
        //待插入的元素
        ListNode curr = head.next;
        while(curr!=null){
            if(curr.val>=lastSorted.val){
                //lastSorted = lastSorted.next;
                lastSorted = curr;
            }
            else{
                ListNode prev = dummyHead;
                //prev向后移,直到找到curr的插入位置
                while(prev.next.val<=curr.val){
                    prev = prev.next;
                }
                lastSorted.next = curr.next;
                //将curr插入
                curr.next = prev.next;
                prev.next = curr;
            }
            curr = lastSorted.next;
        }
        return dummyHead.next;
    }
}

148. 排序链表(归并排序)

题目的进阶问题要求达到O(nlogn) 的时间复杂度和O(1) 的空间复杂度,时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n^2),其中最适合链表的排序算法是归并排序

递归进行如下操作:

  • 找到链表的中点,以中点为分界,将链表拆成两个子链表。
  • 拆成的子链表进行排序
  • 将排序后的子链表进行合并
    递归的终止条件:链表的节点个数小于或等于1

自顶向下归并排序:
时间复杂度:O(nlogn)
空间复杂度:O(logn),递归调用的栈空间

    public ListNode sortList (ListNode head) {
        // write code here
        return sortList(head,null);
    }
    public ListNode sortList(ListNode head,ListNode tail){
        // 递归终止条件
        if(head==null){
            return head;
        }
        if(head.next==tail){
            head.next = null;
            return head;
        }
        // 找到中间节点
        ListNode fast = head;
        ListNode slow = head;
        while(fast!=tail){
            fast = fast.next;
            slow = slow.next;
            if(fast!=tail){
                fast = fast.next;
            }
        }
        ListNode mid = slow;
        ListNode l1 = sortList(head,mid);
        ListNode l2 = sortList(mid,tail);
        //合并
        ListNode sorted = merge(l1,l2);
        return sorted;
    }
    public ListNode merge(ListNode head1,ListNode head2){
        ListNode head = new ListNode(0);
        ListNode l = head;
        while(head1!=null && head2!=null){
            if(head1.val<=head2.val){
                l.next = head1;
                head1 = head1.next;
            }
            else{
                l.next = head2;
                head2 = head2.next;
            }
            l = l.next;
        }
        l.next = head1==null?head2:head1;
        return head.next;
    }

链表经典题目

2.两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {

        ListNode* l=new ListNode;
        ListNode* head=new ListNode;
        l=head;
        int carry=0;
        int sum;
        while(l1!=NULL||l2!=NULL){

            
            int l1val = l1!=NULL?l1->val:0;
            int l2val = l2!=NULL?l2->val:0;
            sum = l1val+l2val+carry;//取l1的值和l2的值相加
            carry = sum/10;//进位

            l->next = new ListNode(sum%10);
            l=l->next;
            
            if(l1!=NULL){l1=l1->next;}
            if(l2!=NULL){l2=l2->next;}
            
            
        }
        

        if(carry){ //最后一位有进位
            l->next = new ListNode(carry);
            l=l->next;
        }

        return head->next;
        
    }
};

有序链表

合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode head = new ListNode();
        ListNode l = head;

        while(l1!=null && l2!=null){
    
            if(l1.val<=l2.val){
                l.next = l1;
                l1 = l1.next;
            }
            else{
                l.next = l2;
                l2 = l2.next;
            }

            l = l.next;
        }
        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        l.next = l1==null?l2:l1;
        return head.next;

    }
}

23. 合并k个升序链表(分治)

给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

思路:
通过二分法把k个链表分割,然后再两两进行合并

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        return merge(lists,0,lists.length-1);   
    }

    public ListNode merge(ListNode[] lists,int l,int r){
        if(l==r){
            return lists[l];
        }
        if(l>r){
            return null;
        }
        int mid = (l+r)>>1;
        return mergeTwoLists(merge(lists,l,mid),merge(lists,mid+1,r));
    }

     public ListNode mergeTwoLists(ListNode l1,ListNode l2){
        ListNode head = new ListNode(0);
    
        ListNode l = head;
        
        while(l1!=null && l2!=null){
            if(l1.val<=l2.val){
                l.next = l1;
                l1 = l1.next;
            }
            else{
                l.next = l2;
                l2 = l2.next;
            }
            l = l.next;
        }
        l.next = l1==null?l2:l1;
        return head.next;
    }
}

解法2:用最小堆找到最小值

    public ListNode mergeKLists(ListNode[] lists) {
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        // 优先级队列:最小堆
        PriorityQueue<ListNode> pq = new PriorityQueue<>(lists.length,((a,b)-> (a.val-b.val));
        // 1、将k个链表的头节点加入最小堆
        for (ListNode head : lists){
            pq.add(head);
        }
        while (p!=null){
            // 得到k个节点的最小值
            ListNode minNode = pd.poll();
            p.next = minNode;
            pq.add(minNode.next);
            p=p.next;
        }
        return dummy.next;

    }

删除排序链表中的重复元素(哑节点)

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:
输入: 1->1->2->3->3
输出: 1->2->3

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode l = head;
        while(l!=null && l.next!=null){
            if(l.next.val==l.val){ //相等就删除下一个节点
                l.next = l.next.next;
            }
            else{ //直到不相等才指向下一个节点
                l = l.next;
            }
            
        }
        return head;

    }
}

给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
示例 1:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
示例 2:
输入: 1->1->1->2->3
输出: 2->3

  • 需要一个虚拟头节点dummyHead,用prev指向dummyHead,用prev来控制新的链表的连接
    这样在删除重复节点后,剩余节点就挂在prev之后
  • 当有重复节点,prev.next = difNode prev的下一个节点为和前面不重复的,这样就把之前那些重复的都删了
  • 没有重复节点,prev = curr
public class Solution {
    /**
     * 
     * @param head ListNode类 
     * @return ListNode类
     */
    public ListNode deleteDuplicates (ListNode head) {
        // write code here
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode prev = dummyHead;
        ListNode curr = prev.next;
        boolean hasRepeatNode = false;
        while(curr!=null){
            hasRepeatNode = false;
            ListNode difNode = curr.next;
            while(difNode!=null && difNode.val==curr.val){
                difNode = difNode.next;
                hasRepeatNode = true;
            }
            if(hasRepeatNode){// 有重复节点
                prev.next = difNode;
            }
            else{
                prev = curr;
            }
            curr = difNode;
        }
        return dummyHead.next;
    }
}

旋转链表

给定一个链表,旋转链表,将链表每个节点向右移动 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

解:

  • 找到旧链表的尾,将其next指向head,同时获得链表长度len
  • 循环len-len%k-1 此得到新链表的尾
  • 新链表尾的next即为新链表的头
class Solution {
    public ListNode rotateRight(ListNode head, int k) {

        if(head==null || head.next ==null){
            return head;
        }
        
        ListNode new_tail = head; //新链表的尾
        ListNode old_tail = head; //旧链表的尾
        int len = 1; //链表长度
        // 找到旧链表的尾部
        while(old_tail.next!=null){
            old_tail = old_tail.next;
            len++;
        }
        old_tail.next = head;

        // 找到新链表的尾
        for(int i = 0;i<len-k%len-1;i++){
            new_tail = new_tail.next;
        }
        head = new_tail.next;
        new_tail.next = null;
        return head;
    }
}

86.划分链表

在这里插入图片描述
思路: 用两个哑节点分别管理小于x的链和大于等于x的链
这里要注意理解题目意思是只需要小于等于x的节点位于链表前面即可,不要求小于在前,等于在中,大于在后。

public class Solution {
    /**
     * 
     * @param head ListNode类 
     * @param x int整型 
     * @return ListNode类
     */
    public ListNode partition (ListNode head, int x) {
        // write code here
        // 哑节点指向两个链表的头部
        ListNode l1 = new ListNode(0);
        ListNode l2 = new ListNode(0);
        // 指针指向两个链表的尾部
        ListNode l1_curr = l1;
        ListNode l2_curr = l2;
        while(head!=null){
            if(head.val<x){
                l1_curr.next = head;
                l1_curr = l1_curr.next;
            }
            else{
                l2_curr.next = head;
                l2_curr = l2_curr.next;
            }
            head = head.next;
        }
        l1_curr.next = l2.next;
        l2_curr.next= null;
        return l1.next;
    }
}

复制带随机指针的链表( LeetCode 138 )

  • 第一次遍历:用hash表存储每个节点,但存储不了每个节点的next和random指针
  • 第二次遍历:找到每个节点的next和random指针
//给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。 
//
// 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 
//指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。 
//
// 例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random 
//--> y 。 
//
// 返回复制链表的头节点。 
//
// 用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示: 
//
// 
// val:一个表示 Node.val 的整数。 
// random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。 
// 
//
// 你的代码 只 接受原链表的头节点 head 作为传入参数。 
//
// 
//
// 示例 1: 
//
// 
//
// 
//输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
//输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
// 
//
// 示例 2: 
//
// 
//
// 
//输入:head = [[1,1],[2,1]]
//输出:[[1,1],[2,1]]
// 
//
// 示例 3: 
//
// 
//
// 
//输入:head = [[3,null],[3,0],[3,null]]
//输出:[[3,null],[3,0],[3,null]]
// 
//
// 
//
// 提示: 
//
// 
// 0 <= n <= 1000
// 
// -10⁴ <= Node.val <= 10⁴ 
// Node.random 为 null 或指向链表中的节点。 
// 
//
// 
//
// Related Topics 哈希表 链表 👍 1326 👎 0


class Solution {
    public Node copyRandomList(Node head) {

        if (head==null){
            return null;
        }
        Node cur = head;
        // key是原节点有next和random, value是和原节点一样的新节点,但没有next和random
        Map<Node,Node> map = new HashMap<>();
        // 第一次遍历存储原节点
        while (cur!=null){
            // 以原链表的节点为 Key,构建一个 Map
            // Map 的 Value 为一个新链表中的节点
            // 新节点的值 val 和原链表的值 val 一样
            // 但原链表中的每个节点都有 next 和 random 指针,而 Map 中的 Value 没有 next 和 random 指针
            map.put(cur, new Node(cur.val));
            cur = cur.next;
        }
        // 第二次遍历 把新节点的next 和random指针都添加上
        cur = head;
        while (cur!=null){
            // 1、找next
            Node curNode = map.get(cur);
            Node nextNode = map.get(cur.next);
            curNode.next = nextNode;
            // 2、找random
            Node randomNode = map.get(cur.random);
            curNode.random = randomNode;

            cur=cur.next;
        }
        return map.get(head);
    }
}
//leetcode submit region end(Prohibit modification and deletion)

328.奇偶链表

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。
第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。
请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。
你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。
示例 1:
输入: head = [1,2,3,4,5]
输出: [1,3,5,2,4]

示例 2:
输入: head = [2,1,3,5,6,4,7]
输出: [2,3,6,7,1,5,4]


class Solution {
    public ListNode oddEvenList(ListNode head) {

        if (head==null || head.next==null){
            return head;
        }
        // 奇数
        ListNode odd = head;
        // 偶数
        ListNode even = head.next;
        // 偶数头结点
        ListNode evenHead = even;
        while(even!=null && even.next!=null){
            odd.next = even.next;
            odd = odd.next;
            even.next = odd.next;
            even = even.next;
        }
        odd.next = evenHead;
        return head;
    }
}

146. LRU缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行


labuladong LRU题解

hashMap get 快,但数据无固定顺序;链表插入删除快,但查找慢。所以我们将hashMap和双向链表结合起来,用hash链表LinkedHashMap。

  • 如果map存在key
    1. 删除key 2. 将key value作为最近使用元素插入
  • map不存在key
    1. 看当前链表的size是否已满,如果满了先删除最久未使用节点,也就是链表首节点
    2. 将key value作为最近使用元素插入
import java.util.HashMap;

class Node {
    public int key,val;

    public Node prev, next;

    public Node(int k, int v){
        this.key = k;
        this.val = v;
    }
}


class DoubleList{
    private Node head,tail;
    // 链表个数
    private int size;
    // 初始化链表
    public DoubleList(){
        head = new Node(0,0);
        tail = new Node(0,0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }
    // 向尾部插入元素
    public void addLast(Node x){
        x.prev = tail.prev;
        tail.prev.next = x;
        x.next = tail;
        tail.prev = x;
        size++;
    }
    // 删除链表的x节点
    public void remove(Node x){
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    }
    // 删除链表中的第一个节点,并返回删除的元素
    public Node removeFirst(){
        if (head.next==tail){
            return null;
        }
        Node first = head.next;
        remove(first);
        return first;
    }
    public int size(){
        return size;
    }
}
class LRUCache {
    // hash表
    private HashMap<Integer,Node> map;
    // 双向链表
    private DoubleList cache;
    // 最大容量
    private int cap;

    public LRUCache(int capacity) {

        cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }
    
    public int get(int key) {
        // 不存在 返回-1
        if (!map.containsKey(key)){
            return -1;
        }
        Node node = map.get(key);
        // 变成最近使用
        makeRecently(key);
        return node.val;
    }
    
    public void put(int key, int value) {

        // 1、key 存在
        if (map.containsKey(key)){
            // 删除旧数据
            deleteKey(key);
            // 将新数据置为最近使用
            addRecently(key,value);
            return ;
        }
        // 2、key不存在
        // 当前容量是否已满,满了就删除最久未使用的节点
        if (cache.size()==cap){
            removeLeastRecently();
        }
        // 将新数据置为最近使用
        addRecently(key,value);
        
    }

    // 将某个key提升为最近最久使用
    public void makeRecently(int key){
        Node x = map.get(key);
        // 先从链表中删除这个节点
        cache.remove(x);
        // 插入到队尾
        cache.addLast(x);
    }

    // 添加最近使用
    public void addRecently(int key, int val){
        // 插入map
        Node x = new Node(key,val);
        map.put(key,x);
        // 添加到队尾
        cache.addLast(x);
    }

    // 删除key
    public void deleteKey(int key){
        Node x = map.get(key);
        cache.remove(x);
        map.remove(key);
    }

    // 删除最久未使用
    public void removeLeastRecently(){
        Node node = cache.removeFirst();
        map.remove(node.key);
    }
}

使用java内置函数 linkedHashMap


class LRUCache {
    // 用java 内置函数 linkedHashMap
    LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
    int cap;
    public LRUCache(int capacity) {
        this.cap = capacity;
    }
    
    public int get(int key) {
        if (!cache.containsKey(key)){
            return -1;
        }
        // 把key变为最近使用
        makeRecently(key);
        return cache.get(key);
    }
    
    public void put(int key, int value) {
        if (cache.containsKey(key)){
            cache.put(key,value);
            makeRecently(key);
            return ;
        }
        if (cache.size()>=cap){
            // 找到链表的头部
            Integer first = cache.keySet().iterator().next();
            cache.remove(first);
        }
        cache.put(key,value);
    }

    public void makeRecently(int key){
        Integer value = cache.get(key);
        cache.remove(key);
        cache.put(key,value);
    }
}

基础操作

插入节点

  1. 在p的结点后插入元素
    ListNode tmp = new ListNode(data);
    tmp.next = p.next;
    p.next = tmp;
    

删除节点

删除单向链表中间的某个节点

/*
实现一种算法,删除单向链表中间的某个节点(即不是第一个或最后一个节点),假定你只能访问该节点。

示例:
输入:单向链表a->b->c->d->e->f中的节点c
结果:不返回任何数据,但该链表变为a->b->d->e->f
*/

// 方法一:自己的方法
class Solution {
    public void deleteNode(ListNode node) {
        while(node.next!=null){
            node.val = node.next.val;
            if(node.next.next==null){
                node.next=null;
                return ;
            }  
            node = node.next;
        }
    }
}

删除p节点后面的元素

if(p==null || p.next == null){
	return ;
}
p.next = p.next.next;

24. 两两交换链表中的节点

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.


迭代:
dummy head
temp start end
temp是游标,起初指向dummy节点,每次要交换的节点就是temp.next和temp.next.next
每次交换的时候我们 得记住他前面的节点和后面的节点,所以

  1. 用temp连接交换后的首节点
    temp.next = end
  2. 用交换后的尾结点 连接下一个节点
    start.next = end.next
  3. 交换
    end.next=start
  4. 移动游标进行下一次交换
    temp = start;
class Solution {
    public ListNode swapPairs(ListNode head) {

        ListNode pre = new ListNode(-1);
        pre.next = head;
        // 前进的游标
        ListNode temp =pre;
        while(temp.next!=null && temp.next.next!=null){
            // 要交换的两个节点
            ListNode start = temp.next;
            ListNode end = temp.next.next;
            // 交换
            // 用temp连接交换后的首节点
            temp.next = end;
            // 用交换后的尾结点 连接下一个节点
            start.next = end.next;
            // 交换
            end.next = start;
            // 移动游标进行下一次交换
            temp = start;
        }
        return pre.next;
    }
}

递归:
关注三个:

  1. 本次递归的终止条件
    只剩下一个结点或者没结点了就没得交换了
  2. 本次递归要做什么
    本次要换的是head和head.next,要把本次换后的节点返回给上级即head.next, 要把本次和下个节点返回的连接起来,交换本次!
  3. 要返回给上一个节点什么
    返回已经处理好的链表
class Solution {
    public ListNode swapPairs(ListNode head) {
        // 终止条件
        if (head==null || head.next==null){
            return head;
        }

        // 递归:交换的是head 和head.next
        ListNode end = head.next;
        // 用交换后的尾结点 连接下一个节点
        head.next = swapPairs(head.next.next);
        // 交换
        end.next = head;

        return end;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Rust 是一种现代的编程语言,特别适合处理内存安全和线程安全的代码。在 LeetCode 中,链表是经常出现的题目练习类型,Rust 语言也是一种非常适合处理链表的语言。接下来,本文将从 Rust 语言的特点、链表的定义和操作,以及 Rust 在 LeetCode链表题目的练习等几个方面进行介绍和讲解。 Rust 语言的特点: Rust 是一种现代化的高性能、系统级、功能强大的编程语言,旨在提高软件的可靠性和安全性。Rust 语言具有如下几个特点: 1. 内存安全性:Rust 语言支持内存安全性和原语级的并发,可以有效地预防内存泄漏,空悬指针以及数据竞争等问题,保证程序的稳定性和可靠性。 2. 高性能:Rust 语言采用了“零成本抽象化”的设计思想,具有 C/C++ 等传统高性能语言的速度和效率。 3. 静态类型检查:Rust 语言支持静态类型检查,可以在编译时检查类型错误,避免一些运行时错误。 链表的定义和操作: 链表是一种数据结构,由一个个节点组成,每个节点保存着数据,并指向下一个节点。链表的定义和操作如下: 1. 定义:链表是由节点组成的数据结构,每个节点包含一个数据元素和一个指向下一个节点的指针。 2. 操作:链表的常用操作包括插入、删除、查找等,其中,插入操作主要包括在链表首尾插入节点和在指定位置插入节点等,删除操作主要包括删除链表首尾节点和删除指定位置节点等,查找操作主要包括根据数据元素查找节点和根据指针查找节点等。 Rust 在 LeetCode链表题目的练习: 在 LeetCode 中,链表是常见的题目类型,而 Rust 语言也是一个非常适合练习链表题目的语言。在 Rust 中,我们可以定义结构体表示链表的节点,使用指针表示节点的指向关系,然后实现各种操作函数来处理链表操作。 例如,针对 LeetCode 中的链表题目,我们可以用 Rust 语言来编写解法,例如,反转链表,合并两个有序链表,删除链表中的重复元素等等,这样可以更好地熟悉 Rust 语言的使用和链表的操作,提高算法和编程能力。 总之,在 Rust 中处理链表是非常方便和高效的,而 LeetCode 中的练习也是一个非常好的机会,让我们更好地掌握 Rust 语言和链表数据结构的知识。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值