面试题15:链表中倒数第K个结点

题目:输入一个链表,输出该链表中倒数第K个结点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾结点是倒数第1个结点。例如一个链表有6个结点,从头结点开始它们的值依次是1、23456。这个链表的倒数第3个结点是值为4的结点。

看到这道题目,最直观的想法,就是先算出链表的长度n,然后倒数第k个结点就是顺序的第(n-k+1)个数,不过这样需要2次遍历链表,如果要求只能遍历链表一次,那么上述算法就不符合要求了。

那我们就使用第二种算法,设定两个指针p1和p2,两个指针刚开始都指向链表的第一个结点,然后让p1指针先走(k-1)步,然后再让两个指针一起往后走,当p1指针指向链表最后一个结点的时候,p2指针刚好指向链表中的倒数第k个结点。

在写代码的时候需要考虑鲁棒性,最好采用防御性编程,就是考虑在哪些地方会出错,然后提前加上错误判断,这样避免因此错误输入而导致程序崩溃。

下面首先给出代码实例,然后再讲解程序鲁棒性:

《面试题15:链表中倒数第K个结点》
《面试题15:链表中倒数第K个结点》
View Code

#include<iostream>
#include<stdlib.h>
#include<stack>
using namespace std;

//链表结构
struct ListNode
{
    int m_nValue;
    ListNode* m_pNext;
};

//创建一个链表结点
ListNode* CreateListNode(int value)
{
    ListNode *pNode=new ListNode();
    pNode->m_nValue=value;
    pNode->m_pNext=NULL;
    return pNode;

}

//遍历链表中的所有结点
void PrintList(ListNode* pHead)
{
    ListNode *pNode=pHead;
    while(pNode!=NULL)
    {
        cout<<pNode->m_nValue<<" ";
        pNode=pNode->m_pNext;
    }
    cout<<endl;
}

//输出链表中的某一结点的值
void PrintListNode(ListNode* pNode)
{ 
    if(pNode == NULL)
    {
        printf("The node is NULL\n");
    }
    else
    {
        printf("The key in node is %d.\n", pNode->m_nValue);
    }
}

//往链表末尾添加结点
/*
注意这里pHead是一个指向指针的指针,在主函数中一般传递的是引用。
因为如果要为链表添加结点,那么就会修改链表结构,所以必须传递引用才能够保存修改后的结构。
*/
void AddToTail(ListNode** pHead,int value)
{
    ListNode* pNew=new ListNode();//新插入的结点
    pNew->m_nValue=value;
    pNew->m_pNext=NULL;

    if(*pHead==NULL)//空链表
    {
        *pHead=pNew;
    }
    else
    {
        ListNode* pNode=*pHead;
        while(pNode->m_pNext!=NULL)
            pNode=pNode->m_pNext;
        pNode->m_pNext=pNew;
    }

}

//非防御性编程,非法输入会导致错误。
ListNode* KthNodeFromEnd(ListNode* pHead,int k)
{
    ListNode* pNode=pHead;//当前结点
    ListNode* pKthNode=pHead;//

    while(k-1>0)
    {
        pNode=pNode->m_pNext;//让pNode先走k-1步
        --k;
    }

    while(pNode->m_pNext!=NULL)
    {
        pNode=pNode->m_pNext;
        pKthNode=pKthNode->m_pNext;
    }

    return pKthNode;
}

//防御性编程,鲁棒性更好
ListNode* KthNodeFromEnd2(ListNode* pHead,int k)
{
    if(pHead==NULL||k==0)
        return NULL;

    ListNode* pNode=pHead;//当前结点
    ListNode* pKthNode=pHead;//

    while(k-1>0)
    {
        if(pNode->m_pNext!=NULL)
        {
            pNode=pNode->m_pNext;//让pNode先走k-1步
            --k;
        }
        else
            return NULL;
    }

    while(pNode->m_pNext!=NULL)
    {
        pNode=pNode->m_pNext;
        pKthNode=pKthNode->m_pNext;
    }

    return pKthNode;
}

