当前位置:   article > 正文

C语言数据结构初阶(6)----链表常见OJ题_c语言链表操作题

c语言链表操作题
CSDN的uu们,大家好!编程能力的提高不仅需要学习新的知识,还需要大量的练习。所以,C语言数据结构初阶的第六讲邀请uu们一起来看看链表的常见oj题目。
  1. 移除链表元素

原题链接: 203. 移除链表元素 - 力扣(Leetcode)
题目描述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

1.1 解法1:三指针

下面我们以一个具体的例子来分析一下:1->2->3->2->4->NULL,假设我们要删除的元素是2,即val==2。

我们可以维护三个指针,prev,cur和next,用cur遍历整个链表,如果说cur->val == val,我们就释放掉cur指向 的节点,然后令prev->next = next,然后更新cur的指向:cur = next,更新prev的指向:prev = cur,直到cur指向NULL结束遍历链表。

我们来看看有什么特殊情况?

以链表:2->3->2->4->NULL为例。如果说要删除的节点是头结点,我们在释放cur指向的节点之后,还需要改变原来链表的头结点,然后更新cur指向的节点!

如果说cur指向的节点的val值等于要删除的val值,并且此时的prev为空,那么我们是不需要令

prev->next = next的,这样会发生空指针的解引用,程序会崩溃!因此我们需要对此情况进行特殊处理!当prev指向空指针时,并且此时cur指向的节点正是要删除的节点!需要跳过prev->next=next这一步。

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

  1. struct ListNode* removeElements(struct ListNode* head, int val){
  2. //处理极端情况
  3. if(!head)
  4. return NULL;
  5. //初始化cur和prev
  6. struct ListNode* prev = NULL, *cur = head;
  7. while(cur)
  8. {
  9. //找到cur的下一个节点
  10. struct ListNode* next = cur->next;
  11. //如果cur指向的节点是要删除的节点
  12. if(cur->val == val)
  13. {
  14. //如果prev==NULL,不需要令prev->next = next
  15. if(!prev)
  16. {
  17. free(cur);
  18. cur = next;
  19. //if(prev == NULL)也说明了删除的是头结点
  20. //更新新的头节点
  21. head = next;
  22. }
  23. else
  24. {
  25. //if(prev!=NULL),释放链接即可
  26. free(cur);
  27. prev->next = next;
  28. cur = next;
  29. }
  30. }
  31. else
  32. {
  33. //如果说cur指向的节点不是要删除的节点
  34. //正常迭代三个指针就行
  35. prev = cur;
  36. cur = next;
  37. }
  38. }
  39. return head;
  40. }

1.2 解法二:尾插法

我们初始化如下指针:cur指针用来遍历整个链表,newHead指针用来当新链表的头结点,tail指针指向新链表的尾节点,方便尾插!当然新的链表你可以整一个哨兵位的头结点,这样就不要对newHead是否为空指针进行特殊判断了!

我们用cur指针遍历整个链表,因为涉及节点的释放,在遍历的过程中我们需要临时保存一下cur节点的下一个节点!例如:用next指针保存cur的下一个节点。在遍历的过程中,如果说cur指向的节点val值不等于要删除的val值,我们就将该节点头插到新的链表。

如果你没有用带哨兵位的头结点,那么就需要对newHead进行判断,如果newHead为空指针,你需要更新newHead的值,如果不为空指针,你只需要令tail->next = cur,然后更新tail指针的指向就行!

如果cur指向的节点的val值等于要删除的val值,我们free掉cur,然后令cur = next即可!直到cur指针完成对链表的遍历。

还有一个要注意的点是:tail在遍历过程中的更新是令tail = cur的,因为我们不知道哪个节点是新链表的尾节点,所以在tail不为空指针的时候需要将tail->next置为NULL。

