C 链表相关面试题

为了方便演示,我们简单的定义几个单链表的操作:

typedef int DataType;

typedef struct LinkListNode
{
    DataType _data;
    struct LinkListNode* _next;
}Node,*pNode,*pList;

void InitList(pList* pplist)
{
    assert(pplist);
    *pplist = NULL;//头指针置空
}

pNode BuyNode(DataType x)
{
    pNode newNode = (pNode)malloc(sizeof(Node));
    if (newNode == NULL)
    {
        perror("BuyNode::malloc");
        exit(EXIT_FAILURE);
    }
    newNode->_data = x;
    newNode->_next = NULL;
    return newNode;
}

void PushBack(pList* pplist,DataType x)
{
    pNode newNode = BuyNode(x);
    pNode cur = *pplist;
    assert(pplist);
    //链表为空,新插入节点为链表头结点
    if (cur == NULL)
    {
        *pplist = newNode;
    }
    else
    {
        while (cur->_next != NULL)
        {
            cur = cur->_next;
        }//cur->_next==NULL;
        cur->_next = newNode;
    }
}

void PopBack(pList* pplist)
{
    pNode cur = *pplist;
    //如果链表为空,直接返回
    assert(pplist);
    if (cur == NULL)
    {
        return;
    }//只有一个节点
    if (cur->_next == NULL)
    {
        free(*pplist);
        *pplist = NULL;
    }
    else
    {
        pNode prev = cur;
        cur = cur->_next;
        while (cur->_next != NULL)
        {
            cur = cur->_next;
            prev = prev->_next;
        }//cur->_next==NULL;
        free(cur);
        cur = NULL;
        prev->_next = NULL;
    }
}

1.逆序打印单链表
void ReversePrint(pList plist)

  • 思路:我们要打印单链表就必须遍历整个链表,逆序打印即先遍历的节点后打印,后遍历的节点先打印。
  • 方法:我们可以采用递归的方式,每遍历一次,打印最后的节点,并且将它Pop掉,直到节点为空。
  • 实现如下:
void ReversePrint(pList plist)
{
    pList cur = plist;
    if (cur == NULL)
    {
        printf("over\n\n");
        return;
    }
    while (cur->_next != NULL)
    {
        cur = cur->_next;
    }
    printf("%d -> ", cur->_data);
    PopBack(&plist);
    ReversePrint(plist);
}

2.删除无头单链表的非尾节点
void EraseNotTail(pNode pos);

  • 思路:首先,这个pos位置可能是头结点,也可能是中间节点。常规思路是:定义一个前驱节点,然后找到pos位置然后删除(但是如果定义前驱指针的话我们必须知道链表的头在哪里);第二种思路是:如果我们只知道pos位置,将pos位置的值与下一个节点的值进行交换,然后删除pos的下一个位置,这个方法不用定义前驱指针。
  • 实现如下:
void EraseNotTail(pNode pos)
{
    //若pos为尾节点,则直接返回
    if (pos->_next == NULL)
    {
        return;
    }
    DataType tmp;
    tmp = pos->_data;
    pos->_data = pos->_next->_data;
    pos->_next->_data = tmp;

    pNode del = pos->_next;
    pos->_next = pos->_next->_next;
    free(del);
    del = NULL;
}

3.在无头单链表的非头结点前插入一个元素
void InsertFrontNode(pNode pos, DataType x);

  • 思路:没有头指针,无法通过设置前驱指针来插入数据。将新节点插入到pos位置之后,然后交换新节点和pos
  • 实现如下:
void InsertFrontNode(pNode pos, DataType x)
{
    if (pos->_next == NULL)
    {
        return;
    }

    pNode newNode = BuyNode(x);
    newNode->_next = pos->_next;
    pos->_next = newNode;

    DataType tmp;
    tmp = pos->_data;
    pos->_data = newNode->_data;
    newNode->_data = tmp;
}