void main()
{
    //创建结点
    ListNode* pNode1=CreateListNode(1);//创建一个结点
    PrintList(pNode1);//打印
    //往链表中添加新结点
    AddToTail(&pNode1,2);//为链表添加一个结点
    AddToTail(&pNode1,3);//为链表添加一个结点
    AddToTail(&pNode1,4);//为链表添加一个结点
    AddToTail(&pNode1,5);//为链表添加一个结点
    AddToTail(&pNode1,6);//为链表添加一个结点
    AddToTail(&pNode1,7);//为链表添加一个结点
    //打印链表
    PrintList(pNode1);//打印
    //反转链表
    ListNode* KthNode=KthNodeFromEnd2(pNode1,3);

    PrintListNode(KthNode);

    system("pause");

}

上述程序中,求倒数第k个结点的第一个方法:ListNode* KthNodeFromEnd(ListNode* pHead,int k)。

这个方法看似满足了我们的题目要求,但是当我们仔细分析,发现有3中方法让程序崩溃

  1. 输入的pListHead为空指针,由于代码会尝试访问空指针指向的内存,程序崩溃
  2. 输入的以pListHead为头结点的链表的结点少于k。这样在while(k-1>0)的循环中,又会出现尝试访问空指针指向的内存。
  3. 输入的参数k小于等于0。根据题意k的最小值应为1。

 考虑上述因素,我们给出了第二个方法:ListNode* KthNodeFromEnd2(ListNode* pHead,int k)。

相关题目

1.求链表的中间结点。如果链表中结点总数为奇数,返回中间结点;如果结点总数是偶数,返回中间两个结点的任意一个。为了解决这个问题,我们也可以定义两个指针,同时从链表的头结点出发,一个指针一次走一步,另一个指针一次走两步。当走得快的指针走到链表的末尾时,走得慢的指针正好在链表的中间。

代码实例如下:

《面试题15:链表中倒数第K个结点》
《面试题15:链表中倒数第K个结点》
View Code

#include<iostream>
#include<stdlib.h>
#include<stack>
using namespace std;

//链表结构
struct ListNode
{
    int m_nValue;
    ListNode* m_pNext;
};

//创建一个链表结点
ListNode* CreateListNode(int value)
{
    ListNode *pNode=new ListNode();
    pNode->m_nValue=value;
    pNode->m_pNext=NULL;
    return pNode;

}

//遍历链表中的所有结点
void PrintList(ListNode* pHead)
{
    ListNode *pNode=pHead;
    while(pNode!=NULL)
    {
        cout<<pNode->m_nValue<<" ";
        pNode=pNode->m_pNext;
    }
    cout<<endl;
}

//输出链表中的某一结点的值
void PrintListNode(ListNode* pNode)
{ 
    if(pNode == NULL)
    {
        printf("The node is NULL\n");
    }
    else
    {
        printf("The key in node is %d.\n", pNode->m_nValue);
    }
}

//往链表末尾添加结点
/*
注意这里pHead是一个指向指针的指针,在主函数中一般传递的是引用。
因为如果要为链表添加结点,那么就会修改链表结构,所以必须传递引用才能够保存修改后的结构。
*/
void AddToTail(ListNode** pHead,int value)
{
    ListNode* pNew=new ListNode();//新插入的结点
    pNew->m_nValue=value;
    pNew->m_pNext=NULL;

    if(*pHead==NULL)//空链表
    {
        *pHead=pNew;
    }
    else
    {
        ListNode* pNode=*pHead;
        while(pNode->m_pNext!=NULL)
            pNode=pNode->m_pNext;
        pNode->m_pNext=pNew;
    }

}

//求链表中间结点
ListNode* MidNodeInList(ListNode* pHead)
{
    if(pHead==NULL)
        return NULL;

    ListNode* pNode=pHead;//当前结点
    ListNode* pMidNode=pHead;//中间结点

    //这里的判断条件是下一个结点不为空且下下个结点也不为空
    while(pNode->m_pNext!=NULL&&pNode->m_pNext->m_pNext!=NULL)
    {
        pNode=pNode->m_pNext->m_pNext;
        pMidNode=pMidNode->m_pNext;
    }
    return pMidNode;
}