例如:1->2->3->4->2,要删除节点值为2的节点。尾插后我们得到新链表:1-3->4,如果我们不在此过程中令tail->next = NULL,那么新链表4这个节点的next,指向的还是原来链表中的尾节点2。因为2这个节点已经被释放了!2这个节点的指针就是个野指针。显然会出问题的。

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

  1. struct ListNode* removeElements(struct ListNode* head, int val)
  2. {
  3. //初始化节点
  4. struct ListNode* cur = head, *newHead = NULL, *tail = NULL;
  5. while(cur)
  6. {
  7. //临时保存cur->next
  8. struct ListNode* next = cur->next;
  9. //删除值等于val的节点
  10. if(cur->val == val)
  11. {
  12. free(cur);
  13. }
  14. else
  15. {
  16. //如果newHead==NULL,改变newHead的指向
  17. if(!newHead)
  18. {
  19. newHead = cur;
  20. tail = cur;
  21. }
  22. else
  23. {
  24. //正常连接
  25. tail->next = cur;
  26. tail = cur;
  27. }
  28. }
  29. cur = next;
  30. //如果tail不为空,需要将tail->next=NULL
  31. if(tail)
  32. tail->next = NULL;
  33. }
  34. return newHead;
  35. }

2. 反转单链表

原题链接: 206. 反转链表 - 力扣(Leetcode)
题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

2.1 反转指针

看到这道题之后我们会很自然地想到只要将链接节点的指针反转过来,改变原来链表的方向,然后再从头到尾遍历输出到数组即可。

我们可以维护三个指针prev,cur,next。prev初始化为空指针,cur初始化为头结点的指针,next初始化为头结点的下一个节点的指针。

为什么要这么做呢?维护prev的目的是方便反转节点指针后,指向新的节点或者NULL,维护cur的目的就是遍历整个的链表,维护next的目的是方便找到cur的下一个节点,因为一旦反转了指针不用一个指针提前找到cur的下一个节点,就真的找不到下一个节点了!

反转一次之后,把cur给给prev,把next给给cur,next的下一个节点的指针给给next,以此类推直到cur为空指针。我们就完成了反转链表。最后将新的头结点prev赋值给原链表的头结点(见下图)。

  1. struct ListNode* reverseList(struct ListNode* head){
  2. //处理特殊情况
  3. if(head == NULL)
  4. return NULL;
  5. struct ListNode* prev = NULL, *cur = head;
  6. while(cur)
  7. {
  8. //保存下一个节点
  9. struct ListNode* next = cur->next;
  10. //链接
  11. cur->next = prev;
  12. prev = cur;
  13. cur = next;
  14. }
  15. //返回
  16. return prev;
  17. }

2.2 头插节点

我们可以维护一个指针newHead,初始化为NULL,然后用一个指针cur遍历原链表,将cur指向的节点头插到新的链表,同样这里需要对newHead进行特殊判断。因为上面那道题我们没有用哨兵位的头结点,所以这里就使用带哨兵位的头结点的链表来完成解题,这样就不需要做特殊判断啦!

这里的头插不是新搞一个链表而是取原节点下来头插哦!因此需要保存cur->next。

  1. struct ListNode* reverseList(struct ListNode* head){
  2. struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
  3. struct ListNode* cur = head;
  4. newHead->next = NULL;
  5. while(cur)
  6. {
  7. //保存下一个节点
  8. struct ListNode* next = cur->next;
  9. //新链表的下一个节点
  10. struct ListNode* pnext = newHead->next;
  11. //链接
  12. newHead->next = cur;
  13. cur->next = pnext;
  14. cur = next;
  15. }
  16. //返回节点
  17. return newHead->next;
  18. }

3. 链表的中间节点

原题链接: 876. 链表的中间结点 - 力扣(Leetcode)
题目描述:给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

3.1 双指针

我们维护两个指针slow,fast均初始化为头结点head,然后让slow向前走一步,fast向后走两步。

当节点个数为偶数时:判断结束的条件就是fast == NULL。

当节点个数为奇数时:判断结束的条件就是fast->next = NULL。

  1. struct ListNode* middleNode(struct ListNode* head){
  2. //初始化节点
  3. struct ListNode* slow = head, *fast = head;
  4. //两个判断结束的条件
  5. while(fast && fast->next)
  6. {
  7. //slow走一步,fast走两步
  8. slow = slow->next;
  9. fast = fast->next->next;
  10. }
  11. //返回slow
  12. return slow;
  13. }

