面試題15:鏈表中倒數第K個結點


題目:輸入一個鏈表,輸出該鏈表中倒數第K個結點。為了符合大多數人的習慣,本題從1開始計數,即鏈表的尾結點是倒數第1個結點。例如一個鏈表有6個結點,從頭結點開始它們的值依次是1、23456。這個鏈表的倒數第3個結點是值為4的結點。

看到這道題目,最直觀的想法,就是先算出鏈表的長度n,然后倒數第k個結點就是順序的第(n-k+1)個數,不過這樣需要2次遍歷鏈表,如果要求只能遍歷鏈表一次,那么上述算法就不符合要求了。

那我們就使用第二種算法,設定兩個指針p1和p2,兩個指針剛開始都指向鏈表的第一個結點,然后讓p1指針先走(k-1)步,然后再讓兩個指針一起往后走,當p1指針指向鏈表最后一個結點的時候,p2指針剛好指向鏈表中的倒數第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.求鏈表的中間結點。如果鏈表中結點總數為奇數,返回中間結點;如果結點總數是偶數,返回中間兩個結點的任意一個。為了解決這個問題,我們也可以定義兩個指針,同時從鏈表的頭結點出發,一個指針一次走一步,另一個指針一次走兩步。當走得快的指針走到鏈表的末尾時,走得慢的指針正好在鏈表的中間。

代碼實例如下:

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)都沒有追上走得慢的指針,那么鏈表就不是環狀結構。

代碼實例如下:

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");

}

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM