25. K组一个翻转链表

题目描述

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

示例1:
image.png

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

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

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

翻转链表的模板

ListNode* reverseList(ListNode* head) {
    ListNode* now = NULL;
    ListNode* next = NULL;
    while(head!=NULL)
    {
        next = head->next;
        head->next = now;
        now = head;
        head = next;
    }
    return now;
}

值得记录的是,链表相关的题目中常用的一个技巧是设置一个空的表头,就像上面代码中的now,设置一个这样的表头能够免去一些繁琐的边界判断。

迭代法求解

ListNode* reverseKGroup(ListNode* head, int k) {
    ListNode *now, *prev, *last, *tmp;
    int cnt = 0;
    ListNode* dummy = new ListNode;
    dummy -> next = head;
    now = last = dummy;
    while(now) {
        ++cnt;
        now = now -> next;
        if(cnt == k && now) {
            prev = last -> next;
            while(--cnt) {
                tmp = last -> next;
                last -> next = tmp -> next;
                tmp -> next = now -> next;
                now -> next = tmp;
            }
            now = last = prev;
        } 
    }
    return dummy -> next;    
}

同样,我们需要设置一个头结点以免去边界判断,在代码中体现为dummy。由于涉及到每K个翻转以及每一段的拼接问题,我们不像模板中那样处理新建的头结点,而保持dummy指针不动,作为最后返回答案的一个标记位置。
同时,用now标记每一节链表翻转后的头(即翻转前的尾,其实上面的last和now互换后更容易理解,这个看个人习惯),last标记上一轮翻转后的尾结点(包括dummy),prev提前记录这一轮翻转后的尾结点(可看做是下一轮的last)。
迭代法翻转的核心思路是 —— last(在链表中的)位置不动,以now为基点,依次将各个结点插入到now的后面。
依次翻转图示如下:
image.png

由于每一次翻转while的判断条件是--cnt,因此每一次只需操作k-1次。
容易得到k-1次操作过程中,图中翻转区结点值的变化:1->2->3->4 —— 2->3->4->1 —— 3->4->2->1 —— 4->3->2->1
空间复杂度:用到5个辅助指针,空间复杂度为O(1)
时间复杂度:仅需观察now结点和小while循环次数。now结点经历了完整的一次遍历链表,而while循环共进行(k-1)*(n/k) ≈ n次。因此相当于遍历了2n次,空间复杂度为O(n)。

递归法求解

ListNode* reverseKGroup(ListNode* head, int k) {
        if(head == NULL || head->next == NULL || k==1)return head;
        ListNode* tail = head;
        int x = k;
        while(x) {
            tail = tail -> next;
            --x;
            if(!tail && x>0) return head;
        }
        ListNode* prev = head, *now = head -> next;
        ListNode *tmp = NULL;
        while(now && now != tail) {
            tmp = now -> next;
            now -> next = prev;
            prev = now;
            now = tmp;
        }
        head -> next = reverseKGroup(tail, k);
        return prev;
    }

递归思路即只关心K个结点的翻转,而不关心超过K个的其他结点。在每次翻转前,先判断结点数是否够K个,若足够,则进行一次翻转,若不足K个(当前结点tial为空结点,但计数未到K——!tail&&x>0),则不需翻转,直接返回头结点。
真正做翻转操作时与翻转模板差异不大,知识尾结点变为了tail而不是NULL。
递归调用时,直接令head->next = 下一轮递归结果,而返回prev(翻转后头结点)。
可以看到,递归法省去手动拼接过程,思路较为清晰。
空间复杂度:由于递归使用了栈,大概进行了n/k次递归,每次递归O(1)复杂度,空间复杂度可表示为O(n/k)
时间复杂度:与迭代法相同,进行了至多两轮遍历,复杂度为O(n)

返回倒数第K个结点

题目描述

题目链接:https://leetcode-cn.com/probl...

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

示例:
输入: 1->2->3->4->5 和 k = 2
输出: 4

快慢指针法

链表经典题,快慢指针,只需一次遍历。时间复杂度O(n),空间复杂度O(1)

int kthToLast(ListNode* head, int k) {
        ListNode* fast = head, *low = head;
        while(k--) {
            fast = fast -> next;
        }
        while(fast) {
            fast = fast -> next;
            low = low -> next;
        }
        return low -> val;
    }

160. 相交链表

题目描述