4. 链表中倒数第K个节点

原题链接: 剑指 Offer 22. 链表中倒数第k个节点 - 力扣(Leetcode)
题目描述:
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

4.1 统计链表长度

题目求的是链表的倒数第 k 个节点,根据题目得知这是一个单向链表,我们无法从后往前找到链表的倒数第 k 个节点,于是我们会想到从前往后去找倒数第 k 个节点。但是我们不知道倒数第 k 个节点前面有多少个节点。于是,我们很自然地想到先求出链表的节点个数 count,然后就求出了倒数第 k 个节点前有 count - k 个节点,然后我们再用一个指针去遍历找到倒数第 k 个节点就可以啦。

  1. struct ListNode* getKthFromEnd(struct ListNode* head, int k){
  2. //deal with special circumstances
  3. if(head == NULL || k <= 0)
  4. return NULL;
  5. struct ListNode* cur = head;
  6. int count = 0;
  7. //statistical the length of list
  8. while(cur)
  9. {
  10. cur = cur->next;
  11. ++count;
  12. }
  13. //deal with special circumstance
  14. if(k > count)
  15. return NULL;
  16. //search the answer
  17. struct ListNode* ret = head;
  18. for(int i = 1; i < count - k + 1; ++i)
  19. {
  20. ret = ret->next;
  21. }
  22. return ret;
  23. }

4.2 快慢指针

遍历两次链表还是太麻烦了,能不能只遍历一次链表就找到结果呢?

答案是肯定的,我们可以维护两个指针slow,fast,将他们均初始化为头结点 head。然后让 slow 指针先不动,让 fast 指针先向后走k-1步,此时slow与fast之间就隔了 k - 1 个节点,然后保持slow与fast之间的距离始终为 k - 1个节点,即之后让 slow 与 fast 同时向后移动。直到 fast 指向尾节点,此时 slow 就指向了链表的倒数第 k 个节点。

下面以链表 1->2->3->4->5->NULL ,k = 3,举例分析:

  1. struct ListNode* getKthFromEnd(struct ListNode* head, int k){
  2. //deal with special circumstances
  3. if(k <= 0)
  4. return NULL;
  5. struct ListNode* fast = head, *slow = head;
  6. while(--k)
  7. {
  8. //head == NULL or k > the length of list
  9. if(fast == NULL)
  10. return NULL;
  11. fast = fast->next;
  12. }
  13. while(fast->next)
  14. {
  15. //move respectively
  16. slow = slow->next;
  17. fast = fast->next;
  18. }
  19. //return result
  20. return slow;
  21. }

5. 合并两个有序的链表

原题链接: 21. 合并两个有序链表 - 力扣(Leetcode)
题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

5.1 归并思想

这道题的思路和合并两个有序数组的思路相似,都是用归并的思想解题。为了方便节点的插入,同样我们新建一个带哨兵位的头结点的链表,然后我们维护两个指针p1,p2初始化为两个链表的头结点,比较两指针指向的节点的val值,将val值较小的节点尾插到新的链表中。依次类推,当有一个指针指向NULL时终止循环,然后链接指向不为NULL的链表,最后返回结果即可。

注意:当待合并的链表中有空的链表时直接返回另一个链表的头结点即可。

下面以链表:0->2->3->NULL

1->4->5->NULL

举例分析:

  1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
  2. //deal with special circumstances
  3. if(!list1 || !list2)
  4. return list1 ? list1 : list2;
  5. //head node of sentinel position
  6. struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
  7. //cur1 and cur2 is used to traverse list
  8. struct ListNode* tail = newHead, *cur1 = list1, *cur2 = list2;
  9. //until one of the lists is empty, the loop ends
  10. while(cur1 && cur2)
  11. {
  12. //compare the val
  13. if(cur1->val < cur2->val)
  14. {
  15. tail->next = cur1;
  16. tail = cur1;
  17. cur1 = cur1->next;
  18. }
  19. else
  20. {
  21. tail->next = cur2;
  22. tail = cur2;
  23. cur2 = cur2->next;
  24. }
  25. }
  26. //connect
  27. if(!cur1)
  28. tail->next = cur2;
  29. else
  30. tail->next = cur1;
  31. //free the head node of sentinel position
  32. struct ListNode* returnNode = newHead->next;
  33. free(newHead);
  34. newHead = NULL;
  35. //return answer
  36. return returnNode;
  37. }