4.约瑟夫环问题
void JosephCycle(pList* pplist, int k);

  • 约瑟夫环问题:已知n个人(以编号0,1,…,n-1分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。
  • 我们要解决的约瑟夫环问题是:有n个人,从下标为1的人开始报数,报到k的人出列,下一个继续从1开始报数,……,重复上述过程,直到最后一个人出列,我们要求的就是最后一个出列的人的位置

思路:

  • 首先,我们分析当n变化时,k=2时,J(k=)(n=)=?的场景:
    n=2时,J2(2)=1;
    n=3时,J2(3)=3;
    n=4时,J2(4)=1;
    n=5时,J2(5)=3;
    n=6时,J2(6)=5;
    n=7时,J2(7)=7;
    n=8时,J2(8)=1;
    ……
  • 通过上面简单的列举,我们很容易可以知道,J2(2^x)=1(x为2的幂);知道这一规律后我们再来分析一下剩下的这些不是2的幂的n对应的结果:n=3时,当出局一人后,又回到了n=2时的场景,只不过下标发生了变化;同样n=5,n=9时,当出局一人后,又回到了n=4,n=8时的场景,我们分别来看一下这两种情况下的坐标是如何发生变化的:
    这里写图片描述
  • 我们可以得出大致的结论:即new=(old-2)%n ( 姑且认为-1%5=4、-1%9=8)
  • 下面我们考虑一般情况:k为任意数时,我们也可以通过上面的方法来考虑,J(k)(n+1)可以通过J(k)(n)来得到;我们随便举一个例子,假设k=5,n=9,我们来观察一下它的坐标变化是否符合我们上边说的规律:
    这里写图片描述
  • 很明显,新的坐标的变化规律是符合我们上面根据特例推出来的规律的,即new=(old-k)%n ==》 old=(new+k)%n。根据前面的思想:J(k)(n+1)可以通过J(k)(n)来得到,J(k)(n)可以通过J(k)(n-1)得到,这是一个不断递归的过程。总结一下:
    J(k)(1)
    J(k)(2)=(J(k)(1)+k)%n
    J(k)(3)=(J(k)(2)+k)%n
    ……
    J(k)(n)=(J(k)(n-1)+k)%n
  • 所以,我们最终推导出来的约瑟夫环问题的通解为:J(k)(n)=(J(k)(n-1)+k)%n
  • 代码实现:
int _JosephCycle(int n, int k)
{
    if (n == 1)
        return 1;
    else
        return (_JosephCycle(n - 1, k) + k) % n;
}

int JosephCycle(pList* pplist, int k)
{
    int n = 0;
    pNode cur = *pplist;
    assert(pplist);
    while (cur)
    {
        n++;
        cur = cur->_next;
    }
    return _JosephCycle(n, k);
}

5.逆序单向链表
void ReverseList(pList* pplist);

  • 思路:如果当前链表为空或者只有一个节点,则不需要处理;否则,将原链表的的每个节点从头开始依次摘下来,将其通过头插法链在以prev为头结点的逆置链表中,每次摘节点之前都要并保存好下一个节点的位置,在处理的过程中,关注第一个节点和最后一个节点的特殊性,整个链表完成逆置后,将prev赋值给头指针*pplist,整个链表就完成了逆置
  • 图示上述过程:
    这里写图片描述
  • 实现如下:
void ReverseList(pList* pplist)
{
    pNode cur = *pplist;
    assert(pplist);
    if (cur == NULL || cur->_next == NULL)
    {
        return;
    }
    else
    {
        pNode tmp = cur->_next;
        cur->_next = NULL;
        pNode prev = *pplist;
        while (tmp)
        {
            prev = cur;
            cur = tmp;
            tmp = tmp->_next;
            cur->_next = prev;
        }//tmp==NULL,即cur为最后一个处理节点
        cur->_next = prev;
        prev = cur;
        *pplist = prev;
    }
}

6.合并两个有序链表,使之合并后依然有序
pList Merge(pList* p1, pList* p2);

  • 思路:假定是将两个升序链表合成一个升序链表(若是两个升序合成一个降序,则只需将链表逆置即可),比较两个两链表的首节点,将值小的那个做为新链表的头节点,依次比较两个链表中的节点,将小的那个链在新链表上,如果两个链表不等长,则必有一个先遍历完,此时只需将剩余元素链在新链表之后即可

  • 实现如下:

pList Merge(pList* p1, pList* p2)
{
    pNode pnew = NULL;
    pNode pcur = NULL;
    pNode h1 = *p1;
    pNode h2 = *p2;
    if (h1 == NULL)
        return h1;
    if (h2 == NULL)
        return h2;

    if ((*p1)->_data < (*p2)->_data)
    {
        pnew = *p1;
        h1 = h1 ->_next;
    }
    else
    {
        pnew = *p2;
        h2 = h2 ->_next;
    }
    pcur = pnew;//保存新链表的头

    while (h1 != NULL && h2 != NULL)
    {
        if (h1->_data <= h2->_data)
        {
            pcur->_next = h1;
            pcur = pcur->_next;
            h1 = h1->_next;
        }
        else
        {
            pcur->_next = h2;
            pcur = pcur->_next;
            h2 = h2->_next;
        }
    } 

    //h1==NULL 
    if (h1 == NULL)
    {
        pcur->_next = h2;
    }
    //或者 h2=NULL
    if (h2 == NULL)
    {
        pcur->_next = h1;
    }
    return pnew;
}

7.查找单链表的中间节点,要求只能遍历一次链表
pNode FindMidNode(pList plist);

  • 思路:设置快慢指针fast,slow,两个指针同时从头结点开始走,fast每次向后移动两位,slow每次向后移动一位,当fast走到链表尽头时,slow指向的位置即为中间位置。
  • 问题:若链表元素为偶数个,fast每次向后移动两位,在最后一次循环前,fast->next存在,所以fast->next->next的访问时合法的;当链表元素为奇数个时,fast每次向后移动两位则刚好将两边遍历完,fast->next不存在,所以访问fast->next->next是非法的
  • 代码实现:
pNode FindMidNode(pList plist)
{
    pNode fast = plist;
    pNode slow = plist;
    while (fast != NULL)
    {
        //fast->_next若为空,fast->_next->_next是非法的
        if (fast->_next)
        {
            fast = fast->_next->_next;
            slow = slow->_next;
        }
        else
        {
            fast = fast->_next;
        }
    }
    return slow;
}

8.查找单链表的倒数第k个节点,要求只能遍历一次链表
pNode FindKNode(pList plist, int k);

  • 思路:倒数第k个节点,即为第n-(k-1)个节点,我们要查找倒数第k个节点,只需要向前走n-(k-1)步,但是为了得到n的值我们还得遍历一次链表,总共需要遍历两次。借鉴上一题中的思路,我们可以考虑使用快慢指针来解决这个问题。让fast指针先走k-1步,然后fast,slow指针同时向后走,fast走到尽头时,slow指向的位置就是倒数第k个节点的位置
  • 代码实现:
pNode FindKNode(pList plist, int k)
{
    pNode fast = plist;
    pNode slow = plist;
    while (k - 1)
    {
        fast = fast->_next;
        k--;
    }//k-1==0,fast走到了第k-1个位置
    while (fast->_next)
    {
        fast = fast->_next;
        slow = slow->_next;
    }
    return slow;
}

9.判断链表是否带环
pNode CheckCircle(pList plist);

  • 思路:定义两个指针fast,slow,两个指针同时从头开始走,fast指针每次走两步,slow指针每次走一步,如果fast和slow走的过程中相遇了(为了代码实现方便我们可以让fast先走一步,这样不会再次判断初始时候他们两个相等的情况了),则说明这个链表是带环的
  • 代码实现:
pNode CheckCircle(pList plist)
{
    if (plist == NULL || (plist)->_next == NULL)
    {
        return NULL;
    }
    //若带环,返回相遇节点;否则返回空
    pNode fast = plist->_next;
    pNode slow = plist;

    while (fast != slow)
    {
        if (fast == NULL || fast->_next == NULL)
            return NULL;
        fast = fast->_next->_next;
        slow = slow->_next;
    }//fast==slow
    return slow;
}

10.求环的长度
int GetCircleLength(pNode meet);

  • 思路:我们首先通过图来分析一下快慢指针走的情况:
    这里写图片描述
  • 按照我们在判断链表是否带环题目中的想法,fast和slow同时走,fast每次走两步,slow每次走一步,若带环,则fast和slow回相遇。假设他们第一次相遇在z点,则此时fast走过的长度为:a+b+c+b;slow走过的长度为:a+b;因为fast速度是slow的两倍,所以有a+b+c+b=2(a+b);即a=c。而环的长度为b+c=b+a,所以从开始到二者第一次相遇,slow走过的长度即循环的次数即为环的长度。
  • 若已知快慢指针相遇节点,即已知位置已经在环中了,则只需通过循环计数直到临时指针到达相遇位置为止,此时的count计的数即为环的长度
  • 代码实现:
int GetCircleLength(pNode meet)
{
    pNode tmp = meet;
    if (meet == NULL)
        return 0;
    int count = 0;
    while (tmp->_next != meet)
    {
        ++count;
        tmp = tmp->_next;
    }//tmp->_next==meet;
    return count;
}

11.求环的入口点
pNode GetCycleEntryNode(pList plist, pNode meet);

  • 思路:已知相遇点和头结点,求环的入口节点。上述对第9题的分析中我们得到a=c,即头指针和相遇节点指针到环入口的距离是相等的。所以可以设置两个指针分别指向头结点和相遇节点,两个指针同时向后走,相遇的地方即为环的入口点
  • 代码实现:
pNode GetCycleEntryNode(pList plist, pNode meet)
{
    pNode p1 = plist;
    pNode p2 = meet;
    while (p1 != p2)
    {
        p1 = p1->_next;
        p2 = p2->_next;
    }//p1==p2
    return p1;
}

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/80204928