题目链接:https://leetcode-cn.com/probl...

编写一个程序,找到两个单链表相交的起始节点。

示例:
image.png
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

注:skipA和skipB仅做理解用,并不作为接口的参数

快慢指针

思路:由题目输入所给的提示可以看出,如果两个链表有相同的结点,那么在公共结点之前,两个链表分别有自己的子链表,而这两条子链表的长度差正好是两条链表的长度差(因为从公共结点开始两个链表都是相同的)。知道了这一点,就有了一个思路——假设两个链表长度差为diff,那么让指向较长链表的指针先走diff步,然后两个指针同时走,再消除了长度差后,两个指针必然同时走到公共结点(如果有的话)。

ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(!headA || !headB) return NULL;
        int len1 = 0, len2 = 0;
        ListNode *node1 = headA, *node2 = headB;
        while(node1) {
            node1 = node1 -> next;
            ++len1;
        }
        while(node2) {
            node2 = node2 -> next;
            ++len2;
        }
        int diff = len1 - len2;
        node1 = headA; node2 = headB;
        if(diff > 0) {
            while(diff--) {
                node1 = node1 -> next;
            }
        }
        else {
            while(diff++ < 0) {
                node2 = node2 -> next;
            }
        }

        while(node1 != node2) {
            node1 = node1 -> next;
            node2 = node2 -> next;
        }
        return node1;
    }

时间复杂度:显然,两个链表都只遍历了两次,时间复杂度为O(m+n)
空间复杂度:O(1)

巧解法

思路:巧解法思路的精髓在于,若两个链表长度为a,b,那么a+b=b+a。这就意味着,让两个链表的末尾都指向对方的开头,形成的两条新链表的长度是一致的。并且,如果两个链表有公共结点,那么形成的两条长链表的结尾必然有公共部分(因为两个原始链表本来就有公共部分,只是原来两个链表的长度可能不同,不好比较,但两条长链表)。画出两个长链表一目了然。基于这个思路,可以得到如下简洁代码:

ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(!headA || !headB) return NULL;
        ListNode *node1 = headA, *node2 = headB;
        while(node1 != node2) {
            node1 = node1 ? node1 -> next : headB;
            node2 = node2 ? node2 -> next : headA; 
        }
        return node1;
    }

找到链表环入口

题目描述

题目链接:https://leetcode-cn.com/probl...

给定一个链表,如果它是有环链表,实现一个算法返回环路的开头节点。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

image.png

输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

剑指offer经典题,快慢指针。

快慢指针法

ListNode *detectCycle(ListNode *head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != nullptr)
        {
            slow = slow->next;
            // 无环的情况
            if (fast->next == nullptr)
            {
                return nullptr;
            }
            fast = fast->next->next;
            // 找到相交点
            if (fast == slow)
            {
                // 作为新的起点继续走
                ListNode* curr = head;
                while (curr != fast)
                {
                    fast = fast->next;
                    curr = curr->next;
                }
                return curr;
            }
        }

        return nullptr;
    }

时间复杂度O(n), 空间复杂度O(1)。

附快慢指针为什么能成功的数学证明:https://leetcode-cn.com/probl...

130. 复制带随机指针的链表

题目描述

给你一个长度为 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:
image.png
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

朴素解法

链表的复制并不复杂,麻烦在random的处理,random指向的随机性,没有办法一次性解决复制(因为新链表中,前面指针的random指向的结点可能还没生成)。
朴素解法的思路:

  1. 先做一遍复制,对于val以及next做深拷贝,而对于random只做浅拷贝
  2. 在复制的同时,用一个map记录新节点和源节点的对应关系。
  3. 做第二遍遍历,根据map中的对应关系,替换random指向的结点。

    class Solution {
    public:
     Node* copyRandomList(Node* head) {
         if(!head) return head;
         unordered_map<Node*, Node*> mp;
         Node* newhead = new Node(head->val);
         newhead->random = head->random;
         Node *last = newhead;
         mp[head] = newhead;
         while(head) {
             head = head->next;
             if(!head) last->next = nullptr;
             else {
                 Node* newNode = new Node(head->val);
                 newNode->random = head->random;
                 last->next = newNode;
                 last = last->next;
                 mp[head] = newNode;
             }
         }
         last = newhead;
         while(last) {
             last->random = mp[last->random];
             last = last->next;
         }
         return newhead;
     }
    };

    递归法

    递归法思路简洁,对于一个结点的拷贝,直接拷贝val,然后递归地创建next结点和random结点。如果next结点和random结点已存在,则直接返回,如果不存在,则继续进行递归创建。为了记录新结点的创建情况,需要用一个map记录。
    PS:用递归法,结点的拷贝是相互独立且不按序的,一般来说,最先被创建的会是最后一个结点

    class Solution {
    public:
     unordered_map<Node*, Node*> mp;
     Node* copyRandomList(Node* head) {
         if(!head) return nullptr;
         if(!mp.count(head)) {
             Node* node = new Node(head->val);
             mp[head] = node;
             node->next = copyRandomList(head->next);
             node->random = copyRandomList(head->random);
         }
         return mp[head];
     }
    };

