单链表面试题

本文详细介绍了链表操作的各种技巧,包括反转链表、查找链表中间结点、倒数第k个结点、合并有序链表、处理不带头结点和哨兵、分割链表、相交链表判断、回文链表识别以及随机链表的复制,为程序员提供实用的链表操作方法和优化策略。
摘要由CSDN通过智能技术生成

目录

反转单链表

链表的中间结点

链表倒数第k个结点的数据

合并两个有序链表

不带头结点和带头结点

分割链表

相交链表

判断回文链表

随机链表的复制

反转单链表

代码示例:

思路:用一个新链表保存反转后的链表,如何实现让原来的头赋值为空,修改反向指的next值

因为单链表无法保存前一个结点值,所以我们用三个指针x1,x2,x3来完成移动和翻转的过程

x1,x2,x3初始化----x2实现反转指向----x1,x2,x3向后移动更新

Node* reverse(Node* head){
    if(head==NULL){
        return NULL;
    }
    Node* x1,*x2,*x3;
    x1=NULL;
    x2=head;
    x3=head->next;

考虑问题?何时结束,结合画图n3为空并不能完成整个链表的翻转,最后一个结点并没有实现反转,所以要结合画图发现当n2为空时完成了操作
    while(x2){
        //翻转指向
        x2->next=x1;
        //移动指针
        x1=x2;
        x2=x3;
//      x3=x3->next; 如果不判断x3会报错移动到最后面会为空 没有next
        if(x3)
            x3=x3->next;
    }

此时原来的尾就为新的头部 返回即可
    return x1;
}

链表的中间结点



思路:双指针(快慢指针),画图分结点为奇偶情况,slow走一步fast走两步
画图可以发现判断条件为偶数时fast为空,奇数时fast->next为空 即slow就是中间结点

