(C++)剑指offer-3:从尾到头打印链表

剑指offer-3:从尾到头打印链表

目录

1问题描述

输入一个链表,从尾到头打印链表每个节点的值。
这里写图片描述

2问题解析

*链表是动态数据结构,找其某个值,只能从头结点开始。
后进先出结构。

  • 数据结构:链表、栈、vector
  • 算法:递归

(1) 从头到尾输出比较简单,一种想法是反转结点的指针。但是会破坏原链表的结构,不推荐;
(2) 从头遍历链表,先访问的后输出,后访问的先输出,“后进先出”,利用栈来实现;
(3) 递归本质上就是一个栈的结构,可以利用递归来实现。
但是当链表比较长的时候,递归会导致函数调用的层级很深,有可能会导致函数调用栈的溢出,故还是推荐使用栈来实现。

3链表学习

3.1数组和链表

如果要保存一些数据类型相同的变量,比如n个int类型的变量,就可以存放在一个数组中,然后通过下标方便的访问。

可是数组的缺点也比较多
(1)第一个就是在声明数组的时候,数组的长度必须是明确的,即便是动态声明一个数组,处理器必须要知道长度才能在内存中找出一段连续的内存来保存你的变量。
(2)第二个缺点也就是上一句中说到的,数组在内存中的地址必须是连续的,这样就可以通过数组首地址再根据下标求出偏移量,快速访问数组中的内容。现在计算机内存越来越大,这也算不上什么缺点。
(3)第三个缺点,也是非常难以克服的缺点,就是在数组中插入一个元素是非常费劲的。比如一个长度为n的数组,你要在数组下标为0处插入一个元素,在不额外申请内存的情况下,就需要把数组中的元素全部后移一位,还要舍弃末尾元素,这个时间开销是很大的,时间复杂度为o(n)。

数组的改良版本就是vector向量
它很好的克服了数组长度固定的缺点,vector的长度是可以动态增加的。如果明白向量的内部实现原理,那么我们知道,vector的内部实现还是数组,只不过在元素数量超过vector长度时,会按乘法或者指数增长的原则,申请一段更大的内存,将原先的数据复制过去,然后释放掉原先的内存。

如果你的数据不需要在数组中进行插入操作,那么数组就是个很好的选择,如果你的元素数组是动态增长的,那么vector就可以满足。

链表很好的克服了数组的缺点,它在内存中不需要连续的内存,插入或者删除操作,o(1)时间复杂度就能解决,长度也是动态增长。如果你的元素需要频繁的进行插入、删除操作,那么链表就是个很好的选择。

下图是数组和链表在内存中的组织形式
这里写图片描述

数组
可以看到数组在内存中是连续的,用起始地址加上偏移地址就可以直接取出其中的元素,起始地址就是数组名,偏移地址就是数组的下标index*sizeof(t),t就是数组的类型。

链表
但是链表在内存中是不连续,为什么叫链表,就是感觉像是用链子把一个个节点串起来的。那么一个个节点是怎么串接起来的呢,就是指针,每一个节点(末尾节点除外)都包含了指向下一个节点的指针,也就是说指针中保存着下一个节点的首地址,这样,1号节点知道2号节点保存在什么地址,2号节点知道3号节点保存在什么地址…以此类推。就像现实中寻宝一样,你只知道1号藏宝点,所以你先到达1号藏宝点,1号藏宝点你会得到2号藏宝点的地址,然后你再到达2号藏宝点…直到你找到了你需要的宝藏。链表的遍历就是用这种原理。

链表已经超出了基本数据类型的范围,所以要使用链表,要么使用STL库,要么自己用基本数据类型实现一个链表。如果是编程中正常的使用,当然是推荐前者,如果想真正搞懂链表这个数据结构,还是推荐后者。那样不仅知道标准库提供的API,也知道这种数据结构内部 的实现原理。这样的一个好处就是在你编程的时候,尤其是对时间空间复杂度比较敏感的程序,你可以根据要求选择一种最适合的数据结构,提高程序运行的效率。

3.2链表的简单使用

一个个节点按顺序串接起来,就构成了链表。显然这一个个节点是很关键的,假设我们要构造一个int类型的链表,那么一个节点中就需要包含两个元素:

  • 一个是当前节点所保存的值,设为int value。
  • 另一个就是指向下一个节点的指针,我们再假设这个节点类是node,那么这个指针就是 node *next。

这里一定不是int *next。因为这个指针指向的下一个元素是一个类的实例,而不是int类型的数据。那么node这个类最简单的实现就如下

class node  
{  
public:  
    int value;  
    node *next;  
    node()  
    {  
        value = 0;  
        next = NULL;  
    }  
};  