6. 链表分割

原题链接: 链表分割_牛客题霸_牛客网 (nowcoder.com)
题目描述:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

6.1 尾插法

我做这道题的时候想的是遍历原链表两次,一次将小于x的节点尾插到新链表,另一次就是将大于等于x的节点尾插到新链表。显然是没有这个必要的!

前面的链表OJ题我们已经可以看到哨兵位的头结点的优势。这里我们维护两个带哨兵位头结点的新链表,bigHead和smallHead,然后用一个指针cur遍历原来的链表,比x小的节点尾插到smallHead,大于等于X的节点尾插到bigHead。最后再将smallHead与bigHead连接起来,返回smallhead的下一个节点即可!尾插的话我们维护两个指针bigTail和smallTail,方便尾插!

注意点:

1:因为原链表的尾节点不一定是最大的节点,所以,在bigHead尾插完毕后需要令bigTail->next = NULL。

以链表1->3->2->1->NULL,x = 2,举例分析:

经过cur的遍历之后不难发现bigTail->next指向的其实是最后那个值为1的节点,如果这时候直接将smallHead与bigHead连接起来,返回结果显然是不对的,因此尾插完毕后需要令

bigHead->next = NULL。

2:当原链表中的所有节点均大于等于x会有问题吗?

以链表1->3->2->1->NULL,x = 1,举例分析:

cur指针遍历原链表后,smallHead中并没有尾插任何元素,现在我们尝试进行连接:

smallTail->next = bigHead->next;
return bigHead->next;

显然连接之后在返回结果是没啥问题的!

  1. class Partition {
  2. public:
  3. ListNode* partition(ListNode* pHead, int x) {
  4. //deal with special circumstance
  5. if(pHead == nullptr)
  6. return nullptr;
  7. //init two lists with head node of the sentinel position
  8. //bigHead list is used to push back the node whose value is greater than x or equal to x
  9. auto bigHead = (ListNode*)malloc(sizeof(ListNode));
  10. auto bigTail = bigHead;
  11. //smallHead list is used to push back the node whose value is less than x
  12. auto smallHead = (ListNode*)malloc(sizeof(ListNode));
  13. auto smallTail = smallHead;
  14. //pointer cur is used to traverse the list
  15. auto cur = pHead;
  16. //until pointer cur point to nullptr, the loop will end
  17. while(cur)
  18. {
  19. //campare the node val with x
  20. if(cur->val < x)
  21. {
  22. //connect to the cur node
  23. smallTail->next = cur;
  24. smallTail = cur;
  25. cur = cur->next;
  26. }
  27. else
  28. {
  29. bigTail->next = cur;
  30. bigTail = cur;
  31. cur = cur->next;
  32. }
  33. }
  34. //bigTail->next must point to nullptr, otherwise it will time limit exceed
  35. bigTail->next = nullptr;
  36. //connection
  37. smallTail->next = bigHead->next;
  38. return smallHead->next;
  39. }
  40. };

7. 链表的回文结构

原题链接: 剑指 Offer II 027. 回文链表 - 力扣(Leetcode)
题目描述:给定一个链表的 头节点 head ,请判断其是否为回文链表。
如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。

7.1 栈

这个题目的知识点他给的是栈哈,我刚开始就是用的栈解题!其实有一种方法是不需要用栈的,将上面几道题的函数拼接一下就可以啦!大家可以先思考思考!这里我们先来看看栈的解法:

栈的特点就是先进后出,因此我们遍历链表,将原链表中前一半的节点的val值压入栈中,紧接着在继续遍历原链表中后半部分的节点,弹出栈顶元素与遍历得到的节点的val值比较,如果说不相等,显然就不是回文结构!当把整个链表均遍历完毕后还没有返回false,那么这个链表就是回文链表。