代码实现:
Node* middleNode(Node* phead){
    快慢指针定义并初始化
    Node* slow,*fast;
    slow=fast=phead;
    迭代条件奇偶两个都需要满足
    while(fast && fast->next){
        slow走一步,fast走两步
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}

链表倒数第k个结点的数据


思路:用双指针表示一段距离,用距离差来实现
        fast先走k步,然后双指针同步走,直到fast为空 画图理解
int kthToLast(Node* head, int k){
    int i=1;  用于标识第几个元素
    Node* fast,*slow;
    fast=slow=head;
    fast先走k步,如果在oj中不判断fast是否为空会异常
    一般第一次要移动一个指针的next都需要提前判断一下
    因为它刚开始是指向头的,也可以判断头是不是为空

    while(i<=k && fast!=NULL){
        fast=fast->next;
        i++;
    }
    两指针一起走
    while(fast){
        slow=slow->next;
        fast=fast->next;
    }
    return slow->data;
}

合并两个有序链表

思路:用一个新链表来实现,每次比较原两链表的元素值,将值小的尾插到新链表,因为设计要找尾所以可以用一个tail来记录尾的位置,这样就不需要每次从头遍历

要考虑的问题:是否一开始有空链表,和两个刚开始都不为空以及两个链表谁先遍历完结束后该怎么处理

Node* mergeTwoLists(Node* list1,Node* list2) {
    Node* head,*tail; 
    head=tail=NULL; //初始化新链的头部和尾部
    if(list1==NULL) //如果一开始就有链表为空直接返回另一个即可
        return list2;
    if(list2==NULL)
        return list1;
    //两个都不为空的情况
    //遇到的问题如果有一个为空则它无法访问其中成员变量 所以两个都不能为空得同时满足
    while(list1!=NULL && list2!=NULL){
        if(list1->data<=list2->data){
            //问题:第一个结点尾插的时候初始化head和tail为空没有next
            //所以需要判断一下是不是第一次插入
            if(head==NULL)
                head=tail=list1;
            else{
                进行尾插
                tail->next=list1;
                tail=tail->next;
            }
            //一定要记住判断完一个结点移动一个结点
            list1=list1->next;
        }
        else{
            if(head==NULL)
                head=tail=list2;
            else{
                tail->next=list2;
                tail=tail->next;
            }
            list2=list2->next;
        }        
    }
    //问题:不确定是哪一个结点先为空的需判断
    if(list1){
        //直接将剩余元素链接到tail后面
        tail->next=list1;
    }
    if(list2){
        tail->next=list2;
    }
    return head; //将新链的头部返回即可
}

不带头结点和带头结点

从上面合并有序链表可以看出不带头结点的链接每次都需要判断以下是不是第一次插入,因为第一次插入head为空,无法访问它的next。

那么有没有办法处理?

那就是用带头结点的哨兵位链表,所谓哨兵就是头结点指向哨兵,它的数据域不存储任何有效数据但是他开辟了空间,它可以访问成员变量,就不需要每次去判断是不是第一次插入。

不带头结点每次都要判断是不是有空的指针,空指针在访问成员变量会报错是一个NULL的无效指针所以每次使用不带头结点的指针都要判断是否为NULL。

c中如何生成一个哨兵呢?刚开始定义结点的时候用malloc分配,而不是Node* 直接定义空指针

Node* newnode=(Node*)malloc(sizeof(Node*))

但是malloc申请的内存如果不释放会存在内存泄露问题

分割链表

思路:定义cur记录遍历的当前位置,用两个新链表less和greater分别存储小于x和大于等于x的元素,合并俩个链表,因为每次都要判断第一个结点是否为空 因此采用带哨兵的头

代码示例:

Node* partition(Node* head, int x) {
    带哨兵的两个新链表
    Node* less,*lesstail,*greater,*greatertail;
    less=lesstail=(Node*)malloc(sizeof(Node)); //sizeof后是跟结构体类型而不是指针Node*,要跟Node
    lesstail->next=NULL;
    greater=greatertail=(Node*)malloc(sizeof(Node));
    greatertail->next=NULL;
    Node* cur=head; //一般不用原head进行移动
    while(cur){

结点值和给定值进行比较
        if(cur->data<x){
            lesstail->next=cur;
            lesstail=cur;

            lesstail=lesstail->next;
        }
        else{
            greatertail->next=cur;
            greatertail=cur;
        }

一定要记住移动当前位置的指针
        cur=cur->next;
    }

链接两个已经遍历完的链表
    lesstail->next=greater->next;

画图时可以看出如果不将greatertail置空,会形成环
    greatertail->next=NULL;
 分配了malloc的空间都需要释放,所以用一个指针保存一下新的链表
    Node* newlist=less->next;
    free(less);
    free(greater);
    return newlist;
}

相交链表

思路:

1.他们有交点的时候,尾结点相同,那么第一步判断是否有交点只需要判断尾结点即可

2.如何求交点?当两个链表长度不相同时,我们采取双指针(fast,slow)用来控制距离差

fast先走距离差步,然后再一起走,直到他们地址相同即为交点

代码示例://是c的注释语句

Node *getIntersectionNode(Node *headA,Node *headB) {
    Node* Atail,*Btail;  //定义两个尾指针
    int n1,n2,n;  //用来记录结点个数,以及求距离差
    n1=n2=1;
    Atail=headA;
    Btail=headB;
    //当有一个链表为空时即无交点
    if(Atail==NULL || Btail==NULL){
        return NULL;
    }
    //两个while循环找尾结点
    while(Atail->next){
        Atail=Atail->next;
        n1++;
    }
    while(Btail->next){
        Btail=Btail->next;
        n2++;
    }
    //如果有交点则寻找交点位置
    if(Atail==Btail){
        //用内置函数求距离差
        n=abs(n1-n2);
        Node* fast,*slow;
        fast=headA;
        slow=headB;
        if(n1<n2){
            fast=headB;
            slow=headA;
        }
        //这步已经很熟了 fast先走距离差步
        while(n--){
            fast=fast->next;
        }
        //这里的fast != slow判断条件不能是fast->next != slow->next
        //因为当长度元素完全相同 有些测试用例无法通过

        while(fast!= slow){
            fast=fast->next;
            slow=slow->next;
        }
        return fast;  //返回slow也可以
    }
    else
        return NULL;
}

判断回文链表

思路:

我们第一反应就是将列表反转然后逐一结点比较,相同则回文,但是我们也可以找中间结点,只需要将后半部分逆置然后比较即可,不需要遍历整个链表。找中间结点和反转链表上面有写过可以直接调用函数,但是建议再写一遍熟悉一下

在c语言中并没有直接的bool类型,所以需要包含一个头文件才能使用该类型并且接收返回值0/1

#include "stdbool.h"

代码示例:
//找中间结点
Node* middle(Node* head){
    Node* slow,*fast;
    if(head==NULL){
        return NULL;
    }
    else{
        slow=fast=head;
        while(fast && fast->next){
            slow=slow->next;
            fast=fast->next->next;
        }
    }
    return slow;
}
//链表逆置
Node* relist(Node* head){
    if(head==NULL)
        return NULL;
    Node* n1,*n2,*n3;
    n1=NULL;
    n2=head;
    n3=head->next;
    while(n2){
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3){
            n3=n3->next;
        }
    }
    return n1;
}