这个类名字为node,包含两个元素,一个是当前node的值,一个是指向下一个节点的指针,还有一个构造函数,分别将value初始化为0、next初始化为NULL。

拿到这个类以后,假设我们生成两个这个类的实例,node1和node2,再将node1的next指针指向node2,就构成了有两个元素的链表。这样如果我们拿到node1这个节点,就可以通过next指针访问node2。比如下面的代码

#include <iostream>  
using namespace std;  
class node  
{  
public:  
    int value;  
    node *next;  
    node()  
    {  
        value = 0;  
        next = NULL;  
    }  
};  
int main()  
{     
    node node1,node2;  
    node1.value = 1;  
    node2.value = 2;  
    node1.next = &node2;  
    cout << (node1.next)->value << endl;  
    cout << node2.value <<endl;
}  

运行结果:
2
2

  • 先生成两个node类的实例,node1和node2,分别将它们的value初始化为1和2。再用&运算符取出node2的首地址,赋值给node1.next。这样node1的next指针就指向了node2。这样我们就可以先拿到node1的next指针,在通过“->”运算符访问node2的value值,输出就是2。

将刚刚的代码稍作修改:

#include <iostream>  
using namespace std;  
class node  
{  
public:  
    int value;  
    node *next;  
    node()  
    {  
        value = 0;  
        next = NULL;  
    }  
};  
int main()  
{     
    node node1,node2;  
    node1.value = 1;  
    node2.value = 2;  
    node1.next = &node2;  
    cout << sizeof(node) << endl;  
    cout << &node1 << endl;  
    cout << &node2 << endl;  
}  

16
0x7ffd16a6ac80
0x7ffd16a6ac90

资料中的运行结果为:
这里写图片描述

这样我们就可以根据输出画出它们在内存中的非常详细结构图
这里写图片描述

3.3链表的进阶使用

上述这样就构成了一个最简单的链表,如果还有新的节点出现,那么就如法炮制,链在表尾或者表头,当然插在中间也是没问题的。

但是这样还有个问题就是node1和node2是我们提前声明好的,而且知道这两个实例的名称,如果我们需要1000甚至跟多节点,这种方式显然是不科学的,而且在很多时候,我们都是动态生成一个类的实例,返回的是这个实例的首地址。

下面的代码我们用一个for循环,生成11个节点,串起来形成一个链表

  • 原理就是先生成一个头结点,然后动态生成10个节点,每生成一个节点,就将这个节点指向头结点,然后更新头结点为当前节点。
#include <iostream>  
using namespace std;  
class node  
{  
public:  
    int value;  
    node *next;  
    node()  
    {  
        value = 0;  
        next = NULL;  
    }  
};  
int main()  
{     
    node *head,*curr;  
    head = new node();  
    head->next = NULL;  
    head->value = 15;  
    for (size_t i = 0; i < 10; i++)  
    {  
        curr = new node();  
        curr->value = i;  
        curr->next = head;  // head是地址,curr也是地址
        head = curr;  
        cout << head->value << endl; 
    }   
}  

0
1
2
3
4
5
6
7
8
9

那么链表该如何遍历呢,刚开头的时候就说,遍历链表需要从头到尾,访问每一个元素,直到链表尾。也就是说不断地访问当前节点的next,直到NULL。下面是链表的遍历输出

#include <iostream>  
using namespace std;  
class node  
{  
public:  
    int value;  
    node *next;  
    node()  
    {  
        value = 0;  
        next = NULL;  
    }  
};  
int main()  
{     
    node *head,*curr;  
    head = new node();  
    head->next = NULL;  
    head->value = 15;  
    for (size_t i = 0; i < 10; i++)  
    {  
        curr = new node();  
        curr->value = i;  
        curr->next = head;  
        head = curr;  
    }  
    while (head!=NULL)  
    {  
        cout << head->value << endl;  
        head = head->next;  
    }  
}  

9
8
7
6
5
4
3
2
1
0
15

3.4链表的高阶使用

链表相对于数组有个非常明显的优点就是能以时间复杂度o(1)完成一个节点的插入或者删除操作。

插入操作的原理很简单,假设现在有三个节点,一个是当前节点curr,一个是当前节点的下一个节点,也就是后继节点,假设为next,还有一个待插入的节点,假设为insert。插入操作就是让当前节点的后继节点指向insert节点,insert节点的后继节点指向next节点。以下是示意图
这里写图片描述

删除操作的原理也是类似的,就是让当前节点的后继节点指向它后继节点的后继节点。示意图如下
这里写图片描述

