【链表复习】C++ 链表复习及题目解析(1)

15 篇文章 0 订阅
15 篇文章 0 订阅

目录

写在前面

链表

链表的概念和结构

链表的概念

链表的分类

单向链表和双向链表

带头链表和不带头链表

循环链表和非循环链表

常用

链表题目

LeetCode 203 之删除某值节点

LeetCode 206 之 反转链表

LeetCode 876 之链表的中间结点

牛客网 链表中的倒数第k 个结点

LeetCode 21 之合并链表


写在前面

链表在面试题和笔试题中出现的频率相当的高,所以我们应该着重的复习链表和经典的链表相关题目。

链表

链表的概念和结构

链表的概念

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

注意:

  1. 链表在逻辑上连续,但是在物理层面不一定连续。

  2. 现实的结点一般是从堆上申请出来的。

  3. 从堆上申请的空间不一定连续。

链表的分类

单向链表和双向链表

按照方向来分,或者说是按照结点中存储的逻辑上其他相邻结点的地址来分。

带头链表和不带头链表

如果有一个头节点,就是带头链表,带头链表处理一些链表相关题目表现很好。

循环链表和非循环链表

判断标准是首位是否相连。

常用

  • 无头单向非循环链表: 结构简单,但是一般不单独用来存储数据,而是作为哈希桶、图的临界表等其他复杂数据结构的子结构。笔试题中出现颇多。

  • 带头双向循环链表:

    结构最复杂,一般用于单独存储数据。虽然结构复杂,但是因为带头而且循环,处理起来比较简单。

链表题目

LeetCode 203 之删除某值节点

题目描述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

题目思路:我们只需要遍历一遍链表,然后遇到需要删除的节点直接修改前一个节点的指向即可。这样就要求我们记住前一个节点。

铺垫:

  1. 首先我们先回顾一下如何删除节点:找到节点的前一个节点,然后进行删除操作。

     if(next->val == val){
     node->next = next->next;
     }
  2. 虚拟头节点:在单向链表中添加虚拟头节点是一个常用而且好用的技巧。

     ListNode* Head = new ListNode;

我的解法1:常规思路,首先看头节点,确保头节点的val不等于val。然后再去遍历后面的链表。

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* removeElements(ListNode* head, int val) {
         if(head == nullptr) return nullptr;
         ListNode* Head = head;
         while(head->val == val){
             if(head->next == nullptr){
                 return nullptr;
            }
             head = head->next;
             Head = head;
        }
         ListNode* next = head->next;
         while(next){
             if(next->val == val){
                 head->next = next->next;
            }
             else{
                 head = head->next;
            }
             if(head == nullptr){
                     next = nullptr;
            }else{
                     next = head->next;
            }
        }
         return Head;
    }
 };

我的解法2:添加虚拟头节点,直接遍历数组即可。

 class Solution {
 public:
     ListNode* removeElements(ListNode* head, int val) {
         if(head == nullptr) return nullptr;
         ListNode* Head = new ListNode;
         Head->next = head;
 ​
         ListNode* next = Head->next;
         ListNode* node = Head;
         while(next){
             if(next->val == val){
                 node->next = next->next;
            }
             else{
                 node = next;
            }
             if(node == nullptr){
                     next = nullptr;
                }else{
                     next = node->next;
                }
        }
         return Head->next;
    }
 };

LeetCode 206 之 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

思路:需要熟悉如何进行节点的反转,因为后一个节点要指向前一个节点,所以我们需要提前保存前一个节点;同时因为修改指向后无法找到下一个节点,所以需要提前保存。

铺垫:熟悉一下节点的反转操作

 ListNode* prev, curr, next;
 next = curr->next;
 curr->next = prev; //反转操作
 prev = curr;
 curr = next;
 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* reverseList(ListNode* head) {
         if(!head) return nullptr;
         ListNode* prev = nullptr;
         ListNode* curr = head;
         ListNode* next = curr->next;
         while(curr){
             next = curr->next;
             curr->next = prev;
             prev = curr;
             curr = next;
        }
         return prev;
    }
 };

LeetCode 876 之链表的中间结点

给你单链表的头结点 head ,请你找出并返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。

思路:首先最容易想到的方法应该是首先遍历一遍整个链表,获取到总共的结点个数,随后再遍历找到中间结点。这种方法非常好实施;