结点拆分法

前两种方法在时间复杂度上都达到了O(n)标准,但在空间上都有瓶颈。两种方法都用一个map去记录新旧结点的对应关系,但实际上这个关系并不复杂,而是非常直接的对应。因此,可以用一个技巧去解决这个对应关系——即直接将新的拷贝结点放在源节点之后,形成一条2n长的链表,然后处理random和next指针。

  1. 第一次循环,建立拷贝结点,放在旧结点之后
  2. 处理新节点的random指针,此时只需根据旧结点的random做一次偏移即可
  3. 分离新旧结点,处理next指针
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(!head) return head;
        Node* node = head;
        while(node) {
            Node* newNode = new Node(node->val);
            newNode->next = node->next;
            node->next = newNode;
            node = node->next->next;
        }
        node = head;
        while(node) {
            if(!node->random) node->next->random = nullptr;
            else node->next->random = node->random->next;
            node = node->next->next;
        }
        Node* newhead = head->next;
        node = head;
        while(node) {
            Node* tmp = node->next;
            node->next = node->next->next;
            if(!tmp->next) tmp->next = nullptr;
            else tmp->next = tmp->next->next;
            node = node->next;
        }
        return newhead;
    }
};

148.排序链表

题目描述

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表。

思路

快排、堆排没法用,想要时间复杂度最低,只能用归并。
链表的归并细节很多,主要有:

  1. 每一次分组的所使用sublen,应从1开始,每一轮归并后乘2,直到sublen >= 链表长度
  2. 对于每一轮每一组链表,都应拆分成两个子链表,用两个表头head1和head2表示
  3. 对于每一轮每一组链表,它的长度很可能小于sublen,甚至小于sublen/2,则head2很可能为nullptr,甚至head1表示的链表也可能只有很短的长度。因此在用sublen取子链表时,对head1要判断now->next是否为nullptr,对head2要判断now和now->next是否为nullptr
  4. 设置dummy使得代码更简洁,但在返回前需要释放,否则会内存泄漏

代码

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if(!head || !head->next) return head;
        int len = 0;
        ListNode *now = head;
        while(now) {
            ++len;
            now = now->next;
        }
        ListNode *dummy = new ListNode(0, head);
        ListNode *prev = dummy;
        for(int sublen = 1; sublen < len; sublen *= 2) {
            prev = dummy;
            now = dummy->next;
            while(now) {
                ListNode *head1 = now;
                for(int i=0; i<sublen-1 && now->next; ++i) {
                    now = now->next;
                }
                ListNode *head2 = now->next;
                now->next = nullptr;
                now = head2;
                for(int i=0; i<sublen-1 && now && now->next; ++i) {
                    now = now->next;
                }
                ListNode *next = now ? now->next : nullptr;
                if(now) now->next = nullptr;
                prev->next = merge(head1, head2);
                while(prev->next) {
                    prev = prev->next;
                }
                now = next;
            }
        }
        head = dummy->next;
        delete dummy;
        return head;
    }

    ListNode* merge(ListNode* head1, ListNode* head2) {
        if(!head1) return head2;
        if(!head2) return head1;
        ListNode* dummy = new ListNode;
        ListNode* now = dummy;
        while(head1 && head2) {
            if(head1->val < head2->val) {
                now->next = head1;
                head1 = head1->next;
            }
            else {
                now->next = head2;
                head2 = head2->next;
            }
            now = now->next;
        } 
        if(head1) now->next = head1;
        if(head2) now->next = head2;
        head1 = dummy->next;
        delete dummy;
        return head1;
    }
};

SalvationN
1 声望0 粉丝