那么插入和删除操作用代码如何实现呢,我们还用原先的链表,先插入一个值为20的节点,输出链表的全部元素。然后再删除链表中这个值为20的元素,输出元素的全部内容。代码如下

#include <iostream>  
using namespace std;  
class node  
{  
public:  
    int value;  
    node *next;  
    node()  
    {  
        value = 0;  
        next = NULL;  
    }  
};  
int main()  
{     
    node *head=NULL,  
        *curr=NULL,         //当前节点  
        *insert=NULL,       //插入节点    
        *next=NULL,         //后继节点  
        *pre=NULL;          //前驱节点  
    head = new node();  
    head->next = NULL;  
    head->value = 15;  
    for (size_t i = 0; i < 10; i++)  
    {  
        curr = new node();  
        curr->value = i;  
        curr->next = head;  
        head = curr;  
    }  
    curr = head;            //取出头结点  
    while (curr->value != 5)  
    {  
        curr = curr->next;  
    }     
                            //找到值为5的节点  
    next = curr->next;       //找到值为5的节点的后继节点  
    insert = new node();    //生成一个新的节点,值为20  
    insert->value = 20;  
    curr->next = insert; //当前节点的后继节点为插入节点  
    insert->next = next; //插入节点的后继节点为值为5的节点的后继节点  
    curr = head;            //遍历链表,输出每一个元素  
    while (curr!=NULL)  
    {  
        cout << curr->value<<endl;  
        curr = curr->next;  
    }  
    curr = head;            //找到头结点  
    while (curr->value!=20)  
    {  
        pre = curr;  
        curr = curr->next;  
    }  
    //找到值为20的节点,注意现在是单向链表,每个节点中不保存它的前驱节点,所以在遍历的过程中要人为保存一下前驱节点  
    next = curr->next;       //找到当前节点的后继节点(当前节点就是值为20的节点)  
    pre->next = next;        //当前节点的前驱节点的后继节点为当前节点的后继节点  
    delete curr;            //删除当前节点  
    curr = head;            //遍历这个链表输出每个元素  
    while (curr != NULL)  
    {  
        cout << curr->value << endl;  
        curr = curr->next;  
    }  
    while (true)  
    {  

    }  
}  

这里写图片描述

至于完整的链表,STL中有标准的库,也有功能非常全面的API,只要我们知道内部的实现原理,调用这些API是非常简单的事,用起来也会得心应手。

参考博客

3.5链表其他操作

// 在末尾加入新的结点
void AddToTail(ListNode** pHead, int value){
    ListNode* pNew = new ListNode();
    pNew->val = value;
    pNew->next = nullptr;

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

        pNode->next = pNew;
    }

    return;
}

// 删除某个值为value的结点
void RemoveNode(ListNode** pHead, int value){
    if(pHead == nullptr || *pHead == nullptr) return;

    ListNode* pToDeleted = nullptr;
    if((*pHead)->val == value){
        pToDeleted = *pHead;
        *pHead = (*pHead)->next;
    }else{
        ListNode* pNode = *pHead;
        while(pNode->next != nullptr && pNode->next->val != value)
            pNode = pNode->next;

        if(pNode->next != nullptr && pNode->next->val == value){
            pToDeleted = pNode->next;
            pNode->next = pNode->next->next;
        }
    }

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

    return;
}

4题目答案

4.1利用递归实现

方法一:链表从尾到头输出,利用递归实现,不使用库函数直接printf输出的时候用递归比较好

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    vector<int> printListFromTailToHead(struct ListNode* head) {
        vector<int> value;
        if(head != NULL)
        {
            value.insert(value.begin(),head->val);
            if(head->next != NULL)
            {
                vector<int> tempVec = printListFromTailToHead(head->next);
                if(tempVec.size()>0)
                value.insert(value.begin(),tempVec.begin(),tempVec.end());  
                //在最前面插入 tempVec.begin()个tempVec.end()
            }         

        }
        return value;
    }
};

4.2用库函数实现

方法二:用库函数,每次扫描一个节点,将该结点数据存入vector中,如果该节点有下一节点,将下一节点数据直接插入vector最前面,直至遍历完。
或者直接加在最后,最后调用reverse

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    vector<int> printListFromTailToHead(struct ListNode* head) {
        vector<int> value;
        if(head != NULL)
        {
            value.insert(value.begin(),head->val);
            while(head->next != NULL)
            {
                value.insert(value.begin(),head->next->val);
                head = head->next;
            }         

        }
        return value;
    }
};