//判断是否回文
bool isPalindrome(Node* head){
    Node* mid=middle(head);
    //只需要逆置后半部分即可,优化了时间复杂度
    Node* re=relist(mid);
    //用来记录位置并且移动,也可以直接在mid和re头结点进行操作,定义指针变量便于看清
    Node* begincur,*recur;
    begincur=head;
    recur=re;
    //当反转后的链表和原链表一个为空则比较结束
    while(begincur && recur){
        if(begincur->data != recur->data)
            return 0;
        begincur=begincur->next;
        recur=recur->next;
    }
    return 1;
}

随机链表的复制

它的结构体中包含了一个指针random

typedef struct Node {

     int data;

     struct Node *next;

     struct Node *random;

  }Node;

思路:

1.复制每个节点的数据域,将它插入到原节点的后面并且修改它的next值,指向下一个节点,就相当于插在每个原节点的中间

2.修改copy后的next域,修改指针(next和random)两个指针都需要修改

3.将copy的每个节点拿出来组成一个新的链表实现复制,并返回头结点

代码如下:

Node* copyRandomList(Node* head) {
    Node* cur=head;
    while(cur){
        //1.插入复制的结点在原结点后面
        Node* copy=(Node*)malloc(sizeof(Node));
        copy->data=cur->data;
        //实现插入结点
        copy->next=cur->next;
        cur->next=copy;
        //移动cur指针
        cur=copy->next;
    }
    //2.根据原结点,处理copy节点的random
    cur = head;
    while(cur){
        Node* copy = cur->next;
        if(cur->random == NULL)
            copy->random = NULL;
        else
        //cur的random是一个指针 将它指向的next给copy的random实现复制随机值
        //可以进行画图理解它的走向,random拥有自己的next和数据值

        copy->random=cur->random->next;
        cur=copy->next;
    }
    //3.将复制的节点移动下来形成新链表用copyhead记录头copytail记录尾
    //用取原节点来进行新copy链表的尾插

    Node* copyhead=NULL,*copytail=NULL;
    cur=head; //控制原链表的移动
    while(cur){
        Node* copy=cur->next;
        Node* next=copy->next;
        //第一次插入节点
        if(copytail==NULL){
            copyhead=copytail=copy;
        }
        //进行尾插到新链表即可
        else{
            copytail->next=copy;
            copytail=copy;
        }
        cur->next=next;
        cur=next;
    }
    return copyhead;
}

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值