注意点:

1:将前一半节点的val值压入栈中需要与题目三:链表的中间节点配合。当链表的节点数为奇数时,最后slow指向的节点时链表的中间节点,这个节点是不用参与与栈顶元素的比较的,因此将slow向后移动一步!当链表的节点数为偶数时,slow指向的节点就是第一个需要与栈顶元素进行比较的节点,无需做任何处理!

2:我们选择用数组模拟实现栈!

如有疑问请参考: http://t.csdn.cn/hvpKx
  1. bool isPalindrome(struct ListNode* head){
  2. //deal with special circumstances
  3. //A is a nullptr
  4. if (!head)
  5. return false;
  6. //only one node
  7. if (!head->next)
  8. return true;
  9. //we chosse simulate a stack
  10. int stack[100010];
  11. int tt = 0;
  12. //using fast and slow pointer traverse the list
  13. struct ListNode* slow = head, *fast = head;
  14. while (fast && fast->next) {
  15. //push the val of slow pointer
  16. stack[++tt] = slow->val;
  17. slow = slow->next;
  18. fast = fast->next->next;
  19. }
  20. //the length of list is odd
  21. if (fast)
  22. slow = slow->next;
  23. //compare with the stack top value
  24. while (slow) {
  25. if (slow->val == stack[tt])
  26. tt--;
  27. else //slow->val is not equal to the stack top value, return false
  28. return false;
  29. slow = slow->next;
  30. }
  31. return true;
  32. }

7.2 部分反转链表

我们需要通过题目三:链表的中间节点找到链表的中间节点!然后将得到的中间节点返回,传入题目2:反转链表的函数,反转链表的函数会返回一个新的节点newHead,然后我们同时遍历newHead指向的链表和原链表,如果出现节点值不相同的情况我们返回false就行了!

注意点:

1:如果我们对奇数链表的中间节点不做处理,我们来看看在遍历newHead指向的链表和原链表时循环条件应该怎么写?

如上图,对于奇数节点的链表,我们找到中间节点就传参给反转链表的函数,得到了新的newHead。

因为在反转链表的过程中我们并没有对图中橙色节点的next指针做处理,他的next还是指向蓝色这个节点的,在遍历head和newHead时我们的循环结束的条件就不好与链表的节点数为偶数的情况统一。因此在返回链表的中间节点时我们对奇数个节点数的链表做同解法一的处理,让slow向后走一步后再返回。

再来看链表个数时偶数的时候,我们就不需要做什么处理了!

想必大家都能看出循环的结束条件就是遍历newHead的指针为空了!为了统一还是有必要做一点点处理的。处理方法与方法一的相同。

  1. //reverse list
  2. struct ListNode* reverseList(struct ListNode* head)
  3. {
  4. struct ListNode* prev = NULL, *cur = head;
  5. struct ListNode* next;
  6. while (cur) {
  7. if (next)
  8. next = cur->next;
  9. cur->next = prev;
  10. prev = cur;
  11. cur = next;
  12. }
  13. return prev;
  14. }
  15. //find middle node
  16. struct ListNode* middleNode(struct ListNode* head)
  17. {
  18. struct ListNode* slow = head, *fast = head;
  19. while (fast && fast->next) {
  20. slow = slow->next;
  21. fast = fast->next->next;
  22. }
  23. //if list's length is odd, slow = slow->next
  24. if(fast)
  25. slow = slow->next;
  26. return slow;
  27. }
  28. bool isPalindrome(struct ListNode* head){
  29. //deal with special circumstance
  30. if (!head)
  31. return false;
  32. //middle node
  33. struct ListNode* mid = middleNode(head);
  34. //reverse the list after middle node
  35. struct ListNode* newHead = reverseList(mid);
  36. //traverse respectively
  37. while (newHead) {
  38. //compare the value
  39. if (head->val != newHead->val)
  40. return false;
  41. //iteration
  42. head = head->next;
  43. newHead = newHead->next;
  44. }
  45. return true;
  46. }

8. 相交链表

原题链接: 160. 相交链表 - 力扣(Leetcode)
题目描述:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

8.1 最好想的思路