void main()
{
    //创建结点
    ListNode* pNode1=CreateListNode(1);//创建一个结点
    PrintList(pNode1);//打印
    //往链表中添加新结点
    AddToTail(&pNode1,2);//为链表添加一个结点
    AddToTail(&pNode1,3);//为链表添加一个结点
    AddToTail(&pNode1,4);//为链表添加一个结点
    AddToTail(&pNode1,5);//为链表添加一个结点
    AddToTail(&pNode1,6);//为链表添加一个结点
    AddToTail(&pNode1,7);//为链表添加一个结点
    //打印链表
    PrintList(pNode1);//打印

    //求链表的中间结点
    ListNode* MidNode=MidNodeInList(pNode1);
    PrintListNode(MidNode);

    system("pause");

}

2.判断一个单向链表是否形成了环状结构。和前面的问题一样,定义两个指针,同时从链表的头结点出发,一个指针一次走一步,另外一个指针一次走两步。如果走得快的指针追上了走得慢的指针,那么链表就是环状结构;如果走得快的指针走到了链表的末尾(m_pNext指向NULL)都没有追上走得慢的指针,那么链表就不是环状结构。

代码实例如下:

《面试题15:链表中倒数第K个结点》
《面试题15:链表中倒数第K个结点》
View Code

#include<iostream>
#include<stdlib.h>
#include<stack>
using namespace std;

//链表结构
struct ListNode
{
    int m_nValue;
    ListNode* m_pNext;
};

//创建一个链表结点
ListNode* CreateListNode(int value)
{
    ListNode *pNode=new ListNode();
    pNode->m_nValue=value;
    pNode->m_pNext=NULL;
    return pNode;

}

//连接两个结点
void ConnectListNode(ListNode* pCurrent,ListNode* pNext)
{
    if(pCurrent==NULL)
    {
        cout<<"前一个结点不能为空"<<endl;
        exit(1);
    }
    else
    {
        pCurrent->m_pNext=pNext;
    }
}

//遍历链表中的所有结点
void PrintList(ListNode* pHead)
{
    ListNode *pNode=pHead;
    while(pNode!=NULL)
    {
        cout<<pNode->m_nValue<<" ";
        pNode=pNode->m_pNext;
    }
    cout<<endl;
}

//输出链表中的某一结点的值
void PrintListNode(ListNode* pNode)
{ 
    if(pNode == NULL)
    {
        printf("The node is NULL\n");
    }
    else
    {
        printf("The key in node is %d.\n", pNode->m_nValue);
    }
}

//判断链表中是否有环
int IsLootList(ListNode* pHead)
{
    if(pHead==NULL)
        return NULL;

    ListNode* pFirst=pHead;//第一个指针,步长为2
    ListNode* pSecond=pHead;//第二个指针,步长为1


    while(pFirst->m_pNext!=NULL&&pFirst->m_pNext->m_pNext!=NULL)
    {
        pFirst=pFirst->m_pNext->m_pNext;
        pSecond=pSecond->m_pNext;
        if(pFirst==pSecond)
            return 1;
    }
    return 0;
}

void main()
{
    //创建结点
    ListNode* pNode1=CreateListNode(1);//创建一个结点
    ListNode* pNode2=CreateListNode(2);//创建一个结点
    ListNode* pNode3=CreateListNode(3);//创建一个结点
    ListNode* pNode4=CreateListNode(4);//创建一个结点
    ListNode* pNode5=CreateListNode(5);//创建一个结点
    ListNode* pNode6=CreateListNode(6);//创建一个结点
    ListNode* pNode7=CreateListNode(7);//创建一个结点
    //连接结点
    ConnectListNode(pNode1,pNode2);//连接两个结点
    ConnectListNode(pNode2,pNode3);//连接两个结点
    ConnectListNode(pNode3,pNode4);//连接两个结点
    ConnectListNode(pNode4,pNode5);//连接两个结点
    ConnectListNode(pNode5,pNode6);//连接两个结点
    ConnectListNode(pNode6,pNode7);//连接两个结点
    ConnectListNode(pNode7,pNode1);//连接两个结点

    //打印链表
    //PrintList(pNode1);//打印
    if(IsLootList(pNode1))
    {
        cout<<"存在环"<<endl;
    }
    else
    {
        cout<<"不存在环"<<endl;
    }

    system("pause");

}

 

 

 

 

    原文作者:xwdreamer
    原文地址: https://www.cnblogs.com/xwdreamer/archive/2012/04/27/2473355.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