• 链表常见面试题分析


    目录

    一、移除链表元素

    二、反转链表

    三、链表的中间结点

    四、链表中倒数第k个结点

    五、合并两个有序链表

    六、链表分割

    七、链表的回文结构

    八、相交链表

    九、复制带随机指针的链表

    十、链表的带环问题

    十一、顺序表和链表的区别

    CPU缓存命中率

    十二、K个一组翻转链表


    接下来我们会从易到难,逐步讲解有关链表的常见面试,部分题目来自Leetcode和牛客

    一、移除链表元素

    LeetoCode链接: 203. 移除链表元素 - 力扣(LeetCode)

    1. class Solution {
    2. public:
    3. ListNode* removeElements(ListNode* head, int val)
    4. {
    5. ListNode* prev = nullptr;
    6. ListNode* current = head;
    7. while (current != nullptr)
    8. {
    9. if (current->val == val) {
    10. if(current == head)//头删
    11. {
    12. head = head->next;
    13. delete current;
    14. current = head;
    15. }
    16. else//普通删除
    17. {
    18. prev->next = current->next;
    19. delete current;
    20. current = prev->next;
    21. }
    22. }
    23. else {
    24. prev = current;
    25. current = current->next;
    26. }
    27. }
    28. return head;
    29. }
    30. };

    二、反转链表

    LeetCode链接: 206. 反转链表 - 力扣(LeetCode)

    思路一:

    1. //头插法
    2. class Solution {
    3. public:
    4. ListNode* reverseList(ListNode* head)
    5. {
    6. //if(head == nullptr) return nullptr;
    7. ListNode* newHead = nullptr, *current = head;
    8. while(current != nullptr)
    9. {
    10. ListNode* next = current->next;
    11. current->next = newHead;
    12. newHead = current;
    13. current = next;
    14. }
    15. return newHead;
    16. }
    17. };

     思路二:

    1. //三指针
    2. class Solution {
    3. public:
    4. ListNode* reverseList(ListNode* head) {
    5. if(head == nullptr) return nullptr;
    6. ListNode* n1 = nullptr, *n2 = head, *n3 = head->next;
    7. while(n2 != nullptr)
    8. {
    9. n2->next = n1;
    10. n1 = n2;
    11. n2 = n3;
    12. if(n3 != nullptr) n3 = n3->next;
    13. }
    14. return n1;
    15. }
    16. };

    思路三:

    1. //递归
    2. class Solution {
    3. public:
    4. ListNode* reverseList(ListNode* head)
    5. {
    6. if(head == nullptr || head->next == nullptr) return head;
    7. ListNode* newHead = reverseList(head->next);
    8. head->next->next = head;
    9. head->next = nullptr;
    10. return newHead;
    11. }
    12. };

    三、链表的中间结点

    LeetCode链接: 876. 链表的中间结点 - 力扣(LeetCode)

    1. struct ListNode* middleNode(struct ListNode* head)
    2. {
    3. struct ListNode* slow = head,*fast = head;
    4. while(fast != NULL&& fast->next != NULL)
    5. {
    6. slow = slow->next;
    7. fast = fast->next->next;
    8. }
    9. return slow;
    10. }

    四、链表中倒数第k个结点

    牛客网链接: 链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

    1. struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    2. if(pListHead == NULL || k == 0) return NULL;
    3. struct ListNode* slow = pListHead , *fast = pListHead;
    4. for(int i = 0;i < k ;++i){
    5. if(fast == NULL) return NULL;
    6. fast = fast->next;
    7. }
    8. while(fast != NULL){
    9. fast = fast->next;
    10. slow = slow->next;
    11. }
    12. return slow;
    13. }

    五、合并两个有序链表

    LeetCode链接: 21. 合并两个有序链表 - 力扣(LeetCode)

    1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    2. struct ListNode* head,*tail;
    3. head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
    4. tail->next = NULL;
    5. while (list1 != NULL && list2 != NULL) {
    6. if (list1->val < list2->val) {
    7. tail->next = list1;
    8. list1 = list1->next;
    9. }
    10. else {//list1->val >= list2->val
    11. tail->next = list2;
    12. list2 = list2->next;
    13. }
    14. tail = tail->next;
    15. }
    16. if (list1 != NULL)
    17. tail->next = list1;
    18. if (list2 != NULL)
    19. tail->next = list2;
    20. struct ListNode* del = head;
    21. head = head->next;
    22. free(del);
    23. return head;
    24. }

    六、链表分割

    牛客网链接: 链表分割_牛客题霸_牛客网

    1. class Partition {
    2. public:
    3. ListNode* partition(ListNode* pHead, int x) {
    4. struct ListNode* less_head,*less_tail;
    5. less_head = less_tail = (struct ListNode*)malloc(sizeof(struct ListNode));
    6. less_tail->next = NULL;
    7. struct ListNode* greater_head,* greater_tail;
    8. greater_head = greater_tail = (struct ListNode*)malloc(sizeof(struct ListNode*));
    9. greater_tail->next = NULL;
    10. struct ListNode* current = pHead;
    11. while(current != NULL){
    12. if(current->val < x){
    13. less_tail->next = current;
    14. less_tail = less_tail->next;
    15. }
    16. else{
    17. greater_tail->next = current;
    18. greater_tail = greater_tail->next;
    19. }
    20. current = current->next;
    21. }
    22. less_tail->next = greater_head->next;
    23. greater_tail->next = NULL;
    24. ListNode* head = less_head->next;
    25. free(less_head);
    26. free(greater_head);
    27. return head;
    28. }
    29. };

    七、链表的回文结构

    牛客网链接: 链表的回文结构_牛客题霸_牛客网

    1. class PalindromeList {
    2. public:
    3. struct ListNode* middleNode(struct ListNode* head){
    4. struct ListNode* slow = head,*fast = head;
    5. while(fast && fast->next){
    6. slow = slow->next;
    7. fast = fast->next->next;
    8. }
    9. return slow;
    10. }
    11. struct ListNode* reverseList(struct ListNode* head){
    12. struct ListNode* cur = head;
    13. struct ListNode* newhead = NULL;
    14. while(cur != NULL){
    15. struct ListNode* next = cur->next;
    16. cur->next = newhead;
    17. newhead = cur;
    18. cur = next;
    19. }
    20. return newhead;
    21. }
    22. bool chkPalindrome(ListNode* A) {
    23. struct ListNode* mid = middleNode(A);
    24. struct ListNode* newhead = reverseList(mid);
    25. while(A && newhead){
    26. if(A->val != newhead->val){
    27. return false;
    28. }
    29. A = A->next;
    30. newhead = newhead->next;
    31. }
    32. return true;
    33. }
    34. };

    八、相交链表

    LeetCode链接: 160. 相交链表 - 力扣(LeetCode)

    1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    2. int lenth_A = 0,lenth_B = 0;
    3. struct ListNode* current_A = headA;
    4. struct ListNode* current_B = headB;
    5. while(current_A->next != NULL){
    6. ++lenth_A;
    7. current_A = current_A->next;
    8. }
    9. while(current_B->next != NULL){
    10. ++lenth_B;
    11. current_B = current_B->next;
    12. }
    13. if(current_A != current_B) return NULL;//尾结点不同,肯定不为相交链表
    14. current_A = headA;
    15. current_B = headB;
    16. if(lenth_A > lenth_B){
    17. for(int i = 0;i < lenth_A - lenth_B; ++i){
    18. current_A = current_A->next;
    19. }
    20. }
    21. else{//lenth_A <= lenth_B
    22. for(int i = 0;i < lenth_B - lenth_A; ++i){
    23. current_B = current_B->next;
    24. }
    25. }
    26. while(current_A != current_B){//此时肯定为相交链表
    27. current_A = current_A->next;
    28. current_B = current_B->next;
    29. }
    30. return current_A;
    31. }

    九、复制带随机指针的链表

    LeetCode链接: 138. 复制带随机指针的链表 - 力扣(LeetCode)

    1. class Solution {
    2. public:
    3. Node* copyRandomList(Node* head)
    4. {
    5. Node* current = head;
    6. //将原链表结点全部拷贝,并将拷贝结点插入原链表其对应的结点的后一个位置
    7. while(current != nullptr) {
    8. Node* copy = new Node(current->val);
    9. copy->next = current->next;
    10. current->next = copy;
    11. current = copy->next;
    12. }
    13. //copy结点random指针 指向 其对应结点的random指针指向的结点的后一个
    14. //若对应结点的random指针指向NULL,copy结点的random指针也指向NULL
    15. current = head;
    16. while(current != nullptr) {
    17. Node* copy = current->next;
    18. if(current->random == nullptr) copy->random = nullptr;
    19. else copy->random = current->random->next;
    20. current = copy->next;
    21. }
    22. //将插入原链表的所有copy结点,全部解除下来并逐个尾插为一个新的链表,将原链表还原
    23. current = head;
    24. Node* newHead = nullptr;
    25. while(current != nullptr)
    26. {
    27. if(newHead == nullptr) newHead = current->next;
    28. Node* copy = current->next;//根据逻辑,只要current不为空,copy必不为空
    29. Node* next = copy->next;//copy不为空并不能保证next不为空
    30. if(next != nullptr) copy->next = next->next;
    31. current->next = next;
    32. current = next;
    33. }
    34. return newHead;
    35. }
    36. };

    十、链表的带环问题

    问题1

    给定一个链表,如何判断链表中是否有环呢?141. 环形链表 - 力扣(LeetCode)

    思路: 使用快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,若链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾

    1. bool hasCycle(struct ListNode *head) {
    2. if(head == NULL) return false;
    3. struct ListNode *slow = head,*fast = head;
    4. while(fast !=NULL && fast->next != NULL){
    5. slow = slow->next;
    6. fast = fast->next->next;
    7. if(slow == fast) return true;
    8. }
    9. return false;
    10. }

    问题2

    给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
    142. 环形链表 II - 力扣(LeetCode)

    方法一: 

    在做这道题之前我们必须了解一个定理: 

    让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。但是为什么呢?

     接下来,理论上已经解决,用代码将其实现即可。

    1. struct ListNode *detectCycle(struct ListNode *head) {
    2. struct ListNode* fast = head,*slow = head;
    3. while(fast && fast->next){
    4. slow = slow->next;
    5. fast = fast->next->next;
    6. if(slow == fast){
    7. struct ListNode* meet = fast;
    8. while(meet != head){
    9. meet = meet->next;
    10. head = head->next;
    11. }
    12. return meet;
    13. }
    14. }
    15. return NULL;
    16. }

    方法二:

    1. struct ListNode *detectCycle(struct ListNode *head) {
    2. struct ListNode* newhead = NULL;
    3. struct ListNode* fast = head,*slow = head;
    4. while(fast && fast->next){
    5. slow = slow->next;
    6. fast = fast->next->next;
    7. if(slow == fast){
    8. newhead = fast->next;
    9. fast->next = NULL;
    10. }
    11. }
    12. if(newhead == NULL) return NULL;
    13. int length1 = 0,length2 = 0;
    14. struct ListNode* cur = head;
    15. while(cur->next != NULL){
    16. ++length1;
    17. cur = cur->next;
    18. }
    19. cur = newhead;
    20. while(cur->next != NULL){
    21. ++length2;
    22. cur = cur->next;
    23. }
    24. if(length1 > length2){
    25. for(int i = 0;i < length1 - length2; ++i){
    26. head = head->next;
    27. }
    28. }
    29. if(length1 < length2){
    30. for(int i = 0;i < length2 - length1; ++i){
    31. newhead = newhead->next;
    32. }
    33. }
    34. while(head != newhead){
    35. head = head->next;
    36. newhead = newhead->next;
    37. }
    38. if(head == newhead) return head;
    39. return NULL;
    40. }

    扩展问题1:

    为什么快指针每次走两步,慢指针走一步可以? 

    若链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度 - 1。

    此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇

    扩展问题2:

    快指针一次走3步,走4步,...n步行吗?  不行

    十一、顺序表和链表的区别

    1.存储空间:
    顺序表存储空间连续。支持随机访问O(1)
    链表的逻辑结构上连续,但物理结构上不一定连续。不支持随机访问O(n) (使用二分查找等算法时,时间复杂度过大)

    2.插入与删除操作:
    顺序表可能需要移动元素,时间复杂度为O(n)。插入时可能需要扩容,可能存在空间浪费(有一定性能消耗)
    链表只需改动指针指向即可。无容量概念,不会浪费空间

    3.应用场景:
    顺序表适用于元素高效存储、频繁访问
    链表适用于任意位置插入删除和删除频繁

    4.缓存利用率:
    顺序表CPU高速缓存命中率高
    链表CPU高速缓存命中率低

    CPU缓存命中率

    主存、高速缓存和寄存器(带电存储)

    磁盘、远程二级存储(不带电存储)

    CPU缓存离CPU核心更近,由于电子信号传输是需要时间的,所以离CPU核心越近,缓存的读写速度就越快。但 CPU 的空间很狭小,离 CPU 越近缓存大小受到的限制也越大。所以,综合硬件布局、性能等因素,CPU缓存通常分为大小不等的三级缓存:L1L2L3。级别越小越接近 CPU,所以速度也更快,同时也代表着容量越小。

    L1 是最接近CPU的, 它容量最小(例如:32K),速度最快,L1缓存每个核上其实有两个L1缓存, 一个用于存数据的 L1d Cache(Data Cache),一个用于存指令的 L1i Cache(Instruction Cache)。

    L2缓存更大一些(例如:256K),速度要慢一些, 一般情况下每个核上都有一个独立的L2缓存;

    L3缓存是三级缓存中最大的一级,同时也是最慢的一级, 在同一个 CPU 插槽之间的核共享一个 L3 缓存

    数据从内存向上,L3->L2->L1,最后到寄存器进行CPU计算。CPU在缓存中找到有用的数据被称为命中率,当缓存中没有CPU所需的数据时(这时称为未命中)CPU才访问内存将数据加载到缓存。CPU并不会一个字节一个字节的加载,效率太低。而是一块一块加载,对于这样的一块一块的数据单位,术语叫“Cache Line”。目前主流的cache line是64字节,也有32和128的。

    一般访问某个数据时,先查看其地址是否在缓存中,在就访问,不在则需加载后访问。(一般来说第一次访问都不命中)                                                                                                                     

    缓存命中率 = 从缓存中读取的次数 / 总读取次数                                                                           

    顺序表的存储是一块连续的内存,CPU一块一块地进行数据加载,当访问arr[0]时,arr[1]大概率已经加载到CPU中,减少了加载的时间。

    链表空间不连续,每次访问下一个结点大概率需要重新加载。

    所以链表缓存命中率低于顺序表,访问速度也低于顺序表

    十二、K个一组翻转链表

    LeetCode链接:25. K 个一组翻转链表 - 力扣(LeetCode)

    1. /**
    2. * Definition for singly-linked list.
    3. * struct ListNode {
    4. * int val;
    5. * ListNode *next;
    6. * ListNode() : val(0), next(nullptr) {}
    7. * ListNode(int x) : val(x), next(nullptr) {}
    8. * ListNode(int x, ListNode *next) : val(x), next(next) {}
    9. * };
    10. */
    11. class Solution {
    12. public:
    13. ListNode* reverseKGroup(ListNode* head, int k)
    14. {
    15. //计算出需要逆转几次
    16. int number = 0;
    17. for(ListNode* current = head; current != nullptr; current = current->next) ++number;
    18. number /= k;
    19. //每次反转k个一组的链表
    20. ListNode* newHead = new ListNode(-1);
    21. ListNode* tail = newHead;
    22. ListNode* current = head;
    23. for(int i = 0; i < number; ++i)
    24. {
    25. ListNode* tmp = current;
    26. for(int j = 0; j < k; ++j)
    27. {
    28. ListNode* next = current->next;
    29. current->next = tail->next;
    30. tail->next = current;
    31. current = next;
    32. }
    33. tail = tmp;
    34. }
    35. //将后续不需反转的结点链接在一起
    36. tail->next = current;
    37. current = newHead->next;
    38. delete newHead;
    39. return current;
    40. }
    41. };

  • 相关阅读:
    延长周末,获得高质量休息:工作与学习党的生活策略
    19Linux基本使用和web程序部署
    miners lamp
    SpringCloud-gateway编码实现路由策略的自动刷新,动态路由
    我们需要工具支持键集分页
    WPS转PDF怎么转换?接下来分享这三个方法和操作步骤给你
    【Java】x-easypdf: 一种简单易用的PDF处理库
    这才是CSDN最系统的网络安全学习路线(建议收藏)
    Ubuntu 20.04 安装 mysql 8
    适用于LLM的代理搜索增强事实评估器 (Search-Augmented Factuality Evaluator,SAFE)
  • 原文地址:https://blog.csdn.net/GG_Bruse/article/details/127625185