【数据结构与算法(三)】——链表

这是第三天,来自《剑指offer》
释放掉被删除节点占用的空间delete
重点是思路、思想要掌握

链表(很重要)

链表(节点+数据)由指针把若干个节点连接成链状结构。链表的创建、插入节点、删除节点等操作需要的代码量很少,适合面试。链表是一个动态的数据结构,其需要对指针进行操作。在创建链表时,无须知道链表的长度。当插入一个节点时,我们只需要为新节点分配内存,然后调整指针的指向来确保新节点被链接到链表当中。就很方便了,想加节点就换指针的指向,减去某个节点也是,不过应该要注意释放内存?这样就没有闲置的内存了,空间利用率还是很高的。
审题-》画图-》做题

单链表

节点定义

struct ListNode
{
    int value;
    ListNode* nextNode;
};

添加节点(在末尾)

注意看注释!

//注意!注意了!!
//函数的第一个参数是指向指针的指针。为什么?
//当我们往空链表中插入一个节点时,新插入的节点就是链表的头指针。
//这个时候就改写了头指针,如果传入的类型不是指向指针的指针,那到
//时候函数结束时,头指针(本来是没有任何指向的,也就是指针的值是0)
//还是没变,还是一个空指针。这个函数返回时,正常是头指针的值不会是“0”
//而且我们改的是指针指向的内存,而不是原来指向内存里的东西(原来也没有指向)
//一句话就是把 *pHead等效看成“int”之类的普通数据类型
void addToTail(ListNode **pHead, int value)
{
    ListNode* pNew = new ListNode();
    pNew->nextNode = nullptr;   //因为是插在末尾
    pNew->value = value;

    if (pHead == nullptr)
        *pHead = pNew;
    else {
        ListNode* pNode = *pHead;
        while (pNode->nextNode != nullptr)
            pNode = pNode->nextNode;
        pNode->nextNode = pNew;
    }
}

删除含有某值的节点(第一个)

想要在链表中查找某一个值的位置,只能顺着头节点开始往下找,沿着指针遍历链表,时间效率O(n),比起数组来说,这是链表的一个缺点

void RemoveNode(ListNode **pHead, int value)
{   //头指针是空的或者根本就没有这个链表存在
    if (pHead == nullptr || *pHead == nullptr)
        return;
    //之后要释放这个内存空间,所以要记录起来位置
    ListNode* pToBeDeleted = nullptr;
    //把头节点单独拎出来说,因为头节点没有前向指针
    if ((*pHead)->value == value) {
        pToBeDeleted = *pHead;
        *pHead = (*pHead)->nextNode;
    }
    else {
        ListNode* pNode = *pHead;
        while (pNode->nextNode != nullptr&&pNode->nextNode->value != value)
            pNode = pNode->nextNode;
        if (pNode->nextNode != nullptr) {
            pToBeDeleted = pNode->nextNode;
            pNode->nextNode = pNode->nextNode->nextNode;
        }
    }
    //释放掉被删除节点占用的空间
    if (pToBeDeleted != nullptr)
    {
        delete pToBeDeleted;
        pToBeDeleted = nullptr;
    }
}

题目

从尾到头打印链表

输入一个链表的头节点,从尾到头反过来打印出每个节点的值
要考虑题目允不允许修改链表结构,比如说指针反转?

思路:

考虑输入的情况:
1、输入的链表只有一个节点,只有头节点
2、输入的链表有多个节点
3、输入的是空链表,头节点为nullptr

思路1:栈#include<stack>

解决这个问题肯定需要遍历链表。在这道题中,遍历到的链表的第一个节点是输出的最后一个节点,不就是先进后出的栈结构,所以我们把遍历到的节点都一个个存放在栈中(用到了新的内存空间),之后再从栈中(栈顶)输出节点中的值就可以了

struct ListNode
{
    int value;
    ListNode* nextNode;
};

void PrintList(ListNode* pHead)
{
    //c++原来还有stack这东西,孤陋寡闻,在下面得熟悉一波
    //stack这简直太方便了,之前还一直自己写栈。
    std::stack<ListNode*> nodes;
    ListNode* pNode = pHead;
    while (pNode != nullptr) {
        nodes.push(pNode);
        pNode = pNode->nextNode;
    }
    while (!nodes.empty()) {
        pNode = nodes.top();
        std::cout << pNode->value;
        nodes.pop();
    }
}