我们无法确定相等的节点会在什么时候出现,即使你想到去比较节点的地址也不行的哦!

我们看到在两个链表的相交节点之前的节点数是不确定的!这限制了我们的解题。

因此我们可以先统计每个链表的长度,找到两个链表中较长的那个。然后让遍历较长的那个链表的指针先向前走两个链表长度之差的绝对值步。这样在相交的节点之前的两个链表的节点数就相等了!我们在同时遍历,找到节点的地址相同的节点就行啦!

测试用例中两个链表中可能不存在交点的,因此我们还得知道如何判断两个链表是否有交点。很简单的,我们在统计链表的长度时,看看两个链表的尾节点的地址是否相同就行啦!

  1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
  2. //deal with special circumstance
  3. if(!headA || !headB)
  4. return NULL;
  5. //count the length of each list
  6. int lenA = 1, lenB = 1;
  7. struct ListNode* cur1 = headA, *cur2 = headB;
  8. while(cur1->next)
  9. {
  10. ++lenA;
  11. cur1 = cur1->next;
  12. }
  13. while(cur2->next)
  14. {
  15. ++lenB;
  16. cur2 = cur2->next;
  17. }
  18. //there is no node of intersection
  19. if(cur1 != cur2)
  20. return NULL;
  21. int count = abs(lenA - lenB);
  22. //hypothesize the longer list is listA
  23. struct ListNode* longList = headA, *shortList = headB;
  24. if(lenA < lenB)
  25. {
  26. //assumption fails
  27. longList = headB;
  28. shortList = headA;
  29. }
  30. //find the returned node
  31. while(count--)
  32. {
  33. longList = longList->next;
  34. }
  35. while(shortList != longList)
  36. {
  37. shortList = shortList->next;
  38. longList = longList->next;
  39. }
  40. return longList;
  41. }

9. 环形链表Ⅰ

原题链接: 141. 环形链表 - 力扣(Leetcode)
题目描述:给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。

9.1 快慢指针

这道题的解题思路和“链表中的倒数第k个节点”的解题思路是相似的。快慢指针的问题,我们维护两个指针slow,fast,初始化为链表的头结点。然后我们让slow向后走一步,而fast向后走两步,如果说链表不存在环,那么fast会最终会指向NULL,此时我们结束循环返回false;

如果链表中存在环,因为fast走得快,slow走得慢,当slow刚进入环指向环内节点时,fast已经走了若干圈环,指向环上的某一个节点,此时slow和fast之间会有一段距离x (x>=0)。

因为fast走得比slow快一步,每一回合slow与fast之间的节点个数都会减一,所以在环内fast在与slow相遇之前一直在追slow,无论在slow进入环时,fast与slow相距多少个节点最终都会相遇,故当slow与fast相遇时即可判断链表中有环。

下面以环形链表1->2->3->4->5->6->7->8->9->10->4来举例分析:

  1. bool hasCycle(struct ListNode *head) {
  2. //deal with special circumstance
  3. if(!head)
  4. return false;
  5. //init two pointers
  6. struct ListNode* slow = head, *fast = head;
  7. //if there is not circle in the list, the fast pointer will point to nullptr
  8. while(fast && fast->next)
  9. {
  10. //fast pointer moves two steps
  11. fast = fast->next->next;
  12. //slow pointer moves one step
  13. slow = slow->next;
  14. //if the slow pointer equals to the fast pointer
  15. //it shows it has circle
  16. if(slow == fast)
  17. return true;
  18. }
  19. return false;
  20. }

9.2 扩展问题

(1):在slow走一步,fast走两步的条件下有环就一定会相遇吗?
(2):如果slow走一步,fast走三步行不行?如果slow走一步,fast走四步呢?

答:

(1):根据上面解题的分析在slow走一步,fast走两步的条件下,有环必定相遇。

(2):slow走一步,fast走三步,以及slow走一步fast走四步均不一定能相遇,证明过程如下:

因为环中的节点个数是不确定的,我就直接画一个圆来代表链表中的环哈。