但是同时也可以使用双指针的方法解,因为中间结点相较于最终结点存在一个明显的二倍关系,可以采用快慢指针,快指针一步走两个结点,慢指针一步走一个结点,当快指针指向nullptr 时,慢指针指向的就恰好是中间结点。

我的解法1:

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* middleNode(ListNode* head) {
         if(!head) return nullptr;
         int num = 0;
         ListNode* begin = head;
         while(begin){
             num++;
             begin = begin->next;
        }
         num /= 2;
         while(num--){
             head = head->next;
        }
         return head;
    }
 };

我的解法2:

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* middleNode(ListNode* head) {
         if(!head) return head;
         ListNode* pfast = head;
         ListNode* pslow = head;
         while(pfast && pfast->next){
             pfast = pfast->next;
             pslow = pslow->next;
             if(pfast) pfast = pfast->next; //判定当pfast 不为空时再继续。
        }
         return pslow;
    }
 };

两种解法相比,第一种比较常规,第二种应用快慢指针,但是实际上,性能方面没有较大差别。

牛客网 链表中的倒数第k 个结点

描述:输入一个链表,输出该链表中倒数第k个结点。

示例1 输入:

 1,{1,2,3,4,5}

返回值:

 {5}

思路:最常规的思路就是遍历找到总共的链表个数n,第n-k+1 个就是要找的倒数第k 个结点。不过需要处理几个细节:(1)pListHead 本身为空可以直接返回。(2)k <= 0 直接返回。(3)k >= 链表中结点的个数,直接返回。 不过本题目仍然可以按照快慢指针的方法去做。想要的最终结果是当快指针走到尽头的时候,慢指针恰好走到倒数第k 个结点上,这并不难做到,只需要让快指针先走k 步即可。

我的解法1:

 /*
 struct ListNode {
 int val;
 struct ListNode *next;
 ListNode(int x) :
 val(x), next(NULL) {
 }
 };*/
 #include <random>
 class Solution {
 public:
     ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
 if(!pListHead || k <= 0) return nullptr;
 int num = 0;
 ListNode* head = pListHead;
 while(head){
 num++;
 head = head->next;
 }
 if(num < k) return nullptr;
 head = pListHead;
 int times = num - k;
 while(times--){
 head = head->next;
 }
 return head;
    }
 };

我的解法2:(我们还是对上面的一些特殊情况进行了优先处理)

 /*
 struct ListNode {
 int val;
 struct ListNode *next;
 ListNode(int x) :
 val(x), next(NULL) {
 }
 };*/
 class Solution {
 public:
     ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
 if(!pListHead || k <= 0) return nullptr;
 ListNode* pfast = pListHead;
 ListNode* pslow = pListHead;
 //pfast 先走k 步,如果走k步还没到就走到尽头了,说明k > 链表中结点个数,返回空
 for(int i = k; i > 0; i--){
 pfast = pfast->next;
 if(!pfast && i == 1) return pslow;
 else if(!pfast) return nullptr;
 }
 while(pfast){
 pfast = pfast->next;
 pslow = pslow->next;
 }
 return pslow;
    }
 };

LeetCode 21 之合并链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

思路:升序排列的方法就是比较两个链表的结点中val 的大小,取小的一个尾插到新链表中。注意在这题中可以使用虚拟头结点来简化过程。

我的解法1:

 class Solution {
 public:
     ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
         ListNode* pcurr1 = list1;
         ListNode* pcurr2 = list2;
         ListNode* head = new ListNode;
         ListNode* newCurr = head;
         while(pcurr1 && pcurr2){
             if(pcurr1->val > pcurr2->val){
                 newCurr->next = pcurr2;
                 pcurr2 = pcurr2->next;
            }else{
                 newCurr->next = pcurr1;
                 pcurr1 = pcurr1->next;
            }
             newCurr = newCurr->next;
        }
       //来判断到底是谁终止了,直接修改newCurr 到未终止的结点的指向即可。
         newCurr->next = (pcurr1 == nullptr) ? pcurr2 : pcurr1;
         return head->next;
    }
 };

我的解法2: 递归如果 list1 或者 list2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 list1 和 list2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。

 class Solution {
 public:
     ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
         if(!list1)  return list2;
         else if(!list2) return list1;
         else if(list1->val < list2->val){
             list1->next = mergeTwoLists(list1->next, list2);
             return list1;
        }else{
             list2->next = mergeTwoLists(list1, list2->next);
             return list2;
        }
    }
 };
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值