思路2:递归本来就是一个栈结构

递归本质上就是一个栈结构。所以!每次访问到一个节点时,先递归输出它后面的节点(再一次调用这个函数,但是这里的参数是指向下一个节点的指针),再输出该节点自身,这样就可以从尾往前输出值了。

//思路清晰!几行搞定!
void PrintList(ListNode* pHead)
{
    if (pHead != nullptr) {
        if (pHead->nextNode != nullptr)
            PrintList(pHead->nextNode);
        std::cout << pHead->value;
    }
}

删除链表的节点1——在O(1)时间内删除链表节点

给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间内删除该节点(传入的是一个节点指针来着)

思路:

常规做法是遍历链表,然后删除节点,方法类似前面的删除末尾节点的操作,这里的时间复杂度是O(n),不符合题目要求。这里有两种方法:
这里写图片描述
1、在删除节点i之前,先从链表的头节点开始遍历到i前面的一个节点h,把h的nextNode指向i的下一个节点j,之后再删除i
2、把节点j的内容复制覆盖节点i,然后把节点i的nextNode指向节点j下一个节点,之后删除节点j【比较好的思路】
其实不是很能理解这两个解法再时间上有多大区别,毕竟都是要遍历,之后进行类似的删除操作。。。。emmmm,好像又突然懂了
第一种方法的话你还得找到指定的节点的前一个节点,因为是单向链表,所以他就不能直接知道前节点的位置,也就是说得从头节点开始遍历。第二种方法的话,根本不需要知道指定节点的前节点,因为你最终要删除的实际上是后一个节点,而不是指定的节点,但是再意义上还是把指定节点(包含某个值的节点【一个指针】)给删了,这样子其实在调用这个函数之前就得先知道这个指定节点的指针(不把时间算在这个函数中了)

void DeleteNode(ListNode** pHead, ListNode* pToBeDelete)
{
    if (!pHead || !pToBeDelete)
        return;
    //要删除的不是末尾节点
    if (pToBeDelete != nullptr) {
        ListNode* pNext = pToBeDelete->nextNode;
        pToBeDelete->value = pNext->value;
        pToBeDelete->nextNode = pNext->nextNode;

        delete pNext;
        pNext = nullptr;
    }//以下两种特殊情况容易忽略,才是考察的重点
    //链表只剩一个节点,删除头节点(也是尾节点)
    else if (pToBeDelete == *pHead) {
        delete pToBeDelete;
        pToBeDelete = nullptr;
        *pHead = nullptr;
    }
    else {//链表中有多个节点,并且删除的是尾节点,所以要先找到尾节点,nextNode=nullptr的节点
        ListNode* pNode = *pHead;
        while (pNode->nextNode != pToBeDelete)
            pNode = pNode->nextNode;

        pNode->nextNode = nullptr;
        delete pToBeDelete;
        pToBeDelete = nullptr;

    }
}

删除链表的节点2——删除链表中重复的节点

在一个排序的链表中,如何删除重复的节点?

思路:

充分考虑重复点的位置以及删除重复点后的节点后的链表的结果:
1、重复的节点位于链表的头部?尾部?中间?
2、链表中没有重复的节点
3、链表中所有节点都是重复的,涵盖了1的所有情况
4、空链表