我们把的得到的结论往Leetcode上去测试,发现slow走一步,fast走三步也是可以通过的,对于本题leetcode上的测试用例似乎并没有考虑到这一点。

  1. bool hasCycle(struct ListNode *head) {
  2. //deal with special circumstance
  3. if(!head)
  4. return false;
  5. //init two pointers
  6. struct ListNode* slow = head, *fast = head;
  7. //if there is not circle in the list, the fast pointer will point to nullptr
  8. while(fast && fast->next && fast->next->next)
  9. {
  10. //fast pointer moves two steps
  11. fast = fast->next->next->next;
  12. //slow pointer moves one step
  13. slow = slow->next;
  14. //if the slow pointer equals to the fast pointer
  15. //it shows it has circle
  16. if(slow == fast)
  17. return true;
  18. }
  19. return false;
  20. }

10. 环形链表Ⅱ

原题链接: 142. 环形链表 II - 力扣(Leetcode)
题目描述:
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。

10.1 转化为两个链表的交点

如果说链表有环的环,那么我们可以找到环形链表中1中fast和slow相遇的节点,让后将环断开。这样一来这个问题就转化为了找两个链表的相交节点啦!

具体怎么做呢?我们假设在环中slow与fast相遇的节点为meet,那么我们找到meet的下一个节点,记为newHead,让后再令meet->next=NULL,这样做就转化成功啦!

  1. struct ListNode *detectCycle(struct ListNode *head) {
  2. //deal with special circumstance
  3. if(!head)
  4. return NULL;
  5. //init two pointers to find the meet node
  6. struct ListNode* slow = head, *fast = head;
  7. while(fast && fast->next)
  8. {
  9. slow = slow->next;
  10. fast = fast->next->next;
  11. //if slow == meet, shows the list has circle
  12. if(slow == fast)
  13. {
  14. //find the meet node
  15. struct ListNode* meet = slow;
  16. //record meet->next
  17. struct ListNode* l1 = head, *l2 = meet->next, *pl2 = meet->next;
  18. meet->next = NULL;
  19. //count the length of each list
  20. //there must be the node of intersection
  21. int len1 = 0, len2 = 0;
  22. while(l1)
  23. {
  24. len1++;
  25. l1 = l1->next;
  26. }
  27. while(pl2)
  28. {
  29. len2++;
  30. pl2 = pl2->next;
  31. }
  32. //find the longList
  33. struct ListNode* longlist = head, *shortlist = l2;
  34. if(len1 < len2)
  35. {
  36. longlist = l2, shortlist = head;
  37. }
  38. int count = abs(len1 - len2);
  39. //let the longList move advanced
  40. while(count--)
  41. {
  42. longlist = longlist -> next;
  43. }
  44. //find the nodes which have same address
  45. while(shortlist != longlist)
  46. {
  47. longlist = longlist->next;
  48. shortlist = shortlist->next;
  49. }
  50. return longlist;
  51. }
  52. }
  53. return NULL;
  54. }

10.2 结论解题

直接上结论:初始化一个指针:meet,让他指向在环形链表Ⅰ中slow与fast相遇的位置,再初始化一个指针:cur,让他指向链表的头结点,然后让cur和meet指针以相同的速度(一次循环走一步)往后走,最终他们会在环形链表的入口处相遇。

嘶,第一次看到这个结论的我是一脸懵啊。但是这个结论是有严格的证明滴,现在我们就来证明证明。

  1. struct ListNode *detectCycle(struct ListNode *head) {
  2. //deal with special circumstance
  3. if(!head)
  4. return NULL;
  5. //init two pointer to find meet node
  6. struct ListNode* slow = head, *fast = head;
  7. while(fast && fast->next){
  8. slow = slow->next;
  9. fast = fast->next->next;
  10. if(fast == slow)
  11. {
  12. //conclusion
  13. struct ListNode* meet = slow, *cur = head;
  14. while(meet!=cur)
  15. {
  16. meet = meet->next;
  17. cur = cur->next;
  18. }
  19. return cur;
  20. }
  21. }
  22. //there is not circle
  23. return NULL;
  24. }

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

