链 表 练 习

labuladong的算法小抄 」学习笔记

内容

一、单链表
    合并两个有序单链表
    合并 k 个升序链表
    删除链表的倒数第 N 个结点
    链表的中点
    环形链表
    相交链表
二、递归反转链表
    递归反转整个链表
    反转前链表前 N 个结点
    反转链表的一部分
三、k 个一组反转链表
    迭代解决反转链表一部分
四、高效判断回文单链表

单链表

例题一:力扣 21. 合并两个有序单链表
  • 这道题没有什么难度,但是在代码中使用了算法题里很常见的「虚拟头节点」技巧,也就是dummy节点。如果不使用dummy虚拟节点,代码会复杂很多,而有了dummy节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性。(dummy:adj - 假的)
例题二: 力扣 23. 合并 k 个升序链表
  • 题目描述:给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
  • 用到优先级队列这种数据结构,把链表节点放入到一个最小堆,就可以每次获得k个节点中的最小节点。「C++ 优先队列的简单使用
  • 时间复杂度分析:优先队列中的元素个数最多是k,所以一次pop或者push方法的时间复杂度是O(logk);所有的链表节点都会被加入和弹出队列,所以算法整体的时间复杂度是O(Nlogk),其中k是链表的条数,N是这些链表的节点总数。
例题三: 力扣 19. 删除链表的倒数第 N 个结点
  • 要求:只遍历一次链表
  • 用快慢指针
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummy = new ListNode(-1,head),*p = dummy,*q = dummy;
        for(int i=0;i<n;i++){
            p = p->next;
        }
        while(p->next!=nullptr){
            p = p->next;
            q = q->next;
        }
        q->next = q->next->next;
        return dummy->next;
};
例题四: 力扣 876. 链表的中点
  • 快慢指针:一个每次走一步,一个每次走两步。
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while( fast!=nullptr && fast->next!=nullptr){
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};
  • 若结点个数为偶数个,那么会返回中间结点的后一个。要是让返回前一个,可以更改slow = slow->nextif( fast != nullptr ) slow = slow->next; 另一种办法为设置虚拟头结点,这样就将偶数个结点变成了奇数个,原来奇数个结点变成偶数个(不影响最后结果)。
例题五: 力扣 141. 环形链表力扣 142. 环形链表II
// 141. 环形链表
bool hasCycle(ListNode *head) {
    ListNode *slow = head,*fast = head;
    while( fast!=nullptr && fast->next!=nullptr ){
        fast = fast->next->next;
        slow = slow->next;
        if(fast==slow){
           return true;
        }
    }
    return false;
}
// 142. 环形链表II
ListNode *detectCycle(ListNode *head) {
        ListNode *slow = head,*fast = head;
        bool flag = false;
        while( fast!=nullptr && fast->next!=nullptr ){
            fast = fast->next->next;
            slow = slow->next;
            if(fast==slow){
                flag = true;
                break;
            }
        }
        ListNode *p = head;
        if(flag){
            while(p!=slow){
                p = p->next;
                slow = slow->next;
            }
            return p;
        }else{
            return nullptr;
        }
    }
例题六: 力扣 160. 相交链表
  • 题目描述:给你输入两个链表的头结点headA和headB,这两个链表可能存在相交。如果相交,你的算法应该返回相交的那个节点;如果没相交,则返回 null。比如题目给我们举的例子,如果输入的两个链表如下图:
    在这里插入图片描述
    那么我们的算法应该返回c1这个节点。
  • 解法:将两个链表拼在一起,两个指针同时向后走。(很神奇)
    在这里插入图片描述
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
         ListNode *p1 = headA,*p2 = headB;
         while(p1!=p2){
             if(p1==nullptr){
                 p1 = headB;
             }else{
                 p1 = p1->next;
             }
             if(p2==nullptr){
                 p2 = headA;
             }else{
                 p2 = p2->next;
             }
         }
         return p1;
    }
};

递归反转链表

递归反转整个链表
ListNode reverse(ListNode head) {
    if (head.next == null) return head;
    ListNode last = reverse(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) { 
        successor = head.next;
        return head;
    }
    ListNode last = reverseN(head.next, n - 1);
    head.next.next = head;
    head.next = successor;
    return last;
}    
反转链表的一部分

给一个索引区间[m,n](索引从 1 开始),仅仅反转区间中的链表元素。

ListNode reverseBetween(ListNode head, int m, int n) {
    if (m == 1) {
        return reverseN(head, n);
    }
    head.next = reverseBetween(head.next, m - 1, n - 1);
    return head;
}

k 个一组反转链表 ( 东哥解答 )

反转整个链表

ListNode reverse(ListNode a) {
    ListNode pre, cur, nxt;
    pre = null; cur = a; nxt = a;
    while (cur != null) {
        nxt = cur.next;
        // 逐个结点反转
        cur.next = pre;
        // 更新指针位置
        pre = cur;
        cur = nxt;
    }
    // 返回反转后的头结点
    return pre;
}

反转区间 [a, b) 的元素,注意是左闭右开

ListNode reverse(ListNode a, ListNode b) {
    ListNode pre, cur, nxt;
    pre = null; cur = a; nxt = a;
    // while 终止的条件改一下就行了
    while (cur != b) {
        nxt = cur.next;
        cur.next = pre;
        pre = cur;
        cur = nxt;
    }
    // 返回反转后的头结点
    return pre;
}

k 个一组反转链表

ListNode reverseKGroup(ListNode head, int k) {
    if (head == null) return null;
    // 区间 [a, b) 包含 k 个待反转元素
    ListNode a, b;
    a = b = head;
    for (int i = 0; i < k; i++) {
        // 不足 k 个,不需要反转,base case
        if (b == null) return head;
        b = b.next;
    }
    // 反转前 k 个元素
    ListNode newHead = reverse(a, b);
    // 递归反转后续链表并连接起来
    a.next = reverseKGroup(b, k);
    return newHead;
}
借着这道题稍加修改 → 迭代解决反转链表一部分
ListNode* reverseBetween(ListNode* head, int left, int right){
        ListNode* dummy = new ListNode(0,head);
        ListNode* p = dummy;
        for(int i=1;i<left;i++){  
            p = p->next;
        }
        // p将指向left位置前一个结点
        p->next = reverseN(p->next,right-left+1);
        return dummy->next;
}
ListNode* reverseN(ListNode* head,int num){
        ListNode *pre=nullptr,*cur=head,*nxt=head;
        while(num>0){
            nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
            num--;
        }
        head->next = nxt;
        return pre;
}

高效判断回文单链表

链表其实也可以有前序遍历和后序遍历:

void traverse(ListNode* head) {
    // 前序遍历代码
    traverse(head->next);
    // 后序遍历代码
}

解决问题:

// 左侧指针
ListNode left;
boolean isPalindrome(ListNode head) {
    left = head;
    return traverse(head);
}
boolean traverse(ListNode right) {
    if (right == null) return true;
    boolean res = traverse(right.next);
    // 后序遍历代码
    res = res && (right.val == left.val);
    left = left.next;
    return res;
}

优化空间复杂度:设置快慢指针,反转后一部分链表,为不破坏原始链表结构,return 之前再反转回来。( 东哥解答

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-月光光-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值