void DeleteDuplication(ListNode** pHead)
{//没有这个链表或者是空链表
    if (pHead == nullptr || *pHead == nullptr)
        return;

    ListNode* pPreNode = nullptr;
    ListNode* pNode = *pHead;
    while (pNode != nullptr) {
        ListNode* pNext = pNode->nextNode;
        bool needDelete = false;   //是否需要删除的标识
        if (pNext != nullptr&& pNext->value == pNode->value) 
            needDelete = true;
        if (!needDelete) {  //不是重复的点,进行新的赋值,进入下一个循环
            pPreNode = pNode;
            pNode = pNode->nextNode;
        }
        else {  //是重复的点
            int value = pNode->value;//记录重复的值
            ListNode* pToBeDelete = pNode;  //记录需要被删除的节点,至少有当前节点和下一个节点
            while (pToBeDelete!=nullptr&&pToBeDelete->value==value)
            {
                pNext = pToBeDelete->nextNode;
                delete pToBeDelete;
                pToBeDelete = nullptr;
                pToBeDelete = pNext;    //可能删掉的节点后面的节点的value和被删掉的节点的value一样
            }
            if (pPreNode == nullptr)//头节点开始就是需要被删除的点,比较特殊
                *pHead = pNext;
            else
                pPreNode->nextNode = pNext;//被删掉的节点的下一个节点
        }
    }
}

链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值一次是 1 、2、3、4、5、6。这个链表的倒数第三个节点的值是4。
假设整个链表有n个节点,那么倒数第k个节点就是从头节点开始的第n-k+1个节点。如果我们能够得到链表中节点的个数n,那么只要从头节点开始往后走n-k+1步就可以了。如何得到节点数n,只需要从头开始遍历链表,每经过一个节点,计数器加1就可以了。就是说要遍历两次链表,第一次统计个数n,第二次找到倒数第k个节点。
有一种只遍历一次链表的方法(用两个指针):第一个指针从链表的头指针开始遍历向前走k-1步,第二个指针保持不动;从第k步开始,第二个指针也开始从链表的头指针开始遍历。两个指针总得保持k-1的距离,这样当第1个指针走到链表的尾节点时,第2个指针正好指向倒数第k个节点。
这里写图片描述

ListNode* FindKthToTail(ListNode* pHead, unsigned int k)
{
    ListNode* phead1=pHead; //走在前面的指针
    ListNode* phead2 = nullptr; //走在后面的指针

    for (unsigned int i = 0; i < k - 1; i++)
        phead1 = phead1->nextNode;  //走k-1步,为什么是k-1?看看上面的图

    phead2 = pHead;
    while (phead1 != nullptr) {
        phead1 = phead1->nextNode;
        phead2 = phead2->nextNode;
    }
    return phead2;
}

这是第四天,接着前一天写,还是链表的内容,所以还是继续写了,后面大部分是链表使用的代码优化。不自觉想表白一波csdn,在这里认识的人比较少,而且大家都是技术交流,不会有太多的互吹互喷什么的,一片净土来着。虽然自己的文章几乎没什么人看,就算阅读量也是自己每天复习点击增加的,之前写了许多文章都扔进了草稿箱,就是怕被大佬们笑话,但是想想,大佬也应该没什么时间做这种无聊的事。自己写博客的好处就是,回顾的时候,自己的话很容易能使自己回到当时学习的那个状态、想法。https://blog.csdn.net/ITTechnologyHome/article/details/53891087 【收藏一篇文章,关于vs与git】

反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

思路:

这里写图片描述
在前一个节点(比如i)指向h之前,要保留节点j,防止断链。还要知道节点h,因为要把i的nextNode指向h。这样一来每调整一个节点的nextNode就要涉及3个节点,可以定义3个指针,分别指向当前遍历到的节点、它的前节点、它的后节点。

ListNode* ReverseList(ListNode* pHead)
{
    ListNode* pNewHead = nullptr;   
    ListNode* pNode = pHead;    
    ListNode* pPreNode = nullptr;   //上一次的当前节点,这一次的上一个节点

    while (pNode != nullptr) {
        ListNode* pNext = pNode->nextNode;//保存下一个节点
        if (pNext == nullptr)   //原链表的尾节点
            pNewHead = pNode;   //新的头就是当前节点。尾节点,最后一步才有这个赋值
        pNode->nextNode = pPreNode;     //当前节点的nextNode改了,这是目的
        pPreNode = pNode;   //完成指向修改任务后就可以为下一轮做准备了
        pNode = pNext;  //使用之前保存好的下一个节点
    }
    return pNewHead;
}

提前想好测试用例很重要,思路才有办法展开,对于链表一般考虑三种情况:空链表,只有一个节点的链表,多节点链表,之后再根据题目具体考虑。

