剑指offer 学习笔记 链表

我们说链表是一种动态数据结构是因为在创建链表时,无需知道链表的长度。当插入一个节点时,我们只需要为新节点分配内存,然后调整指针的指向来确保新节点被链接到链表当中。内存分配不是创建链表时一次性完成的,而是每添加一个节点分配一次内存,由于没有闲置的内存,链表的空间效率比较高。

如果单向链表的节点定义如下:

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;    // 将链表末尾元素的指针指向新节点
    }
}

以上代码中,函数的第一个参数是指向指针的指针,因为当链表为空时,新插入的节点就是头指针,此时我们会改动头指针,因此需要指向指针的指针,否则出了函数头指针还是空。

如想在链表中找到第i个节点,我们只能从头节点开始,沿着指向下一个节点的指针遍历链表,时间效率为O(n)。而在数组中,我们可以根据下标在O(1)的时间内找到第i个元素。

在链表中找到第一个含有某值的节点并删除节点:

void removeNode(ListNode** pHead, int value) {
    if (pHead == nullptr || *pHead == nullptr) {
        return;
    }

    ListNode* toBeDelete = nullptr;
    if ((*pHead)->m_nValue == value) {
        toBeDelete = *pHead;
        *pHead = (*pHead)->m_pNext;
    } else {
        ListNode* pNode = *pHead;

        while (pNode->m_pNext != nullptr && pNode->m_pNext->m_nValue != value) {
            pNode = pNode->m_pNext;
        }

        if (pNode->m_pNext != nullptr) {
            toBeDelete = pNode->m_pNext;
            pNode->m_pNext = pNode->m_pNext->m_pNext;
        }
    }

    if (toBeDelete != nullptr) {
        delete toBeDelete;
        toBeDelete = nullptr;
    }

    return;
}

面试题6:输入一个链表的头节点,从尾到头反过来打印出每个节点的值,链表节点定义如上。

很多人第一反应是从头到尾输出比较简单,于是很自然想到把链表中链接节点的指针反过来,改变链表方向,但该方法会改变链表结构,该方法能否使用取决于能否改变链表结构。

通常打印是一个只读操作,我们不希望打印时修改内容。解决问题过程肯定要遍历链表,遍历顺序是从头到尾,而输出顺序是从尾到头。也就是说,第一个遍历到的节点最后输出,这是典型的后进先出,可以使用栈实现此顺序:

#include <iostream>
#include <stack>
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;    // 尾插
    }
}

void printListReversingly_iteratively(ListNode* pHead) {
    stack<ListNode*> nodes;

    ListNode* pNode = pHead;
    while (pNode != nullptr) {    // 将链表从头到尾压栈
        nodes.push(pNode);
        pNode = pNode->m_pNext;
    }

    while (!nodes.empty()) {    // 打印栈中内容
        cout << nodes.top()->m_nValue << endl;    // 打印栈顶值
        nodes.pop();    // 栈顶元素出栈
    }
}

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);
    printListReversingly_iteratively(pHead);
	return 0;
}

既然想到用栈来实现这个函数,而递归本质上就是一个栈结构,于是很自然又想到用递归来实现:

#include <iostream>
#include <stack>
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;
    }
}

void printListReversingly_Recursively(ListNode* pHead) {
    if (pHead != nullptr) {
        printListReversingly_Recursively(pHead->m_pNext);
        cout << pHead->m_nValue << endl;    // 此句代码要在上句代码(下一层递归)之后
    }
}

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);
    printListReversingly_Recursively(pHead);
	return 0;
}

以上代码很简洁,但当链表很长时,会导致函数调用的层级很深,有可能导致函数调用栈溢出。显然用栈基于循环实现的代码鲁棒性要好一些。

posted @   epiphanyy  阅读(5)  评论(0编辑  收藏  举报  
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示