第二种更好理解
就是依次将链表的值插在vector的最前面,然后输出vector即可
如链表:1 2 3 4 5
插入过程为
1
21
321
4321
54321

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 算法2-8:链表的创建 1. 定义一个链表结构体 2. 初始化头结点 3. 循环读入数据,创建新的结点并插入链表尾部 算法2-9:链表的遍历 1. 从头结点开始,依次遍历链表中的每个结点 2. 对每个结点进行相应的操作 算法2-10:链表的插入 1. 找到要插入的位置 2. 创建新的结点 3. 将新结点插入到链表中 算法2-11:链表的删除 1. 找到要删除的结点 2. 将该结点的前驱结点向该结点的后继结点 3. 释放该结点的内存空间 ### 回答2: 链表是一种非常基础的数据结构,在计算机科学中具有很重要的作用。链表是由若干个节点组成的,每个节点都包含数据和一个向下一个节点的针。链表的优点是可以动态地增加或删除节点,而不需要移动其他节点。链表的基本操作包括创建链表、插入节点、删除节点和遍历链表,下面分别介绍。 首先是创建链表的操作。创建链表需要一个头节点,头节点不包含数据,只是用来表示链表的开始。可以通过如下代码来创建一个空链表: ``` typedef struct Node{ int data; struct Node *next; } Node; Node *create_list() { Node *head = (Node*)malloc(sizeof(Node)); head->next = NULL; return head; } ``` 接下来是插入节点操作。插入节点需要先找到要插入的位置,然后修改向。如果要在链表的头部插入节点,可以直接将头针修改为新节点;如果要在链表的中间或尾部插入节点,则需要遍历链表,找到要插入位置的前一个节点,然后修改前一个节点的next针即可。如下代码就是在链表尾部插入一个值为value的节点: ``` void insert_node_tail(Node *head, int value){ Node *p = head; while(p->next != NULL){ p = p->next; } Node *new_node = (Node*)malloc(sizeof(Node)); new_node->data = value; new_node->next = NULL; p->next = new_node; } ``` 删除节点操作与插入节点操作类似,也需要找到要删除的位置,然后修改向。如果要删除链表头节点,只需将头针修改为下一个节点即可;如果要删除中间或尾部的节点,则需要遍历链表,找到要删除位置的前一个节点,然后将其next向下一个节点的next针即可。如下代码就是删除链表中值为value的第一个节点: ``` void delete_node(Node *head, int value){ Node *p = head; while(p->next != NULL && p->next->data != value){ p = p->next; } if(p->next != NULL){ Node *tmp = p->next; p->next = p->next->next; free(tmp); }else{ printf("值为%d的节点不存在\n", value); } } ``` 最后是遍历链表操作。遍历链表可以使用while循环和针变量遍历整个链表,将节点的值打印出来即可。如下代码就是遍历链表打印出每个节点的值: ``` void traverse_list(Node *head){ Node *p = head->next; while(p != NULL){ printf("%d ",p->data); p = p->next; } } ``` 以上就是链表的基本操作,其中涉及到了针操作、动态内存分配和链表节点的定义等知识点。掌握这些操作能够在实际编程中应用链表这种数据结构,提高算法的效率和代码的可维护性。 ### 回答3: 1.算法2-8:插入节点。 这个算法的目的是向链表中插入新的节点,将新节点放在定节点的后面。 首先,我们需要通过一个循环找到需要插入的位置,即需要插入节点的前一个节点。 然后,我们将新节点连接到链表中,并将定节点的下一个节点连接到新节点上。 最后,我们需要检查该插入的位置是否为空,如果为空,则新节点是链表的最后一个节点。 2.算法2-9:删除节点。 该算法的目的是从链表中删除一个定的节点。 首先,我们需要找到该节点的前一个节点,以便将该节点从链表中删除。 然后,我们需要将该节点从链表中断开,并将该节点的下一个节点连接到前一个节点。 最后,我们需要确保链表中不再有对该节点的引用。 3.算法2-10:查找值。 在链表中查找一个特定的值是很常见的操作。该算法的目的就是查找给定值在链表中的位置。 首先,我们需要从链表的头部开始查找,直到我们找到该值或者到达链表的末尾。 找到值后,我们需要返回该节点在链表中的位置。 如果在整个链表中都没有找到该值,我们需要返回一个错误。 4.算法2-11:遍历链表链表的遍历是按顺序遍历链表中的每个节点。 该算法的目的就是打印链表中的每个节点的值。 首先,我们需要从链表的头部开始,重复访问链表中的每个节点。 我们可以使用一个循环来遍历整个链表,每次访问一个节点,打印该节点的值,然后移动到下一个节点,直到到达链表的末尾。 如果链表为空,我们将不会打印任何值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值