合并两个排序的链表

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

思路:

首先考虑有几种链表:空链表、只有一个节点的链表、多节点链表
1、输入两个链表有多个节点;节点的值都不相同或者两个链表中存在多个值相等的节点
2、两个链表的一个或两个头节点为nullptr,即空链表
3、两个链表中只有一个节点
使用两个指针,分别指向两个链表中的两个节点
!!!在开始写代码之前,要考虑出现空指针的情况(末尾节点、空链表),并考虑清楚怎么处理这些空指针

ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
    if (pHead1 == nullptr)      //链表1是空链表
        return pHead2;
    else if (pHead2 == nullptr)
        return pHead1;

    ListNode* pNewHead = nullptr;     //新的节点要有赋值???一定先赋值吗??
    if (pHead1->value < pHead2->value) {
        pNewHead = pHead1;
        //使用递归的方法,好像构造了一个新的链表一样
        pNewHead->nextNode = Merge(pHead1->nextNode, pHead2->nextNode);
    }
    return pNewHead;
}

两个链表的第一个公共节点

输入两个链表,找出它们的第一个公共节点

思路:

1、蛮力解决?
2、有公共节点的两个链表的特点,对于指针,同一个值只能存储在一个内存中,存一次,所以对于公共节点,它们的内存是共享的,即其实是只有一个这样的节点,因为是一样的,所以它们的nextNode指向的也只能是同一个内存,这样下去,从第一个公共节点开始,之后的节点都是为两个链表共有的,即两个链表是Y形状连接的。
3、如果两个链表有公共的节点,那么公共节点出现在两个链表的尾部。如果我们从两个链表的尾部开始往前比较,那么最后一个相同的节点就是我们要找的节点了,“从后往前比较”所以可以把两个链表入栈,这样就不需要遍历每一个第一个链表的节点的时候去遍历一遍第二个链表(O(m*n)),只需要对对应的节点的值进行比较就可以了,从前往后——》变为从后往前。这种做法是用空间效率换时间效率
4、还有一种思路是和之前的“找到链表中倒数第n个节点是类似的”——先遍历两个链表,计算它们的长度,之后进行长度比较,看看长的链表比短的链表的长度长多少(k),之后长链表的指针先走k步,之后就一起走相同的步数,知道到达同一个节点停止遍历。这样的做法不需要多余的空间,提高了空间效率,其时间效率也不会比2大

ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2)
{
    //遍历两个链表,分别得到链表的长度
    unsigned int len1 = GetListLen(pHead1);
    unsigned int len2 = GetListLen(pHead2);
    //直接假设len1>=len2
    int subLen = len1 - len2;
    ListNode* pLongList = pHead1;
    ListNode* pShortList = pHead2;
    //还是得进行比较,但只关注一种情况,因为前面已经有其他情况的处理方法了
    if (len2 > len1) {
        pLongList = pHead2;
        pShortList = pHead1;
        subLen = len2 - len1;
    }
    //先让长链表走subLen步
    for (int i = 0; i < subLen; i++)
        pLongList = pLongList->nextNode;
    //之后再一起走,然后比较,因为到达第一个相同节点需要走的步数是一样的。都是在链表的倒数
    while ((pLongList != nullptr) && (pShortList != nullptr) && (pLongList->value != pShortList->value)) {
        pLongList = pLongList->nextNode;
        pShortList = pShortList->nextNode;
    }
    return pLongList;
}
//计算链表的长度
unsigned int GetListLen(ListNode* pHead)
{
    unsigned int len = 0;
    //我在想,这里为什么要定义一个新的指针,不可以直接用传入来的参数吗?反正传进来的头指针指向的内存位置还是不会变的,而且这里面又没有改变到值的地方
    //后面再仔细想想,虽然头指针指向的节点的内存不变,而且也没有改变value的地方,但是同样作为元素,nextNode也是元素,这里有改变到节点nextNode的地方,所以。。
    ListNode* pNode = pHead;        
    while (pNode!=nullptr)
    {
        len++;
        pNode = pNode->nextNode;
    }
    return len;
}

猜你喜欢

转载自blog.csdn.net/laon_chan/article/details/80264741