剑指offer学习笔记 链表中倒数第k个节点

鲁棒是英文Robust的音译,有时也翻译成健壮性。鲁棒性指程序能够判断输入是否合乎规范要求,并对不符合要求的输入予以合适的处理。

容错性是鲁棒性的一个重要体现,不鲁棒的软件在发生异常事件时,如用户输入错误的用户名、试图打开的文件不存在或者网络不能连接,会出现不可预见的诡异行为,或者干脆整个软件崩溃。

提高代码的鲁棒性的有效途径是进行预防性编程,预防性编程是一种编程习惯,指预见在什么地方可能会出现问题,并为这些可能出现的问题制定处理方式。如当试图打开的文件不存在时,可以提示用户检查文件名和路径。

面试时最简单实用的防御性编程是在函数入口对用户的输入是否符合要求进行验证。如输入的是一个指针,那么空指针时怎样进行处理。

面试题22:链表中倒数第k个节点。输入一个链表,输出该链表中倒数第k个节点。为符合大多数人习惯,本题从1开始计数,即链表的尾节点是倒数第一个节点。链表节点定义如下:

struct ListNode {
    int m_nValue;
    ListNode *m_pNext;
};

为了得到倒数第k个节点,很自然的想法是先走到链表的尾端,再从尾端回溯k步。可是本题中的链表是单向链表,只有从前向后的指针而没有从后向前的指针,这种思路行不通。

假设整个链表有n个节点,那么倒数第k个节点就是从头结点开始的第n-k+1个节点(头节点为第一个节点)。因此,如果我们能够得到链表的长度n,那么只需要从头向尾找到第n-k+1个节点就可以了。如何得到节点数n?只需从头开始遍历整个链表,设置一个计数器即可。即我们需要遍历链表两次,第一次统计出链表中节点的个数,第二次就能找到倒数第k个节点。

为实现只遍历链表一次就能找到倒数第k个节点,我们可以定义两个指针,第一个指针从链表的头节点开始向尾遍历走k-1步,第二个指针指向头节点,之后一起向后遍历,直到第一个指针指向的节点是尾节点,第二个指针就指向了倒数第k个节点:

#include <iostream>
using namespace std;

struct ListNode {
    int m_nValue;
    ListNode* m_pNext;
};

void AddToTail(ListNode** pHead, int value) {
    ListNode* pNew = new ListNode();    //为新结点分配内存
    pNew->m_nValue = value;
    pNew->m_pNext = nullptr;

    if (*pHead == nullptr) {    //如果是空链表
        *pHead = pNew;
    }
    else {
        ListNode* pNode = *pHead;

        while (pNode->m_pNext != nullptr) {    //找到链表末尾
            pNode = pNode->m_pNext;
        }

        pNode->m_pNext = pNew;    //将链表末尾元素的指针指向新节点
    }
}


ListNode* FindKthToTail(ListNode* head, unsigned k) {
    ListNode* pAhead = head;
    ListNode* pBehind = head;

    for (unsigned i = 0; i < k - 1; ++i) {
        pBehind = pBehind->m_pNext;
    }

    while (pBehind->m_pNext != nullptr) {
        pAhead = pAhead->m_pNext;
        pBehind = pBehind->m_pNext;
    }
    return pAhead;
}

int main() {
    ListNode* pHead = new ListNode();
    pHead->m_nValue = 0;
    pHead->m_pNext = nullptr;
    AddToTail(&pHead, 1);
    AddToTail(&pHead, 2);
    AddToTail(&pHead, 3);
    AddToTail(&pHead, 4);
    AddToTail(&pHead, 5);
    AddToTail(&pHead, 6);
    AddToTail(&pHead, 7);
    cout << (FindKthToTail(pHead, 5)->m_nValue) << endl;
}

很多人能写出来以上代码,但鲁棒性不够好,还是不能通过面试:
1.输入pHead为空指针,代码会访问空指针指向的内存,造成程序崩溃。
2.输入的链表节点数小于k,在for循环中仍然会向后走k-1步,仍然会访问空指针指向的内存。
3.输入的参数k为0,k-1得到的不是-1,而是无符号数0xFFFFFFFF,即4294967295,。

修改后的代码:

#include <iostream>
using namespace std;

struct ListNode {
    int m_nValue;
    ListNode* m_pNext;
};

void AddToTail(ListNode** pHead, int value) {
    ListNode* pNew = new ListNode();    //为新结点分配内存
    pNew->m_nValue = value;
    pNew->m_pNext = nullptr;

    if (*pHead == nullptr) {    //如果是空链表
        *pHead = pNew;
    }
    else {
        ListNode* pNode = *pHead;

        while (pNode->m_pNext != nullptr) {    //找到链表末尾
            pNode = pNode->m_pNext;
        }

        pNode->m_pNext = pNew;    //将链表末尾元素的指针指向新节点
    }
}


ListNode* FindKthToTail(ListNode* head, unsigned k) {
    if (head == nullptr || k < 1) {
        return nullptr;
    }

    ListNode* pAhead = head;
    ListNode* pBehind = head;

    for (unsigned i = 0; i < k - 1 ; ++i) {
        if (pBehind->m_pNext == nullptr) {
            return nullptr;
        }
        else {
            pBehind = pBehind->m_pNext;
        }
    }

    while (pBehind->m_pNext != nullptr) {
        pAhead = pAhead->m_pNext;
        pBehind = pBehind->m_pNext;
    }
    return pAhead;
}

int main() {
    ListNode* pHead = new ListNode();
    pHead->m_nValue = 0;
    pHead->m_pNext = nullptr;
    AddToTail(&pHead, 1);
    AddToTail(&pHead, 2);
    AddToTail(&pHead, 3);
    AddToTail(&pHead, 4);
    AddToTail(&pHead, 5);
    AddToTail(&pHead, 6);
    AddToTail(&pHead, 7);
    ListNode* pNode = FindKthToTail(pHead, 1);
    if (pNode) {
        cout << pNode->m_nValue << endl;
    }
    else {
        cout << "Invalid Input" << endl;
    }
}

相关题目:求链表的中间节点。如果链表中的节点总数为奇数,则返回中间节点;如果链表中节点总数为偶数,则返回中间两个节点的任意一个。我们可以定义两个指针,都初始化为头结点,同时从头结点出发,一个指针一次走一步,另一个指针一次走两步,当走得快的指针走到末尾时,走得慢的指针正好指向链表中间。

发布了193 篇原创文章 · 获赞 11 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/tus00000/article/details/104498551