原题链接: 138. 复制带随机指针的链表 - 力扣(Leetcode)
题目描述:
给你一个长度为 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 作为传入参数。

11.1 解题思路

因为链表中可能存在重复的val值,我们不能仅凭节点的val值来判断random指针的指向。因此可能会想到使用结构体指针数组。遍历原链表在深拷贝节点的同时,将每个节点random指针指向节点的相对位置用数组记录下来:首先我们让原链表中的节点分别对应下标1,2,3,4,5,11这个节点是原链表的第三个节点,因为1这个节点的random指针指向的是11这个节点,所以数组下标为5的位置就存3。

同时将拷贝出的节点的地址记录在另一个数组中。进行random指针的链接时,我们同时遍历两个数组,就可以链接成功了!是不是特别麻烦,这么看来的确是的。有没有更简单的方法呢?答案是肯定的。

更简单的方法分为三步:

第一步:拷贝原节点。我们用一个指针,例如cur,去遍历原链表,每遍历一个节点,我们就开辟一个新的节点。然后将cur指向节点的val值赋给新开辟的节点。最重要的一步来咯:我们将新节点链接到cur指向的节点和cur->next指向的节点之间。至于为什么这么做,且看第二步。要完成这一步操作我们需要维护三个指针:cur,next,copyNode。cur就是遍历原链表的那个节点,next用于保存cur的下一个节点,方便copyNode节点的插入,copyNode就是cur的拷贝节点。

我们来看看完成之后的效果:

第二步:链接random指针。这里就能体现出第一步的妙处所在了。怎么链接呢?这里我们需要维护三个指针:cur,copyNode,next。cur指针用来遍历原链表,copyNode就是cur->next,也就是第一步拷贝的节点,next是copyNode->next。

我们来看怎么链接:

copyNode->random = cur->random->next
是不是非常的厉害,下面演示了random的一次链接,画全的话太混乱了。

一个节点连接完成之后,就令cur = next进行下一个节点的链接就行!当cur->random = NULL时需要特殊处理一下哦!

第三步:取下拷贝节点,恢复原链表。这一步就比较简单啦!同样维护三个指针,cur,copyNode,next。cur指针用于遍历原链表,copyNode用来尾插,next是copyNode->next,cur遍历一个节点后就令cur = next。emm还要有一个哨兵位的头结点phead,用于copyNode的尾插,当然不用哨兵位的头结点就需要对特殊情况进行处理啦!链接就是cur->next = next,就行啦!

  1. struct Node* copyRandomList(struct Node* head) {
  2. //deal with special circumstance
  3. if(!head)
  4. return NULL;
  5. //copy list
  6. struct Node* cur = head;
  7. while(cur)
  8. {
  9. //record the next node to avoid fail to find next node
  10. struct Node* next = cur->next;
  11. //malloc a new node
  12. struct Node* copyNode = (struct Node*)malloc(sizeof(struct Node));
  13. //assignment
  14. copyNode->val = cur->val;
  15. //connect
  16. cur->next = copyNode;
  17. copyNode->next = next;
  18. //iteration
  19. cur = next;
  20. }
  21. //connect the random pointer
  22. cur = head;
  23. while(cur)
  24. {
  25. struct Node* copyNode = cur->next, *next = copyNode->next;
  26. //special circumstance
  27. if(cur->random == NULL)
  28. copyNode->random = NULL;
  29. else
  30. copyNode->random = cur->random->next;
  31. //iteration
  32. cur = next;
  33. }
  34. //resume the original list
  35. cur = head;
  36. //the head node of the sentinel position
  37. struct Node* pHead = (struct Node*)malloc(sizeof(struct Node));
  38. //make push back convenient
  39. struct Node* tail = pHead;
  40. while(cur)
  41. {
  42. struct Node* copyNode = cur->next, *next = copyNode->next;
  43. //resume
  44. cur->next = next;
  45. //push back
  46. tail->next = copyNode;
  47. tail = copyNode;
  48. //iteration
  49. cur = next;
  50. }
  51. //return answer
  52. return pHead->next;
  53. }
链表常见的OJ题讲到这里就差不多啦
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/707974
推荐阅读
相关标签